/************************************************************************ MeOS - Orienteering Software Copyright (C) 2009-2020 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 "oEvent.h" #include "gdioutput.h" #include "meos_util.h" #include #include "Localizer.h" #include #include "gdifonts.h" #include "meosexception.h" #include "speakermonitor.h" #include "oListInfo.h" extern gdioutput *gdi_main; SpeakerMonitor::SpeakerMonitor(oEvent &oe) : oe(oe) { placeLimit = 0; numLimit = 0; maxClassNameWidth = 0; totalResults = false; classWidth = 0; timeWidth = 0; totWidth = 0; extraWidth = 0; } SpeakerMonitor::~SpeakerMonitor() { } void SpeakerMonitor::setClassFilter(const set &filter, const set &cfilter) { classFilter = filter; controlIdFilter = cfilter; oListInfo li; maxClassNameWidth = li.getMaxCharWidth(&oe, classFilter, lClassName, L"", normalText, false); } void SpeakerMonitor::setLimits(int place, int num) { placeLimit = place; numLimit = num; } bool orderResultsInTime(const oEvent::ResultEvent &a, const oEvent::ResultEvent &b) { if (a.time != b.time) return a.time > b.time; if (a.time > 0) { const wstring &na = a.r->getName(); const wstring &nb = b.r->getName(); return CompareString(LOCALE_USER_DEFAULT, 0, na.c_str(), na.length(), nb.c_str(), nb.length()) == CSTR_LESS_THAN; } return a.r->getId() < b.r->getId(); } void SpeakerMonitor::show(gdioutput &gdi) { classWidth = gdi.scaleLength(int(1.5*maxClassNameWidth)); timeWidth = gdi.scaleLength(60); totWidth = gdi.scaleLength(600); extraWidth = gdi.scaleLength(200); dash = makeDash(L"- "); oe.getResultEvents(classFilter, controlIdFilter, results); calculateResults(); orderedResults.resize(results.size()); for (size_t k = 0; k < results.size(); k++) { // Initialize forward map results[k].localIndex = k; orderedResults[k].r = results[k].r; orderedResults[k].totalTime = results[k].runTime; } // Sort sort(results.begin(), results.end(), orderResultsInTime); // Initialize map back for (size_t k = 0; k < results.size(); k++) { orderedResults[results[k].localIndex].eventIx = k; } for (size_t k = 0; k < results.size(); k++) { oEvent::ResultEvent &re(results[results.size()-(1+k)]); if (re.status == StatusOK) { map &dynLead = dynamicTotalLeaderTimes[ResultKey(re.classId(), re.control, re.leg())]; int totTime = re.runTime; if (dynLead.empty() || totTime < dynLead.rbegin()->second) dynLead[re.time] = totTime; } } int order = 0; for (size_t k = 0; k < results.size() && (order < numLimit || numLimit == 0); k++) { if (results[k].time > 0) { oEvent::ResultEvent *ahead = 0, *behind = 0; if (results[k].status == StatusOK) ahead = k > 0 && sameResultPoint(results[k], results[k-1]) ? &results[k-1] : 0; if (results[k].status == StatusOK) behind = (k + 1) < results.size() && sameResultPoint(results[k], results[k+1]) ? &results[k+1] : 0; renderResult(gdi, ahead, results[k], behind, order, true); } } for (size_t k = 0; k < results.size() && (order < numLimit || numLimit == 0); k++) { if (results[k].time == 0) renderResult(gdi, 0, results[k], 0, order, false); else break; } } bool SpeakerMonitor::sameResultPoint(const oEvent::ResultEvent &a, const oEvent::ResultEvent &b) { return a.control == b.control && a.classId() == b.classId() && a.leg() == b.leg(); } void SpeakerMonitor::renderResult(gdioutput &gdi, const oEvent::ResultEvent *ahead, const oEvent::ResultEvent &res, const oEvent::ResultEvent *behind, int &order, bool firstResults) { /*string s = itos(res.localIndex) + ": " + res.r->getName() + " control: " + itos(res.control) + " tid: " + formatTime(res.runTime); gdi.addStringUT(0, s); */ int finishLargePlaceLimit = 5; int radioLargePlaceLimit = 3; const bool showClass = classFilter.size() > 1; if (res.status == StatusOK) { if (res.place > placeLimit && res.r->getSpeakerPriority() == 0 ) { return; } } else if (order < max(2, numLimit/20)) { return; } else if (res.status == StatusNotCompetiting) { return; // DQ on ealier leg, for example } order++; const int extra = 5; int t = res.time; wstring msg = t>0 ? oe.getAbsTime(t) : makeDash(L"--:--:--"); pRunner r = res.r; RECT rc; rc.top = gdi.getCY(); rc.left = gdi.getCX(); int xp = rc.left + extra; int yp = rc.top + extra; bool largeFormat = res.status == StatusOK && ((res.control == oPunch::PunchFinish && res.place <= finishLargePlaceLimit) || res.place <= radioLargePlaceLimit); wstring message; deque details; getMessage(res,message, details); if (largeFormat) { gdi.addStringUT(yp, xp, 0, msg); int dx = 0; if (showClass) { pClass pc = res.r->getClassRef(true); if (pc) { gdi.addStringUT(yp, xp + timeWidth, fontMediumPlus, pc->getName()); dx = classWidth; } } wstring bib = r->getBib(); if (!bib.empty()) msg = bib + L", "; else msg = L""; msg += r->getCompleteIdentification(false); int xlimit = totWidth + extraWidth - (timeWidth + dx); gdi.addStringUT(yp, xp + timeWidth + dx, fontMediumPlus, msg, xlimit); yp += int(gdi.getLineHeight() * 1.5); msg = dash + lang.tl(message); gdi.addStringUT(yp, xp + 10, breakLines, msg, totWidth); for (size_t k = 0; k < details.size(); k++) { gdi.addStringUT(gdi.getCY(), xp + 20, 0, dash + lang.tl(details[k])).setColor(colorDarkGrey); } if (res.control == oPunch::PunchFinish && r->getStatus() == StatusOK) { splitAnalysis(gdi, xp + 20, gdi.getCY(), r); } GDICOLOR color = colorLightRed; if (res.control == oPunch::PunchFinish) { if (r && r->statusOK(true)) color = colorLightGreen; else color = colorLightRed; } else { color = colorLightCyan; } rc.bottom = gdi.getCY() + extra; rc.right = rc.left + totWidth + extraWidth + 2 * extra; gdi.addRectangle(rc, color, true); gdi.dropLine(0.5); } else { if (showClass) { pClass pc = res.r->getClassRef(true); if (pc) msg += L" (" + pc->getName() + L") "; else msg += L" "; } else msg += L" "; msg += r->getCompleteIdentification(false) + L" "; msg += lang.tl(message); gdi.addStringUT(gdi.getCY(), gdi.getCX(), breakLines, msg, totWidth); for (size_t k = 0; k < details.size(); k++) { gdi.addString("", gdi.getCY(), gdi.getCX()+50, 0, details[k]).setColor(colorDarkGrey); } if (res.control == oPunch::PunchFinish && r->getStatus() == StatusOK) { splitAnalysis(gdi, xp, gdi.getCY(), r); } gdi.dropLine(0.5); } } bool compareResult(const oEvent::ResultEvent &a, const oEvent::ResultEvent &b) { if (a.classId() != b.classId()) return a.classId() < b.classId(); if (a.control != b.control) return a.control < b.control; int lega = a.leg(); int legb = b.leg(); if (lega != legb) return legagetId() < b.r->getId(); } void SpeakerMonitor::calculateResults() { // TODO Result modules for (size_t k = 0; k < results.size(); k++) { results[k].runTime = results[k].r->getTotalRunningTime(results[k].time, true, totalResults); if (results[k].status == StatusOK && totalResults) results[k].status = results[k].r->getTotalStatus(); if (results[k].status == StatusOK) results[k].resultScore = results[k].runTime; else results[k].resultScore = RunnerStatusOrderMap[results[k].status] + 3600*24*7; } totalLeaderTimes.clear(); firstTimes.clear(); dynamicTotalLeaderTimes.clear(); runnerToTimeKey.clear(); timeToResultIx.clear(); sort(results.begin(), results.end(), compareResult); int clsId = -1; int ctrlId = -1; int legId = -1; int lastScore = 0; int place = 0; for (size_t k = 0; k < results.size(); k++) { if (results[k].partialCount > 0) continue; // Skip in result calculation int totTime = results[k].r->getTotalRunningTime(results[k].time, true, totalResults); assert(totTime == results[k].runTime); int leg = results[k].leg(); if (clsId != results[k].classId() || ctrlId != results[k].control || legId != leg) { clsId = results[k].classId(); ctrlId = results[k].control; legId = leg; place = 1; ResultKey key(clsId, ctrlId, leg); totalLeaderTimes[key] = totTime; } else if (lastScore != results[k].resultScore) place++; ResultKey key(clsId, ctrlId, leg); lastScore = results[k].resultScore; if (results[k].status == StatusOK) { results[k].place = place; runnerToTimeKey[results[k].r->getId()].push_back(ResultInfo(key, results[k].time, totTime)); if (results[k].time > 0) { int val = firstTimes[key]; firstTimes[key] = (val > 0 && val < results[k].time) ? val : results[k].time; } } else results[k].place = 0; } // Sort times for each runner for (map >::iterator it = runnerToTimeKey.begin(); it != runnerToTimeKey.end(); ++it) { sort(it->second.begin(), it->second.end(), timeSort); } } void SpeakerMonitor::handle(gdioutput &gdi, BaseInfo &info, GuiEventType type) { } void SpeakerMonitor::splitAnalysis(gdioutput &gdi, int xp, int yp, pRunner r) { vector delta; r->getSplitAnalysis(delta); wstring timeloss = lang.tl("Bommade kontroller: "); pCourse pc = 0; bool first = true; const int charlimit = 90; for (size_t j = 0; j 0) { if (pc == 0) { pc = r->getCourse(true); if (pc == 0) break; } if (!first) timeloss += L" | "; else first = false; timeloss += pc->getControlOrdinal(j) + L". " + formatTime(delta[j]); } if (timeloss.length() > charlimit || (!timeloss.empty() && !first && j+1 == delta.size())) { gdi.addStringUT(yp, xp, 0, timeloss).setColor(colorDarkRed); yp += gdi.getLineHeight(); timeloss = L""; } } if (first) { gdi.addString("", yp, xp, 0, "Inga bommar registrerade").setColor(colorDarkGreen); } } wstring getOrder(int k); wstring getNumber(int k); void getTimeAfterDetail(wstring &detail, int timeAfter, int deltaTime, bool wasAfter); wstring getTimeDesc(int t1, int t2) { int tb = abs(t1 - t2); wstring stime; if (tb == 1) stime = L"1" + lang.tl(L" sekund"); else if (tb <= 60) stime = itow(tb) + lang.tl(L" sekunder"); else stime = formatTime(tb); return stime; } void SpeakerMonitor::getMessage(const oEvent::ResultEvent &res, wstring &message, deque &details) { pClass cls = res.r->getClassRef(true); if (!cls) return; wstring &msg = message; int totTime = res.runTime; int leaderTime = getLeaderTime(res); int timeAfter = totTime - leaderTime; ResultKey key(res.classId(), res.control, res.leg()); vector &rResults = runnerToTimeKey[res.r->getId()]; ResultInfo *preRes = 0; pRunner preRunner = 0; for (size_t k = 0; k < rResults.size(); k++) { if (rResults[k] == key) { if (k > 0) { preRes = &rResults[k-1]; preRunner = res.r; } else { int leg = res.r->getLegNumber(); if (leg > 0 && res.r->getTeam()) { while (leg > 0) { pRunner pr = res.r->getTeam()->getRunner(--leg); // TODO: Pursuit if (pr && pr->prelStatusOK(false, false) && pr->getFinishTime() == res.r->getStartTime()) { vector &rResultsP = runnerToTimeKey[pr->getId()]; if (!rResultsP.empty()) { preRes = &rResultsP.back(); preRunner = pr; } break; } } } } } } if (res.status != StatusOK) { if (res.status != StatusDQ) msg = L"är inte godkänd."; else msg = L"är diskvalificerad."; return; } bool hasPrevRes = false; int deltaTime = 0; const oEvent::ResultEvent *ahead = getAdjacentResult(res, -1); const oEvent::ResultEvent *behind = getAdjacentResult(res, +1); if (preRes != 0) { int prevLeader = getDynamicLeaderTime(*preRes, preRes->time); int prevAfter = preRes->totTime - prevLeader; // May be negative (for leader) int after = totTime - getDynamicLeaderTime(res, res.time); hasPrevRes = totTime > 0 && preRes->totTime > 0; if (after == 0) { // Takes (and holds) the lead in a relay (for example) if (!behind) { hasPrevRes = false; } else { after = totTime - behind->runTime; // A negative number. Time ahead. } } if (prevAfter == 0) { // Took (and holds) the lead int rix = getResultIx(*preRes); const oEvent::ResultEvent *prevBehind = 0; if (rix != -1) prevBehind = getAdjacentResult(results[rix], +1); if (!prevBehind) { hasPrevRes = false; } else { prevAfter = preRes->totTime - prevBehind->runTime; // A negative number. Time ahead } } deltaTime = after - prevAfter; // Positive -> more after. } wstring timeS = formatTime(res.runTime); wstring detail; const wstring *cname = 0; if (timeAfter > 0 && ahead) cname = &ahead->r->getName(); else if (timeAfter < 0 && behind) cname = &behind->r->getName(); int ta = timeAfter; if (res.place == 1 && behind && behind->status == StatusOK) ta = res.runTime - behind->runTime; getTimeAfterDetail(detail, ta, deltaTime, hasPrevRes); if (!detail.empty()) details.push_back(detail); pCourse crs = res.r->getCourse(false); wstring location, locverb, thelocation; bool finish = res.control == oPunch::PunchFinish; bool changeover = false; int leg = res.r->getLegNumber(); int numstage = cls->getNumStages(); if (finish && res.r->getTeam() != 0 && (leg + 1) < numstage) { for (int k = leg + 1; k < numstage; k++) if (!cls->isParallel(k) && !cls->isOptional(k)) { finish = false; changeover = true; break; } } if (finish) { location = L"i mål"; locverb = L"går i mål"; thelocation = lang.tl(L"målet") + L"#"; } else if (changeover) { location = L"vid växeln"; locverb = L"växlar"; thelocation = lang.tl(L"växeln") + L"#"; } else { thelocation = crs ? (crs->getRadioName(res.control) + L"#") : L"#"; } bool sharedPlace = (ahead && ahead->place == res.place) || (behind && behind->place == res.place); if (finish || changeover) { if (res.place == 1) { if ((ahead == 0 && behind == 0) || firstTimes[key] == res.time) msg = L"är först " + location + L" med tiden X.#" + timeS; else if (!sharedPlace) { msg = L"tar ledningen med tiden X.#" + timeS; if (behind && res.place>2) { wstring stime(getTimeDesc(behind->runTime, totTime)); details.push_back(L"är X före Y#" + stime + L"#" + behind->r->getCompleteIdentification(false)); } } else msg = L"går upp i delad ledning med tiden X.#" + timeS; } else { if (firstTimes[key] == res.time) { msg = L"var först " + location + L" med tiden X.#" + timeS; if (!sharedPlace) { details.push_front(L"är nu på X plats med tiden Y.#" + getOrder(res.place) + L"#" + timeS); if (ahead && res.place != 2) { wstring stime(getTimeDesc(ahead->runTime, totTime)); details.push_back(L"är X efter Y#" + stime + L"#" + ahead->r->getCompleteIdentification(false)); } } else { details.push_back(L"är nu på delad X plats med tiden Y.#" + getOrder(res.place) + L"#" + timeS); wstring share; getSharedResult(res, share); details.push_back(share); } } else if (!sharedPlace) { msg = locverb + L" på X plats med tiden Y.#" + getOrder(res.place) + L"#" + timeS; if (ahead && res.place != 2) { wstring stime(getTimeDesc(ahead->runTime, totTime)); details.push_back(L"är X efter Y#" + stime + L"#" + ahead->r->getCompleteIdentification(false)); } } else { msg = locverb + L" på delad X plats med tiden Y.#" + getOrder(res.place) + L"#" + timeS; wstring share; getSharedResult(res, share); details.push_back(share); } } if (changeover && res.r->getTeam() && res.r->getTeam()->getClassRef(true) != 0) { wstring vxl = L"skickar ut X.#"; pTeam t = res.r->getTeam(); pClass cls = t->getClassRef(true); bool second = false; int nextLeg = cls->getNextBaseLeg(res.r->getLegNumber()); if (nextLeg > 0) { for (int k = nextLeg; k < t->getNumRunners(); k++) { pRunner r = t->getRunner(k); if (!r && !vxl.empty()) continue; if (second) vxl += L", "; second = true; vxl += r ? r->getName() : L"?"; if (!(cls->isParallel(k) || cls->isOptional(k))) break; } } details.push_front(vxl); } } else { if (res.place == 1) { if ((ahead == 0 && behind == 0) || res.time == firstTimes[key]) msg = L"är först vid X med tiden Y.#" + thelocation + timeS; else if (!sharedPlace) { msg = L"tar ledningen vid X med tiden Y.#" + thelocation + timeS; if (behind) { wstring stime(getTimeDesc(behind->runTime, totTime)); details.push_back(L"är X före Y#" + stime + L"#" + behind->r->getCompleteIdentification(false)); } } else msg = L"går upp i delad ledning vid X med tiden Y.#" + thelocation + timeS; } else { if (firstTimes[key] == res.time) { msg = L"var först vid X med tiden Y.#" + thelocation + timeS; if (!sharedPlace) { details.push_front(L"är nu på X plats med tiden Y.#" + getOrder(res.place) + L"#" + timeS); if (ahead) { wstring stime(getTimeDesc(ahead->runTime, totTime)); details.push_back(L"är X efter Y#" + stime + L"#" + ahead->r->getCompleteIdentification(false)); } } else { details.push_back(L"är nu på delad X plats med tiden Y.#" + getOrder(res.place) + L"#" + timeS); wstring share; getSharedResult(res, share); details.push_back(share); } } else if (!sharedPlace) { msg = L"stämplar vid X som Y, på tiden Z.#" + thelocation + getNumber(res.place) + L"#" + timeS; if (ahead && res.place>2) { wstring stime(getTimeDesc(ahead->runTime, totTime)); details.push_back(L"är X efter Y#" + stime + L"#" + ahead->r->getCompleteIdentification(false)); } } else { msg = L"stämplar vid X som delad Y med tiden Z.#" + thelocation + getNumber(res.place) + L"#" + timeS; wstring share; getSharedResult(res, share); details.push_back(share); } } } return; } void SpeakerMonitor::getSharedResult(const oEvent::ResultEvent &res, wstring &detail) const { vector shared; getSharedResult(res, shared); if (!shared.empty()) detail = L"delar placering med X.#"; else detail = L""; for (size_t k = 0; k < shared.size(); k++) { if (k > 0) detail += L", "; detail += shared[k]->getCompleteIdentification(false); } } void SpeakerMonitor::getSharedResult(const oEvent::ResultEvent &res, vector &shared) const { shared.clear(); if (res.status != StatusOK) return; int p = 1; const oEvent::ResultEvent * ar;; while ( (ar = getAdjacentResult(res, p++)) != 0 && ar->place == res.place) { shared.push_back(ar->r); } p = 1; while ( (ar = getAdjacentResult(res, -(p++))) != 0 && ar->place == res.place) { shared.push_back(ar->r); } } const oEvent::ResultEvent *SpeakerMonitor::getAdjacentResult(const oEvent::ResultEvent &res, int delta) const { while (true) { int orderIx = res.localIndex + delta; if (orderIx < 0 || orderIx >= int(orderedResults.size()) ) return 0; int aIx = orderedResults[orderIx].eventIx; if (!sameResultPoint(res, results[aIx])) return 0; if (results[aIx].partialCount == 0) return &results[aIx]; if (delta < 0) delta--; else delta++; } } int SpeakerMonitor::getLeaderTime(const oEvent::ResultEvent &res) { return totalLeaderTimes[ResultKey(res.classId(), res.control, res.leg())]; } int SpeakerMonitor::getDynamicLeaderTime(const oEvent::ResultEvent &res, int time) { return getDynamicLeaderTime(ResultKey(res.classId(), res.control, res.leg()), time); } int SpeakerMonitor::getDynamicLeaderTime(const ResultKey &res, int time) { map &leaderMap = dynamicTotalLeaderTimes[res]; map::iterator g = leaderMap.lower_bound(time); if (leaderMap.empty()) return 0; if (g != leaderMap.end()) { if (g != leaderMap.begin()) --g; return g->second; } else return leaderMap.rbegin()->second; } int SpeakerMonitor::getResultIx(const ResultInfo &rinfo) { if (timeToResultIx.empty()) { for (size_t k = 0; k < results.size(); k++) { timeToResultIx.insert(make_pair(results[k].time, k)); } } pair::iterator, multimap::iterator> range = timeToResultIx.equal_range(rinfo.time); while (range.first != range.second) { int ix = range.first->second; if (results[ix].classId() == rinfo.classId && results[ix].leg() == rinfo.leg && results[ix].control == rinfo.radio) { return ix; } ++range.first; } return -1; }