meos-2024/code/qualification_final.cpp
2024-04-07 08:37:06 +02:00

837 lines
26 KiB
C++

/************************************************************************
MeOS - Orienteering Software
Copyright (C) 2009-2024 Melin Software HB
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/>.
Melin Software HB - software@melin.nu - www.melin.nu
Eksoppsvägen 16, SE-75646 UPPSALA, Sweden
************************************************************************/
#include "StdAfx.h"
#include "qualification_final.h"
#include "meos_util.h"
#include "meosexception.h"
#include <set>
#include <algorithm>
#include "xmlparser.h"
#include "oRunner.h"
#include "localizer.h"
pair<int, int> QualificationFinal::getPrelFinalFromPlace(int instance, int orderPlace, int numSharedPlaceNext) {
pair<int, int> key(instance, orderPlace);
int iter = 0;
while (numSharedPlaceNext >= 0) {
auto res = sourcePlaceToFinalOrder.find(key);
if (res != sourcePlaceToFinalOrder.end()) {
if (iter >= 2) { // For three in a shared last place
pair<int, int> key2 = key;
int extraSub = ((iter + 1) % 2);
key2.second -= extraSub;
auto res2 = sourcePlaceToFinalOrder.find(key2);
if (res2 != sourcePlaceToFinalOrder.end()) {
auto ans = res2->second;
ans.second += iter + extraSub;
++numExtraAssigned[ans.first]; // More than specified
return ans;
}
}
auto ans = res->second;
ans.second += iter;
if (iter > 0)
++numExtraAssigned[ans.first]; // More than specified
return ans;
}
--key.second;
--numSharedPlaceNext;
++iter;
}
return make_pair(0, -1);
}
bool QualificationFinal::noQualification(int instance) const {
if (size_t(instance) >= classDefinition.size())
return false;
if (instance > 0 && classDefinition[instance].level > classDefinition[instance - 1].level)
return false; // Second level
return classDefinition[instance].qualificationMap.empty() &&
classDefinition[instance].numTimeQualifications == 0 &&
classDefinition[instance].extraQualification == QFClass::ExtraQualType::None;
}
void QualificationFinal::getBaseClassInstances(set<int> &base) const {
for (size_t k = 0; k < classDefinition.size(); k++) {
if (noQualification(k))
base.insert(k+1);
else break;
}
}
void QualificationFinal::exportXML(const wstring& file) const {
xmlparser xml;
xml.openOutputT(file.c_str(), false, "QualificationRules");
int cLevel = -1;
for (int j = 0; j < classDefinition.size(); j++) {
int level = getLevel(j+1);
if (level != cLevel) {
if (cLevel >= 0)
xml.endTag();
cLevel = level;
if (classDefinition[j].rankLevel)
xml.startTag("Level", "distribution", "Ranking");
else
xml.startTag("Level");
}
vector<wstring> pv;
pv.emplace_back(L"name");
pv.emplace_back(classDefinition[j].name.empty() ? lang.tl("Kval") + itow(j + 1) : classDefinition[j].name);
pv.emplace_back(L"id");
pv.emplace_back(itow(j + 1));
xml.startTag("Class", pv);
vector<pair<string, wstring>> psv(2);;
psv[0].first = "id";
psv[1].first = "place";
for (auto& qmap : classDefinition[j].qualificationMap) {
psv[0].second = itow(qmap.first);
psv[1].second = itow(qmap.second);
xml.write("Qualification", psv, L"");
}
if (classDefinition[j].numTimeQualifications > 0) {
psv[0].first = "time";
psv[0].second = itow(classDefinition[j].numTimeQualifications);
psv.resize(1);
xml.write("Qualification", psv, L"");
}
if (classDefinition[j].extraQualification != QFClass::ExtraQualType::None) {
psv[0].first = "type";
switch (classDefinition[j].extraQualification) {
case QFClass::ExtraQualType::All:
psv.resize(1);
psv[0].second = L"All";
break;
case QFClass::ExtraQualType::NBest:
psv.resize(2);
psv[0].second = L"Best";
psv[1].first = "number";
psv[1].second = itow(classDefinition[j].extraQualData);
break;
case QFClass::ExtraQualType::TimeLimit:
psv.resize(2);
psv[0].second = L"Time";
psv[1].first = "limit";
psv[1].second = formatTimeMS(classDefinition[j].extraQualData, false);
break;
}
xml.write("Remaining", psv, L"");
}
xml.endTag();
}
xml.closeOut();
}
void QualificationFinal::importXML(const wstring &file) {
xmlparser xml;
xml.read(file);
auto qr = xml.getObject("QualificationRules");
xmlList levels;
qr.getObjects("Level", levels);
map<int, int> idToIndex;
map<int, set<int> > qualificationRelations;
int numBaseLevels = 0;
for (size_t iLevel = 0; iLevel < levels.size(); iLevel++) {
auto &level = levels[iLevel];
wstring rankS;
level.getObjectString("distribution", rankS);
bool rankSort = false;
if (rankS == L"Ranking")
rankSort = true;
else if (!rankS.empty())
throw meosException(L"Unknown distribution: " + rankS);
xmlList classes;
level.getObjects("Class", classes);
for (auto &cls : classes) {
wstring name;
cls.getObjectString("name", name);
if (name.empty())
cls.getObjectString("Name", name);
if (name.empty())
throw meosException("Klassen måste ha ett namn.");
int classId = cls.getObjectInt("id");
if (!(classId>0))
throw meosException("Id must be a positive integer.");
if (idToIndex.count(classId))
throw meosException("Duplicate class with id " + itos(classId));
xmlList rules;
cls.getObjects("Qualification", rules);
xmlobject remaining = cls.getObject("Remaining");
if (rules.empty() && !remaining)
numBaseLevels = 1; // Instance zero is not used as qualification,
idToIndex[classId] = classDefinition.size() + numBaseLevels;
classDefinition.emplace_back();
classDefinition.back().level = iLevel;
if (remaining) {
wstring rtype;
remaining.getObjectString("type", rtype);
if (rtype == L"All") {
classDefinition.back().extraQualification = QFClass::ExtraQualType::All;
}
else if (rtype == L"Best") {
classDefinition.back().extraQualification = QFClass::ExtraQualType::NBest;
classDefinition.back().extraQualData = remaining.getObjectInt("number");
if (classDefinition.back().extraQualData > 10000 || classDefinition.back().extraQualData < 0)
classDefinition.back().extraQualData = 0;
}
else if (rtype == L"Time") {
classDefinition.back().extraQualification = QFClass::ExtraQualType::TimeLimit;
wstring wt;
remaining.getObjectString("limit", wt);
classDefinition.back().extraQualData = convertAbsoluteTimeMS(wt);
if (classDefinition.back().extraQualData == NOTIME || classDefinition.back().extraQualData < 0)
classDefinition.back().extraQualData = 0;
}
}
classDefinition.back().name = name;
classDefinition.back().rankLevel = rankSort;
for (auto &qf : rules) {
int place = qf.getObjectInt("place");
if (place > 0) {
int id = qf.getObjectInt("id");
if (id == 0 && iLevel == 0)
classDefinition.back().qualificationMap.push_back(make_pair(0, place));
else if (idToIndex.count(id)) {
classDefinition.back().qualificationMap.push_back(make_pair(idToIndex[id], place));
qualificationRelations[classId].insert(id);
}
else
throw meosException("Unknown class with id " + itos(id));
}
else {
int numTime = qf.getObjectInt("time");
if (numTime > 0)
classDefinition.back().numTimeQualifications += numTime;
else if (qf.got("time")) {
throw meosException(L"Empty time qualification for " + name);
}
else throw meosException(L"Unknown classification rule for " + name);
}
}
}
}
initgmap(true);
}
void QualificationFinal::init(const wstring &def) {
serializedFrom = def;
vector<wstring> races, rtdef, rdef;
split(def, L"|", races);
classDefinition.resize(races.size());
bool valid = true;
bool first = true;
for (size_t k = 0; k < races.size(); k++) {
if (races[k].size() > 0 && races[k][0] == '@') {
// Explicit level, since 4.0
classDefinition[k].level = _wtoi(races[k].c_str() + 1);
int mx = 1;
while (mx < races[k].size() && races[k][mx] != '@')
mx++;
int mx2 = mx + 1;
while (mx2 < races[k].size() && races[k][mx2] != '@')
mx2++;
if (mx2 >= races[k].size() || classDefinition[k].level > k) {
valid = false;
break;
}
races[k][mx2] = 0;
classDefinition[k].name = races[k].c_str() + mx + 1;
races[k] = races[k].substr(mx2 + 1);
}
bool rankLevel = false;
if (races[k].size() > 0 && races[k][0] == 'R') {
rankLevel = true;
races[k] = races[k].substr(1);
}
if (races[k].size() > 0 && QFClass::deserialType(races[k][0]) != QFClass::ExtraQualType::None) {
first = false;
classDefinition[k].extraQualification = QFClass::deserialType(races[k][0]);
if (classDefinition[k].extraQualification == QFClass::ExtraQualType::NBest ||
classDefinition[k].extraQualification == QFClass::ExtraQualType::TimeLimit) {
classDefinition[k].extraQualData = _wtoi(races[k].c_str() + 1);
}
int end = 1;
while (end < races[k].size() && races[k][end-1] != ';')
end++;
races[k] = races[k].substr(end);
}
split(races[k], L"T", rtdef);
classDefinition[k].qualificationMap.clear();
classDefinition[k].numTimeQualifications = 0;
classDefinition[k].rankLevel = rankLevel;
if (rtdef.empty())
continue; // Remaining qualified
first = false;
split(rtdef[0], L";", rdef);
bool thisValid = rdef.size()%2 == 0 &&
(rdef.size() > 0 || (rtdef.size() == 2 && !rtdef[1].empty()));
if (!thisValid)
continue;
for (size_t j = 0; j < rdef.size(); j+=2) {
int src = _wtoi(rdef[j].c_str());
if (src > k || src <= 0) {
thisValid = false;
break;
}
const wstring &rd = rdef[j + 1];
int d1 = _wtoi(rd.c_str());
if (d1 < 1 || d1>1000) {
thisValid = false;
break;
}
int d2 = d1;
size_t range = rd.find_first_of('-', 0);
if (range < rd.size())
d2 = _wtoi(rd.c_str()+range+1);
if (d1 > d2) {
thisValid = false;
break;
}
while (d1 <= d2) {
classDefinition[k].qualificationMap.emplace_back(src, d1);
d1++;
}
}
if (!thisValid)
classDefinition[k].qualificationMap.clear();
if (rtdef.size() > 1) {
int numTime = _wtoi(rtdef[1].c_str());
classDefinition[k].numTimeQualifications = numTime;
}
}
if (!valid)
classDefinition.clear();
initgmap(false);
}
void QualificationFinal::setClasses(const vector<QFClass>& def) {
classDefinition = def;
initgmap(true);
wstring tmp;
encode(tmp);
}
wstring QualificationFinal::validName(const wstring& name) {
wstring out;
out.reserve(name.size());
for (int j = 0; j < name.length(); j++)
if (isValidNameChar(name[j]))
out.push_back(name[j]);
return out;
}
bool QualificationFinal::isValidNameChar(wchar_t c) {
if (c == 0 || c == '|' || c == '@')
return false;
return true;
}
void QualificationFinal::encode(wstring &output) const {
output.clear();
for (size_t k = 0; k < classDefinition.size(); k++) {
if (k > 0)
output.append(L"|");
output.append(L"@"+ itow(getLevel(k+1)) + L"@" + validName(classDefinition[k].name) + L"@");
if (classDefinition[k].rankLevel)
output.append(L"R");
output.append(classDefinition[k].serialExtra());
auto &qm = classDefinition[k].qualificationMap;
for (size_t j = 0; j < qm.size(); j++) {
if (j > 0)
output.append(L";");
size_t i = j;
while ((i + 1) < qm.size() && qm[i + 1].first == qm[i].first
&& qm[i + 1].second == qm[i].second+1) {
i++;
}
output.append(itow(qm[j].first) + L";");
if (i <= j + 1) {
output.append(itow(qm[j].second));
}
else {
output.append(itow(qm[j].second) + L"-" + itow(qm[i].second));
j = i;
}
}
if (classDefinition[k].numTimeQualifications > 0) {
output.append(L"T");
output.append(itow(classDefinition[k].numTimeQualifications));
}
}
serializedFrom = output;
}
wstring QFClass::serialExtra() const {
if (extraQualification == ExtraQualType::All)
return L"A;";
else if (extraQualification == ExtraQualType::TimeLimit)
return L"L" + itow(extraQualData) + L";";
else if (extraQualification == ExtraQualType::NBest)
return L"B" + itow(extraQualData) + L";";
return L"";
}
int QFClass::getMinQualInst() const {
if (qualificationMap.empty())
return -1;
int ret = 1024;
for (auto& cp : qualificationMap)
ret = min(ret, cp.first);
return ret;
}
void QualificationFinal::initgmap(bool check) {
level2DefningInstance.clear();
sourcePlaceToFinalOrder.clear();
levels.clear();
levels.resize(getNumLevels(), LevelInfo::Normal);
for (int ix = 0; ix < (int)classDefinition.size(); ix++) {
auto &c = classDefinition[ix];
if (c.rankLevel)
levels[getLevel(ix+1)] = LevelInfo::RankSort;
for (int k = 0; k < (int)c.qualificationMap.size(); k++) {
const pair<int, int> &sd = c.qualificationMap[k];
if (check && sourcePlaceToFinalOrder.count(sd))
throw meosException(L"Inconsistent qualification rule, X#" + c.name + + L"/" + itow(sd.first));
sourcePlaceToFinalOrder[sd] = make_pair(ix+1, k);
}
}
}
int QualificationFinal::getLevel(int instance) const {
if (instance == 0)
return 0;
instance--;
if (classDefinition[instance].level >= 0)
return classDefinition[instance].level;
int level = 0;
int src = instance;
while (!classDefinition[instance].qualificationMap.empty()) {
instance = classDefinition[instance].qualificationMap.front().first - 1;
if (instance < 0)
break;
level++;
if (size_t(level) > classDefinition.size())
throw meosException("Internal error");
}
classDefinition[src].level = level;
return level;
}
bool QualificationFinal::isFinalClass(int instance) const {
return instance > 0 && getLevel(instance) + 1 == getNumLevels();
}
int QualificationFinal::getNumLevels() const {
return getLevel(classDefinition.size()) + 1;
}
int QualificationFinal::getMinInstance(const int levelIn) const {
int level = levelIn;
auto res = level2DefningInstance.find(levelIn);
if (res != level2DefningInstance.end())
return res->second;
int minInst = classDefinition.size() - 1;
for (int i = classDefinition.size() - 1; i >= 0; i--) {
if (i == 0 && minInst == 1)
break; // No need to include base instance.
int thisLevel = getLevel(i+1);
if (thisLevel > level) {
int minQualInst = classDefinition[i].getMinQualInst();
if (minQualInst >= 0) {
// If there are direct qualification to this level from an
// earlier level, we must start from that level.
int qualLevel = getLevel(minQualInst+1);
if (qualLevel < level) {
level = qualLevel;
i = classDefinition.size(); // Restart
}
}
}
else if (thisLevel == level)
minInst = i;
}
level2DefningInstance[levelIn] = minInst;
return minInst;
}
int QualificationFinal::getHeatFromClass(int finalClassId, int baseClassId) const {
if (baseClassId == baseId) {
int fci = (finalClassId - baseId) / maxClassId;
if (fci * maxClassId + baseClassId == finalClassId)
return fci;
else
return -1;
}
return 0;
}
/** Clear previous staus data*/
void QualificationFinal::prepareCalculations() {
storedInfoLookup.clear();
storedInfo.clear();
numExtraAssigned.clear();
}
/** Retuns the final class and the order within that class. */
void QualificationFinal::provideQualificationResult(pRunner r, int instance, int orderPlace, int numSharedPlace) {
storedInfo.resize(getNumClasses());
pair<int, int> res = getPrelFinalFromPlace(instance, orderPlace, numSharedPlace);
int level = getLevel(instance);
storedInfo[level].emplace_back(r, res.first, res.second);
}
void QualificationFinal::provideUnqualified(int level, pRunner r) {
storedInfo.resize(getNumClasses());
storedInfo[level].emplace_back(r, -1, storedInfo[level].size());
}
/** Do internal calculations. */
void QualificationFinal::computeFinals() {
if (storedInfo.empty())
return;
int nl = getNumLevels();
// Handle time qualifications
vector<pair<int, ResultInfo*>> timeToIx;
set<pair<int, ResultInfo*>> remainingTimeQualified;
int lastLevel = -1;
vector<int> levelClasses;
auto distributeRemaning = [&]() {
if (levelClasses.empty())
return;
int cIx = 0;
for (auto& tq : remainingTimeQualified) {
tq.second->instance = levelClasses[cIx];
tq.second->order = 100 + cIx;
cIx++;
if (cIx >= levelClasses.size())
cIx = 0;
}
};
for (int j = 1; j < classDefinition.size(); j++) {
int level = getLevel(j + 1) - 1;
if (level != lastLevel) {
distributeRemaning();
levelClasses.clear();
remainingTimeQualified.clear();
lastLevel = level;
}
levelClasses.push_back(j + 1);
int NT = classDefinition[j].numTimeQualifications;
auto res = numExtraAssigned.find(j + 1);
if (res != numExtraAssigned.end()) {
NT -= res->second; // Already assigned from (duplicate) places
}
for (int i = 0; i < NT; i++) {
timeToIx.clear();
int order = 0;
// Select the best time from unqualified competitors
if (level < storedInfo.size()) {
for (auto& res : storedInfo[level]) {
if (res.instance == 0) {
int t = res.r->getRunningTime(false);
if (timeToIx.empty() || t <= timeToIx.front().first) {
// Select the current best time
if (!timeToIx.empty() && t < timeToIx.front().first)
timeToIx.clear(); // Found better time
timeToIx.emplace_back(t, &res);
}
}
else {
order = std::max(res.order, order);
}
}
}
if (!timeToIx.empty()) {
timeToIx[0].second->instance = j + 1;
timeToIx[0].second->order = ++order;
for (int j = 1; j < timeToIx.size(); j++) {
// Qualified by time, but not yet in a race (tie position)
remainingTimeQualified.insert(timeToIx[j]);
}
}
}
}
// Allow both to advance on tie
distributeRemaning();
for (int j = 1; j < classDefinition.size(); j++) {
int level = getLevel(j + 1) - 1;
if (classDefinition[j].extraQualification == QFClass::ExtraQualType::All) {
// All remaining
for (auto& si : storedInfo[level]) {
if (si.instance <= 0)
si.instance = j + 1;
}
}
else if (classDefinition[j].extraQualification == QFClass::ExtraQualType::NBest) {
// N best competitors
int count = 0;
for (auto& si : storedInfo[level]) {
if (si.instance <= 0) {
si.instance = j + 1;
if (++count >= classDefinition[j].extraQualData)
break;
}
}
}
else if (classDefinition[j].extraQualification == QFClass::ExtraQualType::TimeLimit) {
// Fixed time limit
for (auto& si : storedInfo[level]) {
if (si.instance == 0 &&
si.r->getStatus() == StatusOK &&
si.r->getRunningTime(false) < classDefinition[j].extraQualData){
si.instance = j + 1;
}
}
}
}
for (int level = 1; level < nl; level++) {
vector<int> placeCount(classDefinition.size(), 0);
if (levels[level] == LevelInfo::RankSort) {
vector<pair<int, int>> rankIx;
vector<pair<int, int>> placeQueue;
set<int> numClassCount;
auto &si = storedInfo[level-1];
if (si.empty())
continue;
rankIx.reserve(si.size());
placeQueue.reserve(si.size());
for (int i = 0; size_t(i) < si.size(); i++) {
if (si[i].instance > 0) {
int rank = si[i].r->getRanking();
rankIx.emplace_back(rank, i);
placeQueue.emplace_back(si[i].order, si[i].instance);
numClassCount.insert(si[i].instance);
}
}
int numClass = numClassCount.size();
vector<int> rotmap;
if (numClass == 2) {
rotmap = { 1,0 };
}
else if (numClass == 3) {
rotmap = { 2,0,1 };
}
else if (numClass == 4) {
rotmap = { 3, 2, 0, 1 };
}
else if (numClass == 5) {
rotmap = { 4, 3, 0, 2, 1 };
}
else if (numClass == 6) {
rotmap = { 5, 4, 3, 0, 2, 1 };
}
else {
rotmap.resize(numClass);
for (int k = 0; k < numClass; k++)
rotmap[k] = (k + 1) % numClass;
}
sort(rankIx.begin(), rankIx.end());
sort(placeQueue.begin(), placeQueue.end());
vector<int> placementOrder;
for (size_t ix = 0; ix < placeQueue.size(); ix++) {
placementOrder.push_back(ix);
}
for (size_t i = numClass; i + numClass <= placementOrder.size(); i += numClass * 2) {
reverse(placementOrder.begin() + i, placementOrder.begin() + i + numClass);
}
reverse(placementOrder.begin(), placementOrder.end());
for (auto &rankRes : rankIx) {
si[rankRes.second].instance = placeQueue[placementOrder.back()].second;
si[rankRes.second].order = placeQueue[placementOrder.back()].first;
placementOrder.pop_back();
}
}
}
for (auto & si : storedInfo) {
for (auto &res : si) {
storedInfoLookup[res.r->getId()] = &res;
}
}
}
/** Retuns the final class and the order within that class. */
pair<int, int> QualificationFinal::getNextFinal(int runnerId) const {
auto res = storedInfoLookup.find(runnerId);
if (res == storedInfoLookup.end()) {
return make_pair(0, -1);
}
return make_pair(max(res->second->instance, 0), res->second->order);
}
void QualificationFinal::printScheme(const oClass& cls, gdioutput &gdi) const {
vector<wstring> cname;
for (size_t i = 0; i < classDefinition.size(); i++) {
pClass inst = cls.getVirtualClass(i + 1);
cname.push_back(inst ? inst->getName() : lang.tl("Saknad klass"));
}
int ylimit = gdi.getHeight();
gdi.pushY();
for (size_t i = 0; i < classDefinition.size(); i++) {
gdi.dropLine();
gdi.addStringUT(1, itow(i+1) + L" " + cname[i]);
if (getLevel(i + 1) == 0) {
//gdi.addStringUT(italicText, "Kval").setColor(GDICOLOR::colorDarkGrey);
}
else {
gdi.addString("", italicText, classDefinition[i].getQualInfo()).setColor(GDICOLOR::colorDarkGreen);
}
gdi.dropLine(0.2);
int rankingBased = 0;
vector<wstring> dst;
int misCntLimit = 0;
for (int place = 1; place < 100; place++) {
auto res = sourcePlaceToFinalOrder.find(make_pair(i + 1, place));
if (res != sourcePlaceToFinalOrder.end()) {
int level = getLevel(res->second.first);
if (levels[level] == LevelInfo::RankSort)
++rankingBased;
else
dst.push_back(itow(place) + L". ➞ " + cname[res->second.first - 1]);
}
else {
if (++misCntLimit > 10)
break;
}
}
if (rankingBased) {
gdi.addString("", 0, L" X går vidare, klass enligt ranking#" + itow(rankingBased));
}
for (auto &c : dst) {
gdi.addStringUT(0, L" " + c);
}
if (gdi.getCY() > ylimit) {
gdi.newColumn();
gdi.popY();
gdi.pushX();
}
}
gdi.refresh();
}
wstring QFClass::getQualInfo() const {
if (extraQualification == ExtraQualType::All)
return L"Alla övriga";
else if (extraQualification == ExtraQualType::NBest)
return L"X bästa#" + itow(extraQualData);
else if (extraQualification == ExtraQualType::TimeLimit)
return L"Tidsgräns X#" + formatTimeMS(extraQualData, false);
int nq = qualificationMap.size() + numTimeQualifications;
if (nq > 0 && numTimeQualifications == 0)
return L"X kvalificerade#" + itow(nq);
else if (nq > 0)
return L"X kvalificerade#" + itow(qualificationMap.size()) + L" + " + itow(numTimeQualifications);
return L"Ingen";
}
bool QualificationFinal::hasRemainingClass() const {
for (auto& c : classDefinition) {
if (c.extraQualification != QFClass::ExtraQualType::None)
return true;
}
return false;
}