/************************************************************************ 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 . 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 #include #include "xmlparser.h" #include "oRunner.h" #include "localizer.h" pair QualificationFinal::getPrelFinalFromPlace(int instance, int orderPlace, int numSharedPlaceNext) { pair 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 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; return classDefinition[instance].qualificationMap.empty() && classDefinition[instance].numTimeQualifications == 0 && classDefinition[instance].extraQualification == QFClass::ExtraQualType::None; } void QualificationFinal::getBaseClassInstances(set &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 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> 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 idToIndex; map > 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 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& 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 &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 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> timeToIx; set> remainingTimeQualified; int lastLevel = -1; vector 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 placeCount(classDefinition.size(), 0); if (levels[level] == LevelInfo::RankSort) { vector> rankIx; vector> placeQueue; set 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 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 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 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 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 dst; 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 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; }