mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-13 16:18:42 +00:00
Readable Segments in ErgFileOverview (#3686)
Model and Display Segments - Fixes #3685
This commit is contained in:
committed by
GitHub
parent
487ec866f5
commit
34526f2fe9
@@ -324,25 +324,31 @@ void TTSReader::segmentRange(int version, ByteArray &data) {
|
||||
void TTSReader::segmentInfo(int version, ByteArray &data) {
|
||||
|
||||
if ((version == 1104) && (data.size() == 8)) {
|
||||
double startKM = (getUInt(data, 0) / 100000.0);
|
||||
double endKM = (getUInt(data, 4) / 100000.0);
|
||||
unsigned startCM = getUInt(data, 0);
|
||||
unsigned endCM = getUInt(data, 4);
|
||||
|
||||
double startKM = (startCM / 100) / 1000.;
|
||||
double endKM = (endCM / 100) / 1000.;
|
||||
|
||||
pendingSegment.startKM = startKM;
|
||||
pendingSegment.endKM = endKM;
|
||||
pendingSegment.endKM = endKM;
|
||||
|
||||
DEBUG_LOG << "[segment range] " << startKM << "-" << endKM << "\n";
|
||||
}
|
||||
|
||||
if ((version == 1000) && (data.size() == 10)) {
|
||||
double startKM = (getUInt(data, 2) / 100000.0);
|
||||
double endKM = (getUInt(data, 6) / 100000.0);
|
||||
unsigned startCM = getUInt(data, 2);
|
||||
unsigned endCM = getUInt(data, 6);
|
||||
|
||||
// NOTE: From the wattzapp debug print it looks like this 3rd value is a divisor.
|
||||
// In my test files its always 1. so no harm to divide - but beware I'm just guessing.
|
||||
double divisor = getUShort(data, 0);
|
||||
|
||||
pendingSegment.startKM = startKM / divisor;
|
||||
pendingSegment.endKM = endKM / divisor;
|
||||
double startKM = ((startCM / 100) / 1000.) / divisor;
|
||||
double endKM = ((endCM / 100) / 1000.) / divisor;
|
||||
|
||||
pendingSegment.startKM = startKM;
|
||||
pendingSegment.endKM = endKM;
|
||||
|
||||
DEBUG_LOG << "[segment range] " << (getUInt(data, 2) / 100000.0) << "-" << (getUInt(data, 6) / 100000.0) << "/" << getUShort(data, 0) << "\n";
|
||||
}
|
||||
|
||||
@@ -974,52 +974,52 @@ void ErgFile::parseTTS()
|
||||
|
||||
// Populate lap and text lists with route and segment info from tts.
|
||||
// Push everything in segment order, then sort by location.
|
||||
const std::vector<NS_TTSReader::Segment> segments = ttsReader.getSegments();
|
||||
std::vector<NS_TTSReader::Segment> segments = ttsReader.getSegments();
|
||||
int segmentCount = (int)segments.size();
|
||||
for (int i = 0; i < segmentCount; i++) {
|
||||
const NS_TTSReader::Segment &segment = segments[i];
|
||||
if (segmentCount) {
|
||||
std::sort(segments.begin(), segments.end(), [](const NS_TTSReader::Segment& a, const NS_TTSReader::Segment& b) {
|
||||
// Segment sort order:
|
||||
// 1) Start Distance, first comes first
|
||||
// 2) Segment Distance, longest comes first
|
||||
if (a.startKM != b.startKM) return a.startKM < b.startKM;
|
||||
return (a.endKM - a.startKM) > (b.endKM - b.startKM);
|
||||
});
|
||||
|
||||
// Truncate distances to meter precision for text name.
|
||||
int segmentStart = (int)(segment.startKM * 1000);
|
||||
int segmentEnd = (int)(segment.endKM * 1000);
|
||||
// Now segments are sorted by start and longer duration.
|
||||
|
||||
std::wstring rangeString = L" ["
|
||||
+ std::to_wstring(segmentStart)
|
||||
+ L"m ->"
|
||||
+ std::to_wstring(segmentEnd)
|
||||
+ L"m]";
|
||||
for (int i = 0; i < segmentCount; i++) {
|
||||
const NS_TTSReader::Segment& segment = segments[i];
|
||||
|
||||
// Populate Texts with segment text.
|
||||
if (segment.name.length() + segment.description.length() > 0) {
|
||||
QString text(QString::fromStdWString(
|
||||
segment.name + L": " +
|
||||
segment.description +
|
||||
rangeString));
|
||||
// Truncate distances to meter precision for text name.
|
||||
int segmentStart = (int)(segment.startKM * 1000);
|
||||
int segmentEnd = (int)(segment.endKM * 1000);
|
||||
|
||||
double x = segment.startKM * 1000.;
|
||||
int duration = (segment.endKM - segment.startKM) * 1000.;
|
||||
Texts << ErgFileText(x, duration, text);
|
||||
std::wstring rangeString = L" ["
|
||||
+ std::to_wstring(segmentStart)
|
||||
+ L"m ->"
|
||||
+ std::to_wstring(segmentEnd)
|
||||
+ L"m]";
|
||||
|
||||
// Populate Texts with segment text.
|
||||
if (segment.name.length() + segment.description.length() > 0) {
|
||||
QString text(QString::fromStdWString(
|
||||
segment.name + L": " +
|
||||
segment.description +
|
||||
rangeString));
|
||||
|
||||
double x = segment.startKM * 1000.;
|
||||
int duration = (segment.endKM - segment.startKM) * 1000.;
|
||||
Texts << ErgFileText(x, duration, text);
|
||||
}
|
||||
|
||||
// Populate Laps with segment info.
|
||||
// In order to support navigation, each segment is converted into
|
||||
// two laps that are linked by sharing a non-zero group id.
|
||||
Laps << ErgFileLap(segment.startKM * 1000., (2 * i) + 1, i + 1,
|
||||
QString::fromStdWString(segment.name));
|
||||
|
||||
Laps << ErgFileLap(segment.endKM * 1000, (2 * i) + 2, i + 1, "");
|
||||
}
|
||||
|
||||
// Populate Laps with segment info. Create a lap for the start and another for the end.
|
||||
ErgFileLap add;
|
||||
|
||||
// Segment Start
|
||||
add.x = segment.startKM * 1000.;
|
||||
add.LapNum = (i * 2) + 1;
|
||||
add.selected = false;
|
||||
|
||||
add.name = QObject::tr("Starting") + ": " + QString::fromStdWString(segment.name + rangeString);
|
||||
|
||||
Laps << add;
|
||||
|
||||
// Segment End
|
||||
add.x = segment.endKM * 1000.;
|
||||
add.LapNum = (i * 2) + 2;
|
||||
add.selected = false;
|
||||
add.name = QObject::tr("Ending") + ": " + QString::fromStdWString(segment.name + rangeString);
|
||||
|
||||
Laps << add;
|
||||
}
|
||||
|
||||
// The following sort preserves segment overlap. If A is a superset of B then order will be:
|
||||
@@ -1044,7 +1044,7 @@ void ErgFile::parseTTS()
|
||||
int xx = 0;
|
||||
qDebug() << "LAPS:";
|
||||
for (const auto &a : Laps) {
|
||||
qDebug() << xx << ": " << a.LapNum << " start:" << a.x / 1000. << "km, name: " << a.name;
|
||||
qDebug() << xx << ": " << a.LapNum << " start:" << a.x / 1000. << "km, rangeId: " << a.lapRangeId << "km, name: " << a.name;
|
||||
xx++;
|
||||
}
|
||||
|
||||
@@ -1473,9 +1473,15 @@ void ErgFile::sortLaps() const
|
||||
if (Laps.count()) {
|
||||
// Sort laps by start, then by existing lap num
|
||||
std::sort(Laps.begin(), Laps.end(), [](const ErgFileLap& a, const ErgFileLap& b) {
|
||||
// If start is the same then lesser lap num comes first.
|
||||
if (a.x == b.x) return a.LapNum < b.LapNum;
|
||||
return a.x < b.x;
|
||||
// 1) Start, first comes first
|
||||
if (a.x != b.x) return a.x < b.x;
|
||||
|
||||
// 2) range id, lowest first. This ordering is chosen prior to segments being
|
||||
// devolved into lap markers, so can be used to impose semantic order on laps.
|
||||
if (a.lapRangeId != b.lapRangeId) return a.lapRangeId < b.lapRangeId;
|
||||
|
||||
// 3) LapNum
|
||||
return a.LapNum < b.LapNum;
|
||||
});
|
||||
|
||||
// Renumber laps to follow the new entry distance order:
|
||||
@@ -1580,18 +1586,14 @@ ErgFile::addNewLap(double loc) const
|
||||
{
|
||||
if (isValid())
|
||||
{
|
||||
ErgFileLap add;
|
||||
ErgFileLap add(loc, Laps.count(), "user lap");
|
||||
|
||||
add.x = loc;
|
||||
add.LapNum = Laps.count();
|
||||
add.selected = false;
|
||||
add.name = "user lap";
|
||||
Laps.append(add);
|
||||
|
||||
sortLaps();
|
||||
|
||||
auto itr = std::find_if(Laps.begin(), Laps.end(), [&add](const ErgFileLap& otherLap) {
|
||||
return add.x == otherLap.x && add.name == otherLap.name;
|
||||
return add.x == otherLap.x && add.lapRangeId == otherLap.lapRangeId && add.name == otherLap.name;
|
||||
});
|
||||
if (itr != Laps.end())
|
||||
return (*itr).LapNum;
|
||||
|
||||
@@ -87,12 +87,14 @@ class ErgFileText
|
||||
class ErgFileLap
|
||||
{
|
||||
public:
|
||||
ErgFileLap() : name(""), x(0), LapNum(0), selected(false) {}
|
||||
ErgFileLap(double x, int LapNum, const QString& name) : name(name), x(x), LapNum(LapNum), selected(false) {}
|
||||
ErgFileLap() : name(""), x(0), LapNum(0), lapRangeId(0), selected(false) {}
|
||||
ErgFileLap(double x, int LapNum, const QString& name) : name(name), x(x), LapNum(LapNum), lapRangeId(0), selected(false) {}
|
||||
ErgFileLap(double x, int LapNum, int lapRangeId, const QString& name) : name(name), x(x), LapNum(LapNum), lapRangeId(lapRangeId), selected(false) {}
|
||||
|
||||
QString name;
|
||||
double x; // when does this LAP marker occur? (time in msecs or distance in meters
|
||||
int LapNum; // from 1 - n
|
||||
int lapRangeId;// for grouping lap markers into ranges. Value of zero is considered 'ungrouped'.
|
||||
bool selected; // used by the editor
|
||||
};
|
||||
|
||||
|
||||
@@ -16,11 +16,12 @@
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
|
||||
#include "ErgFilePlot.h"
|
||||
#include "WPrime.h"
|
||||
#include "Context.h"
|
||||
|
||||
#include <unordered_map>
|
||||
|
||||
// Bridge between QwtPlot and ErgFile to avoid having to
|
||||
// create a separate array for the ergfile data, we plot
|
||||
// directly from the ErgFile points array
|
||||
@@ -288,6 +289,124 @@ ErgFilePlot::configChanged(qint32)
|
||||
replot();
|
||||
}
|
||||
|
||||
|
||||
// Distribute segments into rows for non-overlapped display.
|
||||
//
|
||||
// This is currently based strictly on segment start and end and ignores
|
||||
// text length. Ideally text length would be used to raise segment
|
||||
// size for purposes of display packing, which would allow cause lap
|
||||
// markers to be shown without their names stomping all over their
|
||||
// adjacent bretheren.
|
||||
class LapRowDistributor {
|
||||
|
||||
const QList<ErgFileLap>& laps;
|
||||
std::unordered_map<int, std::tuple<int, int>> lapRangeIdMap;
|
||||
std::vector<int> segmentRowMap;
|
||||
|
||||
public:
|
||||
|
||||
enum ResultEnum { Failed = 0, StartOfRange, EndOfRange, InternalRange, SimpleLap };
|
||||
|
||||
ResultEnum GetInfo(int i, int& row) {
|
||||
|
||||
if (i < 0 || i > laps.count())
|
||||
return Failed;
|
||||
|
||||
int lapRangeId = laps.at(i).lapRangeId;
|
||||
|
||||
row = segmentRowMap[std::get<0>(lapRangeIdMap[lapRangeId])];
|
||||
|
||||
if (lapRangeId) {
|
||||
auto range = lapRangeIdMap.find(lapRangeId);
|
||||
if (range != lapRangeIdMap.end()) {
|
||||
if (std::get<0>(range->second) == i) return StartOfRange;
|
||||
if (std::get<1>(range->second) == i) return EndOfRange;
|
||||
}
|
||||
return InternalRange;
|
||||
}
|
||||
|
||||
return SimpleLap;
|
||||
}
|
||||
|
||||
LapRowDistributor(const QList<ErgFileLap> &laps) : laps(laps), segmentRowMap(laps.count(), -1) {
|
||||
|
||||
// Part 1:
|
||||
//
|
||||
// Build mapping from lapRangeId to index of first/last lap markers in the
|
||||
// group.
|
||||
//
|
||||
// Map provides instant access to start and end of rangeid.
|
||||
int lapCount = laps.count();
|
||||
|
||||
int maxRangeId = 0;
|
||||
for (int i = 0; i < lapCount; i++) {
|
||||
maxRangeId = std::max(maxRangeId, laps.at(i).lapRangeId);
|
||||
}
|
||||
|
||||
for (int i = 0; i < lapCount; i++) {
|
||||
const ErgFileLap& lap = laps.at(i);
|
||||
|
||||
int startIdx = i, endIdx = i;
|
||||
|
||||
auto e = lapRangeIdMap.find(lap.lapRangeId);
|
||||
if (e != lapRangeIdMap.end()) {
|
||||
std::tie(startIdx, endIdx) = e->second;
|
||||
if (lap.x < laps.at(startIdx).x)
|
||||
startIdx = i;
|
||||
|
||||
if (lap.x > laps.at(endIdx).x)
|
||||
endIdx = i;
|
||||
}
|
||||
|
||||
lapRangeIdMap[lap.lapRangeId] = std::make_tuple(startIdx, endIdx);
|
||||
}
|
||||
|
||||
// Part 2: Generate segmentRowMap, this is a map from lap to what row that lap should be printed upon.
|
||||
|
||||
// Tracks what segments are live at what row during search
|
||||
std::vector<int> segmentRowLiveMap;
|
||||
for (int i = 0; i < lapCount; i++) {
|
||||
const ErgFileLap& lap = laps.at(i);
|
||||
|
||||
// Space is only computed for first lap in range group.
|
||||
if (std::get<0>(lapRangeIdMap[lap.lapRangeId]) != i) {
|
||||
continue;
|
||||
}
|
||||
|
||||
double startM = lap.x;
|
||||
|
||||
// Age-out all rows of segments that end at or before startKM
|
||||
// Assign first available that is available
|
||||
int row = -1;
|
||||
for (int r = 0; r < segmentRowLiveMap.size(); r++) {
|
||||
int v = segmentRowLiveMap[r];
|
||||
if (v >= 0) {
|
||||
double endM = laps.at(std::get<1>(lapRangeIdMap[laps.at(v).lapRangeId])).x;
|
||||
if (endM <= startM) {
|
||||
v = -1;
|
||||
segmentRowLiveMap[r] = v;
|
||||
}
|
||||
}
|
||||
|
||||
// Take first free row we encounter.
|
||||
if (row < 0 && v < 0) {
|
||||
segmentRowLiveMap[r] = i;
|
||||
row = r;
|
||||
}
|
||||
}
|
||||
|
||||
// If no free rows then push a new one on the end
|
||||
if (row < 0) {
|
||||
segmentRowLiveMap.push_back(i);
|
||||
row = (int)(segmentRowLiveMap.size() - 1);
|
||||
}
|
||||
|
||||
// Record the row that this segment was assigned
|
||||
segmentRowMap[i] = row;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void
|
||||
ErgFilePlot::setData(ErgFile *ergfile)
|
||||
{
|
||||
@@ -346,11 +465,44 @@ ErgFilePlot::setData(ErgFile *ergfile)
|
||||
|
||||
}
|
||||
|
||||
LapRowDistributor lapRowDistributor(ergFile->Laps);
|
||||
|
||||
// set up again
|
||||
for(int i=0; i < ergFile->Laps.count(); i++) {
|
||||
|
||||
// Show Lap Number
|
||||
QwtText text(ergFile->Laps.at(i).name != "" ? ergFile->Laps.at(i).name : QString::number(ergFile->Laps.at(i).LapNum));
|
||||
const ErgFileLap& lap = ergFile->Laps.at(i);
|
||||
|
||||
int row = 0;
|
||||
LapRowDistributor::ResultEnum distributionResult = lapRowDistributor.GetInfo(i, row);
|
||||
|
||||
// Danger: ASCII ART. Somebody please replace this with graphics?
|
||||
QString decName;
|
||||
switch(distributionResult) {
|
||||
case LapRowDistributor::StartOfRange:
|
||||
decName = "<" + lap.name;
|
||||
break;
|
||||
case LapRowDistributor::EndOfRange:
|
||||
decName = ">";
|
||||
break;
|
||||
case LapRowDistributor::SimpleLap:
|
||||
decName = QString::number(lap.LapNum) + ":" + lap.name;
|
||||
break;
|
||||
|
||||
case LapRowDistributor::InternalRange:
|
||||
case LapRowDistributor::Failed:
|
||||
default:
|
||||
// Nothing to do.
|
||||
break;
|
||||
};
|
||||
|
||||
// Literal row translation. We loves ascii art...
|
||||
QString prefix;
|
||||
for (int r = 0; r < row; r++)
|
||||
prefix = prefix + "\n";
|
||||
|
||||
QwtText text(prefix + decName);
|
||||
|
||||
text.setFont(QFont("Helvetica", 10, QFont::Bold));
|
||||
text.setColor(GColor(CPLOTMARKER));
|
||||
|
||||
@@ -358,8 +510,11 @@ ErgFilePlot::setData(ErgFile *ergfile)
|
||||
QwtPlotMarker *add = new QwtPlotMarker();
|
||||
add->setLineStyle(QwtPlotMarker::VLine);
|
||||
add->setLinePen(QPen(GColor(CPLOTMARKER), 0, Qt::DashDotLine));
|
||||
add->setLabelAlignment(Qt::AlignRight | Qt::AlignTop);
|
||||
add->setValue(ergFile->Laps.at(i).x, 0.0);
|
||||
add->setLabelAlignment(
|
||||
(LapRowDistributor::EndOfRange == distributionResult)
|
||||
? Qt::AlignLeft | Qt::AlignTop
|
||||
: Qt::AlignRight | Qt::AlignTop);
|
||||
add->setValue(lap.x, 0);
|
||||
add->setLabel(text);
|
||||
add->attach(this);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user