Readable Segments in ErgFileOverview (#3686)

Model and Display Segments - Fixes #3685
This commit is contained in:
ericchristoffersen
2020-12-01 10:49:59 -08:00
committed by GitHub
parent 487ec866f5
commit 34526f2fe9
4 changed files with 228 additions and 63 deletions

View File

@@ -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";
}

View File

@@ -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;

View File

@@ -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
};

View File

@@ -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);