/************************************************************************ 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 "meos_util.h" #include "infoserver.h" #include "xmlparser.h" #include "oEvent.h" #include "download.h" #include "progress.h" #include "meosException.h" #include "gdioutput.h" void base64_encode(const vector &input, string &output); extern gdioutput *gdi_main; // Encode a vector vector int {{1}, {1,2,3}, {}, {4,5}} as "1;1,2,3;;4,5" static void packIntInt(const vector< vector > &v, wstring &def) { def = L""; for (size_t j = 0; j < v.size(); j++) { if (j>0) def += L";"; for (size_t k = 0; k < v[j].size(); k++) { if (k>0) def += L","; def += itow(v[j][k]); } } } // Encode a vector vector int {1,2,3} as "1,2,3" static void packInt(const vector &v, wstring &def) { def = L""; for (size_t j = 0; j < v.size(); j++) { if (j>0) def += L","; def += itow(v[j]); } } InfoBase::InfoBase(int idIn) : id(idIn), committed(false){ } InfoBase::InfoBase(const InfoBase &src) : id(src.id), committed(src.committed){ } void InfoBase::operator=(const InfoBase &src) { } InfoBase::~InfoBase() { } int InfoBase::convertRelativeTime(const oBase &elem, int t) { return t+elem.getEvent()->getZeroTimeNum(); } InfoCompetition::InfoCompetition(int id) : InfoBase(id) { forceComplete = true; includeTotal = false; } InfoRadioControl::InfoRadioControl(int id) : InfoBase(id) { } InfoClass::InfoClass(int id) : InfoBase(id) { } InfoMeosStatus::InfoMeosStatus() : InfoBase(0) { } InfoOrganization::InfoOrganization(int id) : InfoBase(id) { } InfoBaseCompetitor::InfoBaseCompetitor(int id) : InfoBase(id) { organizationId = 0; classId = 0; status = 0; startTime = 0; runningTime = 0; } InfoCompetitor::InfoCompetitor(int id) : InfoBaseCompetitor(id) { totalStatus = 0; inputTime = 0; course = 0; } InfoTeam::InfoTeam(int id) : InfoBaseCompetitor(id) { } bool InfoCompetition::synchronize(oEvent &oe, bool onlyCmp, const set &includeCls, const set &ctrls, bool allowDeletion) { bool changed = false; if (oe.getName() != name) { name = oe.getName(); changed = true; } if (oe.getDate() != date) { date = oe.getDate(); changed = true; } if (oe.getDCI().getString("Organizer") != organizer) { organizer = oe.getDCI().getString("Organizer"); changed = true; } if (oe.getDCI().getString("Homepage") != homepage) { homepage = oe.getDCI().getString("Homepage"); changed = true; } if (changed) needCommit(*this); if (onlyCmp) return changed; vector ctrl; oe.getControls(ctrl, true); set knownId; for (size_t k = 0; k < ctrl.size(); k++) { vector ids; ctrl[k]->getCourseControls(ids); for (size_t j = 0; j < ids.size(); j++) { int wid = ids[j]; if (!ctrls.count(ids[j])) continue; knownId.insert(wid); map::iterator res = controls.find(wid); if (res == controls.end()) res = controls.insert(make_pair(wid, InfoRadioControl(wid))).first; if (res->second.synchronize(*ctrl[k], ids.size() > 1 ? j+1 : 0)) needCommit(res->second); } } // Check if something was deleted for (map::iterator it = controls.begin(); it != controls.end();) { if (!knownId.count(it->first)) { controls.erase(it++); forceComplete = true; } else ++it; } knownId.clear(); vector cls; oe.getClasses(cls, false); for (size_t k = 0; k < cls.size(); k++) { if (cls[k]->getQualificationFinal()) continue; int wid = cls[k]->getId(); if (!includeCls.count(wid)) continue; knownId.insert(wid); map::iterator res = classes.find(wid); if (res == classes.end()) res = classes.insert(make_pair(wid, InfoClass(wid))).first; if (res->second.synchronize(withCourse, *cls[k], ctrls)) needCommit(res->second); } // Check if something was deleted for (map::iterator it = classes.begin(); it != classes.end();) { if (!knownId.count(it->first)) { classes.erase(it++); forceComplete = true; } else ++it; } knownId.clear(); vector clb; oe.getClubs(clb, false); for (size_t k = 0; k < clb.size(); k++) { int wid = clb[k]->getId(); knownId.insert(wid); map::iterator res = organizations.find(wid); if (res == organizations.end()) res = organizations.insert(make_pair(wid, InfoOrganization(wid))).first; if (res->second.synchronize(*clb[k])) needCommit(res->second); } // Check if something was deleted for (map::iterator it = organizations.begin(); it != organizations.end();) { if (!knownId.count(it->first)) { int oid = it->first; organizations.erase(it++); if (allowDeletion) deleteMap.emplace_back("org", oid); else forceComplete = true; } else ++it; } knownId.clear(); vector t; oe.getTeams(0, t, false); for (size_t k = 0; k < t.size(); k++) { int cid = t[k]->getClassId(true); if (!includeCls.count(cid)) continue; if (cid != 0 && t[k]->getClassRef(false)->getQualificationFinal() != nullptr) continue; int wid = t[k]->getId(); knownId.insert(wid); map::iterator res = teams.find(wid); if (res == teams.end()) res = teams.insert(make_pair(wid, InfoTeam(wid))).first; if (res->second.synchronize(*t[k])) needCommit(res->second); } // Check if something was deleted for (map::iterator it = teams.begin(); it != teams.end();) { if (!knownId.count(it->first)) { int tid = it->first; teams.erase(it++); if (allowDeletion) deleteMap.emplace_back("tm", tid); else forceComplete = true; } else ++it; } knownId.clear(); vector r; oe.getRunners(0, 0, r, false); for (size_t k = 0; k < r.size(); k++) { int cid = r[k]->getClassId(true); if (!includeCls.count(cid)) continue; if (cid != 0 && r[k]->getClassRef(true)->getQualificationFinal() != nullptr) continue; int wid = r[k]->getId(); knownId.insert(wid); map::iterator res = competitors.find(wid); if (res == competitors.end()) res = competitors.insert(make_pair(wid, InfoCompetitor(wid))).first; if (res->second.synchronize(*this, *r[k])) needCommit(res->second); } // Check if something was deleted for (map::iterator it = competitors.begin(); it != competitors.end();) { if (!knownId.count(it->first)) { int rid = it->first; competitors.erase(it++); if (allowDeletion) deleteMap.emplace_back("cmp", rid); else forceComplete = true; } else ++it; } knownId.clear(); return !toCommit.empty() || forceComplete || !deleteMap.empty(); } void InfoCompetition::needCommit(InfoBase &obj) { toCommit.push_back(&obj); } bool InfoRadioControl::synchronize(oControl &c, int number) { wstring n = c.hasName() ? c.getName() : c.getString(); if (number > 0) n = n + L"-" + itow(number); if (n == name) return false; else { name = n; modified(); } return true; } void InfoRadioControl::serialize(xmlbuffer &xml, bool diffOnly) const { vector< pair > prop; prop.push_back(make_pair("id", itow(getId()))); xml.write("ctrl", prop, name); } bool InfoClass::synchronize(bool includeCourses, oClass &c, const set &ctrls) { const wstring &n = c.getName(); int no = c.getSortIndex(); bool mod = false; vector< vector > rc; size_t s = c.getNumStages(); if (includeCourses) { set crsSet; for (size_t i = 0; i <= s; i++) { vector crs; c.getCourses(i, crs); for (pCourse pc : crs) crsSet.insert(pc->getId()); } vector newCrs(crsSet.begin(), crsSet.end()); if (newCrs != courses) { courses = newCrs; mod = true; } } if (s > 0) { linearLegNumberToActual.clear(); for (size_t k = 0; k < s; k++) { if (!c.isParallel(k) && !c.isOptional(k)) { pCourse pc = c.getCourse(k, 0, true); // Get a course representative for the leg. rc.push_back(vector()); if (pc) { vector ctrl; pc->getControls(ctrl); for (size_t j = 0; j < ctrl.size(); j++) { if (ctrls.count(pc->getCourseControlId(j))) { rc.back().push_back(pc->getCourseControlId(j)); } } } } // Setup transformation map (flat to 2D) linearLegNumberToActual.push_back(max(0, rc.size()-1)); } } else { // Single stage linearLegNumberToActual.resize(1, 0); pCourse pc = c.getCourse(true); // Get a course representative for the leg. rc.push_back(vector()); if (pc) { vector ctrl; pc->getControls(ctrl); for (size_t j = 0; j < ctrl.size(); j++) { if (ctrls.count(pc->getCourseControlId(j))) { rc.back().push_back(pc->getCourseControlId(j)); } } } } if (radioControls != rc) { radioControls = rc; mod = true; } if (n != name || no != sortOrder) { name = n; sortOrder = no; mod = true; } if (mod) modified(); return mod; } void InfoClass::serialize(xmlbuffer &xml, bool diffOnly) const { vector< pair > prop; prop.push_back(make_pair("id", itow(getId()))); prop.push_back(make_pair("ord", itow(sortOrder))); wstring def; packIntInt(radioControls, def); prop.push_back(make_pair("radio", def)); if (courses.size() > 0) { packInt(courses, def); prop.push_back(make_pair("crs", def)); } xml.write("cls", prop, name); } void InfoMeosStatus::setOnDatabase(const bool flag) { onDatabase = flag; } void InfoMeosStatus::setEventNameId(const wstring & str) { eventNameId = str; } void InfoMeosStatus::serialize(xmlbuffer &xml, bool diffOnly) const { vector> prop; prop.push_back(make_pair("version", getMeosCompectVersion())); prop.push_back(make_pair("eventNameId", eventNameId)); prop.push_back(make_pair("onDatabase", itow(onDatabase))); // 1 is true, 0 is false xml.write("status", prop, L""); } bool InfoOrganization::synchronize(oClub &c) { const wstring &n = c.getDisplayName(); const wstring &nat = c.getDCI().getString("Nationality"); if (n == name && nat == nationality) return false; else { nationality = nat; name = n; modified(); } return true; } void InfoOrganization::serialize(xmlbuffer &xml, bool diffOnly) const { vector< pair > prop; prop.push_back(make_pair("id", itow(getId()))); if (!nationality.empty()) prop.emplace_back("nat", nationality); xml.write("org", prop, name); } void InfoCompetition::serialize(xmlbuffer &xml, bool diffOnly) const { vector< pair > prop; prop.push_back(make_pair("date", date)); prop.push_back(make_pair("organizer", organizer)); prop.push_back(make_pair("homepage", homepage)); xml.write("competition", prop, name); } void InfoBaseCompetitor::serialize(xmlbuffer &xml, bool diffOnly, int course) const { vector< pair > prop; prop.reserve(10); prop.emplace_back("org", itow(organizationId)); prop.emplace_back("cls", itow(classId)); prop.emplace_back("stat", itow(status)); prop.emplace_back("st", itow(startTime)); prop.emplace_back("rt", itow(runningTime)); if (course != 0) prop.emplace_back("crs", itow(course)); if (!bib.empty()) prop.emplace_back("bib", bib); if (!nationality.empty()) prop.emplace_back("nat", nationality); xml.write("base", prop, name); } bool InfoBaseCompetitor::synchronizeBase(oAbstractRunner &bc) { const wstring &n = bc.getName(); bool ch = false; if (n != name) { name = n; ch = true; } int cid = bc.getClubId(); if (cid != organizationId) { organizationId = cid; ch = true; } int cls = bc.getClassId(true); if (cls != classId) { classId = cls; ch = true; } const wstring &nat = bc.getDCI().getString("Nationality"); if (nat != nationality) { nationality = nat; ch = true; } RunnerStatus s = bc.getStatusComputed(true); int rt = bc.getRunningTime(true) * (10/timeConstSecond); if (rt > 0) { if (s == RunnerStatus::StatusUnknown) s = RunnerStatus::StatusOK; if (s == RunnerStatus::StatusNoTiming) rt = 0; } else if (isPossibleResultStatus(s)) s = StatusUnknown; if (status != s) { status = s; ch = true; } int st = -1; if (bc.startTimeAvailable()) st = convertRelativeTime(bc, bc.getStartTime()) * (10 / timeConstSecond); if (st != startTime) { startTime = st; ch = true; } if (rt != runningTime) { runningTime = rt; ch = true; } wstring newBib = bc.getBib(); if (bib != newBib) { bib = newBib; ch = true; } return ch; } bool InfoCompetitor::synchronize(bool useTotalResults, bool useCourse, oRunner &r) { bool ch = synchronizeBase(r); bool isQF = r.getClassRef(false) && r.getClassRef(false)->getQualificationFinal() != nullptr; changeTotalSt = r.getEvent()->hasPrevStage() || (r.getLegNumber()>0 && !isQF); // Always write full attributes int s = StatusOK; int legInput = 0; int oldCourse = course; if (useCourse) { auto crs = r.getCourse(false); course = crs ? crs->getId() : 0; } else { course = 0; } if (oldCourse != course) ch = true; pTeam t = r.getTeam(); if (useTotalResults) { legInput = r.getTotalTimeInput() * (10 / timeConstSecond); s = r.getTotalStatusInput(); } else if (t && !isQF && r.getLegNumber() > 0) { int ltu = r.getLegNumber(); pClass cls = t->getClassRef(true); if (cls) { LegTypes lt = cls->getLegType(ltu); while (ltu > 0 && (lt == LTParallelOptional || lt == LTParallel|| lt == LTExtra || lt == LTIgnore) ) { ltu--; lt = cls->getLegType(ltu); } } if (ltu > 0) { legInput = t->getLegRunningTime(ltu - 1, true, false) * (10 / timeConstSecond); s = t->getLegStatus(ltu - 1, true, false); } } if (totalStatus != s) { totalStatus = s; ch = true; changeTotalSt = true; } if (legInput != inputTime) { inputTime = legInput; ch = true; changeTotalSt = true; } return ch; } bool InfoCompetitor::synchronize(const InfoCompetition &cmp, oRunner &r) { bool useTotalResults = cmp.includeTotalResults(); bool inludeCourse = cmp.includeCourse(); bool ch = synchronize(useTotalResults, inludeCourse, r); int cno = r.getCardNo(); if (cno != cardNo) { cardNo = cno; changeCard = true; ch = true; } bool nr; if ((r.getStatus() == StatusUnknown || isPossibleResultStatus(r.getStatus())) && r.getFinishTime() <= 0) { vector pv; r.getEvent()->getPunchesForRunner(r.getId(), false, pv); nr = pv.size() > 0; } else { nr = false; } if (isRunning != nr) { isRunning = nr; ch = true; } vector newRT; if (r.getClassId(false) > 0 && r.getStatusComputed(true) != RunnerStatus::StatusNoTiming) { const vector &radios = cmp.getControls(r.getClassId(true), r.getLegNumber()); for (size_t k = 0; k < radios.size(); k++) { RadioTime radioTime; RunnerStatus s_split; radioTime.radioId = radios[k]; r.getSplitTime(radioTime.radioId, s_split, radioTime.runningTime); if (radioTime.runningTime > 0) { radioTime.runningTime *= (10 / timeConstSecond); newRT.push_back(radioTime); } } } changeRadio = radioTimes.size() > 0; // Always write full attributes if (newRT != radioTimes) { ch = true; changeRadio = true; radioTimes.swap(newRT); } if (ch) modified(); return ch; } void InfoCompetitor::serialize(xmlbuffer &xml, bool diffOnly) const { vector< pair > sprop; sprop.reserve(3); sprop.emplace_back("id", itow(getId())); if (changeCard || !diffOnly) { sprop.emplace_back("card", itow(cardNo)); changeCard = false; } if (isRunning) sprop.emplace_back("competing", L"true"); xmlbuffer &subTag = xml.startTag("cmp", sprop); InfoBaseCompetitor::serialize(subTag, diffOnly, course); if (radioTimes.size() > 0 && (!diffOnly || changeRadio)) { string radio; radio.reserve(radioTimes.size() * 12); for (size_t k = 0; k < radioTimes.size(); k++) { if (k>0) radio+=";"; radio+=itos(radioTimes[k].radioId); radio+=","; radio+=itos(radioTimes[k].runningTime); } vector< pair > eprop; subTag.write("radio", eprop, radio); } if (!diffOnly || changeTotalSt) { vector< pair > prop; prop.push_back(make_pair("it", itos(inputTime))); prop.push_back(make_pair("tstat", itos(totalStatus))); subTag.write("input", prop, ""); } xml.endTag(); } bool InfoTeam::synchronize(oTeam &t) { bool ch = synchronizeBase(t); const pClass cls = t.getClassRef(true); if (cls) { vector< vector > r; size_t s = cls->getNumStages(); for (size_t k = 0; k < s; k++) { pRunner rr = t.getRunner(k); int rid = rr != 0 ? rr->getId() : 0; if (cls->isParallel(k) || cls->isOptional(k)) { if (r.empty()) r.push_back(vector()); // This is not a valid case, really r.back().push_back(rid); } else r.push_back(vector(1, rid)); } if (r != competitors) { r.swap(competitors); ch = true; } } if (ch) modified(); return ch; } void InfoTeam::serialize(xmlbuffer &xml, bool diffOnly) const { vector< pair > prop; prop.push_back(make_pair("id", itow(getId()))); xmlbuffer &sub = xml.startTag("tm", prop); InfoBaseCompetitor::serialize(sub, diffOnly, 0); wstring def; packIntInt(competitors, def); prop.clear(); sub.write("r", prop, def); sub.endTag(); } const vector &InfoCompetition::getControls(int classId, int legNumber) const { map::const_iterator res = classes.find(classId); if (res != classes.end()) { if (size_t(legNumber) < res->second.linearLegNumberToActual.size()) legNumber = res->second.linearLegNumberToActual[legNumber]; else legNumber = 0; const vector< vector > &c = res->second.radioControls; if (size_t(legNumber) < c.size()) return c[legNumber]; } throw meosException("Internal class definition error"); } void InfoCompetition::getCompleteXML(xmlbuffer &xml) { xml.setComplete(true); serialize(xml, false); for(map::iterator it = controls.begin(); it != controls.end(); ++it) { it->second.serialize(xml, false); } for(map::iterator it = classes.begin(); it != classes.end(); ++it) { it->second.serialize(xml, false); } for(map::iterator it = organizations.begin(); it != organizations.end(); ++it) { it->second.serialize(xml, false); } for(map::iterator it = teams.begin(); it != teams.end(); ++it) { it->second.serialize(xml, false); } for(map::iterator it = competitors.begin(); it != competitors.end(); ++it) { it->second.serialize(xml, false); } } void InfoCompetition::getDiffXML(xmlbuffer &xml) { if (forceComplete) { getCompleteXML(xml); return; } xml.setComplete(false); vector> prop = { make_pair("id", L""), make_pair("delete", L"true") }; for (auto &dm : deleteMap) { prop[0].second = itow(dm.second); xml.startTag(dm.first.c_str(), prop); xml.endTag(); } for (list::iterator it = toCommit.begin(); it != toCommit.end(); ++it) { (*it)->serialize(xml, true); } } void InfoCompetition::commitComplete() { deleteMap.clear(); toCommit.clear(); forceComplete = false; } static char encoding_table[] = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'}; static int mod_table[] = {0, 2, 1}; void base64_encode(const vector &input, string &encoded_data) { size_t input_length = input.size(); size_t output_length = 4 * ((input_length + 2) / 3); encoded_data.resize(output_length); for (size_t i = 0, j = 0; i < input_length;) { unsigned octet_a = i < input_length ? input[i++] : 0; unsigned octet_b = i < input_length ? input[i++] : 0; unsigned octet_c = i < input_length ? input[i++] : 0; unsigned triple = (octet_a << 0x10) + (octet_b << 0x08) + octet_c; encoded_data[j++] = encoding_table[(triple >> 3 * 6) & 0x3F]; encoded_data[j++] = encoding_table[(triple >> 2 * 6) & 0x3F]; encoded_data[j++] = encoding_table[(triple >> 1 * 6) & 0x3F]; encoded_data[j++] = encoding_table[(triple >> 0 * 6) & 0x3F]; } for (int i = 0; i < mod_table[input_length % 3]; i++) encoded_data[output_length - 1 - i] = '='; } xmlbuffer &xmlbuffer::startTag(const char *tag, const vector< pair > &prop) { blocks.push_back(block()); blocks.back().tag = tag; blocks.back().prop = prop; blocks.back().subValues.push_back(xmlbuffer()); return blocks.back().subValues.back(); } void xmlbuffer::endTag() { } void xmlbuffer::write(const char *tag, const vector< pair > &prop, const string &value) { blocks.push_back(block()); blocks.back().tag = tag; for (size_t k = 0; k < prop.size(); k++) blocks.back().prop.push_back(make_pair(prop[k].first, gdi_main->widen(prop[k].second))); blocks.back().value = gdi_main->widen(value); } void xmlbuffer::write(const char *tag, const vector< pair > &prop, const wstring &value) { blocks.push_back(block()); blocks.back().tag = tag; blocks.back().prop = prop; blocks.back().value = value; } void xmlbuffer::startXML(xmlparser &xml, const wstring &dest) { xml.openOutput(dest.c_str(), false); if (complete) { xml.startTag("MOPComplete", "xmlns", "http://www.melin.nu/mop"); complete = false; } else xml.startTag("MOPDiff", "xmlns", "http://www.melin.nu/mop"); } bool xmlbuffer::commit(xmlparser &xml, int count) { vector p2; while (count>0 && !blocks.empty()) { block &block = blocks.front(); if (block.subValues.empty()) { xml.write(block.tag.c_str(), block.prop, block.value); } else { if (block.prop.size() > 1) { p2.resize(block.prop.size() * 2); for (size_t k = 0; k < block.prop.size(); k++) { p2[k * 2] = gdi_main->widen(block.prop[k].first); p2[k * 2 + 1] = std::move(block.prop[k].second); } xml.startTag(block.tag.c_str(), p2); } else if (block.prop.size() == 1) { xml.startTag(block.tag.c_str(), block.prop[0].first.c_str(), block.prop[0].second); } else if (block.prop.empty()) { xml.startTag(block.tag.c_str()); } for (size_t k = 0; k < block.subValues.size(); k++) block.subValues[k].commit(xml, numeric_limits::max()); xml.endTag(); } count--; blocks.pop_front(); } return !blocks.empty(); } void xmlbuffer::commitCopy(xmlparser &xml) { vector p2; for (block &block : blocks) { if (block.subValues.empty()) { xml.write(block.tag.c_str(), block.prop, block.value); } else { if (block.prop.size() > 1) { p2.resize(block.prop.size() * 2); for (size_t k = 0; k < block.prop.size(); k++) { p2[k * 2] = gdi_main->widen(block.prop[k].first); p2[k * 2 + 1] = block.prop[k].second; } xml.startTag(block.tag.c_str(), p2); } else if (block.prop.size() == 1) { xml.startTag(block.tag.c_str(), block.prop[0].first.c_str(), block.prop[0].second); } else if (block.prop.empty()) { xml.startTag(block.tag.c_str()); } for (size_t k = 0; k < block.subValues.size(); k++) block.subValues[k].commitCopy(xml); xml.endTag(); } } }