From 5e242c81c08d2e1a22618ec2130d811e1ab8aad3 Mon Sep 17 00:00:00 2001 From: Erik Melin <31467290+erikmelin@users.noreply.github.com> Date: Sat, 1 Aug 2020 22:55:57 +0200 Subject: [PATCH] MeOS version 3.7SD.1205 RC1 --- code/RunnerDB.cpp | 2 +- code/RunnerDB.h | 1 + code/TabClass.cpp | 299 +++++++- code/TabClass.h | 7 +- code/TabCompetition.cpp | 238 +++++- code/TabCompetition.h | 7 +- code/TabCourse.cpp | 2 +- code/TimeStamp.cpp | 23 + code/TimeStamp.h | 4 +- code/csvparser.cpp | 2 +- code/english.lng | 28 +- code/gdioutput.h | 3 + code/iof30interface.cpp | 48 +- code/iof30interface.h | 10 + code/meos.cpp | 7 +- code/meosdb/MeosSQL.cpp | 163 +++-- code/meosdb/MeosSQL.h | 1 + code/meosvc15.vcxproj | 1 + code/meosversion.cpp | 11 +- code/oBase.cpp | 25 + code/oBase.h | 9 +- code/oCard.cpp | 18 +- code/oCard.h | 5 +- code/oClass.cpp | 10 +- code/oClass.h | 2 + code/oClub.cpp | 4 +- code/oClub.h | 2 + code/oControl.cpp | 2 +- code/oControl.h | 2 + code/oCourse.cpp | 9 +- code/oCourse.h | 4 +- code/oEvent.cpp | 841 +++------------------- code/oEvent.h | 37 +- code/oEventDraw.cpp | 917 +++++++++++++++++++++--- code/oEventDraw.h | 5 +- code/oEventResult.cpp | 10 +- code/oFreePunch.cpp | 6 +- code/oFreePunch.h | 2 + code/oImportExport.cpp | 4 +- code/oPunch.h | 3 + code/oRunner.cpp | 2 +- code/oRunner.h | 17 +- code/oTeam.cpp | 18 +- code/oTeam.h | 10 +- code/oTeamEvent.cpp | 61 +- code/oevent_transfer.cpp | 1473 ++++++++++++++++++++++++++++++++++++++ code/swedish.lng | 26 + 47 files changed, 3323 insertions(+), 1058 deletions(-) create mode 100644 code/oevent_transfer.cpp diff --git a/code/RunnerDB.cpp b/code/RunnerDB.cpp index b5259de..8314d08 100644 --- a/code/RunnerDB.cpp +++ b/code/RunnerDB.cpp @@ -1211,7 +1211,7 @@ void RunnerDB::generateRunnerTableData(Table &table, oDBRunnerEntry *addEntry) table.reserve(rdb.size()); oRDB.resize(rdb.size(), oDBRunnerEntry(oe)); - for (size_t k = 0; kgetStartGroups(true).size() > 0) { + gdi.addButton("DrawStartGroups", "Lotta med startgrupper", ClassesCB); + gdi.popX(); + gdi.dropLine(3); + }*/ gdi.addButton("DrawAll", "Manuell lottning", ClassesCB).setExtra(1); gdi.addButton("Simultaneous", "Gemensam start", ClassesCB); @@ -1268,7 +1274,7 @@ int TabClass::classCB(gdioutput &gdi, int type, void *data) EditChanged=false; } else { - firstStart = gdi.getText("FirstStart"); + firstStart = gdi.getText("FirstStart", true); minInterval = gdi.getText("MinInterval"); vacances = gdi.getText("Vacances"); //pairwise = gdi.isChecked("Pairwise"); @@ -1299,8 +1305,18 @@ int TabClass::classCB(gdioutput &gdi, int type, void *data) by = gdi.getHeight(); gdi.enableEditControls(true); } + bool hasGroups = oe->getStartGroups(true).size() > 0; - loadReadyToDistribute(gdi, bx, by); + if (!hasGroups) { + loadReadyToDistribute(gdi, bx, by); + } + else { + gdi.fillRight(); + gdi.addButton("DrawGroupsManual", "Lotta", ClassesCB); + gdi.addButton("EraseStartAll", "Radera starttider...", ClassesCB); + + gdi.refresh(); + } } else if (bi.id == "HelpDraw") { @@ -1363,10 +1379,62 @@ int TabClass::classCB(gdioutput &gdi, int type, void *data) } gdi.enableEditControls(false); - oe->optimizeStartOrder(gdi, drawInfo, cInfo); + vector> outLines; + oe->optimizeStartOrder(outLines, drawInfo, cInfo); + for (auto &ol : outLines) + gdi.addString("", ol.first, ol.second); showClassSettings(gdi); } + else if (bi.id == "DrawGroupsManual") { + set classes; + gdi.getSelection("Classes", classes); + if (classes.empty()) + throw meosException("Ingen klass vald."); + + readDrawInfo(gdi, drawInfo); + if (drawInfo.baseInterval <= 0 || drawInfo.baseInterval == NOTIME) + throw meosException("Ogiltigt basintervall."); + + if (drawInfo.minClassInterval <= 0 || drawInfo.minClassInterval == NOTIME) + throw meosException("Ogiltigt minimalt intervall."); + + if (drawInfo.minClassInterval < drawInfo.baseInterval) { + throw meosException("Startintervallet får inte vara kortare än basintervallet."); + } + + if (drawInfo.minClassInterval % drawInfo.baseInterval != 0) { + throw meosException("Ett startintervall måste vara en multipel av basintervallet."); + } + + vector spec; + for (int id : classes) { + //int classID, int leg, int firstStart, int interval, int vacances, oEvent::VacantPosition vp) + int nVac = int(drawInfo.vacancyFactor * oe->getClass(id)->getNumRunners(true, true, true) + 0.5); + nVac = min(max(drawInfo.minVacancy, nVac), drawInfo.maxVacancy); + + spec.emplace_back(id, 0, 0, 120, nVac, oEvent::VacantPosition::Mixed); + } + + oEvent::DrawMethod method = (oEvent::DrawMethod)gdi.getSelectedItem("Method").first; + + bool moveRunners = gdi.isChecked("MoveRunners"); + + oe->drawListStartGroups(spec, method, 1, oEvent::DrawType::DrawAll, moveRunners, &drawInfo); + + oe->addAutoBib(); + + clearPage(gdi, false); + gdi.addButton("Cancel", "Återgå", ClassesCB); + + oListParam par; + oListInfo info; + par.listCode = EStdStartList; + par.selection = classes; + oe->generateListInfo(par, info); + oe->generateList(gdi, false, info, true); + gdi.refresh(); + } else if (bi.id == "LoadSettings") { set classes; gdi.getSelection("Classes", classes); @@ -1434,7 +1502,11 @@ int TabClass::classCB(gdioutput &gdi, int type, void *data) else if (bi.id == "DrawAdjust") { readClassSettings(gdi); gdi.restore("ReadyToDistribute"); - oe->optimizeStartOrder(gdi, drawInfo, cInfo); + vector> outLines; + oe->optimizeStartOrder(outLines, drawInfo, cInfo); + for (auto &ol : outLines) + gdi.addString("", ol.first, ol.second); + showClassSettings(gdi); } else if (bi.id == "DrawAllAdjust") { @@ -1447,10 +1519,12 @@ int TabClass::classCB(gdioutput &gdi, int type, void *data) oe->addAutoBib(); loadPage(gdi); } - else if (bi.id=="DoDraw" || bi.id=="DoDrawAfter" || bi.id=="DoDrawBefore"){ + else if (bi.id=="DoDraw" || bi.id=="DoDrawAfter" || bi.id=="DoDrawBefore" || bi.id == "DoDrawGroups"){ if (!checkClassSelected(gdi)) return false; + bool withGroups = bi.id == "DoDrawGroups"; + DWORD cid=ClassId; pClass pc = oe->getClass(cid); oEvent::DrawMethod method = oEvent::DrawMethod(gdi.getSelectedItem("Method").first); @@ -1517,8 +1591,10 @@ int TabClass::classCB(gdioutput &gdi, int type, void *data) if (method == oEvent::DrawMethod::Random || method == oEvent::DrawMethod::SOFT || method == oEvent::DrawMethod::MeOS) { vector spec; spec.emplace_back(cid, leg, t, interval, vacanses, vp); - - oe->drawList(spec, method, pairSize, dtype); + if (withGroups) + oe->drawListStartGroups(spec, method, pairSize, dtype); + else + oe->drawList(spec, method, pairSize, dtype); } else if (method == oEvent::DrawMethod::Clumped) oe->drawListClumped(cid, t, interval, vacanses); @@ -1704,6 +1780,12 @@ int TabClass::classCB(gdioutput &gdi, int type, void *data) selectClass(gdi, pc->getId()); } } + else if (bi.id == "StartGroups") { + loadStartGroupSettings(gdi, true); + } + else if (bi.id == "DrawStartGroups") { + drawStartGroups(gdi); + } else if (bi.id=="Bibs") { save(gdi, true); if (!checkClassSelected(gdi)) @@ -3395,6 +3477,9 @@ bool TabClass::loadPage(gdioutput &gdi) if (oe->getMeOSFeatures().hasFeature(MeOSFeatures::MultipleRaces)) func.push_back(ButtonData("QualificationFinal", "Kval/final-schema", false)); + if (showAdvanced) + func.push_back(ButtonData("StartGroups", "Startgrupper", true)); + RECT funRect; funRect.right = gdi.getCX() - 7; funRect.top = gdi.getCY() - 2; @@ -3411,14 +3496,20 @@ bool TabClass::loadPage(gdioutput &gdi) int xlimit = gdi.getWidth() - button_w/2; for (size_t k = 0; k < func.size(); k++) { + TextInfo ti; + ti.xp = 0; + ti.yp = 0; + ti.text = lang.tl(func[k].label); + gdi.calcStringSize(ti); + if (gdi.getCX() + ti.realWidth > xlimit) { + gdi.popX(); + gdi.dropLine(2.5); + } + ButtonInfo &bi = gdi.addButton(func[k].id, func[k].label, ClassesCB); if (!func[k].global) bi.isEdit(true); funRect.left = max(funRect.left, gdi.getCX() + 7); - if ( gdi.getCX() > xlimit && k+1 < func.size()) { - gdi.popX(); - gdi.dropLine(2.5); - } } gdi.dropLine(2.5); @@ -3701,8 +3792,11 @@ void TabClass::drawDialog(gdioutput &gdi, oEvent::DrawMethod method, const oClas gdi.fillRight(); - if (method != oEvent::DrawMethod::Simultaneous) + if (method != oEvent::DrawMethod::Simultaneous) { gdi.addButton("DoDraw", "Lotta klassen", ClassesCB, "Lotta om hela klassen"); + if (oe->getStartGroups(true).size() > 0) + gdi.addButton("DoDrawGroups", "Lotta med startgrupper", ClassesCB, "Lotta om hela klassen"); + } else gdi.addButton("DoDraw", "Tilldela", ClassesCB, "Tilldela starttider"); @@ -4501,7 +4595,7 @@ void TabClass::readDrawInfo(gdioutput &gdi, DrawInfo &drawInfoOut) { int minVacancy = gdi.getTextNo("VacancesMin"); setDefaultVacant(gdi.getText("Vacances")); double vacancyFactor = 0.01*_wtof(gdi.getText("Vacances").c_str()); - double extraFactor = 0.01*_wtof(gdi.getText("Extra").c_str()); + double extraFactor = 0.01*_wtof(gdi.getText("Extra", true).c_str()); drawInfoOut.changedVacancyInfo = drawInfoOut.maxVacancy != maxVacancy || @@ -4522,9 +4616,9 @@ void TabClass::readDrawInfo(gdioutput &gdi, DrawInfo &drawInfoOut) { drawInfoOut.coursesTogether = gdi.isChecked("CoursesTogether"); drawInfoOut.minClassInterval = convertAbsoluteTimeMS(gdi.getText("MinInterval")); - drawInfoOut.maxClassInterval = convertAbsoluteTimeMS(gdi.getText("MaxInterval")); + drawInfoOut.maxClassInterval = convertAbsoluteTimeMS(gdi.getText("MaxInterval", true)); drawInfoOut.nFields = gdi.getTextNo("nFields"); - drawInfoOut.firstStart = oe->getRelativeTime(gdi.getText("FirstStart")); + drawInfoOut.firstStart = oe->getRelativeTime(gdi.getText("FirstStart", true)); } void TabClass::writeDrawInfo(gdioutput &gdi, const DrawInfo &drawInfoIn) { @@ -4746,15 +4840,26 @@ void TabClass::loadBasicDrawSetup(gdioutput &gdi, int &bx, int &by, const wstrin const set &clsId) { showClassSelection(gdi, bx, by, DrawClassesCB); - + bool hasGroups = oe->getStartGroups(true).size() > 0; + gdi.setSelection("Classes", clsId); + + int xb = 0, yb = 0; + + if (hasGroups) { + gdi.addString("", fontMediumPlus, "Lotta med startgrupper"); + gdi.dropLine(1.5); + xb = gdi.getCX() - gdi.scaleLength(5); + yb = gdi.getCY() - gdi.scaleLength(5); + } gdi.addString("", 1, "Grundinställningar"); gdi.pushX(); gdi.fillRight(); - gdi.addInput("FirstStart", firstStart, 10, 0, L"Första start:"); + if (!hasGroups) + gdi.addInput("FirstStart", firstStart, 10, 0, L"Första start:"); gdi.addInput("nFields", L"10", 10, 0, L"Max parallellt startande:"); gdi.popX(); gdi.dropLine(3); @@ -4775,7 +4880,9 @@ void TabClass::loadBasicDrawSetup(gdioutput &gdi, int &bx, int &by, const wstrin gdi.dropLine(4); gdi.fillDown(); gdi.addCheckbox("AllowNeighbours", "Tillåt samma bana inom basintervall", 0, oe->getPropertyInt("DrawInterlace", 1) != 0); - gdi.addCheckbox("CoursesTogether", "Lotta klasser med samma bana gemensamt", 0, false); + + if (!hasGroups) + gdi.addCheckbox("CoursesTogether", "Lotta klasser med samma bana gemensamt", 0, false); gdi.dropLine(0.5); gdi.addString("", 1, "Startintervall"); @@ -4783,7 +4890,9 @@ void TabClass::loadBasicDrawSetup(gdioutput &gdi, int &bx, int &by, const wstrin gdi.fillRight(); gdi.addInput("BaseInterval", L"1:00", 10, 0, L"Basintervall (min):"); gdi.addInput("MinInterval", minInterval, 10, 0, L"Minsta intervall i klass:"); - gdi.addInput("MaxInterval", minInterval, 10, 0, L"Största intervall i klass:"); + + if (!hasGroups) + gdi.addInput("MaxInterval", minInterval, 10, 0, L"Största intervall i klass:"); gdi.popX(); gdi.dropLine(4); @@ -4797,7 +4906,24 @@ void TabClass::loadBasicDrawSetup(gdioutput &gdi, int &bx, int &by, const wstrin gdi.addInput("VacancesMin", zeroVac ? L"0" : L"1", 6, 0, L"Min. vakanser (per klass):"); gdi.addInput("VacancesMax", zeroVac ? L"0" : L"10", 6, 0, L"Max. vakanser (per klass):"); - gdi.addInput("Extra", L"0%", 6, 0, L"Förväntad andel efteranmälda:"); + + if (!hasGroups) + gdi.addInput("Extra", L"0%", 6, 0, L"Förväntad andel efteranmälda:"); + + if (hasGroups) { + gdi.fillDown(); + gdi.popX(); + gdi.dropLine(4); + gdi.addString("", 1, "Lottning"); + gdi.dropLine(0.4); + createDrawMethod(gdi); + gdi.addCheckbox("MoveRunners", "Flytta deltagare från överfulla grupper", 0, true); + + int xr = gdi.getWidth(); + int bt = gdi.getCY(); + RECT rc = { xb, yb, xr, bt }; + gdi.addRectangle(rc, colorLightCyan); + } gdi.dropLine(4); gdi.fillDown(); @@ -4903,3 +5029,136 @@ void TabClass::fillResultModules(gdioutput &gdi, pClass pc) { gdi.selectItemByData("Module", current); hideEditResultModule(gdi, current); } + +class StartGroupHandler : public GuiHandler { + oEvent &oe; + TabClass *tc; + bool lock = false; +public: + StartGroupHandler(TabClass *tc, oEvent *oe) : oe(*oe), tc(tc) {} + + void handle(gdioutput &gdi, BaseInfo &info, GuiEventType type) override { + if (type == GuiEventType::GUI_INPUT) { + int id = info.getExtraInt(); + InputInfo &ii = dynamic_cast(info); + + if (ii.id[0] == 'g') { + int idNew = _wtoi(ii.text.c_str()); + if (idNew != id) { + if (oe.getStartGroup(idNew).first == -1) { + auto d = oe.getStartGroup(id); + oe.setStartGroup(idNew, d.first, d.second); + oe.setStartGroup(id, -1, -1); + string rowIx = ii.id.substr(5); + gdi.getBaseInfo("group" + rowIx).setExtra(idNew); + gdi.getBaseInfo("first" + rowIx).setExtra(idNew); + gdi.getBaseInfo("last" + rowIx).setExtra(idNew); + gdi.getBaseInfo("D" + rowIx).setExtra(idNew); + ii.setBgColor(colorDefault); + } + else { + ii.setBgColor(colorLightRed); + } + } + } + else if (ii.id[0] == 'f') { + auto d = oe.getStartGroup(id); + d.first = oe.getRelativeTime(ii.text); + oe.setStartGroup(id, d.first, d.second); + } + else if (ii.id[0] == 'l') { + auto d = oe.getStartGroup(id); + d.second = oe.getRelativeTime(ii.text); + oe.setStartGroup(id, d.first, d.second); + } + } + else if (type == GuiEventType::GUI_BUTTON) { + if (info.id == "AddGroup") { + int id = 1; + int firstStart = 3600; + int length = 3600; + for (auto &g : oe.getStartGroups(false)) { + id = max(id, g.first+1); + firstStart = max(firstStart, g.second.second); + if (g.second.first < g.second.second) + length = min(length, g.second.second - g.second.first); + } + oe.setStartGroup(id, firstStart, firstStart + length); + tc->loadStartGroupSettings(gdi, false); + } + else if (info.id[0] == 'D') { + int id = info.getExtraInt(); + oe.setStartGroup(id, -1, -1); + tc->loadStartGroupSettings(gdi, false); + } + else if (info.id == "Save") { + oe.synchronize(); + oe.updateStartGroups(); + oe.synchronize(true); + tc->loadPage(gdi); + } + else if (info.id == "Cancel") { + tc->loadPage(gdi); + } + } + } +}; + +void TabClass::loadStartGroupSettings(gdioutput &gdi, bool reload) { + clearPage(gdi, false); + auto &sg = oe->getStartGroups(reload); + + if (!startGroupHandler) + startGroupHandler = make_shared(this, oe); + + GuiHandler *sgh = startGroupHandler.get(); + + gdi.addString("", boldLarge, "Startgrupper"); + int row = 0; + gdi.dropLine(0.5); + gdi.addString("", 10, L"help:startgroup"); + + int idPos = gdi.getCX(); + int firstPos = idPos + gdi.scaleLength(120); + int lastPos = firstPos + gdi.scaleLength(120); + int bPos = lastPos + gdi.scaleLength(120); + bool first = true; + + for (auto &g : sg) { + if (first) { + int y = gdi.getCY(); + gdi.addString("", y, idPos, 0, "Id"); + gdi.addString("", y, firstPos, 0, "Start"); + gdi.addString("", y, lastPos, 0, "Slut"); + first = false; + } + int cy = gdi.getCY(); + string srow = itos(row++); + gdi.addInput(idPos, cy, "group" + srow, itow(g.first), 8).setHandler(sgh).setExtra(g.first); + gdi.addInput(firstPos, cy, "first" + srow, oe->getAbsTime(g.second.first), 10).setHandler(sgh).setExtra(g.first); + gdi.addInput(lastPos, cy, "last" + srow, oe->getAbsTime(g.second.second), 8).setHandler(sgh).setExtra(g.first); + gdi.addButton(bPos, cy, "D" + srow, L"Ta bort").setHandler(sgh).setExtra(g.first); + } + + if (sg.size() == 1) + gdi.addString("", 10, "Tips: ställ in rätt tid innan du lägger till fler grupper."); + + gdi.dropLine(); + gdi.fillRight(); + + gdi.addButton("AddGroup", "Ny startgrupp").setHandler(sgh); + gdi.addButton("Save", "Spara").setHandler(sgh); + gdi.addButton("Cancel", "Avbryt").setHandler(sgh); + + gdi.refresh(); +} + +void TabClass::drawStartGroups(gdioutput &gdi) { + clearPage(gdi, false); + if (!startGroupHandler) + startGroupHandler = make_shared(this, oe); + gdi.addString("", boldLarge, "Lotta med startgrupper"); + + gdi.addButton("Cancel", "Stäng", ClassesCB); + gdi.refresh(); +} diff --git a/code/TabClass.h b/code/TabClass.h index 0f1da64..48d79cb 100644 --- a/code/TabClass.h +++ b/code/TabClass.h @@ -163,8 +163,13 @@ class TabClass : vector currentResultModuleTags; void fillResultModules(gdioutput &gdi, pClass pc); + + shared_ptr startGroupHandler; + public: - + void loadStartGroupSettings(gdioutput &gdi, bool reload); + void drawStartGroups(gdioutput &gdi); + void clearCompetitionData(); void closeWindow(gdioutput &gdi); diff --git a/code/TabCompetition.cpp b/code/TabCompetition.cpp index 0a1441e..6a2231c 100644 --- a/code/TabCompetition.cpp +++ b/code/TabCompetition.cpp @@ -163,7 +163,7 @@ bool TabCompetition::importFile(HWND hWnd, gdioutput &gdi) return false; gdi.setWaitCursor(true); - if (oe->open(fileName, true)) { + if (oe->open(fileName, true, false)) { gdi.setWindowTitle(oe->getTitleName()); resetSaveTimer(); return true; @@ -562,7 +562,7 @@ int TabCompetition::competitionCB(gdioutput &gdi, int type, void *data) gdi.refresh(); } else if (bi.id=="Test") { - checkRentCards(gdi); + //mergeCompetition(gdi); } else if (bi.id=="Report") { gdi.clearPage(true); @@ -892,7 +892,7 @@ int TabCompetition::competitionCB(gdioutput &gdi, int type, void *data) oEvent nextStage(gdi); if (!file.empty()) - success = nextStage.open(file.c_str(), false); + success = nextStage.open(file.c_str(), false, false); if (success) success = nextStage.getNameId(0) == oe->getDCI().getString("PostEvent"); @@ -1871,8 +1871,11 @@ int TabCompetition::competitionCB(gdioutput &gdi, int type, void *data) save(gdi, true); exportFileAs(hWndMain, gdi); } + else if (bi.id == "Merge") { + mergeCompetition(gdi); + } else if (bi.id=="Duplicate") { - oe->duplicate(); + oe->duplicate(L""); gdi.alert("Skapade en lokal kopia av tävlingen."); } else if (bi.id=="Import") { @@ -2310,7 +2313,7 @@ int TabCompetition::restoreCB(gdioutput &gdi, int type, void *data) { if (ti.id == "") { wstring fi(bi.FullPath); - if (!oe->open(fi, false)) { + if (!oe->open(fi, false, false)) { gdi.alert("Kunde inte öppna tävlingen."); } else { @@ -2404,6 +2407,7 @@ void TabCompetition::loadAboutPage(gdioutput &gdi) const "\n\nRussian Translation by Paul A. Kazakov and Albert Salihov" "\n\nOriginal French Translation by Jerome Monclard" "\n\nAdaption to French conditions and extended translation by Pierre Gaufillet" + "\n\nMore French translations and documentation by Titouan Savart" "\n\nCzech Translation by Marek Kustka" "\n\nSpanish Translation by Manuel Pedre" "\n\nHelp with English documentation: Torbjörn Wikström"); @@ -2658,6 +2662,9 @@ bool TabCompetition::loadPage(gdioutput &gdi) } gdi.addButton(gdi.getCX(), gdi.getCY(), bw, "SaveAs", "Säkerhetskopiera", CompetitionCB, "", false, false); + gdi.addButton(gdi.getCX(), gdi.getCY(), bw, "Merge", "Slå ihop tävlingar", + CompetitionCB, "", false, false); + if (oe->getMeOSFeatures().hasFeature(MeOSFeatures::Network)) { gdi.addButton(gdi.getCX(), gdi.getCY(), bw, "ConnectMySQL", "Databasanslutning", CompetitionCB, "", false, false); @@ -3448,6 +3455,18 @@ void TabCompetition::entryForm(gdioutput &gdi, bool isGuide) { gdi.addInput("FileName", L"", 48, 0, L"Anmälningar (IOF (xml) eller OE-CSV)"); gdi.dropLine(); gdi.addButton("BrowseEntries", "Bläddra...", CompetitionCB).setExtra(L"FileName"); + gdi.popX(); + + gdi.dropLine(2.5); + gdi.addInput("FileNameService", L"", 48, 0, L"Tjänster (IOF XML)"); + gdi.dropLine(); + gdi.addButton("BrowseEntries", "Bläddra...", CompetitionCB).setExtra(L"FileNameService"); + gdi.popX(); + + gdi.dropLine(2.5); + gdi.addInput("FileNameServiceReq", L"", 48, 0, L"Tjänstebeställningar (IOF XML)"); + gdi.dropLine(); + gdi.addButton("BrowseEntries", "Bläddra...", CompetitionCB).setExtra(L"FileNameServiceReq"); gdi.popX(); gdi.dropLine(3.2); @@ -3467,23 +3486,22 @@ void TabCompetition::entryForm(gdioutput &gdi, bool isGuide) { } FlowOperation TabCompetition::saveEntries(gdioutput &gdi, bool removeRemoved, bool isGuide) { - wstring filename[5]; - filename[0] = gdi.getText("FileNameCmp"); - filename[1] = gdi.getText("FileNameCls"); - filename[2] = gdi.getText("FileNameClb"); - filename[3] = gdi.getText("FileName"); - filename[4] = gdi.getText("FileNameRank"); - //csvparser csv; + vector fields = { "FileNameCmp", "FileNameCls", "FileNameClb", "FileName", + "FileNameRank", "FileNameService", "FileNameServiceReq"}; - for (int i = 0; i<5; i++) { + vector filename; + for (string &fn : fields) + filename.push_back(gdi.getText(fn)); + + for (size_t i = 0; i &c } } -void TabCompetition::checkRentCards(gdioutput &gdi) { +void TabCompetition::mergeCompetition(gdioutput &gdi) { + + class MergeHandler : public GuiHandler { + TabCompetition *tc; + public: + + MergeHandler(TabCompetition *tc) : tc(tc) {} + + void handle(gdioutput &gdi, BaseInfo &info, GuiEventType type) final { + if (type == GuiEventType::GUI_BUTTON) { + ButtonInfo bi = dynamic_cast(info); + + if (bi.id == "Cancel") { + if (tc->mergeEvent) + tc->mergeEvent->clear(); + tc->loadPage(gdi); + } + else if (bi.id == "Merge") { + if (!tc->mergeEvent) + return; + int numAdd, numRemove, numUpdate; + + wstring anno = lang.tl(L"Kopia (X)#MRG " + wstring(getLocalTime())); + tc->oe->duplicate(anno); + + tc->oe->merge(*tc->mergeEvent, numAdd, numRemove, numUpdate); + + + gdi.clearPage(true); + gdi.addString("", fontMediumPlus, "Sammanslagning klar."); + gdi.dropLine(); + + gdi.addString("", 1, "Sammanfattning, uppdateradet poster"); + gdi.addString("", 0, "Tillagda: X#" + itos(numAdd)); + gdi.addString("", 0, "Uppdaterade: X#" + itos(numUpdate)); + gdi.addString("", 0, "Borttagna: X#" + itos(numRemove)); + + gdi.dropLine(); + gdi.addString("", 0, L"Skapade lokal säkerhetskopia (X) innan sammanslagning#" + anno); + + tc->mergeEvent->clear(); + + gdi.dropLine(3); + gdi.addButton("Cancel", "Återgå").setHandler(this); + gdi.refresh(); + } + else if (bi.id == "Browse") { + wstring fn = gdi.browseForOpen({ make_pair(L"xml", L"*.xml") }, L"xml"); + if (fn.empty()) + return; + + tc->mergeFile = fn; + gdi.setText("File", fn); + gdi.enableInput("Read"); + } + else if (bi.id == "Read") { + tc->mergeEvent = make_shared(gdi); + + if (!tc->mergeEvent->open(tc->mergeFile, true, true)) + return; + + gdi.restore("merge", false); + gdi.fillDown(); + gdi.addStringUT(1, tc->mergeEvent->getName()); + + gdi.dropLine(); + bool error = false; + if (tc->mergeEvent->getNameId(0) == tc->oe->getNameId(0)) { + if (tc->mergeEvent->getMergeTag() == tc->oe->getMergeTag()) { + gdi.addString("", 1, "Fel: En tävling kan inte slås ihop med sig själv.").setColor(colorRed); + error = true; + } + else { + gdi.addString("", 1, "Samma bastävling").setColor(colorDarkGreen); + wstring info = tc->oe->getMergeInfo(tc->mergeEvent->getMergeTag()); + string mod = tc->mergeEvent->getLastModified(); + + if (info.empty()) { + gdi.addString("", 0, "Denna datakälla är aldrig tidigare infogad"); + } + else { + string merged = gdi.narrow(info); + + if (mod <= merged) { + gdi.addString("", 1, "Fel: Denna tävlingsversion är redan infogad.").setColor(colorRed); + error = true; + } + else { + TimeStamp ts; + ts.setStamp(merged); + gdi.addString("", 0, "Tidigare infogad version: X#" + ts.getStampStringN()); + } + } + + if (!error) { + TimeStamp ts; + ts.setStamp(mod); + gdi.addString("", 0, "Infoga version: X#" + ts.getStampStringN()); + } + } + } + else { + gdi.addString("", 1, "Varning: Olika bastävlingar").setColor(colorRed); + gdi.addString("", 0, "Sammanslagning fungerar om samma uppsättning banor/kontroller används"); + } + + gdi.dropLine(); + + gdi.pushX(); + gdi.fillRight(); + if (!error) { + gdi.addString("", 1, "Klasser: "); + + vector cls; + tc->mergeEvent->getClasses(cls, false); + int xw, yw; + gdi.getTargetDimension(xw, yw); + int limit = (xw * 2) / 3; + + for (pClass c : cls) { + gdi.addStringUT(0, c->getName()); + if (gdi.getCX() >= limit) { + gdi.popX(); + gdi.dropLine(); + } + } + + gdi.fillDown(); + gdi.popX(); + gdi.dropLine(); + + gdi.addString("", 1, "Antal deltagare: X#" + itos(tc->mergeEvent->getNumRunners())); + } + + gdi.dropLine(); + gdi.fillRight(); + if (!error) + gdi.addButton("Merge", "Slå ihop").setHandler(tc->mergeHandler.get()); + gdi.addButton("Cancel", "Avbryt").setHandler(tc->mergeHandler.get()); + + gdi.refresh(); + } + } + } + }; + + mergeHandler = make_shared(this); + gdi.clearPage(false); - wstring fn = gdi.browseForOpen({ make_pair(L"csv", L"*.csv") }, L"csv"); - if (!fn.empty()) { - csvparser csv; - list> data; - csv.parse(fn, data); - set rentCards; - for (auto &c : data) { - if (c.size() > 0) { - int cn = _wtoi(c[0].c_str()); - rentCards.insert(cn); - } - } - - vector runners; - oe->getRunners(0, 0, runners); - int bcf = oe->getBaseCardFee(); - for (pRunner r : runners) { - if (rentCards.count(r->getCardNo()) && r->getDCI().getInt("CardFee") == 0) { - gdi.addStringUT(0, r->getCompleteIdentification()); - r->getDI().setInt("CardFee", bcf); - } - } - } + gdi.addString("", boldLarge, "Slå ihop tävlingar"); + gdi.setRestorePoint("merge"); gdi.dropLine(); - gdi.addButton("Cancel", "OK", CompetitionCB); + gdi.addString("", 10, "help:merge"); + gdi.dropLine(); + + gdi.pushX(); + gdi.fillRight(); + gdi.addInput("File", mergeFile, 32, nullptr, L"Tävling:"); + gdi.dropLine(0.8); + gdi.addButton("Browse", "Bläddra...").setHandler(mergeHandler.get()); + + gdi.dropLine(4); + gdi.popX(); + gdi.fillRight(); + gdi.addButton("Read", "Fortsätt").setHandler(mergeHandler.get()); + gdi.addButton("Cancel", "Avbryt").setHandler(mergeHandler.get()); + gdi.setInputStatus("Read", !mergeFile.empty()); + gdi.refresh(); + gdi.refresh(); } diff --git a/code/TabCompetition.h b/code/TabCompetition.h index 465db29..2143e7b 100644 --- a/code/TabCompetition.h +++ b/code/TabCompetition.h @@ -134,9 +134,10 @@ class TabCompetition : void listBackups(gdioutput &gdi); - - void checkRentCards(gdioutput &gdi); - + shared_ptr mergeHandler; + shared_ptr mergeEvent; + void mergeCompetition(gdioutput &gdi); + wstring mergeFile; protected: void clearCompetitionData(); diff --git a/code/TabCourse.cpp b/code/TabCourse.cpp index fb3e15f..3e751a3 100644 --- a/code/TabCourse.cpp +++ b/code/TabCourse.cpp @@ -263,7 +263,7 @@ void TabCourse::save(gdioutput &gdi, int canSwitchViewMode) { pc->setName(name); - bool changedCourse = pc->importControls(gdi.narrow(gdi.getText("Controls")), true); + bool changedCourse = pc->importControls(gdi.narrow(gdi.getText("Controls")), true, true); pc->setLength(gdi.getTextNo("Length")); pc->getDI().setInt("Climb", gdi.getTextNo("Climb")); pc->setNumberMaps(gdi.getTextNo("NumberMaps")); diff --git a/code/TimeStamp.cpp b/code/TimeStamp.cpp index c71264b..b63b9f7 100644 --- a/code/TimeStamp.cpp +++ b/code/TimeStamp.cpp @@ -100,6 +100,16 @@ const string &TimeStamp::getStamp() const return stampCode; } +const string &TimeStamp::getStamp(const string &sqlStampIn) const { + stampCode.resize(8 + 6 + 1); + int outIx = 0; + for (char c : sqlStampIn) { + if (c >= '0' && c <= '9' && outIx < 8 + 6) + stampCode[outIx++] = c; + } + return stampCode; +} + wstring TimeStamp::getStampString() const { __int64 ft64=(__int64(Time)+minYearConstant*365*24*3600)*10000000; @@ -113,6 +123,19 @@ wstring TimeStamp::getStampString() const return bf; } +string TimeStamp::getStampStringN() const +{ + __int64 ft64 = (__int64(Time) + minYearConstant * 365 * 24 * 3600) * 10000000; + FILETIME &ft = *(FILETIME*)&ft64; + SYSTEMTIME st; + FileTimeToSystemTime(&ft, &st); + + char bf[32]; + sprintf_s(bf, "%d-%02d-%02d %02d:%02d:%02d", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond); + + return bf; +} + void TimeStamp::setStamp(const string &s) { if (s.size()<14) diff --git a/code/TimeStamp.h b/code/TimeStamp.h index 412c901..8cfd497 100644 --- a/code/TimeStamp.h +++ b/code/TimeStamp.h @@ -37,8 +37,10 @@ class TimeStamp { public: void setStamp(const string &s); const string &getStamp() const; - + const string &getStamp(const string &sqlStampIn) const; + wstring getStampString() const; + string getStampStringN() const; int getAge() const; unsigned int getModificationTime() const {return Time;} diff --git a/code/csvparser.cpp b/code/csvparser.cpp index f86ac13..290eeed 100644 --- a/code/csvparser.cpp +++ b/code/csvparser.cpp @@ -588,7 +588,7 @@ bool csvparser::importOCAD_CSV(oEvent &event, const wstring &file, bool addClass } else { // Reset control - pc->importControls("", false); + pc->importControls("", true, false); pc->setLength(int(Length*1000)); } diff --git a/code/english.lng b/code/english.lng index 557cdc2..e96d16d 100644 --- a/code/english.lng +++ b/code/english.lng @@ -2171,7 +2171,7 @@ Klasserna X och Y har samma externa id. Använd tabelläget för att ändra id = Vill du koppla isär X från inläst bricka Y? = Would you like to disconnect X from the read out card Y? RunnerRogainingPointGross = Rogaining points before reduction Samlade poäng = Collected points -Tidsavdrag = Deduction +Tidsavdrag = Reduction X p = X p Bricka X används också av = Card X is also used by reused card = reused card @@ -2494,3 +2494,29 @@ Lottat = Drawn Sist = Last Fakturadatum = Fakturadatum Youth Cup X = Youth Cup X +Ny startgrupp = New starting group +Slut = End +Startgrupper = Starting groups +help:startgroup = Starting groups are used to control start list drawing. Competitors in a group will begin starting at the start time of the group. +Tips: ställ in rätt tid innan du lägger till fler grupper = Hint: Enter the correct times before adding more groups. +Familj = Family +Startgrupp = Starting group +Slå ihop tävlingar = Merge competitions +help:merge = It is possible to merge competitions and results, provided that they are based on the same set of courses and controls. Different groups of participants can complete the competition on different occasions and then the different competitions can be merged into one competition with a common list of results. Another possibility is to have different locations for different classes. If it is not possible to set up a common network, you can periodically exchange competition files to incorporate changes.\n\n1. Prepare the whole competition.\n2. Save a copy and import it to the outsourced computers (or local area networks).\n3. To transfer changes, export the contest from the outsourced computer (or computers) and merge it with this feature. Then export a copy from the main computer and make the corresponding import on the outsourced computers. \n4. The procedure can be repeated several times to continuously transfer the results.\n\nNote: If you make changes to (for example) the same participant in several places, some of the changes will be overwritten without warning. Make sure that each outsourced place only changes in its part of the competition.\n\nTip: Make a transfer as soon as the outsourced competitions have started before any change has been made, to test that everything has been set up correctly. +Denna datakälla är aldrig tidigare infogad = This data source has never been merged +Fel: Denna tävlingsversion är redan infogad = Error: This version has already been merged +Infoga version: X = Merge version: X +Samma bastävling = Same base competition +Sammanslagning fungerar om samma uppsättning banor/kontroller används = The merge will work if the same set of courses and controls are used +Varning: Olika bastävlingar = Warning: Different base competitions +Borttagna: X = Borttagna: X +Fel: En tävling kan inte slås ihop med sig själv = Error: Cannot merge a competition with itself +Sammanfattning, uppdateradet poster = Summary, updated data entities +Sammanslagning klar = Merge complete +Skapade lokal säkerhetskopia (X) innan sammanslagning = Created local backup (X) before merge +Tillagda: X = Added: X +Uppdaterade: X = Modified: X +Tjänstebeställningar (IOF XML) = Service Requests (IOF XML) +Tjänster (IOF XML) = Services (IOF XML) +Flytta deltagare från överfulla grupper = Move competitors from full groups +Lotta med startgrupper = Draw with Starting Groups diff --git a/code/gdioutput.h b/code/gdioutput.h index 2f3c42d..7c7cedc 100644 --- a/code/gdioutput.h +++ b/code/gdioutput.h @@ -605,6 +605,9 @@ public: const wstring &getText(const char *id, bool acceptMissing = false) const; + BaseInfo &getBaseInfo(const string &id) const { + return getBaseInfo(id.c_str()); + } BaseInfo &getBaseInfo(const char *id) const; BaseInfo &getBaseInfo(const wchar_t *id) const { return getBaseInfo(narrow(id).c_str()); diff --git a/code/iof30interface.cpp b/code/iof30interface.cpp index 49efeb3..c714c07 100644 --- a/code/iof30interface.cpp +++ b/code/iof30interface.cpp @@ -1064,6 +1064,10 @@ void IOF30Interface::readServiceRequestList(gdioutput &gdi, xmlobject &xo, int & xo.getObjects("PersonServiceRequest", req); entrySourceId = 0; + auto &sg = oe.getStartGroups(true); + + bool importStartGroups = sg.size() > 0; + for (auto &rx : req) { xmlobject xPers = rx.getObject("Person"); pRunner r = 0; @@ -1075,9 +1079,13 @@ void IOF30Interface::readServiceRequestList(gdioutput &gdi, xmlobject &xo, int & if (xreq) { auto xServ = xreq.getObject("Service"); string type; - if (xServ && xServ.getObjectString("type", type)=="StartGroup") { + if (xServ && (xServ.getObjectString("type", type)=="StartGroup" || importStartGroups)) { int id = xServ.getObjectInt("Id"); - r->getDI().setInt("Heat", id); + if (!importStartGroups) + r->getDI().setInt("Heat", id); + + if (sg.count(id)) + r->setStartGroup(id); } } } @@ -1389,7 +1397,41 @@ void IOF30Interface::readEvent(gdioutput &gdi, const xmlobject &xo, DI.setString("LateEntryFactor", lf); } } + oe.synchronize(); + xmlList xService; + xo.getObjects("Service", xService); + services.clear(); + + for (auto &s : xService) { + int id = s.getObjectInt("Id"); + if (id > 0) { + xmlList nameList; + s.getObjects("Name", nameList); + for (auto s : nameList) { + services.emplace_back(id, s.getw()); + } + } + } + + // This is a "hack" to interpret services of the from "XXXX 14:00 - 15:00 XXXX" as a start group. + for (auto &srv : services) { + vector parts; + split(srv.name, L" -‒–—‐", parts); + vector times; + for (auto &p : parts) { + for (auto &c : p) { + if (c == '.') + c = ':'; + } + int t = oe.getRelativeTime(p); + if (t > 0) + times.push_back(t); + } + if (times.size() == 2 && times[0] < times[1]) + oe.setStartGroup(srv.id, times[0], times[1]); + } + oe.updateStartGroups(); } void IOF30Interface::setupClassConfig(int classId, const xmlobject &xTeam, map > &teamClassConfig) { @@ -3894,7 +3936,7 @@ pCourse IOF30Interface::readCourse(const xmlobject &xcrs) { if (pc) { pc->setName(name); pc->setLength(len); - pc->importControls("", false); + pc->importControls("", true, false); for (size_t i = 0; iaddControl(ctrlCode[i]->getId()); } diff --git a/code/iof30interface.h b/code/iof30interface.h index 0c0205d..7b8f5ad 100644 --- a/code/iof30interface.h +++ b/code/iof30interface.h @@ -48,6 +48,14 @@ typedef oClub * pClub; typedef oTeam * pTeam; typedef oCourse *pCourse; +struct XMLService { + int id; + wstring name; + + XMLService(int id, const wstring &name) : id(id), name(name) {} + XMLService() {} +}; + class IOF30Interface { oEvent &oe; @@ -67,6 +75,8 @@ class IOF30Interface { set matchedClasses; + list services; + struct LegInfo { int maxRunners; int minRunners; diff --git a/code/meos.cpp b/code/meos.cpp index 7dd945b..3c35252 100644 --- a/code/meos.cpp +++ b/code/meos.cpp @@ -116,7 +116,7 @@ HHOOK g_hhk; //- handle to the hook procedure. HWND hMainTab=NULL; -list *tabList=0; +list *tabList = nullptr; void scrollVertical(gdioutput *gdi, int yInc, HWND hWnd); static int currentFocusIx = 0; @@ -481,7 +481,7 @@ int APIENTRY WinMain(HINSTANCE hInstance, tabAutoRegister(0); tabList->clear(); delete tabList; - tabList=0; + tabList = nullptr; delete autoTask; autoTask = 0; @@ -996,6 +996,9 @@ void createTabs(bool force, bool onlyMain, bool skipTeam, bool skipSpeaker, skipRunners==skipRunnersP && skipControls==skipControlsP && skipCourses == skipCoursesP) return; + if (!tabList) + return; + onlyMainP = onlyMain; skipTeamP = skipTeam; skipSpeakerP = skipSpeaker; diff --git a/code/meosdb/MeosSQL.cpp b/code/meosdb/MeosSQL.cpp index 662023d..622fecf 100644 --- a/code/meosdb/MeosSQL.cpp +++ b/code/meosdb/MeosSQL.cpp @@ -755,82 +755,89 @@ OpFailStatus MeosSQL::SyncUpdate(oEvent *oe) if (syncUpdate(queryset, "oEvent", oe) == opStatusFail) return opStatusFail; } + writeTime = true; + try { + con.query().exec("DELETE FROM oCard"); + { + list::iterator it = oe->Cards.begin(); + while (it != oe->Cards.end()) { + if (!it->isRemoved() && syncUpdate(&*it, true) == opStatusFail) + return opStatusFail; + ++it; + } + } - con.query().exec("DELETE FROM oCard"); - { - list::iterator it=oe->Cards.begin(); - while(it!=oe->Cards.end()){ - if (!it->isRemoved() && syncUpdate(&*it, true) == opStatusFail) - return opStatusFail; - ++it; + con.query().exec("DELETE FROM oClub"); + { + list::iterator it = oe->Clubs.begin(); + while (it != oe->Clubs.end()) { + if (!it->isRemoved() && syncUpdate(&*it, true) == opStatusFail) + return opStatusFail; + ++it; + } + } + con.query().exec("DELETE FROM oControl"); + { + list::iterator it = oe->Controls.begin(); + while (it != oe->Controls.end()) { + if (!it->isRemoved() && syncUpdate(&*it, true) == opStatusFail) + return opStatusFail; + ++it; + } + } + con.query().exec("DELETE FROM oCourse"); + { + list::iterator it = oe->Courses.begin(); + while (it != oe->Courses.end()) { + if (!it->isRemoved() && syncUpdate(&*it, true) == opStatusFail) + return opStatusFail; + ++it; + } + } + con.query().exec("DELETE FROM oClass"); + { + list::iterator it = oe->Classes.begin(); + while (it != oe->Classes.end()) { + if (!it->isRemoved() && syncUpdate(&*it, true) == opStatusFail) + return opStatusFail; + ++it; + } + } + con.query().exec("DELETE FROM oRunner"); + { + list::iterator it = oe->Runners.begin(); + while (it != oe->Runners.end()) { + if (!it->isRemoved() && syncUpdate(&*it, true) == opStatusFail) + return opStatusFail; + ++it; + } } - } - con.query().exec("DELETE FROM oClub"); - { - list::iterator it=oe->Clubs.begin(); - while(it!=oe->Clubs.end()){ - if (!it->isRemoved() && syncUpdate(&*it, true) == opStatusFail) - return opStatusFail; - ++it; + con.query().exec("DELETE FROM oTeam"); + { + list::iterator it = oe->Teams.begin(); + while (it != oe->Teams.end()) { + if (!it->isRemoved() && syncUpdate(&*it, true) == opStatusFail) + return opStatusFail; + ++it; + } } - } - con.query().exec("DELETE FROM oControl"); - { - list::iterator it=oe->Controls.begin(); - while(it!=oe->Controls.end()){ - if (!it->isRemoved() && syncUpdate(&*it, true) == opStatusFail) - return opStatusFail; - ++it; - } - } - con.query().exec("DELETE FROM oCourse"); - { - list::iterator it=oe->Courses.begin(); - while(it!=oe->Courses.end()){ - if (!it->isRemoved() && syncUpdate(&*it, true) == opStatusFail) - return opStatusFail; - ++it; - } - } - con.query().exec("DELETE FROM oClass"); - { - list::iterator it=oe->Classes.begin(); - while(it!=oe->Classes.end()){ - if (!it->isRemoved() && syncUpdate(&*it, true) == opStatusFail) - return opStatusFail; - ++it; - } - } - con.query().exec("DELETE FROM oRunner"); - { - list::iterator it=oe->Runners.begin(); - while(it!=oe->Runners.end()){ - if (!it->isRemoved() && syncUpdate(&*it, true) == opStatusFail) - return opStatusFail; - ++it; - } - } - con.query().exec("DELETE FROM oTeam"); - { - list::iterator it=oe->Teams.begin(); - while(it!=oe->Teams.end()){ - if (!it->isRemoved() && syncUpdate(&*it, true) == opStatusFail) - return opStatusFail; - ++it; + con.query().exec("DELETE FROM oPunch"); + { + list::iterator it = oe->punches.begin(); + while (it != oe->punches.end()) { + if (!it->isRemoved() && syncUpdate(&*it, true) == opStatusFail) + return opStatusFail; + ++it; + } } } - - con.query().exec("DELETE FROM oPunch"); - { - list::iterator it=oe->punches.begin(); - while(it!=oe->punches.end()){ - if (!it->isRemoved() && syncUpdate(&*it, true) == opStatusFail) - return opStatusFail; - ++it; - } + catch (...) { + writeTime = false; + throw; } + writeTime = false; return retValue; } @@ -1566,7 +1573,7 @@ OpFailStatus MeosSQL::storeCourse(const Row &row, oCourse &c, OpFailStatus success = opStatusOK; c.Name = fromUTF((string)row["Name"]); - c.importControls(string(row["Controls"]), false); + c.importControls(string(row["Controls"]), false, false); c.Length = row["Length"]; c.importLegLengths(string(row["Legs"]), false); @@ -1997,8 +2004,12 @@ OpFailStatus MeosSQL::syncRead(bool forceRead, oRunner *r) } string MeosSQL::andWhereOld(oBase *ob) { - if (ob->sqlUpdated.empty()) - return " AND Counter!=" + itos(ob->counter); + if (ob->sqlUpdated.empty()) { + if (ob->counter != 0) + return " AND Counter!=" + itos(ob->counter); + else + return ""; + } else return " AND (Counter!=" + itos(ob->counter) + " OR Modified!='" + ob->sqlUpdated + "')"; } @@ -2804,11 +2815,14 @@ mysqlpp::ResNSel MeosSQL::updateCounter(const char *oTable, int id, mysqlpp::Que query.reset(); query << "UPDATE " << oTable << " SET Counter=" << counter; + if (writeTime) + query << ", Modified=Modified"; + if (updateqry != 0) query << "," << updateqry->str(); query << " WHERE Id=" << id; - + mysqlpp::ResNSel res = query.execute(); query.exec("UNLOCK TABLES"); @@ -2870,6 +2884,10 @@ OpFailStatus MeosSQL::syncUpdate(mysqlpp::Query &updateqry, if (setId) query << ", Id=" << ob->Id; + if (writeTime) { + query << ", Modified='" << ob->getTimeStampN() << "'"; + } + mysqlpp::ResNSel res=query.execute(); if (res) { if (ob->Id > 0 && ob->Id!=(int)res.insert_id) { @@ -3184,6 +3202,7 @@ bool MeosSQL::syncListClass(oEvent *oe) { if (!c) { oClass oc(oe, Id); + oc.setImplicitlyCreated(); st = syncRead(true, &oc, false); c = oe->addClass(oc); if (c != 0) { diff --git a/code/meosdb/MeosSQL.h b/code/meosdb/MeosSQL.h index f528fc1..1e12dc2 100644 --- a/code/meosdb/MeosSQL.h +++ b/code/meosdb/MeosSQL.h @@ -58,6 +58,7 @@ protected: mysqlpp::Connection con; string CmpDataBase; void alert(const string &s); + bool writeTime = false; vector missingObjects; diff --git a/code/meosvc15.vcxproj b/code/meosvc15.vcxproj index 53575e1..75166f6 100644 --- a/code/meosvc15.vcxproj +++ b/code/meosvc15.vcxproj @@ -319,6 +319,7 @@ + diff --git a/code/meosversion.cpp b/code/meosversion.cpp index 7ccf81a..a32b2f9 100644 --- a/code/meosversion.cpp +++ b/code/meosversion.cpp @@ -30,22 +30,22 @@ //V35: abcdef //V36: abcdef int getMeosBuild() { - string revision("$Rev: 1014 $"); + string revision("$Rev: 1031 $"); return 174 + atoi(revision.substr(5, string::npos).c_str()); } -//V37: a +//V37: ab wstring getMeosDate() { - wstring date(L"$Date: 2020-05-01 16:22:46 +0200 (fr, 01 maj 2020) $"); + wstring date(L"$Date: 2020-08-01 10:38:11 +0200 (lö, 01 aug 2020) $"); return date.substr(7,10); } wstring getBuildType() { - return L"RC2"; // No parantheses (...) + return L"RC1"; // No parantheses (...) } wstring getMajorVersion() { - return L"3.7"; + return L"3.7SD"; } wstring getMeosFullVersion() { @@ -146,6 +146,7 @@ void getSupporters(vector &supp, vector &developSupp) supp.emplace_back(L"Thomas Engberg, VK Uvarna"); supp.emplace_back(L"LG Axmalm, Sävedalens AIK"); supp.emplace_back(L"Falköpings AIK OK"); + developSupp.push_back(L"Karlskrona SOK"); reverse(supp.begin(), supp.end()); } diff --git a/code/oBase.cpp b/code/oBase.cpp index 53e78a5..0fc8e26 100644 --- a/code/oBase.cpp +++ b/code/oBase.cpp @@ -215,8 +215,33 @@ wstring oBase::getTimeStamp() const { else return Modified.getStampString(); } +string oBase::getTimeStampN() const { + if (oe && oe->isClient() && !sqlUpdated.empty()) { + return sqlUpdated; + } + else return Modified.getStampStringN(); +} + + +const string &oBase::getStamp() const { + if (oe && oe->isClient() && !sqlUpdated.empty()) { + return Modified.getStamp(sqlUpdated); + } + else + return Modified.getStamp(); +} + void oBase::changeId(int newId) { Id = newId; + oe->updateFreeId(this); +} + +void oBase::addToEvent(oEvent *e, const oBase *src) { + oe = e; + addedToEvent = true; + oe->updateFreeId(this); + if (src) + Modified = src->Modified; } oDataInterface oBase::getDI(void) { diff --git a/code/oBase.h b/code/oBase.h index bedf2f4..9c2263b 100644 --- a/code/oBase.h +++ b/code/oBase.h @@ -114,6 +114,9 @@ protected: void setLocalObject() { localObject = true; } + // Merge into this entity + virtual void merge(const oBase &input) = 0; + public: void update(SqlUpdated &info) const; @@ -154,13 +157,15 @@ public: bool synchronize(bool writeOnly=false); wstring getTimeStamp() const; - + string getTimeStampN() const; + const string &getStamp() const; + bool existInDB() const { return !sqlUpdated.empty(); } void setImplicitlyCreated() { implicitlyAdded = true; } bool isImplicitlyCreated() const { return implicitlyAdded; } bool isAddedToEvent() const { return addedToEvent; } - void addToEvent() { addedToEvent = true; } + void addToEvent(oEvent *e, const oBase *src); oDataInterface getDI(); diff --git a/code/oCard.cpp b/code/oCard.cpp index 157f5b2..8e849c2 100644 --- a/code/oCard.cpp +++ b/code/oCard.cpp @@ -70,7 +70,7 @@ bool oCard::Write(xmlparser &xml) xml.write("Punches", getPunchString()); xml.write("ReadId", readId); xml.write("Id", Id); - xml.write("Updated", Modified.getStamp()); + xml.write("Updated", getStamp()); xml.endTag(); return true; @@ -101,6 +101,17 @@ void oCard::Set(const xmlobject &xo) } } +pair oCard::getCardHash() const { + int a = cardNo; + int b = readId; + + for (auto &p : punches) { + a = a * 31 + p.getTimeInt() * 997 + p.getTypeCode(); + b = b * 41 + p.getTimeInt() * 97 + p.getTypeCode(); + } + return make_pair(a, b); +} + void oCard::setCardNo(int c) { if (cardNo!=c) @@ -520,8 +531,9 @@ pCard oEvent::addCard(const oCard &oc) return 0; Cards.push_back(oc); - Cards.back().addToEvent(); - + Cards.back().tOwner = nullptr; + Cards.back().addToEvent(this, &oc); + qFreeCardId = max(oc.Id, qFreeCardId); return &Cards.back(); } diff --git a/code/oCard.h b/code/oCard.h index 6780c90..17853cb 100644 --- a/code/oCard.h +++ b/code/oCard.h @@ -65,8 +65,6 @@ protected: /** Get internal data buffers for DI */ oDataContainer &getDataBuffers(pvoid &data, pvoid &olddata, pvectorstr &strData) const; - static bool comparePunchTime(oPunch *p1, oPunch *p2); - void changedObject(); mutable string punchString; @@ -132,6 +130,9 @@ public: void importPunches(const string &s); const string &getPunchString() const; + void merge(const oBase &input) final; + pair getCardHash() const; + void Set(const xmlobject &xo); bool Write(xmlparser &xml); diff --git a/code/oClass.cpp b/code/oClass.cpp index 18a665c..cba148b 100644 --- a/code/oClass.cpp +++ b/code/oClass.cpp @@ -107,7 +107,7 @@ bool oClass::Write(xmlparser &xml) xml.startTag("Class"); xml.write("Id", Id); - xml.write("Updated", Modified.getStamp()); + xml.write("Updated", getStamp()); xml.write("Name", Name); if (Course) @@ -719,13 +719,13 @@ pClass oEvent::addClass(const wstring &pname, int CourseId, int classId) c.Course=getCourse(CourseId); Classes.push_back(c); - Classes.back().addToEvent(); + Classes.back().addToEvent(this, &c); Classes.back().synchronize(); updateTabs(); return &Classes.back(); } -pClass oEvent::addClass(oClass &c) +pClass oEvent::addClass(const oClass &c) { if (c.Id==0) return 0; @@ -736,9 +736,9 @@ pClass oEvent::addClass(oClass &c) } Classes.push_back(c); - Classes.back().addToEvent(); + Classes.back().addToEvent(this, &c); - if (!Classes.back().existInDB() && !c.isImplicitlyCreated()) { + if (HasDBConnection && !Classes.back().existInDB() && !c.isImplicitlyCreated()) { Classes.back().changed = true; Classes.back().synchronize(); } diff --git a/code/oClass.h b/code/oClass.h index a4f1a07..5fd1bc8 100644 --- a/code/oClass.h +++ b/code/oClass.h @@ -707,6 +707,8 @@ public: void setResultModule(const string &tag); const string &getResultModuleTag() const; + void merge(const oBase &input) final; + oClass(oEvent *poe); oClass(oEvent *poe, int id); virtual ~oClass(); diff --git a/code/oClub.cpp b/code/oClub.cpp index b2e9d46..e3ece12 100644 --- a/code/oClub.cpp +++ b/code/oClub.cpp @@ -72,7 +72,7 @@ bool oClub::write(xmlparser &xml) xml.startTag("Club"); xml.write("Id", Id); - xml.write("Updated", Modified.getStamp()); + xml.write("Updated", getStamp()); xml.write("Name", name); for (size_t k=0;k &numbers) const; + void merge(const oBase &input) final; + void set(const xmlobject *xo); void set(int pId, int pNumber, wstring pName); bool write(xmlparser &xml); diff --git a/code/oCourse.cpp b/code/oCourse.cpp index 2533f54..435229e 100644 --- a/code/oCourse.cpp +++ b/code/oCourse.cpp @@ -84,7 +84,7 @@ bool oCourse::Write(xmlparser &xml) xml.startTag("Course"); xml.write("Id", Id); - xml.write("Updated", Modified.getStamp()); + xml.write("Updated", getStamp()); xml.write("Name", Name); xml.write("Length", Length); xml.write("Controls", getControls()); @@ -114,7 +114,7 @@ void oCourse::Set(const xmlobject *xo) Name=it->getw(); } else if (it->is("Controls")){ - importControls(it->getRaw(), false); + importControls(it->getRaw(), false, false); } else if (it->is("Legs")) { importLegLengths(it->getRaw(), false); @@ -266,7 +266,7 @@ void oCourse::splitControls(const string &ctrls, vector &nr) { } } -bool oCourse::importControls(const string &ctrls, bool updateLegLengths) { +bool oCourse::importControls(const string &ctrls, bool setChanged, bool updateLegLengths) { int oldNC = nControls; vector oldC; for (int k = 0; kgetId(); if (changed) { - updateChanged(); + if (setChanged) + updateChanged(); oe->punchIndex.clear(); } diff --git a/code/oCourse.h b/code/oCourse.h index 7eed4cc..5c35d44 100644 --- a/code/oCourse.h +++ b/code/oCourse.h @@ -209,7 +209,7 @@ public: bool fillCourse(gdioutput &gdi, const string &name); /** Returns true if changed. */ - bool importControls(const string &cstring, bool updateLegLengths); + bool importControls(const string &cstring, bool setChanged, bool updateLegLengths); void importLegLengths(const string &legs, bool setChanged); /** Returns the length of the i:th leg (or 0 if unknown)*/ @@ -239,6 +239,8 @@ public: wstring getStart() const; void setStart(const wstring &start, bool sync); + void merge(const oBase &input) final; + bool Write(xmlparser &xml); oCourse(oEvent *poe, int id); diff --git a/code/oEvent.cpp b/code/oEvent.cpp index 12a1205..97d67b8 100644 --- a/code/oEvent.cpp +++ b/code/oEvent.cpp @@ -64,7 +64,7 @@ #include "Table.h" //Version of database -int oEvent::dbVersion = 83; +int oEvent::dbVersion = 84; class RelativeTimeFormatter : public oDataDefiner { string name; @@ -343,6 +343,10 @@ oEvent::oEvent(gdioutput &gdi):oBase(0), gdibase(gdi) oEventData->addVariableString("PayModes", "Betalsätt"); oEventData->addVariableInt("TransferFlags", oDataContainer::oIS32, "Överföring"); oEventData->addVariableDate("InvoiceDate", "Fakturadatum"); + oEventData->addVariableString("StartGroups", "Startgrupper"); + oEventData->addVariableString("MergeTag", 12, "Tag"); + oEventData->addVariableString("MergeInfo", "MergeInfo"); + oEventData->addVariableString("ImportStamp", 14, "Stamp"); oEventData->initData(this, dataSize); @@ -407,6 +411,8 @@ oEvent::oEvent(gdioutput &gdi):oBase(0), gdibase(gdi) oRunnerData->addVariableInt("Reference", oDataContainer::oIS32, "Referens", make_shared()); oRunnerData->addVariableInt("NoRestart", oDataContainer::oIS8U, "Ej omstart", make_shared("NoRestart")); oRunnerData->addVariableString("InputResult", "Tidigare resultat", make_shared()); + oRunnerData->addVariableInt("StartGroup", oDataContainer::oIS32, "Startgrupp"); + oRunnerData->addVariableInt("Family", oDataContainer::oIS32, "Familj"); oControlData=new oDataContainer(oControl::dataSize); oControlData->addVariableInt("TimeAdjust", oDataContainer::oIS32, "Tidsjustering"); @@ -673,7 +679,7 @@ pControl oEvent::addControl(const oControl &oc) qFreeControlId = max (qFreeControlId, Id); Controls.push_back(oc); - oe->Controls.back().addToEvent(); + oe->Controls.back().addToEvent(this, &oc); return &Controls.back(); } @@ -818,7 +824,7 @@ bool oEvent::writeCards(xmlparser &xml) return true; } -void oEvent::duplicate() { +void oEvent::duplicate(const wstring &annotationIn) { wchar_t file[260]; wchar_t filename[64]; wchar_t nameid[64]; @@ -853,18 +859,26 @@ void oEvent::duplicate() { swprintf_s(filename, L"%d/%d %d:%02d", st.wDay, st.wMonth, st.wHour, st.wMinute); - wstring anno = lang.tl(L"Kopia (X)#" + wstring(filename)); - anno = oldAnno.empty() ? anno : oldAnno + L" " + anno; - setAnnotation(anno); - + if (annotationIn.empty()) { + wstring anno = lang.tl(L"Kopia (X)#" + wstring(filename)); + anno = oldAnno.empty() ? anno : oldAnno + L" " + anno; + setAnnotation(anno); + } + else { + setAnnotation(annotationIn); + } + wstring oldTag = getMergeTag(); try { + getMergeTag(true); save(); } catch(...) { + getDI().setString("MergeTag", oldTag); // Restore in case of error wcscpy_s(CurrentFile, oldFile); currentNameId = oldId; setAnnotation(oldAnno); + synchronize(true); throw; } @@ -872,6 +886,8 @@ void oEvent::duplicate() { wcscpy_s(CurrentFile, oldFile); currentNameId = oldId; setAnnotation(oldAnno); + getDI().setString("MergeTag", oldTag); + synchronize(true); } bool oEvent::save() @@ -979,7 +995,7 @@ bool oEvent::save(const wstring &fileIn) { xml.write("NameId", currentNameId); xml.write("Annotation", Annotation); xml.write("Id", Id); - xml.write("Updated", Modified.getStamp()); + xml.write("Updated", getStamp()); oEventData->write(this, xml); @@ -1082,13 +1098,17 @@ bool oEvent::open(int id) if (it->Server.empty()) { if (id == it->Id) { CompetitionInfo ci=*it; //Take copy - return open(ci.FullPath.c_str()); + return open(ci.FullPath.c_str(), false, false); } } else if (!it->Server.empty()) { if (id == (10000000+it->Id)) { CompetitionInfo ci=*it; //Take copy - return readSynchronize(ci); + if (readSynchronize(ci)) { + getMergeTag(); + return true; + } + return false; } } } @@ -1116,8 +1136,7 @@ static void toc(const string &str) { } -bool oEvent::open(const wstring &file, bool Import) -{ +bool oEvent::open(const wstring &file, bool Import, bool forMerge) { if (!Import) openFileLock->lockFile(file); @@ -1159,6 +1178,13 @@ bool oEvent::open(const wstring &file, bool Import) bool res = open(xml); if (res && !Import) openFileLock->lockFile(file); + + getMergeTag(Import && !forMerge); + + if (Import && !forMerge) { + getDI().setString("ImportStamp", gdibase.widen(getLastModified())); + } + return res; } @@ -1265,7 +1291,7 @@ bool oEvent::open(const xmlparser &xml) { c.Set(&*it); if (c.Id>0 && knownClass.count(c.Id) == 0) { Classes.push_back(c); - Classes.back().addToEvent(); + Classes.back().addToEvent(this, &c); knownClass.insert(c.Id); } } @@ -1330,8 +1356,8 @@ bool oEvent::open(const xmlparser &xml) { oTeam t(this, 0); t.set(*it); if (t.Id>0){ - Teams.push_back(t); - teamById[t.Id] = &Teams.back(); + //Teams.push_back(t); + addTeam(t, false); Teams.back().apply(ChangeType::Quiet, nullptr); } } @@ -1635,10 +1661,10 @@ pCourse oEvent::addCourse(const oCourse &oc) qFreeCourseId=max(qFreeCourseId, oc.getId()); pCourse pc = &Courses.back(); - pc->addToEvent(); + pc->addToEvent(this, &oc); - if (!pc->existInDB() && !pc->isImplicitlyCreated()) { - pc->updateChanged(); + if (HasDBConnection && !pc->existInDB() && !pc->isImplicitlyCreated()) { + pc->changed = true; pc->synchronize(); } courseIdIndex[oc.Id] = pc; @@ -1786,7 +1812,7 @@ pRunner oEvent::addRunner(const oRunner &r, bool updateStartNo) { Runners.push_back(r); pRunner pr=&Runners.back(); - pr->addToEvent(); + pr->addToEvent(this, &r); for (size_t i = 0; i < pr->multiRunner.size(); i++) { if (pr->multiRunner[i]) { @@ -2112,7 +2138,7 @@ pCard oEvent::allocateCard(pRunner owner) c.tOwner = owner; Cards.push_back(c); pCard newCard = &Cards.back(); - newCard->addToEvent(); + newCard->addToEvent(this, &c); return newCard; } @@ -3686,6 +3712,7 @@ void oEvent::newCompetition(const wstring &name) openFileLock->unlockFile(); clear(); + SYSTEMTIME st; GetLocalTime(&st); @@ -3695,6 +3722,9 @@ void oEvent::newCompetition(const wstring &name) Name = name; oEventData->initData(this, sizeof(oData)); + if (!name.empty() && name != L"-") + getMergeTag(); + getDI().setString("Organizer", getPropertyString("Organizer", L"")); getDI().setString("Street", getPropertyString("Street", L"")); getDI().setString("Address", getPropertyString("Address", L"")); @@ -5855,719 +5885,6 @@ void oEvent::setCurrency(int factor, const wstring &symbol, const wstring &separ } } -wstring oEvent::cloneCompetition(bool cloneRunners, bool cloneTimes, - bool cloneCourses, bool cloneResult, bool addToDate) { - - if (cloneResult) { - cloneTimes = true; - cloneCourses = true; - } - if (cloneTimes) - cloneRunners = true; - - oEvent ce(gdibase); - ce.newCompetition(Name); - ce.ZeroTime = ZeroTime; - ce.Date = Date; - - if (addToDate) { - SYSTEMTIME st; - convertDateYMS(Date, st, false); - __int64 absD = SystemTimeToInt64Second(st); - absD += 3600*24; - ce.Date = convertSystemDate(Int64SecondToSystemTime(absD)); - } - int len = Name.length(); - if (len > 2 && isdigit(Name[len-1]) && !isdigit(Name[len-2])) { - ++ce.Name[len-1]; // E1 -> E2 - } - else - ce.Name += L" E2"; - - memcpy(ce.oData, oData, sizeof(oData)); - - for (oClubList::iterator it = Clubs.begin(); it != Clubs.end(); ++it) { - if (it->isRemoved()) - continue; - pClub pc = ce.addClub(it->name, it->Id); - memcpy(pc->oData, it->oData, sizeof(pc->oData)); - } - - if (cloneCourses) { - for (oControlList::iterator it = Controls.begin(); it != Controls.end(); ++it) { - if (it->isRemoved()) - continue; - pControl pc = ce.addControl(it->Id, 100, it->Name); - pc->setNumbers(it->codeNumbers()); - pc->Status = it->Status; - memcpy(pc->oData, it->oData, sizeof(pc->oData)); - } - - for (oCourseList::iterator it = Courses.begin(); it != Courses.end(); ++it) { - if (it->isRemoved()) - continue; - pCourse pc = ce.addCourse(it->Name, it->Length, it->Id); - pc->importControls(it->getControls(), false); - pc->legLengths = it->legLengths; - memcpy(pc->oData, it->oData, sizeof(pc->oData)); - } - } - - for (oClassList::iterator it = Classes.begin(); it != Classes.end(); ++it) { - if (it->isRemoved()) - continue; - pClass pc = ce.addClass(it->Name, 0, it->Id); - memcpy(pc->oData, it->oData, sizeof(pc->oData)); - pc->setNumStages(it->getNumStages()); - pc->legInfo = it->legInfo; - - if (cloneCourses) { - pc->Course = ce.getCourse(it->getCourseId()); - pc->MultiCourse = it->MultiCourse; // Points to wrong competition, but valid for now... - } - } - - if (cloneRunners) { - for (oRunnerList::iterator it = Runners.begin(); it != Runners.end(); ++it) { - if (it->isRemoved()) - continue; - - oRunner r(&ce, it->Id); - r.sName = it->sName; - r.getRealName(r.sName, r.tRealName); - r.StartNo = it->StartNo; - r.cardNumber = it->cardNumber; - r.Club = ce.getClub(it->getClubId()); - r.Class = ce.getClass(it->getClassId(false)); - - if (cloneCourses) - r.Course = ce.getCourse(it->getCourseId()); - - pRunner pr = ce.addRunner(r, false); - - pr->decodeMultiR(it->codeMultiR()); - memcpy(pr->oData, it->oData, sizeof(pr->oData)); - - if (cloneTimes) { - pr->startTime = it->startTime; - } - - if (cloneResult) { - if (it->Card) { - pr->Card = ce.addCard(*it->Card); - pr->Card->tOwner = pr; - } - pr->FinishTime = it->FinishTime; - pr->status = it->status; - } - } - - for (oTeamList::iterator it = Teams.begin(); it != Teams.end(); ++it) { - if (it->skip()) - continue; - - oTeam t(&ce, it->Id); - - t.sName = it->sName; - t.StartNo = it->StartNo; - t.Club = ce.getClub(it->getClubId()); - t.Class = ce.getClass(it->getClassId(false)); - - if (cloneTimes) - t.startTime = it->startTime; - - pTeam pt = ce.addTeam(t, false); - memcpy(pt->oData, it->oData, sizeof(pt->oData)); - - pt->Runners.resize(it->Runners.size()); - for (size_t k = 0; kRunners.size(); k++) { - int id = it->Runners[k] ? it->Runners[k]->Id : 0; - if (id) - pt->Runners[k] = ce.getRunner(id, 0); - } - - t.apply(ChangeType::Update, nullptr); - } - - for (oRunnerList::iterator it = ce.Runners.begin(); it != ce.Runners.end(); ++it) { - it->createMultiRunner(false, false); - } - } - - vector changedClass, changedClassNoResult, assignedVacant, newEntries, notTransfered, noAssign; - set dummy; - transferResult(ce, dummy, TransferAnyway, false, changedClass, changedClassNoResult, assignedVacant, newEntries, notTransfered, noAssign); - - vector newEntriesT, notTransferedT, noAssignT; - transferResult(ce, TransferAnyway, newEntriesT, notTransferedT, noAssignT); - - int eventNumberCurrent = getStageNumber(); - if (eventNumberCurrent <= 0) { - eventNumberCurrent = 1; - setStageNumber(eventNumberCurrent); - } - - ce.getDI().setString("PreEvent", currentNameId); - ce.setStageNumber(eventNumberCurrent + 1); - getDI().setString("PostEvent", ce.currentNameId); - - int nf = getMeOSFeatures().getNumFeatures(); - for (int k = 0; k < nf; k++) { - MeOSFeatures::Feature f = getMeOSFeatures().getFeature(k); - if (getMeOSFeatures().hasFeature(f)) - ce.getMeOSFeatures().useFeature(f, true, ce); - } - - // Transfer lists and list configurations. - if (listContainer) { - loadGeneralResults(false, false); - swap(ce.generalResults, generalResults); - try { - ce.listContainer = new MetaListContainer(&ce, *listContainer); - ce.save(); - } - catch (...) { - swap(ce.generalResults, generalResults); - throw; - } - - swap(ce.generalResults, generalResults); - } - return ce.CurrentFile; -} - -void oEvent::transferListsAndSave(const oEvent &src) { - src.loadGeneralResults(false, false); - swap(src.generalResults, generalResults); - try { - src.getListContainer().synchronizeTo(getListContainer()); - save(); - } - catch (...) { - swap(src.generalResults, generalResults); - throw; - } - - swap(src.generalResults, generalResults); -} - - -bool checkTargetClass(pRunner target, pRunner source, - const oClassList &Classes, - const vector &targetVacant, - vector &changedClass, - oEvent::ChangedClassMethod changeClassMethod) { - if (changeClassMethod == oEvent::TransferAnyway) - return true; - - if (!compareClassName(target->getClass(false), source->getClass(false))) { - // Store all vacant positions in the right class - int targetClass = -1; - - if (target->getStatus() == StatusOK) { - // There is already a result. Do not change class! - return false; - } - - if (changeClassMethod == oEvent::TransferNoResult) - return false; // Do not allow change class, do not transfer result - - for (oClassList::const_iterator cit = Classes.begin(); cit != Classes.end(); ++cit) { - if (cit->isRemoved()) - continue; - if (compareClassName(cit->getName(), source->getClass(false))) { - targetClass = cit->getId(); - - if (targetClass == source->getClassId(false) || cit->getName() == source->getClass(false)) - break; // Assume exact match - } - } - - if (targetClass != -1) { - set vacantIx; - for (size_t j = 0; j < targetVacant.size(); j++) { - if (!targetVacant[j]) - continue; - if (targetVacant[j]->getClassId(false) == targetClass) - vacantIx.insert(j); - } - int posToUse = -1; - if (vacantIx.size() == 1) - posToUse = *vacantIx.begin(); - else if (vacantIx.size() > 1) { - wstring srcBib = source->getBib(); - if (srcBib.length() > 0) { - for (set::iterator tit = vacantIx.begin(); tit != vacantIx.end(); ++tit) { - if (targetVacant[*tit]->getBib() == srcBib) { - posToUse = *tit; - break; - } - } - } - - if (posToUse == -1) - posToUse = *vacantIx.begin(); - } - - if (posToUse != -1) { - // Change class or change class vacant - changedClass.push_back(target); - - int oldStart = target->getStartTime(); - wstring oldBib = target->getBib(); - int oldSN = target->getStartNo(); - int oldClass = target->getClassId(false); - pRunner tgt = targetVacant[posToUse]; - target->cloneStartTime(tgt); - target->setBib(tgt->getBib(), 0, false); - target->setStartNo(tgt->getStartNo(), oBase::ChangeType::Update); - target->setClassId(tgt->getClassId(false), false); - - tgt->setStartTime(oldStart, true, oBase::ChangeType::Update); - tgt->setBib(oldBib, 0, false); - tgt->setStartNo(oldSN, oBase::ChangeType::Update); - tgt->setClassId(oldClass, false); - return true; // Changed to correct class - } - else if (changeClassMethod == oEvent::ChangeClass) { - // Simpliy change class - target->setClassId(targetClass, false); - return true; - } - } - return false; // Wrong class, ChangeClass (but failed) - } - - return true; // Same class, OK -} - -void oEvent::transferResult(oEvent &ce, - const set &allowNewEntries, - ChangedClassMethod changeClassMethod, - bool transferAllNoCompete, - vector &changedClass, - vector &changedClassNoResult, - vector &assignedVacant, - vector &newEntries, - vector ¬Transfered, - vector &noAssignmentTarget) { - - inthashmap processed(ce.Runners.size()); - inthashmap used(Runners.size()); - - changedClass.clear(); - changedClassNoResult.clear(); - assignedVacant.clear(); - newEntries.clear(); - notTransfered.clear(); - noAssignmentTarget.clear(); - - vector targetRunners; - vector targetVacant; - - targetRunners.reserve(ce.Runners.size()); - for (oRunnerList::iterator it = ce.Runners.begin(); it != ce.Runners.end(); ++it) { - if (!it->skip()) { - if (!it->isVacant()) - targetRunners.push_back(&*it); - else - targetVacant.push_back(&*it); - } - } - - calculateResults({}, ResultType::TotalResult); - // Lookup by id - for (size_t k = 0; k < targetRunners.size(); k++) { - pRunner it = targetRunners[k]; - pRunner r = getRunner(it->Id, 0); - if (!r) - continue; - - __int64 id1 = r->getExtIdentifier(); - __int64 id2 = it->getExtIdentifier(); - - if (id1>0 && id2>0 && id1 != id2) - continue; - - wstring cnA = canonizeName(it->sName.c_str()); - wstring cnB = canonizeName(r->sName.c_str()); - wstring ccnA = canonizeName(it->getClub().c_str()); - wstring ccnB = canonizeName(r->getClub().c_str()); - - if ((id1>0 && id1==id2) || - (r->cardNumber>0 && r->cardNumber == it->cardNumber) || - (it->sName == r->sName) || (cnA == cnB && ccnA == ccnB)) { - processed.insert(it->Id, 1); - used.insert(r->Id, 1); - if (checkTargetClass(it, r, ce.Classes, targetVacant, changedClass, changeClassMethod)) - it->setInputData(*r); - else { - it->resetInputData(); - changedClassNoResult.push_back(it); - } - } - } - - if (processed.size() < int(targetRunners.size())) { - // Lookup by card - int v; - for (size_t k = 0; k < targetRunners.size(); k++) { - pRunner it = targetRunners[k]; - if (processed.lookup(it->Id, v)) - continue; - if (it->getCardNo() > 0) { - pRunner r = getRunnerByCardNo(it->getCardNo(), 0, CardLookupProperty::Any); - - if (!r || used.lookup(r->Id, v)) - continue; - - __int64 id1 = r->getExtIdentifier(); - __int64 id2 = it->getExtIdentifier(); - - if (id1>0 && id2>0 && id1 != id2) - continue; - - if ((id1>0 && id1==id2) || (it->sName == r->sName && it->getClub() == r->getClub())) { - processed.insert(it->Id, 1); - used.insert(r->Id, 1); - if (checkTargetClass(it, r, ce.Classes, targetVacant, changedClass, changeClassMethod)) - it->setInputData(*r); - else { - it->resetInputData(); - changedClassNoResult.push_back(it); - } - } - } - } - } - - int v = -1; - - // Store remaining runners - vector remainingRunners; - for (oRunnerList::iterator it2 = Runners.begin(); it2 != Runners.end(); ++it2) { - if (it2->skip() || used.lookup(it2->Id, v)) - continue; - if (it2->isVacant()) - continue; // Ignore vacancies on source side - - remainingRunners.push_back(&*it2); - } - - if (processed.size() < int(targetRunners.size()) && !remainingRunners.empty()) { - // Lookup by name / ext id - vector cnd; - for (size_t k = 0; k < targetRunners.size(); k++) { - pRunner it = targetRunners[k]; - if (processed.lookup(it->Id, v)) - continue; - - __int64 id1 = it->getExtIdentifier(); - - cnd.clear(); - for (size_t j = 0; j < remainingRunners.size(); j++) { - pRunner src = remainingRunners[j]; - if (!src) - continue; - - if (id1 > 0) { - __int64 id2 = src->getExtIdentifier(); - if (id2 == id1) { - cnd.clear(); - cnd.push_back(j); - break; //This is the one, if they have the same Id there will be a unique match below - } - } - if (it->sName == src->sName && it->getClub() == src->getClub()) - cnd.push_back(j); - } - - if (cnd.size() == 1) { - pRunner &src = remainingRunners[cnd[0]]; - processed.insert(it->Id, 1); - used.insert(src->Id, 1); - if (checkTargetClass(it, src, ce.Classes, targetVacant, changedClass, changeClassMethod)) { - it->setInputData(*src); - } - else { - it->resetInputData(); - changedClassNoResult.push_back(it); - } - src = 0; - } - else if (cnd.size() > 0) { // More than one candidate - int winnerIx = -1; - int point = -1; - for (size_t j = 0; j < cnd.size(); j++) { - pRunner src = remainingRunners[cnd[j]]; - int p = 0; - if (src->getClass(false) == it->getClass(false)) - p += 1; - if (src->getBirthYear() == it->getBirthYear()) - p += 2; - if (p > point) { - winnerIx = cnd[j]; - point = p; - } - } - - if (winnerIx != -1) { - processed.insert(it->Id, 1); - pRunner winner = remainingRunners[winnerIx]; - remainingRunners[winnerIx] = 0; - - used.insert(winner->Id, 1); - if (checkTargetClass(it, winner, ce.Classes, targetVacant, changedClass, changeClassMethod)) { - it->setInputData(*winner); - } - else { - it->resetInputData(); - changedClassNoResult.push_back(it); - } - } - } - } - } - - // Transfer vacancies - for (size_t k = 0; k < remainingRunners.size(); k++) { - pRunner src = remainingRunners[k]; - if (!src || used.lookup(src->Id, v)) - continue; - - bool forceSkip = src->hasFlag(oAbstractRunner::FlagTransferSpecified) && - !src->hasFlag(oAbstractRunner::FlagTransferNew); - - if (forceSkip) { - notTransfered.push_back(src); - continue; - } - - pRunner targetVacant = ce.getRunner(src->getId(), 0); - if (targetVacant && targetVacant->isVacant() && compareClassName(targetVacant->getClass(false), src->getClass(false)) ) { - targetVacant->setName(src->getName(), false); - targetVacant->setClub(src->getClub()); - targetVacant->setCardNo(src->getCardNo(), false); - targetVacant->cloneData(src); - assignedVacant.push_back(targetVacant); - } - else { - pClass dstClass = ce.getClass(src->getClassId(false)); - if (dstClass && compareClassName(dstClass->getName(), src->getClass(false))) { - if ( (!src->hasFlag(oAbstractRunner::FlagTransferSpecified) && allowNewEntries.count(src->getClassId(false))) - || src->hasFlag(oAbstractRunner::FlagTransferNew)) { - if (src->getClubId() > 0) - ce.getClubCreate(src->getClubId(), src->getClub()); - pRunner dst = ce.addRunner(src->getName(), src->getClubId(), src->getClassId(false), - src->getCardNo(), src->getBirthYear(), true); - dst->cloneData(src); - dst->setInputData(*src); - newEntries.push_back(dst); - } - else if (transferAllNoCompete) { - if (src->getClubId() > 0) - ce.getClubCreate(src->getClubId(), src->getClub()); - pRunner dst = ce.addRunner(src->getName(), src->getClubId(), src->getClassId(false), - 0, src->getBirthYear(), true); - dst->cloneData(src); - dst->setInputData(*src); - dst->setStatus(StatusNotCompetiting, true, ChangeType::Update); - notTransfered.push_back(dst); - } - else - notTransfered.push_back(src); - } - } - } - - // Runners on target side not assigned a result - for (size_t k = 0; k < targetRunners.size(); k++) { - if (targetRunners[k] && !processed.count(targetRunners[k]->Id)) { - noAssignmentTarget.push_back(targetRunners[k]); - if (targetRunners[k]->inputStatus == StatusUnknown || - (targetRunners[k]->inputStatus == StatusOK && targetRunners[k]->inputTime == 0)) { - targetRunners[k]->inputStatus = StatusNotCompetiting; - } - } - } -} - -void oEvent::transferResult(oEvent &ce, - ChangedClassMethod changeClassMethod, - vector &newEntries, - vector ¬Transfered, - vector &noAssignmentTarget) { - - inthashmap processed(ce.Teams.size()); - inthashmap used(Teams.size()); - - newEntries.clear(); - notTransfered.clear(); - noAssignmentTarget.clear(); - - vector targetTeams; - - targetTeams.reserve(ce.Teams.size()); - for (oTeamList::iterator it = ce.Teams.begin(); it != ce.Teams.end(); ++it) { - if (!it->skip()) { - targetTeams.push_back(&*it); - } - } - - calculateTeamResults(set(), ResultType::TotalResult); - // Lookup by id - for (size_t k = 0; k < targetTeams.size(); k++) { - pTeam it = targetTeams[k]; - pTeam t = getTeam(it->Id); - if (!t) - continue; - - __int64 id1 = t->getExtIdentifier(); - __int64 id2 = it->getExtIdentifier(); - - if (id1>0 && id2>0 && id1 != id2) - continue; - - if ((id1>0 && id1==id2) || (it->sName == t->sName && it->getClub() == t->getClub())) { - processed.insert(it->Id, 1); - used.insert(t->Id, 1); - it->setInputData(*t); - //checkTargetClass(it, r, ce.Classes, targetVacant, changedClass); - } - } - - int v = -1; - - // Store remaining runners - vector remainingTeams; - for (oTeamList::iterator it2 = Teams.begin(); it2 != Teams.end(); ++it2) { - if (it2->skip() || used.lookup(it2->Id, v)) - continue; - if (it2->isVacant()) - continue; // Ignore vacancies on source side - - remainingTeams.push_back(&*it2); - } - - if (processed.size() < int(targetTeams.size()) && !remainingTeams.empty()) { - // Lookup by name / ext id - vector cnd; - for (size_t k = 0; k < targetTeams.size(); k++) { - pTeam it = targetTeams[k]; - if (processed.lookup(it->Id, v)) - continue; - - __int64 id1 = it->getExtIdentifier(); - - cnd.clear(); - for (size_t j = 0; j < remainingTeams.size(); j++) { - pTeam src = remainingTeams[j]; - if (!src) - continue; - - if (id1 > 0) { - __int64 id2 = src->getExtIdentifier(); - if (id2 == id1) { - cnd.clear(); - cnd.push_back(j); - break; //This is the one, if they have the same Id there will be a unique match below - } - } - - if (it->sName == src->sName && it->getClub() == src->getClub()) - cnd.push_back(j); - } - - if (cnd.size() == 1) { - pTeam &src = remainingTeams[cnd[0]]; - processed.insert(it->Id, 1); - used.insert(src->Id, 1); - it->setInputData(*src); - //checkTargetClass(it, src, ce.Classes, targetVacant, changedClass); - src = 0; - } - else if (cnd.size() > 0) { // More than one candidate - int winnerIx = -1; - int point = -1; - for (size_t j = 0; j < cnd.size(); j++) { - pTeam src = remainingTeams[cnd[j]]; - int p = 0; - if (src->getClass(false) == it->getClass(false)) - p += 1; - if (p > point) { - winnerIx = cnd[j]; - point = p; - } - } - - if (winnerIx != -1) { - processed.insert(it->Id, 1); - pTeam winner = remainingTeams[winnerIx]; - remainingTeams[winnerIx] = 0; - - used.insert(winner->Id, 1); - it->setInputData(*winner); - //checkTargetClass(it, winner, ce.Classes, targetVacant, changedClass); - } - } - } - } -/* - // Transfer vacancies - for (size_t k = 0; k < remainingRunners.size(); k++) { - pRunner src = remainingRunners[k]; - if (!src || used.lookup(src->Id, v)) - continue; - - pRunner targetVacant = ce.getRunner(src->getId(), 0); - if (targetVacant && targetVacant->isVacant() && compareClassName(targetVacant->getClass(), src->getClass()) ) { - targetVacant->setName(src->getName()); - targetVacant->setClub(src->getClub()); - targetVacant->setCardNo(src->getCardNo(), false); - targetVacant->cloneData(src); - assignedVacant.push_back(targetVacant); - } - else { - pClass dstClass = ce.getClass(src->getClassId()); - if (dstClass && compareClassName(dstClass->getName(), src->getClass())) { - if (allowNewEntries.count(src->getClassId())) { - if (src->getClubId() > 0) - ce.getClubCreate(src->getClubId(), src->getClub()); - pRunner dst = ce.addRunner(src->getName(), src->getClubId(), src->getClassId(), - src->getCardNo(), src->getBirthYear(), true); - dst->cloneData(src); - dst->setInputData(*src); - newEntries.push_back(dst); - } - else if (transferAllNoCompete) { - if (src->getClubId() > 0) - ce.getClubCreate(src->getClubId(), src->getClub()); - pRunner dst = ce.addRunner(src->getName(), src->getClubId(), src->getClassId(), - 0, src->getBirthYear(), true); - dst->cloneData(src); - dst->setInputData(*src); - dst->setStatus(StatusNotCompetiting); - notTransfered.push_back(dst); - } - else - notTransfered.push_back(src); - } - } - } - - // Runners on target side not assigned a result - for (size_t k = 0; k < targetRunners.size(); k++) { - if (targetRunners[k] && !processed.count(targetRunners[k]->Id)) { - noAssignmentTarget.push_back(targetRunners[k]); - if (targetRunners[k]->inputStatus == StatusUnknown || - (targetRunners[k]->inputStatus == StatusOK && targetRunners[k]->inputTime == 0)) { - targetRunners[k]->inputStatus = StatusNotCompetiting; - } - } - - }*/ -} - MetaListContainer &oEvent::getListContainer() const { if (!listContainer) throw std::exception("Nullpointer exception"); @@ -6945,3 +6262,61 @@ void oEvent::setFlag(TransferFlags flag, bool onoff) { cf = onoff ? (cf | flag) : (cf & (~flag)); getDI().setInt("TransferFlags", cf); } + +string oEvent::encodeStartGroups() const { + string ss; + string tmp; + for (auto &sg : startGroups) { + tmp = itos(sg.first) + "," + + itos(sg.second.first) + "," + itos(sg.second.second); + if (ss.empty()) + ss = tmp; + else + ss += ";" + tmp; + } + return ss; +} + +void oEvent::decodeStartGroups(const string &enc) const { + vector g, sg; + split(enc, ";", g); + startGroups.clear(); + for (string &grp : g) { + split(grp, ",", sg); + if (sg.size() == 3) { + int id = atoi(sg[0].c_str()); + int start = atoi(sg[1].c_str()); + int end = atoi(sg[2].c_str()); + startGroups.emplace(id, make_pair(start, end)); + } + } +} + +void oEvent::setStartGroup(int id, int firstStart, int lastStart) { + if (firstStart < 0) + startGroups.erase(id); + else + startGroups[id] = make_pair(firstStart, lastStart); +} + +void oEvent::updateStartGroups() { + getDI().setString("StartGroups", gdibase.widen(encodeStartGroups())); +} + +void oEvent::readStartGroups() const { + auto &sg = getDCI().getString("StartGroups"); + decodeStartGroups(gdibase.narrow(sg)); +} + +const map> &oEvent::getStartGroups(bool reload) const { + if (reload) + readStartGroups(); + return startGroups; +} + +pair oEvent::getStartGroup(int id) const { + if (startGroups.count(id)) + return startGroups.find(id)->second; + else + return make_pair(-1, -1); +} diff --git a/code/oEvent.h b/code/oEvent.h index 1608514..5a063c1 100644 --- a/code/oEvent.h +++ b/code/oEvent.h @@ -442,10 +442,24 @@ protected: mutable vector generalResults; + // Start group id -> first, last start + mutable map> startGroups; + + string encodeStartGroups() const; + void decodeStartGroups(const string &enc) const; + // Temporarily disable recaluclate leader times bool disableRecalculate; public: + void setStartGroup(int id, int firstStart, int lastStart); + void updateStartGroups(); // Update to source + void readStartGroups() const; // Read from source. + + pair getStartGroup(int id) const; + const map> &getStartGroups(bool reload) const; + + enum TransferFlags { FlagManualName = 1, FlagManualDateTime = 2, @@ -949,7 +963,7 @@ public: bool exportOECSV(const wchar_t *file, int LanguageTypeIndex, bool includeSplits); bool save(); - void duplicate(); + void duplicate(const wstring &annotation); void newCompetition(const wstring &Name); void clearListedCmp(); bool enumerateCompetitions(const wchar_t *path, const wchar_t *extension); @@ -1053,11 +1067,15 @@ public: pCard allocateCard(pRunner owner); /** Optimize the start order based on drawInfo. Result in cInfo */ - void optimizeStartOrder(gdioutput &gdi, DrawInfo &drawInfo, vector &cInfo); + void optimizeStartOrder(vector> &outLines, DrawInfo &drawInfo, vector &cInfo); void loadDrawSettings(const set &classes, DrawInfo &drawInfo, vector &cInfo) const; void drawRemaining(DrawMethod method, bool placeAfter); + void drawListStartGroups(const vector &spec, + DrawMethod method, int pairSize, DrawType drawType, + bool limitGroupSize = true, + DrawInfo *di = nullptr); void drawList(const vector &spec, DrawMethod method, int pairSize, DrawType drawType); void drawListClumped(int classID, int firstStart, int interval, int vacances); @@ -1176,7 +1194,7 @@ public: void fillFees(gdioutput &gdi, const string &name, bool onlyDirect, bool withAuto) const; wstring getAutoClassName() const; pClass addClass(const wstring &pname, int CourseId = 0, int classId = 0); - pClass addClass(oClass &c); + pClass addClass(const oClass &c); /** Get a class if it exists, or create it. exactNames is a set of class names that must be matched exactly. It is extended with the name of the class added. The purpose is to allow very @@ -1242,7 +1260,7 @@ public: const vector< pair > &fillControlTypes(vector< pair > &out); bool open(int id); - bool open(const wstring &file, bool import=false); + bool open(const wstring &file, bool import, bool forMerge); bool open(const xmlparser &xml); bool save(const wstring &file); @@ -1283,6 +1301,8 @@ protected: /** type: 0 control, 1 start, 2 finish*/ bool addXMLControl(const xmlobject &xcontrol, int type); + void merge(const oBase &src) final; + public: const shared_ptr &getGeneralResult(const string &tag, wstring &sourceFileOut) const; @@ -1293,8 +1313,11 @@ public: void getPredefinedClassTypes(map &types) const; + void merge(oEvent &src, int &numAdd, int &numRemove, int &numUpdate); + string getLastModified() const; + wstring cloneCompetition(bool cloneRunners, bool cloneTimes, - bool cloneCourses, bool cloneResult, bool addToDate); + bool cloneCourses, bool cloneResult, bool addToDate); enum ChangedClassMethod { ChangeClassVacant, @@ -1323,6 +1346,10 @@ public: void transferListsAndSave(const oEvent &src); + wstring getMergeTag(bool forceReset = false); + wstring getMergeInfo(const wstring &tag) const; + void addMergeInfo(const wstring &tag, const wstring &version); + enum MultiStageType { MultiStageNone = 0, MultiStageSeparateEntry = 1, diff --git a/code/oEventDraw.cpp b/code/oEventDraw.cpp index b05c09d..0b6add4 100644 --- a/code/oEventDraw.cpp +++ b/code/oEventDraw.cpp @@ -27,6 +27,8 @@ #include #include #include +#include +#include #include "oEvent.h" #include "gdioutput.h" @@ -422,7 +424,6 @@ namespace { } } - class DrawOptimAlgo { private: oEvent * oe; @@ -503,7 +504,17 @@ private: if (!drawClass) continue; - int nr = c_it->getNumRunners(true, true, true); + int nr = 0; + if (ci.startGroupId == 0) + nr = c_it->getNumRunners(true, true, true); + else { + vector cr; + oe->getRunners(c_it->getId(), 0, cr, false); + for (pRunner r : cr) + if (r->getStartGroup(true) == ci.startGroupId) + nr++; + } + if (ci.nVacant == -1 || !ci.nVacantSpecified || di.changedVacancyInfo) { // Auto initialize int nVacancies = int(nr * di.vacancyFactor + 0.5); @@ -623,9 +634,11 @@ public: di.baseInterval = 1; di.minClassInterval = 0; } - + int startGroup = 0; // Calculate an estimated maximal class intervall for (size_t k = 0; k < cInfo.size(); k++) { + if (cInfo[k].startGroupId != 0) + startGroup = cInfo[k].startGroupId; // Need to be constant int quotient = maxSize / (cInfo[k].nRunners*di.baseInterval); if (quotient*di.baseInterval > di.maxClassInterval) @@ -645,13 +658,18 @@ public: // Fill up with non-drawn classes for (auto &it : Runners) { + if (it->isRemoved()) + continue; int st = it->getStartTime(); int relSt = st - di.firstStart; int relPos = relSt / di.baseInterval; if (st > 0 && relSt >= 0 && relPos < 3000 && (relSt%di.baseInterval) == 0) { - if (otherClasses.count(it->getClassId(false)) == 0) - continue; + int cid = it->getClassId(true); + if (otherClasses.count(cid) == 0) { + if (startGroup == 0 || startGroup == it->getStartGroup(true)) + continue; + } pClass cls = it->getClassRef(true); if (cls) { if (!di.startName.empty() && cls->getStart() != di.startName) @@ -661,16 +679,33 @@ public: continue; } - ClassInfo &ci = otherClasses[it->getClassId(false)]; + int unique, courseId; + auto res = otherClasses.find(cid); + if (res != otherClasses.end()) { + unique = res->second.unique; + courseId = res->second.courseId; + } + else { + res = di.classes.find(cid); + if (res != di.classes.end()) { + unique = res->second.unique; + courseId = res->second.courseId; + } + else { + int unique = 12345678; + int courseId = it->getCourse(false) ? it->getCourse(false)->getId() : 0; + } + } + int k = 0; - while (true) { + while (true) { if (k == StartField.size()) { StartField.push_back(vector< pair >()); StartField.back().resize(3000); } if (StartField[k][relPos].first == 0) { - StartField[k][relPos].first = ci.unique; - StartField[k][relPos].second = ci.courseId; + StartField[k][relPos].first = unique; + StartField[k][relPos].second = courseId; break; } k++; @@ -703,8 +738,14 @@ public: int minPos = 1000000; int minEndPos = 1000000; int minInterval = cInfo[k].interval; + int startV = di.minClassInterval / di.baseInterval; + int endV = cInfo[k].interval; - for (int i = di.minClassInterval / di.baseInterval; i <= cInfo[k].interval; i++) { + if (cInfo[k].fixedInterval != 0) { + endV = startV = cInfo[k].fixedInterval; + } + + for (int i = startV; i <= endV; i++) { int startpos = alternator % max(1, (bestEndPos - cInfo[k].nRunners * i) / 3); startpos = 0; @@ -744,7 +785,7 @@ public: } }; -void oEvent::optimizeStartOrder(gdioutput &gdi, DrawInfo &di, vector &cInfo) +void oEvent::optimizeStartOrder(vector> &outLines, DrawInfo &di, vector &cInfo) { if (Classes.size()==0) return; @@ -823,11 +864,11 @@ void oEvent::optimizeStartOrder(gdioutput &gdi, DrawInfo &di, vector vector< vector > > startField(di.nFields); drawOptim.optimizeStartOrder(startField, di, cInfo, opt.nControls, opt.alternator); - gdi.addString("", 0, "Identifierar X unika inledningar på banorna.#" + itos(di.numDistinctInit)); - gdi.addString("", 0, "Största gruppen med samma inledning har X platser.#" + itos(di.numRunnerSameInitMax)); - gdi.addString("", 0, "Antal löpare på vanligaste banan X.#" + itos(di.numRunnerSameCourseMax)); - gdi.addString("", 0, "Kortast teoretiska startdjup utan krockar är X minuter.#" + itos(di.minimalStartDepth/60)); - gdi.dropLine(); + outLines.emplace_back(0, L"Identifierar X unika inledningar på banorna.#" + itow(di.numDistinctInit)); + outLines.emplace_back(0, L"Största gruppen med samma inledning har X platser.#" + itow(di.numRunnerSameInitMax)); + outLines.emplace_back(0, L"Antal löpare på vanligaste banan X.#" + itow(di.numRunnerSameCourseMax)); + outLines.emplace_back(0, L"Kortast teoretiska startdjup utan krockar är X minuter.#" + itow(di.minimalStartDepth/60)); + outLines.emplace_back(0, L""); //Find last starter int last = opt.last; @@ -837,17 +878,17 @@ void oEvent::optimizeStartOrder(gdioutput &gdi, DrawInfo &di, vector laststart=max(laststart, ci.firstStart+(ci.nRunners-1)*ci.interval); } - gdi.addString("", 0, "Faktiskt startdjup: X minuter.#" + itos(((last+1) * di.baseInterval)/60)); + outLines.emplace_back(0, L"Faktiskt startdjup: X minuter.#" + itow(((last+1) * di.baseInterval)/60)); - gdi.addString("", 1, L"Sista start (nu tilldelad): X.#" + + outLines.emplace_back(1, L"Sista start (nu tilldelad): X.#" + oe->getAbsTime(laststart*di.baseInterval+di.firstStart)); - gdi.dropLine(); + outLines.emplace_back(0, L""); int nr; int T=0; int sum=0; - gdi.addString("", 1, "Antal startande per intervall (inklusive redan lottade):"); + outLines.emplace_back(1, L"Antal startande per intervall (inklusive redan lottade):"); string str=""; int empty=4; @@ -867,8 +908,8 @@ void oEvent::optimizeStartOrder(gdioutput &gdi, DrawInfo &di, vector str+=bf; } - gdi.addStringUT(10, str); - gdi.dropLine(); + outLines.emplace_back(10, L"#" + gdibase.widen(str)); + outLines.emplace_back(0, L""); } void oEvent::loadDrawSettings(const set &classes, DrawInfo &drawInfo, vector &cInfo) const { @@ -967,6 +1008,653 @@ void oEvent::drawRemaining(DrawMethod method, bool placeAfter) } } +struct GroupInfo { + int firstStart = 0; + int unassigned = 0; + int ix; + vector rPerGroup; + vector vacantPerGroup; +}; + +namespace { + bool sameFamily(pRunner a, pRunner b) { + wstring af = a->getFamilyName(); + wstring bf = b->getFamilyName(); + if (af.length() == bf.length()) + return af == bf; + if (af.length() > bf.length()) + swap(af, bf); + + vector bff; + split(bf, L" -", bff); + for (wstring &bfs : bff) { + if (bfs == af) + return true; + } + + return false; + //return af.find(bf) != wstring::npos || bf.find(af) != wstring::npos; + } + + template + void groupUnassignedRunners(vector &rIn, + vector, bool>> &rGroups, + int maxPerGroup, RND rnd) { + + map> families; + for (pRunner &r : rIn) { + int fam = r->getDCI().getInt("Family"); + if (fam != 0) { + families[fam].push_back(r); + r = nullptr; + } + } + + map> clubs; + for (pRunner &r : rIn) { + if (r) + clubs[r->getClubId()].push_back(r); + } + + // Merge families to clubs, if appropriate + for (auto &fam : families) { + int cid = fam.second[0]->getClubId(); + if (cid != 0 && clubs.count(cid) && int(clubs[cid].size() + fam.second.size()) < maxPerGroup) { + clubs[cid].insert(clubs[cid].end(), fam.second.begin(), fam.second.end()); + fam.second.clear(); + } + } + + vector> rawGroups; + for (auto &g : families) { + if (g.second.size() > 0) { + rawGroups.emplace_back(); + rawGroups.back().swap(g.second); + } + } + + for (auto &g : clubs) { + if (g.second.size() > 0) { + rawGroups.emplace_back(); + rawGroups.back().swap(g.second); + } + } + + shuffle(rawGroups.begin(), rawGroups.end(), rnd); + + stable_sort(rawGroups.begin(), rawGroups.end(), + [](const vector &a, const vector &b) {return (a.size()/4) < (b.size()/4); }); + + for (auto &g : rawGroups) { + if (int(g.size()) <= maxPerGroup) + rGroups.emplace_back(g, false); + else { + int nSplit = (g.size() + maxPerGroup - 1) / maxPerGroup; + sort(g.begin(), g.end(), [](const pRunner &a, const pRunner &b) { + wstring n1 = a->getFamilyName(); + wstring n2 = b->getFamilyName(); + return n1 < n2; + }); + + vector> famGroups(1); + for (pRunner r : g) { + if (famGroups.back().empty()) + famGroups.back().push_back(r); + else { + if (!sameFamily(famGroups.back().back(), r)) + famGroups.emplace_back(); + famGroups.back().push_back(r); + } + } + + shuffle(famGroups.begin(), famGroups.end(), rnd); + + stable_sort(famGroups.begin(), famGroups.end(), + [](const vector &a, const vector &b) {return (a.size() / 4) < (b.size() / 4); }); + + size_t nPerGroup = g.size() / nSplit + 1; + bool brk = false; + while (!famGroups.empty()) { + rGroups.emplace_back(vector(), brk); + auto &dst = rGroups.back().first; + brk = true; + while (!famGroups.empty() && dst.size() + famGroups.back().size() <= nPerGroup) { + dst.insert(dst.end(), famGroups.back().begin(), famGroups.back().end()); + famGroups.pop_back(); + } + } + } + } + } + + void printGroups(gdioutput &gdibase, const list &Runners) { + map> rbg; + for (const oRunner &r : Runners) { + rbg[r.getStartGroup(true)].push_back(&r); + } + gdibase.dropLine(); + gdibase.addString("", 0, "List groups"); + gdibase.dropLine(); + + map ccCount; + for (auto rr : rbg) { + auto &vr = rr.second; + sort(vr.begin(), vr.end(), [](const oRunner *a, const oRunner *b) { + if (a->getClassId(true) != b->getClassId(true)) + return a->getClassId(true) < b->getClassId(true); + else + return a->getClubId() < b->getClubId(); }); + + gdibase.dropLine(); + gdibase.addString("", 1, "Group: " + itos(rr.first)); + int cls = -1; + + for (const oRunner *r : vr) { + if (cls != r->getClassId(true)) { + gdibase.dropLine(); + gdibase.addString("", 1, r->getClass(true)); + cls = r->getClassId(true); + } + ++ccCount[cls]; + gdibase.addString("", 0, itow(ccCount[cls]) + L": " + r->getCompleteIdentification()); + } + } + } +} + +void oEvent::drawListStartGroups(const vector &spec, + DrawMethod method, int pairSize, DrawType drawType, + bool limitGroupSize, + DrawInfo *diIn) { + int nParallel = -1; + if (diIn) + nParallel = diIn->nFields; + + pRunner alice = getRunner(155227, 0); + + constexpr bool logOutput = false; + auto &sgMap = getStartGroups(true); + if (sgMap.empty()) + throw meosException("No start group defined"); + + map gInfo; + vector> orderedStartGroups; + for (auto &sg : sgMap) { + orderedStartGroups.emplace_back(sg.second.first, sg.first); + } + // Order by start time + sort(orderedStartGroups.begin(), orderedStartGroups.end()); + + int fs = orderedStartGroups[0].first; // First start + vector> rPerGroupTotal; + map gId2Ix; + for (auto &sg : orderedStartGroups) { + gId2Ix[sg.second] = rPerGroupTotal.size(); + rPerGroupTotal.emplace_back(0, sg.second); + } + + for (size_t k = 0; k < spec.size(); k++) { + auto &s = spec[k]; + auto &gi = gInfo[s.classID]; + gi.ix = k; + gi.firstStart = fs; + gi.rPerGroup.resize(sgMap.size()); + } + + vector unassigned; + int total = 0; + for (auto &r : Runners) { + if (r.isRemoved() || r.isVacant()) + continue; + r.tmpStartGroup = 0; + int clsId = r.getClassId(true); + auto res = gInfo.find(clsId); + if (res != gInfo.end()) { + total++; + int id = r.getStartGroup(false); + auto idRes = gId2Ix.find(id); + if (idRes != gId2Ix.end()) { + ++res->second.rPerGroup[idRes->second]; + ++rPerGroupTotal[idRes->second].first; + } + else { + ++res->second.unassigned; + unassigned.push_back(&r); + } + } + } + + vector, bool>> uaGroups; + unsigned seed = (unsigned)chrono::system_clock::now().time_since_epoch().count(); + auto rnd = std::default_random_engine(seed); + + int maxPerGroup = Runners.size(); + if (limitGroupSize) + maxPerGroup = max(int(Runners.size() / sgMap.size()) / 4, 4); + + groupUnassignedRunners(unassigned, uaGroups, maxPerGroup, rnd ); + + int nPerGroupAvg = (total * 9) / (sgMap.size() * 10); + shuffle(rPerGroupTotal.begin(), rPerGroupTotal.end(), rnd); + + // Assign to groups + while (!uaGroups.empty()) { + stable_sort(rPerGroupTotal.begin(), rPerGroupTotal.end(), + [](const pair &a, const pair &b) {return (a.first / 4) < (b.first / 4); }); + + // Setup map to next start group + map nextGroup; + auto getNextGroup = [&](int ix) { + if (nextGroup.empty()) { + for (size_t k = 0; k < rPerGroupTotal.size(); k++) { + int srcId = rPerGroupTotal[k].second; + for (size_t j = 0; j < orderedStartGroups.size(); j++) { + if (orderedStartGroups[j].second == srcId) { + int nextGrpId; + if (j + 1 < orderedStartGroups.size()) + nextGrpId = orderedStartGroups[j + 1].second; + else + nextGrpId = orderedStartGroups[0].second; + + for (size_t kk = 0; kk < rPerGroupTotal.size(); kk++) { + if (rPerGroupTotal[kk].second == nextGrpId) { + nextGroup[k] = kk; + break; + } + } + break; + } + } + } + } + return nextGroup[ix]; + }; + + int nextGroupToUse = -1; + for (size_t k = 0; k < rPerGroupTotal.size(); k++) { + if (nextGroupToUse == -1) + nextGroupToUse = k; + + int &nGroup = rPerGroupTotal[nextGroupToUse].first; + int groupId = rPerGroupTotal[nextGroupToUse].second; + int currentGroup = nextGroupToUse; + nextGroupToUse = -1; + + if (logOutput) { + gdibase.dropLine(); + gdibase.addString("", 1, "Group: " + itos(groupId) + " (" + itos(nGroup) + ")"); + } + + while (nGroup <= nPerGroupAvg && !uaGroups.empty()) { + for (pRunner ua : uaGroups.back().first) { + ua->tmpStartGroup = groupId; + auto &gi = gInfo[ua->getClassId(true)]; + int j = gId2Ix[groupId]; + ++gi.rPerGroup[j]; + nGroup++; + } + bool skip = uaGroups.back().second; + uaGroups.pop_back(); + if (skip) { + nextGroupToUse = getNextGroup(currentGroup); + break; // Assign to next group (same club) + } + } + } + nPerGroupAvg++; // Ensure convergance + } + + // vacantPerGroup + for (size_t k = 0; k < spec.size(); k++) { + auto &gi = gInfo[spec[k].classID]; + gi.vacantPerGroup.resize(sgMap.size()); + for (int j = 0; j < spec[k].vacances; j++) + ++gi.vacantPerGroup[GetRandomNumber(sgMap.size())]; + } + + if (logOutput) + printGroups(gdibase, Runners); + + map rPerGroup; + // Ensure not too many competitors in same class per groups + vector>> countClassGroupClub(spec.size()); + vector rl; + map classFractions; + map, double> clubFractions; + int rTot = 0; + for (size_t k = 0; k < spec.size(); k++) { + auto &countGroupClub = countClassGroupClub[k]; + int cls = spec[k].classID; + getRunners(cls, 0, rl, false); + classFractions[cls] = rl.size(); + rTot += rl.size(); + for (pRunner r : rl) { + int gid = r->getStartGroup(true); + ++rPerGroup[gid]; + int club = r->getClubId(); + if (club != 0) { + ++clubFractions[make_pair(club,cls)]; + ++countGroupClub[gid][club]; // Count per club + } + ++countGroupClub[gid][0]; // Count all + } + } + double q = 1.0 / rTot; + for (auto &e : classFractions) + e.second *= q; + + for (auto &e : clubFractions) + e.second *= (q / classFractions[e.first.second]); + + vector> moveFromGroupClassClub; + // For each start group, count class members in each class and per club + vector> numberClub; + for (size_t j = 0; j < orderedStartGroups.size(); j++) { + auto &g = orderedStartGroups[j]; + int groupId = g.second; + + for (size_t k = 0; k < spec.size(); k++) { + auto &countGroupClub = countClassGroupClub[k]; + int cls = spec[k].classID; + const int nTotal = countGroupClub[groupId][0]; + int clubSwitch = 0; // 0 any club, or specified club id. + int numNeedSwitch = 0; + if (nTotal > 3) { + numberClub.clear(); + for (auto &clubCount : countGroupClub[groupId]) { + if (clubCount.first != 0) + numberClub.emplace_back(clubCount.second, clubCount.first); + } + if (numberClub.empty()) + continue; + + sort(numberClub.begin(), numberClub.end()); + int clbId = numberClub.back().second; + int limit = max(nTotal / 2, int(nTotal*clubFractions[make_pair(clbId, cls)])); + int limitTot = int(rPerGroup[groupId] * max(0.3, classFractions[cls])); + if (numberClub.size() == 1 || numberClub.back().first > limit) { + numNeedSwitch = numberClub.back().first - limit; + clubSwitch = clbId; + } + else if (nTotal > limitTot) { + // Move from any club + numNeedSwitch = nTotal - limitTot; + clubSwitch = 0; + } + } + + if (numNeedSwitch > 0) { + moveFromGroupClassClub.emplace_back(groupId, cls, clubSwitch, numNeedSwitch); + } + } + } + + if (moveFromGroupClassClub.size() > 0 && limitGroupSize) { + vector> runnersPerGroup(orderedStartGroups.size()); + map groupId2Ix; + for (size_t j = 0; j < orderedStartGroups.size(); j++) + groupId2Ix[orderedStartGroups[j].second] = j; + + for (size_t k = 0; k < spec.size(); k++) { + getRunners(spec[k].classID, 0, rl, false); + for (pRunner r : rl) { + runnersPerGroup[groupId2Ix[r->tmpStartGroup]].push_back(r); + } + } + + for (size_t j = 0; j < orderedStartGroups.size(); j++) { + shuffle(runnersPerGroup[j].begin(), runnersPerGroup[j].end(), rnd); + stable_sort(runnersPerGroup[j].begin(), runnersPerGroup[j].end(), [](pRunner a, pRunner b) { + int specA = a->getStartGroup(false); + int specB = b->getStartGroup(false); + if (specA != specB) + return specA < specB; + return a->getEntryDate(false) > b->getEntryDate(false); + }); + } + + map, int>> classClubCountByGroup; + for (auto ms : moveFromGroupClassClub) { + int groupId = get<0>(ms); + int clsId = get<1>(ms); + int clubId = get<2>(ms); + int cnt = get<3>(ms); + classClubCountByGroup[groupId][make_pair(clsId, clubId)] = cnt; + } + + for (size_t j = 0; j < orderedStartGroups.size(); j++) { + int thisGroupId = orderedStartGroups[j].second; + auto &classClubCount = classClubCountByGroup[thisGroupId]; + for (pRunner &r : runnersPerGroup[j]) { + if (!r) + continue; + int cls = r->getClassId(true); + int club = r->getClubId(); + auto res = classClubCount.find(make_pair(cls, club)); + int type = 0; + if (res != classClubCount.end()) { + if (res->second > 0) { + --res->second; + type = 1; + } + } + else { + res = classClubCount.find(make_pair(cls, 0)); + if (res != classClubCount.end()) { + if (res->second > 0) { + --res->second; + type = 2; + } + } + } + if (type == 0) + continue; //do not move + + constexpr int done = 100; + for (int iter = 0; iter < 5; iter++) { + int nextG = -1; + if ((iter & 1) == 0) + nextG = j + (iter / 2 + 1); + else + nextG = j - (iter / 2 + 1); + + if (size_t(nextG) >= runnersPerGroup.size()) + continue; + + int nextGroupId = orderedStartGroups[nextG].second; + auto &nextClassClubCount = classClubCountByGroup[nextGroupId]; + + if (type == 1 && nextClassClubCount.find(make_pair(cls, club)) != nextClassClubCount.end()) + continue; + + for (pRunner &rr : runnersPerGroup[nextG]) { + if (rr) { + if (type == 1 && rr->getClassId(true) == cls && rr->getClubId() != club) { + rr->tmpStartGroup = orderedStartGroups[j].second; + r->tmpStartGroup = orderedStartGroups[nextG].second; + rr = nullptr; + r = nullptr; + iter = done; + break; + } + else if (type == 2 && rr->getClassId(true) != cls) { + rr->tmpStartGroup = orderedStartGroups[j].second; + r->tmpStartGroup = orderedStartGroups[nextG].second; + rr = nullptr; + r = nullptr; + iter = done; + break; + } + } + } + } + + } + } + } + + if (logOutput) { + gdibase.addString("", 0, "Reordered groups"); + printGroups(gdibase, Runners); + } + + if (spec.size() == 1) { + for (size_t j = 0; j < orderedStartGroups.size(); j++) { + auto &sg = orderedStartGroups[j]; + int groupId = sg.second; + int firstStart = getStartGroup(groupId).first; + + vector specLoc = spec; + for (size_t k = 0; k < specLoc.size(); k++) { + auto &gi = gInfo[specLoc[k].classID]; + specLoc[k].startGroup = groupId; + specLoc[k].firstStart = max(gi.firstStart, firstStart); + specLoc[k].vacances = gi.vacantPerGroup[j]; + gi.firstStart = specLoc[k].firstStart + specLoc[k].interval * (specLoc[k].vacances + gi.rPerGroup[j]); + } + drawList(specLoc, method, pairSize, drawType); + } + } + else { + int leg = spec[0].leg; + VacantPosition vp = VacantPosition::Mixed; + DrawInfo di; + di.baseInterval = 60; + di.allowNeighbourSameCourse = true; + + di.extraFactor = 0; + di.minClassInterval = di.baseInterval * 2; + di.maxClassInterval = di.baseInterval * 8; + + di.minVacancy = 1; + di.maxVacancy = 100; + di.vacancyFactor = 0; + if (diIn) { + di.baseInterval = diIn->baseInterval; + di.minClassInterval = diIn->minClassInterval; + di.maxCommonControl = diIn->maxCommonControl; + di.allowNeighbourSameCourse = diIn->allowNeighbourSameCourse; + } + + // Update runner per group/class counters + for (auto &gi : gInfo) { + fill(gi.second.rPerGroup.begin(), gi.second.rPerGroup.end(), 0); + } + + rPerGroup.clear(); + for (size_t k = 0; k < spec.size(); k++) { + getRunners(spec[k].classID, 0, rl, false); + auto &gi = gInfo[spec[k].classID]; + for (pRunner r : rl) { + r->setStartTime(0, true, oBase::ChangeType::Update, false); + int gid = r->getStartGroup(true); + ++rPerGroup[gid]; + ++gi.rPerGroup[gId2Ix[gid]]; + } + } + + map rPerGroup; + for (auto > : rPerGroupTotal) + rPerGroup[gt.second] = gt.first; + + for (size_t j = 0; j < orderedStartGroups.size(); j++) { + auto &sg = orderedStartGroups[j]; + int groupId = sg.second; + + int firstStart = getStartGroup(groupId).first; + + int nspos = (oe->getStartGroup(groupId).second - firstStart) / di.baseInterval; + int optimalParallel = rPerGroup[groupId] / nspos; + + if (nParallel <= 0) + di.nFields = max(3, min(optimalParallel + 2, 100)); + else + di.nFields = nParallel; + + di.firstStart = firstStart; + di.changedVacancyInfo = false; + + vector cInfo; + vector> outLines; + di.vacancyFactor = 0; + auto &group = sgMap.find(groupId); + int length = max(300, group->second.second - group->second.first); + int slots = length / di.baseInterval; + di.classes.clear(); + for (size_t k = 0; k < spec.size(); k++) { + auto &gi = gInfo[spec[k].classID]; + int rClassGroup = gi.rPerGroup[j]; + if (rClassGroup == 0) + continue; + + di.classes[spec[k].classID] = ClassInfo(getClass(spec[k].classID)); + ClassInfo &ci = di.classes[spec[k].classID]; + ci.startGroupId = groupId; + if (gi.firstStart > firstStart) { + ci.firstStart = (gi.firstStart - firstStart) / di.baseInterval; + ci.hasFixedTime = true; + } + ci.nVacant = gi.vacantPerGroup[j]; + ci.nVacantSpecified = true; + ci.nExtraSpecified = true; + + if (rClassGroup == 1) + ci.fixedInterval = 4; + else { + int q = slots / (rClassGroup-1); + if (q >= 12) + ci.fixedInterval = 8; + else if (q >= 6) + ci.fixedInterval = 4; + else + ci.fixedInterval = 2; + } + if (ci.fixedInterval > 2) + ci.interval = ci.fixedInterval; + else + ci.interval = spec[k].interval / di.baseInterval; + } + + if (logOutput) { + gdibase.dropLine(); + gdibase.addString("", 1, "Behandlar grupp: " + itos(groupId)); + } + + optimizeStartOrder(outLines, di, cInfo); + + if (logOutput) { + for (auto &ol : outLines) { + gdibase.addString("", ol.first, ol.second); + } + } + + int laststart = 0; + for (size_t k = 0; k specx; + specx.emplace_back(ci.classId, leg, + di.firstStart + di.baseInterval * ci.firstStart, + di.baseInterval * ci.interval, ci.nVacant, vp); + + auto &gi = gInfo[cInfo[k].classId]; + gi.firstStart = specx[0].firstStart + specx[0].interval * (specx[0].vacances + gi.rPerGroup[j]); + + specx.back().startGroup = groupId; + drawList(specx, method, pairSize, drawType); + } + } + } +} + void oEvent::drawList(const vector &spec, DrawMethod method, int pairSize, DrawType drawType) { @@ -1014,13 +1702,18 @@ void oEvent::drawList(const vector &spec, //Only remove vacances on leg 0. vector toRemove; //Remove old vacances - for (it=Runners.begin(); it != Runners.end(); ++it) { + for (it = Runners.begin(); it != Runners.end(); ++it) { if (clsIdClearVac.count(it->getClassId(true))) { if (it->isRemoved()) continue; if (it->tInTeam) continue; // Cannot remove team runners - if (it->getClubId()==VacantClubId) { + int k = clsId2Ix.find(it->getClassId(true))->second; + if (spec[k].startGroup > 0 && + it->getStartGroup(true) > 0 && + it->getStartGroup(true) != spec[k].startGroup) + continue; + if (it->getClubId() == VacantClubId) { toRemove.push_back(it->getId()); } } @@ -1034,7 +1727,7 @@ void oEvent::drawList(const vector &spec, if (!clsIdClearVac.count(spec[k].classID)) continue; for (int i = 0; i < spec[k].vacances; i++) { - oe->addRunnerVacant(spec[k].classID); + oe->addRunnerVacant(spec[k].classID)->setStartGroup(spec[k].startGroup); } } } @@ -1045,6 +1738,9 @@ void oEvent::drawList(const vector &spec, if (it->getStatus() == StatusNotCompetiting) continue; int ix = clsId2Ix[cid]; + if (spec[ix].startGroup != 0 && it->getStartGroup(true) != spec[ix].startGroup) + continue; + if (it->legToRun() == spec[ix].leg || spec[ix].leg == -1) { runners.push_back(&*it); spec[ix].ntimes++; @@ -1174,7 +1870,13 @@ void oEvent::drawList(const vector &spec, minStartNo = min(minStartNo, runners[k]->getStartNo()); newStartNo.emplace_back(stimes[k], k); } - + /* + gdibase.dropLine(); + gdibase.addString("", 1, L"Draw: " + oe->getClass(spec[0].classID)->getName()); + for (unsigned k = 0; k < stimes.size(); k++) { + gdibase.addString("", 0, runners[k]->getCompleteIdentification() + L" " + runners[k]->getStartTimeS()); + } + */ sort(newStartNo.begin(), newStartNo.end()); //CurrentSortOrder = SortByStartTime; //sort(runners.begin(), runners.end()); @@ -1388,7 +2090,7 @@ void oEvent::automaticDrawAll(gdioutput &gdi, continue; vector spec; spec.emplace_back(it->getId(), 0, iFirstStart, 0, 0, vp); - oe->drawList(spec, DrawMethod::Random, 1, DrawType::DrawAll); + drawList(spec, DrawMethod::Random, 1, DrawType::DrawAll); } return; } @@ -1469,87 +2171,110 @@ void oEvent::automaticDrawAll(gdioutput &gdi, if (maxRunners==0) continue; - int maxParallell = 15; + if (getStartGroups(true).size() == 0) { - if (runnersStart < 100) - maxParallell = 4; - else if (runnersStart < 300) - maxParallell = 6; - else if (runnersStart < 700) - maxParallell = 10; - else if (runnersStart < 1000) - maxParallell = 12; - else - maxParallell = 15; + int maxParallell = 15; - int optimalParallel = runnersStart / (maxRunners*2); // Min is every second interval + if (runnersStart < 100) + maxParallell = 4; + else if (runnersStart < 300) + maxParallell = 6; + else if (runnersStart < 700) + maxParallell = 10; + else if (runnersStart < 1000) + maxParallell = 12; + else + maxParallell = 15; - di.nFields = max(3, min (optimalParallel + 2, 15)); - di.baseInterval = baseInterval; - di.extraFactor = extraFactor; - di.firstStart = iFirstStart; - di.minClassInterval = baseInterval * 2; - di.maxClassInterval = di.minClassInterval; + int optimalParallel = runnersStart / (maxRunners * 2); // Min is every second interval - di.minVacancy = 1; - di.maxVacancy = 100; - di.vacancyFactor = vacancy; - di.allowNeighbourSameCourse = allowNeighbourSameCourse; + di.nFields = max(3, min(optimalParallel + 2, 15)); + di.baseInterval = baseInterval; + di.extraFactor = extraFactor; + di.firstStart = iFirstStart; + di.minClassInterval = baseInterval * 2; + di.maxClassInterval = di.minClassInterval; - di.startName = start; + di.minVacancy = 1; + di.maxVacancy = 100; + di.vacancyFactor = vacancy; + di.allowNeighbourSameCourse = allowNeighbourSameCourse; - for (oClassList::iterator it = Classes.begin(); it!=Classes.end(); ++it) { - if (it->getStart() != start) - continue; - if (notDrawn.count(it->getId())==0) - continue; // Only not drawn classes - if (it->hasFreeStart()) - continue; + di.startName = start; - di.classes[it->getId()] = ClassInfo(&*it); - } + for (oClassList::iterator it = Classes.begin(); it != Classes.end(); ++it) { + if (it->getStart() != start) + continue; + if (notDrawn.count(it->getId()) == 0) + continue; // Only not drawn classes + if (it->hasFreeStart()) + continue; - if (di.classes.size()==0) - continue; - - gdi.dropLine(); - gdi.addStringUT(1, lang.tl(L"Optimerar startfördelning ") + start); - gdi.refreshFast(); - gdi.dropLine(); - vector cInfo; - optimizeStartOrder(gdi, di, cInfo); - - - int laststart=0; - for (size_t k=0;kgetClassType() == oClassRelay) { - gdi.addString("", 0, L"Hoppar över stafettklass: X#" + - getClass(ci.classId)->getName()).setColor(colorRed); - continue; + di.classes[it->getId()] = ClassInfo(&*it); } - gdi.addString("", 0, L"Lottar: X#" + getClass(ci.classId)->getName()); - vector spec; - spec.emplace_back(ci.classId, leg, - di.firstStart + di.baseInterval * ci.firstStart, - di.baseInterval * ci.interval, ci.nVacant, vp); + if (di.classes.size() == 0) + continue; - drawList(spec, method, pairSize, DrawType::DrawAll); - gdi.scrollToBottom(); + gdi.dropLine(); + gdi.addStringUT(1, lang.tl(L"Optimerar startfördelning ") + start); gdi.refreshFast(); - drawn++; + gdi.dropLine(); + vector cInfo; + vector> outLines; + optimizeStartOrder(outLines, di, cInfo); + for (auto &ol : outLines) + gdi.addString("", ol.first, ol.second); + + int laststart = 0; + for (size_t k = 0; k < cInfo.size(); k++) { + const ClassInfo &ci = cInfo[k]; + laststart = max(laststart, ci.firstStart + ci.nRunners*ci.interval); + } + + gdi.addStringUT(1, lang.tl("Sista start (nu tilldelad)") + L": " + + getAbsTime((laststart)*di.baseInterval + di.firstStart)); + gdi.dropLine(); + gdi.refreshFast(); + + for (size_t k = 0; k < cInfo.size(); k++) { + const ClassInfo &ci = cInfo[k]; + + if (getClass(ci.classId)->getClassType() == oClassRelay) { + gdi.addString("", 0, L"Hoppar över stafettklass: X#" + + getClass(ci.classId)->getName()).setColor(colorRed); + continue; + } + + gdi.addString("", 0, L"Lottar: X#" + getClass(ci.classId)->getName()); + vector spec; + spec.emplace_back(ci.classId, leg, + di.firstStart + di.baseInterval * ci.firstStart, + di.baseInterval * ci.interval, ci.nVacant, vp); + + drawList(spec, method, pairSize, DrawType::DrawAll); + gdi.scrollToBottom(); + gdi.refreshFast(); + drawn++; + } + } + else { + vector spec; + for (oClassList::iterator it = Classes.begin(); it != Classes.end(); ++it) { + if (it->getStart() != start) + continue; + if (notDrawn.count(it->getId()) == 0) + continue; // Only not drawn classes + if (it->hasFreeStart()) + continue; + //int classID, int leg, int firstStart, int interval, int vacances, oEvent::VacantPosition vp) + spec.emplace_back(it->getId(), 0, 0, 120, 1, VacantPosition::Mixed); + drawn++; + } + + if (spec.size() == 0) + continue; + drawListStartGroups(spec, method, pairSize, DrawType::DrawAll); } } diff --git a/code/oEventDraw.h b/code/oEventDraw.h index cb713d8..f3016b9 100644 --- a/code/oEventDraw.h +++ b/code/oEventDraw.h @@ -25,7 +25,8 @@ #include "oEvent.h" struct ClassDrawSpecification { - int classID; + int classID; + int startGroup = 0; int leg; mutable int firstStart; mutable int interval; @@ -42,10 +43,12 @@ struct ClassDrawSpecification { /** Struct with info to draw a class */ struct ClassInfo { int classId; + int startGroupId; pClass pc; int firstStart; int interval; + int fixedInterval; int unique; int courseId; diff --git a/code/oEventResult.cpp b/code/oEventResult.cpp index b5d2ca9..c771d76 100644 --- a/code/oEventResult.cpp +++ b/code/oEventResult.cpp @@ -422,13 +422,13 @@ bool oEvent::calculateTeamResults(vector &teams, int leg, ResultTy if (invalidClass) { p = 0; } - else if (it->_cachedStatus == StatusOK) { + else if (it->tmpCachedStatus == StatusOK) { cPlace++; - if (it->_sortTime > cTime) + if (it->tmpSortTime > cTime) vPlace = cPlace; - cTime = it->_sortTime; + cTime = it->tmpSortTime; p = vPlace; } @@ -443,8 +443,8 @@ bool oEvent::calculateTeamResults(vector &teams, int leg, ResultTy else { it->getTeamPlace(sleg).p.update(*this, p, tmpDefaultResult); res.version = tmpDefaultResult ? -1 : dataRevision; - res.status = it->_cachedStatus; - res.time = it->_sortTime; + res.status = it->tmpCachedStatus; + res.time = it->tmpDefinedTime; it->setComputedResult(sleg, res); } } diff --git a/code/oFreePunch.cpp b/code/oFreePunch.cpp index 83cb78f..c5db348 100644 --- a/code/oFreePunch.cpp +++ b/code/oFreePunch.cpp @@ -66,7 +66,7 @@ bool oFreePunch::Write(xmlparser &xml) xml.write("Time", Time); xml.write("Type", Type); xml.write("Id", Id); - xml.write("Updated", Modified.getStamp()); + xml.write("Updated", getStamp()); xml.endTag(); return true; @@ -446,7 +446,7 @@ pFreePunch oEvent::addFreePunch(int time, int type, int card, bool updateStartFi punches.push_back(ofp); pFreePunch fp=&punches.back(); - fp->addToEvent(); + fp->addToEvent(this, &ofp); oFreePunch::rehashPunches(*this, card, fp); insertIntoPunchHash(card, type, time); @@ -516,7 +516,7 @@ pFreePunch oEvent::addFreePunch(oFreePunch &fp) { insertIntoPunchHash(fp.CardNo, fp.Type, fp.Time); punches.push_back(fp); pFreePunch fpz=&punches.back(); - fpz->addToEvent(); + fpz->addToEvent(this, &fp); oFreePunch::rehashPunches(*this, fp.CardNo, fpz); if (!fpz->existInDB() && HasDBConnection) { diff --git a/code/oFreePunch.h b/code/oFreePunch.h index 0dd3ce2..431d894 100644 --- a/code/oFreePunch.h +++ b/code/oFreePunch.h @@ -86,6 +86,8 @@ public: static void rehashPunches(oEvent &oe, int cardNo, pFreePunch newPunch); static bool disableHashing; + void merge(const oBase &input) final; + oFreePunch(oEvent *poe, int card, int time, int type); oFreePunch(oEvent *poe, int id); virtual ~oFreePunch(void); diff --git a/code/oImportExport.cpp b/code/oImportExport.cpp index a755e6e..71608b5 100644 --- a/code/oImportExport.cpp +++ b/code/oImportExport.cpp @@ -1587,9 +1587,9 @@ bool oEvent::addXMLCourse(const xmlobject &xcrs, bool addClasses, set & pc->setName(cname); pc->setLength(len); - pc->importControls("", false); + pc->importControls("", true, false); for (size_t i = 0; i30 && ctrlCode[i]<1000) + if (ctrlCode[i]>=30 && ctrlCode[i]<1024) pc->addControl(ctrlCode[i]); } if (pc->getNumControls() + 1 == legLen.size()) diff --git a/code/oPunch.h b/code/oPunch.h index 03cb79a..6600a08 100644 --- a/code/oPunch.h +++ b/code/oPunch.h @@ -98,6 +98,9 @@ public: string codeString() const; void appendCodeString(string &dst) const; + + void merge(const oBase &input) override; + oPunch(oEvent *poe); virtual ~oPunch(); diff --git a/code/oRunner.cpp b/code/oRunner.cpp index 65b710f..184d16a 100644 --- a/code/oRunner.cpp +++ b/code/oRunner.cpp @@ -368,7 +368,7 @@ bool oRunner::Write(xmlparser &xml) xml.startTag("Runner"); xml.write("Id", Id); - xml.write("Updated", Modified.getStamp()); + xml.write("Updated", getStamp()); xml.write("Name", sName); xml.write("Start", startTime); xml.write("Finish", FinishTime); diff --git a/code/oRunner.h b/code/oRunner.h index 422dd01..27ff0fa 100644 --- a/code/oRunner.h +++ b/code/oRunner.h @@ -201,6 +201,8 @@ public: bool preventRestart() const; void preventRestart(bool state); + void merge(const oBase &input) override; + /** Call this method after doing something to just this runner/team that changed the time/status etc, that effects the result. May make a global evaluation of the class. @@ -658,9 +660,20 @@ protected: bool isHiredCard(int card) const; + int tmpStartGroup = 0; public: static const shared_ptr &getTable(oEvent *oe); + int getStartGroup(bool useTmpStartGroup) const { + if (useTmpStartGroup && tmpStartGroup) + return tmpStartGroup; + return getDCI().getInt("StartGroup"); + } + + void setStartGroup(int sg) { + getDI().setInt("StartGroup", sg); + } + // Get the leg defineing parallel results for this runner (in a team) int getParResultLeg() const; @@ -902,7 +915,7 @@ public: void setNumShortening(int numShorten); pCard getCard() const {return Card;} - int getCardId(){if (Card) return Card->Id; else return 0;} + int getCardId() const {if (Card) return Card->Id; else return 0;} bool operator<(const oRunner &c) const; bool static CompareCardNumber(const oRunner &a, const oRunner &b) { return a.cardNumber < b.cardNumber; } @@ -945,6 +958,8 @@ public: /** Formats extra line for runner []-syntax, or if r is null, checks validity and throws on error.*/ static wstring formatExtraLine(pRunner r, const wstring &input); + void merge(const oBase &input) final; + virtual ~oRunner(); friend class MeosSQL; diff --git a/code/oTeam.cpp b/code/oTeam.cpp index 15a7d68..04271d9 100644 --- a/code/oTeam.cpp +++ b/code/oTeam.cpp @@ -69,7 +69,7 @@ bool oTeam::write(xmlparser &xml) xml.startTag("Team"); xml.write("Id", Id); xml.write("StartNo", StartNo); - xml.write("Updated", Modified.getStamp()); + xml.write("Updated", getStamp()); xml.write("Name", sName); xml.write("Start", startTime); xml.write("Finish", FinishTime); @@ -814,10 +814,10 @@ bool oTeam::compareResult(const oTeam &a, const oTeam &b) } else return false; } - else if (a._sortStatus!=b._sortStatus) - return a._sortStatus tComputedResults; - mutable int _sortTime; - mutable int _sortStatus; - mutable RunnerStatus _cachedStatus; + void setTmpTime(int t) const { tmpSortTime = tmpDefinedTime = t; } + mutable int tmpSortTime; + mutable int tmpDefinedTime; + mutable int tmpSortStatus; + mutable RunnerStatus tmpCachedStatus; mutable vector< vector< vector > > resultCalculationCache; @@ -277,6 +279,8 @@ public: void set(const xmlobject &xo); bool write(xmlparser &xml); + void merge(const oBase &input) final; + oTeam(oEvent *poe, int id); oTeam(oEvent *poe); virtual ~oTeam(void); diff --git a/code/oTeamEvent.cpp b/code/oTeamEvent.cpp index 90f4e3a..bfee4cf 100644 --- a/code/oTeamEvent.cpp +++ b/code/oTeamEvent.cpp @@ -161,7 +161,7 @@ pTeam oEvent::addTeam(const wstring &pname, int ClubId, int ClassId) bibStartNoToRunnerTeam.clear(); Teams.push_back(t); pTeam pt = &Teams.back(); - pt->addToEvent(); + pt->addToEvent(this, &t); teamById[t.Id] = pt; oe->updateTabs(); @@ -184,7 +184,7 @@ pTeam oEvent::addTeam(const oTeam &t, bool autoAssignStartNo) { Teams.push_back(t); pTeam pt = &Teams.back(); - pt->addToEvent(); + pt->addToEvent(this, &t); for (size_t i = 0; i < pt->Runners.size(); i++) { if (pt->Runners[i]) { @@ -850,23 +850,24 @@ void oTeam::fillInSortData(SortOrder so, int leg, bool linearLeg, map leg = 0; if (unsigned(leg) < Runners.size()) hasRunner = true; - _cachedStatus = StatusUnknown; - _sortStatus = 0; - _sortTime = getLegStartTime(leg); - if (_sortTime <= 0) - _sortStatus = 1; + tmpCachedStatus = StatusUnknown; + tmpSortStatus = 0; + setTmpTime(getLegStartTime(leg)); + if (tmpSortTime <= 0) + tmpSortStatus = 1; return; } else if (so == ClassPoints) { bool totalResult = so == ClassTotalResult; - _sortTime = getRunningTime(true) - 7 * 24 * 3600 * getRogainingPoints(true, totalResult); - _cachedStatus = getLegStatus(-1, true, totalResult); + setTmpTime(getRunningTime(true)); + tmpSortTime -= 7 * 24 * 3600 * getRogainingPoints(true, totalResult); + tmpCachedStatus = getLegStatus(-1, true, totalResult); } else if (so == ClassKnockoutTotalResult) { hasRunner = true; - _cachedStatus = StatusUnknown; - _sortStatus = 0; - _sortTime = 0; + tmpCachedStatus = StatusUnknown; + tmpSortStatus = 0; + setTmpTime(0); // Count number of races with results int numResult = 0; @@ -880,15 +881,15 @@ void oTeam::fillInSortData(SortOrder so, int leg, bool linearLeg, map numResult++; lastClassHeat = r->getDCI().getInt("Heat"); - _cachedStatus = r->tStatus; - _sortTime = r->getRunningTime(false); + tmpCachedStatus = r->tStatus; + setTmpTime(r->getRunningTime(false)); } } if (lastClassHeat > 50 || lastClassHeat < 0) lastClassHeat = 0; - unsigned rawStatus = _cachedStatus; - _sortStatus = RunnerStatusOrderMap[rawStatus < 100u ? rawStatus : 0] - (numResult * 100 + lastClassHeat) * 1000; + unsigned rawStatus = tmpCachedStatus; + tmpSortStatus = RunnerStatusOrderMap[rawStatus < 100u ? rawStatus : 0] - (numResult * 100 + lastClassHeat) * 1000; return; } @@ -926,36 +927,38 @@ void oTeam::fillInSortData(SortOrder so, int leg, bool linearLeg, map pRunner r = getRunner(lg); if (r) { if (so == ClassDefaultResult) { - _sortTime = r->getRunningTime(false); - _cachedStatus = r->getStatus(); + setTmpTime(r->getRunningTime(false)); + tmpCachedStatus = r->getStatus(); } else { - _sortTime = r->getRunningTime(true); - _cachedStatus = r->getStatusComputed(); + setTmpTime(r->getRunningTime(true)); + tmpCachedStatus = r->getStatusComputed(); } } else { - _sortTime = 0; - _cachedStatus = StatusUnknown; + setTmpTime(0); + tmpCachedStatus = StatusUnknown; } } else { if (so == ClassDefaultResult) { - _sortTime = getLegRunningTime(lg, false, totalResult) + getNumShortening(lg) * 3600 * 24 * 10; - _cachedStatus = getLegStatus(lg, false, totalResult); + setTmpTime(getLegRunningTime(lg, false, totalResult)); + tmpSortTime += getNumShortening(lg) * 3600 * 24 * 10; + tmpCachedStatus = getLegStatus(lg, false, totalResult); } else { - _sortTime = getLegRunningTime(lg, true, totalResult) + getNumShortening(lg) * 3600 * 24 * 10; - _cachedStatus = getLegStatus(lg, true, totalResult); + setTmpTime(getLegRunningTime(lg, true, totalResult)); + tmpSortTime += getNumShortening(lg) * 3600 * 24 * 10; + tmpCachedStatus = getLegStatus(lg, true, totalResult); } // Ensure number of restarts has effect on final result if (lg == lastIndex) - _sortTime += tNumRestarts * 24 * 3600; + tmpSortTime += tNumRestarts * 24 * 3600; } } - unsigned rawStatus = _cachedStatus; - _sortStatus = RunnerStatusOrderMap[rawStatus < 100u ? rawStatus : 0]; + unsigned rawStatus = tmpCachedStatus; + tmpSortStatus = RunnerStatusOrderMap[rawStatus < 100u ? rawStatus : 0]; } bool oEvent::sortTeams(SortOrder so, int leg, bool linearLeg) { diff --git a/code/oevent_transfer.cpp b/code/oevent_transfer.cpp new file mode 100644 index 0000000..a7eb2af --- /dev/null +++ b/code/oevent_transfer.cpp @@ -0,0 +1,1473 @@ +/************************************************************************ +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 fro 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 +Eksoppsvgen 16, SE-75646 UPPSALA, Sweden + +************************************************************************/ + +#include "stdafx.h" + +#include +#include +#include +#include +#include +#include + +#include "oEvent.h" +#include "oDataContainer.h" +#include "metalist.h" +#include "generalresult.h" + +#include "meosException.h" +#include "meos.h" +#include "meos_util.h" +#include "intkeymapimpl.hpp" + +#include "MeOSFeatures.h" + + + +void oEvent::merge(oEvent &src, int &numAdd, int &numRemove, int &numUpdate) { + numAdd = 0; + numRemove = 0; + numUpdate = 0; + + wstring mergeTag = src.getMergeTag(); + wstring mergeTime = getMergeInfo(mergeTag); + wstring baseTime, reverseMerge; + string addMinTime = src.getLastModified(); + if (src.currentNameId == currentNameId) { + // Get base version time + if (baseTime.empty()) { + baseTime = src.getDCI().getString("ImportStamp"); + if (baseTime.empty()) + baseTime = getDCI().getString("ImportStamp"); + } + + reverseMerge = src.getMergeInfo(getMergeTag()); + if (reverseMerge.empty()) + reverseMerge = baseTime; + } + + if (mergeTime.empty()) + mergeTime = baseTime; + + string previousMergeTime(mergeTime.begin(), mergeTime.end()); + string thisMergeTime; + string bt(reverseMerge.begin(), reverseMerge.end()); + set rControl, rRunner, rTeam, rCourse, rClub, rClass; + + auto updateNewItem = [&addMinTime, &numAdd](oBase *pNew, const oBase &src) { + if (pNew) { + numAdd++; + pNew->merge(src); + pNew->synchronize(); + if (pNew->Modified.getStamp() < addMinTime) + pNew->Modified.setStamp(addMinTime); + } + }; + + auto mergeItem = [&numUpdate](oBase *pExisting, const oBase &src) { + numUpdate++; + string oldStamp = pExisting->Modified.getStamp(); + pExisting->merge(src); + if (pExisting->Modified.getStamp() < oldStamp) + pExisting->Modified.setStamp(oldStamp); + }; + + auto computeRemove = [&bt](const auto &list, const set &existing, set &remove) { + for (auto &c : list) { + if (!c.isRemoved() && !existing.count(c.Id) && c.getStamp() < bt) + remove.insert(c.Id); + } + }; + + { + map ctrl; + for (oControl &c : Controls) { + if (!c.isRemoved()) + ctrl[c.Id] = &c; + } + + set srcControl; + for (const oControl &c : src.Controls) { + const string &stmp = c.getStamp(); + if (!c.isRemoved()) { + if (stmp > previousMergeTime) { + if (stmp > thisMergeTime) + thisMergeTime = stmp; + auto mc = ctrl.find(c.Id); + if (mc != ctrl.end()) { + mergeItem(mc->second, c); + } + else { + pControl pNew = addControl(c); + updateNewItem(pNew, c); + } + } + srcControl.insert(c.Id); + } + } + + computeRemove(Controls, srcControl, rControl); + } + + { + map crs; + for (oCourse &c : Courses) { + if (!c.isRemoved()) + crs[c.Id] = &c; + } + + set srcCourse; + for (const oCourse &c : src.Courses) { + const string &stmp = c.getStamp(); + if (!c.isRemoved()) { + bool okMerge = stmp > previousMergeTime; + if (stmp > thisMergeTime) + thisMergeTime = stmp; + auto mc = crs.find(c.Id); + if (mc != crs.end()) { + if (okMerge) + mergeItem(mc->second, c); + } + else if (okMerge) { + pCourse pNew = addCourse(c); + updateNewItem(pNew, c); + } + srcCourse.insert(c.Id); + } + } + + computeRemove(Courses, srcCourse, rCourse); + } + + { + map cls; + map clsN; + + for (oClass &c : Classes) { + if (!c.isRemoved()) { + cls[c.Id] = &c; + clsN[c.Name] = &c; + } + } + + set srcClass; + for (oClass &c : src.Classes) { + const string &stmp = c.getStamp(); + bool merged = false; + if (!c.isRemoved()) { + bool okMerge = stmp > previousMergeTime; + + if (stmp > thisMergeTime) + thisMergeTime = stmp; + auto mc = cls.find(c.Id); + + if (mc != cls.end()) { + if (compareClassName(mc->second->Name, c.Name)) { + if (okMerge) + mergeItem(mc->second, c); + merged = true; + } + } + + auto updateIdCls = [&](int id) { + pClass other = src.getClass(id); + if (other) + other->changeId(c.Id); + c.changeId(id); + }; + + if (!merged) { + auto mcN = clsN.find(c.Name); + if (mcN != clsN.end()) { + if (okMerge) + mergeItem(mcN->second, c); + merged = true; + updateIdCls(mcN->second->Id); + } + } + + if (!merged && okMerge) { + if (cls.count(c.Id)) { + int newId = max(getFreeClassId(), src.getFreeClassId()); + c.changeId(newId); + } + pClass pNew = addClass(c); + updateNewItem(pNew, c); + } + + srcClass.insert(c.Id); + } + } + + computeRemove(Classes, srcClass, rClass); + } + + { // Removing card not supported --> maybe too riskful... + map crd; + map, pCard> crdByHash; + for (oCard &c : Cards) { + if (!c.isRemoved()) { + crd[c.Id] = &c; + crdByHash[c.getCardHash()] = &c; + } + } + + for (oCard &c : src.Cards) { + const string &stmp = c.getStamp(); + if (!c.isRemoved() && stmp > previousMergeTime) { + if (stmp > thisMergeTime) + thisMergeTime = stmp; + + bool merged = false; + auto mc = crd.find(c.Id); + if (mc != crd.end()) { + auto p1 = c.getNumPunches() > 0 ? c.getPunchByIndex(c.getNumPunches() - 1)->getTimeInt() : 0; + auto c2 = mc->second; + auto p2 = c2->getNumPunches() > 0 ? c2->getPunchByIndex(c2->getNumPunches() - 1)->getTimeInt() : 0; + if (p1 == p2) + mergeItem(mc->second, c), merged = true; + } + + auto updateIdCrd = [&](int id) { + pCard other = src.getCard(id); + if (other) + other->changeId(c.Id); + c.changeId(id); + }; + + if (!merged) { + auto mcN = crdByHash.find(c.getCardHash()); + if (mcN != crdByHash.end()) { + mergeItem(mcN->second, c); + merged = true; + updateIdCrd(mcN->second->Id); + } + } + + if (!merged) { + if (crd.count(c.Id)) { + int newId = max(getFreeCardId(), src.getFreeCardId()); + c.changeId(newId); + } + pCard pNew = addCard(c); + updateNewItem(pNew, c); + } + } + } + } + + { + map clb; + map clbByExt; + map clbByName; + + for (oClub &c : Clubs) { + if (!c.isRemoved()) { + clb[c.Id] = &c; + if (c.getExtIdentifier() != 0) + clbByExt[c.getExtIdentifier()] = &c; + clbByName[c.getName()] = &c; + } + } + + set srcClub; + for (oClub &c : src.Clubs) { + const string &stmp = c.getStamp(); + if (!c.isRemoved()) { + bool okMerge = stmp > previousMergeTime; + + if (stmp > thisMergeTime) + thisMergeTime = stmp; + + bool merged = false; + auto mc = clb.find(c.Id); + if (mc != clb.end()) { + if ((c.getExtIdentifier() != 0 && c.getExtIdentifier() == mc->second->getExtIdentifier()) + || c.getName() == mc->second->getName()) { + if (okMerge) + mergeItem(mc->second, c); + merged = true; + } + } + + auto updateIdClb = [&](int id) { + pClub other = src.getClub(id); + if (other) + other->changeId(c.Id); + c.changeId(id); + }; + + if (!merged && c.getExtIdentifier() != 0) { + auto mcN = clbByExt.find(c.getExtIdentifier()); + if (mcN != clbByExt.end()) { + if (okMerge) + mergeItem(mcN->second, c); + merged = true; + updateIdClb(mcN->second->Id); + } + } + + if (!merged) { + auto mcN = clbByName.find(c.getName()); + if (mcN != clbByName.end()) { + if (okMerge) + mergeItem(mcN->second, c); + merged = true; + updateIdClb(mcN->second->Id); + } + } + + if (!merged && okMerge) { + if (clb.count(c.Id)) { + int newId = max(getFreeClubId(), src.getFreeClubId()); + c.changeId(newId); + } + pClub pNew = addClub(c); + updateNewItem(pNew, c); + } + + srcClub.insert(c.Id); + } + } + + computeRemove(Clubs, srcClub, rClub); + } + + { + map rn; + map rnByExt; + map, pRunner> rnByCardName; + + for (oRunner &r : Runners) { + if (!r.isRemoved()) { + rn[r.Id] = &r; + if (r.getExtIdentifier() != 0) + rnByExt[r.getExtIdentifier()] = &r; + rnByCardName[make_pair(r.getCardNo(), r.sName)] = &r; + } + } + set srcRunner; + for (oRunner &r : src.Runners) { + const string &stmp = r.getStamp(); + if (!r.isRemoved()) { + bool okMerge = stmp > previousMergeTime; + if (stmp > thisMergeTime) + thisMergeTime = stmp; + + bool merged = false; + auto mc = rn.find(r.Id); + if (mc != rn.end()) { + if ((r.getExtIdentifier() != 0 && r.getExtIdentifier() == mc->second->getExtIdentifier()) + || (r.sName == mc->second->sName && r.getClubId() == mc->second->getClubId()) + || r.getCardNo() == mc->second->getCardNo() + || mc->second->isVacant()) { + + if (okMerge) + mergeItem(mc->second, r); + merged = true; + } + } + + auto updateIdR = [&](int id) { + pRunner other = src.getRunner(id, 0); + if (other) + other->changeId(src.Id); + r.changeId(id); + }; + + if (!merged && r.getExtIdentifier() != 0) { + auto mcN = rnByExt.find(r.getExtIdentifier()); + if (mcN != rnByExt.end()) { + if (okMerge) + mergeItem(mcN->second, r); + merged = true; + updateIdR(mcN->second->Id); + } + } + + if (!merged) { + auto mcN = rnByCardName.find(make_pair(r.getCardNo(), r.sName)); + if (mcN != rnByCardName.end()) { + if (okMerge) + mergeItem(mcN->second, r); + merged = true; + updateIdR(mcN->second->Id); + } + } + + if (!merged && okMerge) { + if (rn.count(r.Id)) { + int newId = max(getFreeRunnerId(), src.getFreeRunnerId()); + r.changeId(newId); + } + pRunner pNew = addRunner(r, false); + updateNewItem(pNew, r); + } + + srcRunner.insert(r.Id); + } + } + + computeRemove(Runners, srcRunner, rRunner); + } + + { + map tm; + map, pTeam> tmByClassName; + + for (oTeam &t : Teams) { + if (!t.isRemoved()) { + tm[t.Id] = &t; + tmByClassName[make_pair(t.getClassId(false), t.getName())] = &t; + } + } + + set srcTeam; + for (oTeam &t : src.Teams) { + const string &stmp = t.getStamp(); + if (!t.isRemoved()) { + bool okMerge = stmp > previousMergeTime; + if (stmp > thisMergeTime) + thisMergeTime = stmp; + + bool merged = false; + auto mc = tm.find(t.Id); + if (mc != tm.end()) { + if (t.getClubId() == mc->second->getClubId()) { + if (okMerge) + mergeItem(mc->second, t); + merged = true; + } + } + + auto updateIdT = [&](int id) { + pTeam other = src.getTeam(id); + if (other) + other->changeId(src.Id); + t.changeId(id); + }; + + + if (!merged) { + auto mcN = tmByClassName.find(make_pair(t.getClassId(false), t.getName())); + if (mcN != tmByClassName.end()) { + if (okMerge) + mergeItem(mcN->second, t); + merged = true; + updateIdT(mcN->second->Id); + } + } + + if (!merged && okMerge) { + if (tm.count(t.Id)) { + int newId = max(getFreeTeamId(), src.getFreeTeamId()); + t.changeId(newId); + } + pTeam pNew = addTeam(t, false); + updateNewItem(pNew, t); + } + + srcTeam.insert(t.Id); + } + } + + computeRemove(Teams, srcTeam, rTeam); + } + + auto removeEnts = [&numRemove](const set &ids, auto get) { + for (int id : ids) { + pBase b = get(id); + if (b && b->canRemove()) { + b->remove(); + numRemove++; + } + } + }; + + removeEnts(rTeam, [this](int id) -> pBase {return getTeam(id); }); + removeEnts(rRunner, [this](int id) -> pBase {return getRunner(id, 0); }); + removeEnts(rClub, [this](int id) -> pBase {return getClub(id); }); + removeEnts(rClass, [this](int id) -> pBase {return getClass(id); }); + removeEnts(rCourse, [this](int id) -> pBase {return getCourse(id); }); + removeEnts(rControl, [this](int id) -> pBase {return getControl(id); }); + + wstring mtOut(thisMergeTime.begin(), thisMergeTime.end()); + addMergeInfo(mergeTag, mtOut); + synchronize(); +} + +wstring oEvent::cloneCompetition(bool cloneRunners, bool cloneTimes, + bool cloneCourses, bool cloneResult, bool addToDate) { + + if (cloneResult) { + cloneTimes = true; + cloneCourses = true; + } + if (cloneTimes) + cloneRunners = true; + + oEvent ce(gdibase); + ce.newCompetition(Name); + ce.ZeroTime = ZeroTime; + ce.Date = Date; + + if (addToDate) { + SYSTEMTIME st; + convertDateYMS(Date, st, false); + __int64 absD = SystemTimeToInt64Second(st); + absD += 3600 * 24; + ce.Date = convertSystemDate(Int64SecondToSystemTime(absD)); + } + int len = Name.length(); + if (len > 2 && isdigit(Name[len - 1]) && !isdigit(Name[len - 2])) { + ++ce.Name[len - 1]; // E1 -> E2 + } + else + ce.Name += L" E2"; + + memcpy(ce.oData, oData, sizeof(oData)); + + for (oClubList::iterator it = Clubs.begin(); it != Clubs.end(); ++it) { + if (it->isRemoved()) + continue; + pClub pc = ce.addClub(it->name, it->Id); + memcpy(pc->oData, it->oData, sizeof(pc->oData)); + } + + if (cloneCourses) { + for (oControlList::iterator it = Controls.begin(); it != Controls.end(); ++it) { + if (it->isRemoved()) + continue; + pControl pc = ce.addControl(it->Id, 100, it->Name); + pc->setNumbers(it->codeNumbers()); + pc->Status = it->Status; + memcpy(pc->oData, it->oData, sizeof(pc->oData)); + } + + for (oCourseList::iterator it = Courses.begin(); it != Courses.end(); ++it) { + if (it->isRemoved()) + continue; + pCourse pc = ce.addCourse(it->Name, it->Length, it->Id); + pc->importControls(it->getControls(), false, false); + pc->legLengths = it->legLengths; + memcpy(pc->oData, it->oData, sizeof(pc->oData)); + } + } + + for (oClassList::iterator it = Classes.begin(); it != Classes.end(); ++it) { + if (it->isRemoved()) + continue; + pClass pc = ce.addClass(it->Name, 0, it->Id); + memcpy(pc->oData, it->oData, sizeof(pc->oData)); + pc->setNumStages(it->getNumStages()); + pc->legInfo = it->legInfo; + + if (cloneCourses) { + pc->Course = ce.getCourse(it->getCourseId()); + pc->MultiCourse = it->MultiCourse; // Points to wrong competition, but valid for now... + } + } + + if (cloneRunners) { + for (oRunnerList::iterator it = Runners.begin(); it != Runners.end(); ++it) { + if (it->isRemoved()) + continue; + + oRunner r(&ce, it->Id); + r.sName = it->sName; + r.getRealName(r.sName, r.tRealName); + r.StartNo = it->StartNo; + r.cardNumber = it->cardNumber; + r.Club = ce.getClub(it->getClubId()); + r.Class = ce.getClass(it->getClassId(false)); + + if (cloneCourses) + r.Course = ce.getCourse(it->getCourseId()); + + pRunner pr = ce.addRunner(r, false); + + pr->decodeMultiR(it->codeMultiR()); + memcpy(pr->oData, it->oData, sizeof(pr->oData)); + + if (cloneTimes) { + pr->startTime = it->startTime; + } + + if (cloneResult) { + if (it->Card) { + pr->Card = ce.addCard(*it->Card); + pr->Card->tOwner = pr; + } + pr->FinishTime = it->FinishTime; + pr->status = it->status; + } + } + + for (oTeamList::iterator it = Teams.begin(); it != Teams.end(); ++it) { + if (it->skip()) + continue; + + oTeam t(&ce, it->Id); + + t.sName = it->sName; + t.StartNo = it->StartNo; + t.Club = ce.getClub(it->getClubId()); + t.Class = ce.getClass(it->getClassId(false)); + + if (cloneTimes) + t.startTime = it->startTime; + + pTeam pt = ce.addTeam(t, false); + memcpy(pt->oData, it->oData, sizeof(pt->oData)); + + pt->Runners.resize(it->Runners.size()); + for (size_t k = 0; kRunners.size(); k++) { + int id = it->Runners[k] ? it->Runners[k]->Id : 0; + if (id) + pt->Runners[k] = ce.getRunner(id, 0); + } + + t.apply(ChangeType::Update, nullptr); + } + + for (oRunnerList::iterator it = ce.Runners.begin(); it != ce.Runners.end(); ++it) { + it->createMultiRunner(false, false); + } + } + + vector changedClass, changedClassNoResult, assignedVacant, newEntries, notTransfered, noAssign; + set dummy; + transferResult(ce, dummy, TransferAnyway, false, changedClass, changedClassNoResult, assignedVacant, newEntries, notTransfered, noAssign); + + vector newEntriesT, notTransferedT, noAssignT; + transferResult(ce, TransferAnyway, newEntriesT, notTransferedT, noAssignT); + + int eventNumberCurrent = getStageNumber(); + if (eventNumberCurrent <= 0) { + eventNumberCurrent = 1; + setStageNumber(eventNumberCurrent); + } + + ce.getDI().setString("PreEvent", currentNameId); + ce.setStageNumber(eventNumberCurrent + 1); + getDI().setString("PostEvent", ce.currentNameId); + + int nf = getMeOSFeatures().getNumFeatures(); + for (int k = 0; k < nf; k++) { + MeOSFeatures::Feature f = getMeOSFeatures().getFeature(k); + if (getMeOSFeatures().hasFeature(f)) + ce.getMeOSFeatures().useFeature(f, true, ce); + } + + // Transfer lists and list configurations. + if (listContainer) { + loadGeneralResults(false, false); + swap(ce.generalResults, generalResults); + try { + ce.listContainer = new MetaListContainer(&ce, *listContainer); + ce.save(); + } + catch (...) { + swap(ce.generalResults, generalResults); + throw; + } + + swap(ce.generalResults, generalResults); + } + return ce.CurrentFile; +} + +void oEvent::transferListsAndSave(const oEvent &src) { + src.loadGeneralResults(false, false); + swap(src.generalResults, generalResults); + try { + src.getListContainer().synchronizeTo(getListContainer()); + save(); + } + catch (...) { + swap(src.generalResults, generalResults); + throw; + } + + swap(src.generalResults, generalResults); +} + + +bool checkTargetClass(pRunner target, pRunner source, + const oClassList &Classes, + const vector &targetVacant, + vector &changedClass, + oEvent::ChangedClassMethod changeClassMethod) { + if (changeClassMethod == oEvent::TransferAnyway) + return true; + + if (!compareClassName(target->getClass(false), source->getClass(false))) { + // Store all vacant positions in the right class + int targetClass = -1; + + if (target->getStatus() == StatusOK) { + // There is already a result. Do not change class! + return false; + } + + if (changeClassMethod == oEvent::TransferNoResult) + return false; // Do not allow change class, do not transfer result + + for (oClassList::const_iterator cit = Classes.begin(); cit != Classes.end(); ++cit) { + if (cit->isRemoved()) + continue; + if (compareClassName(cit->getName(), source->getClass(false))) { + targetClass = cit->getId(); + + if (targetClass == source->getClassId(false) || cit->getName() == source->getClass(false)) + break; // Assume exact match + } + } + + if (targetClass != -1) { + set vacantIx; + for (size_t j = 0; j < targetVacant.size(); j++) { + if (!targetVacant[j]) + continue; + if (targetVacant[j]->getClassId(false) == targetClass) + vacantIx.insert(j); + } + int posToUse = -1; + if (vacantIx.size() == 1) + posToUse = *vacantIx.begin(); + else if (vacantIx.size() > 1) { + wstring srcBib = source->getBib(); + if (srcBib.length() > 0) { + for (set::iterator tit = vacantIx.begin(); tit != vacantIx.end(); ++tit) { + if (targetVacant[*tit]->getBib() == srcBib) { + posToUse = *tit; + break; + } + } + } + + if (posToUse == -1) + posToUse = *vacantIx.begin(); + } + + if (posToUse != -1) { + // Change class or change class vacant + changedClass.push_back(target); + + int oldStart = target->getStartTime(); + wstring oldBib = target->getBib(); + int oldSN = target->getStartNo(); + int oldClass = target->getClassId(false); + pRunner tgt = targetVacant[posToUse]; + target->cloneStartTime(tgt); + target->setBib(tgt->getBib(), 0, false); + target->setStartNo(tgt->getStartNo(), oBase::ChangeType::Update); + target->setClassId(tgt->getClassId(false), false); + + tgt->setStartTime(oldStart, true, oBase::ChangeType::Update); + tgt->setBib(oldBib, 0, false); + tgt->setStartNo(oldSN, oBase::ChangeType::Update); + tgt->setClassId(oldClass, false); + return true; // Changed to correct class + } + else if (changeClassMethod == oEvent::ChangeClass) { + // Simpliy change class + target->setClassId(targetClass, false); + return true; + } + } + return false; // Wrong class, ChangeClass (but failed) + } + + return true; // Same class, OK +} + +void oEvent::transferResult(oEvent &ce, + const set &allowNewEntries, + ChangedClassMethod changeClassMethod, + bool transferAllNoCompete, + vector &changedClass, + vector &changedClassNoResult, + vector &assignedVacant, + vector &newEntries, + vector ¬Transfered, + vector &noAssignmentTarget) { + + inthashmap processed(ce.Runners.size()); + inthashmap used(Runners.size()); + + changedClass.clear(); + changedClassNoResult.clear(); + assignedVacant.clear(); + newEntries.clear(); + notTransfered.clear(); + noAssignmentTarget.clear(); + + vector targetRunners; + vector targetVacant; + + targetRunners.reserve(ce.Runners.size()); + for (oRunnerList::iterator it = ce.Runners.begin(); it != ce.Runners.end(); ++it) { + if (!it->skip()) { + if (!it->isVacant()) + targetRunners.push_back(&*it); + else + targetVacant.push_back(&*it); + } + } + + calculateResults({}, ResultType::TotalResult); + // Lookup by id + for (size_t k = 0; k < targetRunners.size(); k++) { + pRunner it = targetRunners[k]; + pRunner r = getRunner(it->Id, 0); + if (!r) + continue; + + __int64 id1 = r->getExtIdentifier(); + __int64 id2 = it->getExtIdentifier(); + + if (id1>0 && id2>0 && id1 != id2) + continue; + + wstring cnA = canonizeName(it->sName.c_str()); + wstring cnB = canonizeName(r->sName.c_str()); + wstring ccnA = canonizeName(it->getClub().c_str()); + wstring ccnB = canonizeName(r->getClub().c_str()); + + if ((id1>0 && id1 == id2) || + (r->cardNumber>0 && r->cardNumber == it->cardNumber) || + (it->sName == r->sName) || (cnA == cnB && ccnA == ccnB)) { + processed.insert(it->Id, 1); + used.insert(r->Id, 1); + if (checkTargetClass(it, r, ce.Classes, targetVacant, changedClass, changeClassMethod)) + it->setInputData(*r); + else { + it->resetInputData(); + changedClassNoResult.push_back(it); + } + } + } + + if (processed.size() < int(targetRunners.size())) { + // Lookup by card + int v; + for (size_t k = 0; k < targetRunners.size(); k++) { + pRunner it = targetRunners[k]; + if (processed.lookup(it->Id, v)) + continue; + if (it->getCardNo() > 0) { + pRunner r = getRunnerByCardNo(it->getCardNo(), 0, CardLookupProperty::Any); + + if (!r || used.lookup(r->Id, v)) + continue; + + __int64 id1 = r->getExtIdentifier(); + __int64 id2 = it->getExtIdentifier(); + + if (id1>0 && id2>0 && id1 != id2) + continue; + + if ((id1>0 && id1 == id2) || (it->sName == r->sName && it->getClub() == r->getClub())) { + processed.insert(it->Id, 1); + used.insert(r->Id, 1); + if (checkTargetClass(it, r, ce.Classes, targetVacant, changedClass, changeClassMethod)) + it->setInputData(*r); + else { + it->resetInputData(); + changedClassNoResult.push_back(it); + } + } + } + } + } + + int v = -1; + + // Store remaining runners + vector remainingRunners; + for (oRunnerList::iterator it2 = Runners.begin(); it2 != Runners.end(); ++it2) { + if (it2->skip() || used.lookup(it2->Id, v)) + continue; + if (it2->isVacant()) + continue; // Ignore vacancies on source side + + remainingRunners.push_back(&*it2); + } + + if (processed.size() < int(targetRunners.size()) && !remainingRunners.empty()) { + // Lookup by name / ext id + vector cnd; + for (size_t k = 0; k < targetRunners.size(); k++) { + pRunner it = targetRunners[k]; + if (processed.lookup(it->Id, v)) + continue; + + __int64 id1 = it->getExtIdentifier(); + + cnd.clear(); + for (size_t j = 0; j < remainingRunners.size(); j++) { + pRunner src = remainingRunners[j]; + if (!src) + continue; + + if (id1 > 0) { + __int64 id2 = src->getExtIdentifier(); + if (id2 == id1) { + cnd.clear(); + cnd.push_back(j); + break; //This is the one, if they have the same Id there will be a unique match below + } + } + if (it->sName == src->sName && it->getClub() == src->getClub()) + cnd.push_back(j); + } + + if (cnd.size() == 1) { + pRunner &src = remainingRunners[cnd[0]]; + processed.insert(it->Id, 1); + used.insert(src->Id, 1); + if (checkTargetClass(it, src, ce.Classes, targetVacant, changedClass, changeClassMethod)) { + it->setInputData(*src); + } + else { + it->resetInputData(); + changedClassNoResult.push_back(it); + } + src = 0; + } + else if (cnd.size() > 0) { // More than one candidate + int winnerIx = -1; + int point = -1; + for (size_t j = 0; j < cnd.size(); j++) { + pRunner src = remainingRunners[cnd[j]]; + int p = 0; + if (src->getClass(false) == it->getClass(false)) + p += 1; + if (src->getBirthYear() == it->getBirthYear()) + p += 2; + if (p > point) { + winnerIx = cnd[j]; + point = p; + } + } + + if (winnerIx != -1) { + processed.insert(it->Id, 1); + pRunner winner = remainingRunners[winnerIx]; + remainingRunners[winnerIx] = 0; + + used.insert(winner->Id, 1); + if (checkTargetClass(it, winner, ce.Classes, targetVacant, changedClass, changeClassMethod)) { + it->setInputData(*winner); + } + else { + it->resetInputData(); + changedClassNoResult.push_back(it); + } + } + } + } + } + + // Transfer vacancies + for (size_t k = 0; k < remainingRunners.size(); k++) { + pRunner src = remainingRunners[k]; + if (!src || used.lookup(src->Id, v)) + continue; + + bool forceSkip = src->hasFlag(oAbstractRunner::FlagTransferSpecified) && + !src->hasFlag(oAbstractRunner::FlagTransferNew); + + if (forceSkip) { + notTransfered.push_back(src); + continue; + } + + pRunner targetVacant = ce.getRunner(src->getId(), 0); + if (targetVacant && targetVacant->isVacant() && compareClassName(targetVacant->getClass(false), src->getClass(false))) { + targetVacant->setName(src->getName(), false); + targetVacant->setClub(src->getClub()); + targetVacant->setCardNo(src->getCardNo(), false); + targetVacant->cloneData(src); + assignedVacant.push_back(targetVacant); + } + else { + pClass dstClass = ce.getClass(src->getClassId(false)); + if (dstClass && compareClassName(dstClass->getName(), src->getClass(false))) { + if ((!src->hasFlag(oAbstractRunner::FlagTransferSpecified) && allowNewEntries.count(src->getClassId(false))) + || src->hasFlag(oAbstractRunner::FlagTransferNew)) { + if (src->getClubId() > 0) + ce.getClubCreate(src->getClubId(), src->getClub()); + pRunner dst = ce.addRunner(src->getName(), src->getClubId(), src->getClassId(false), + src->getCardNo(), src->getBirthYear(), true); + dst->cloneData(src); + dst->setInputData(*src); + newEntries.push_back(dst); + } + else if (transferAllNoCompete) { + if (src->getClubId() > 0) + ce.getClubCreate(src->getClubId(), src->getClub()); + pRunner dst = ce.addRunner(src->getName(), src->getClubId(), src->getClassId(false), + 0, src->getBirthYear(), true); + dst->cloneData(src); + dst->setInputData(*src); + dst->setStatus(StatusNotCompetiting, true, ChangeType::Update); + notTransfered.push_back(dst); + } + else + notTransfered.push_back(src); + } + } + } + + // Runners on target side not assigned a result + for (size_t k = 0; k < targetRunners.size(); k++) { + if (targetRunners[k] && !processed.count(targetRunners[k]->Id)) { + noAssignmentTarget.push_back(targetRunners[k]); + if (targetRunners[k]->inputStatus == StatusUnknown || + (targetRunners[k]->inputStatus == StatusOK && targetRunners[k]->inputTime == 0)) { + targetRunners[k]->inputStatus = StatusNotCompetiting; + } + } + } +} + +void oEvent::transferResult(oEvent &ce, + ChangedClassMethod changeClassMethod, + vector &newEntries, + vector ¬Transfered, + vector &noAssignmentTarget) { + + inthashmap processed(ce.Teams.size()); + inthashmap used(Teams.size()); + + newEntries.clear(); + notTransfered.clear(); + noAssignmentTarget.clear(); + + vector targetTeams; + + targetTeams.reserve(ce.Teams.size()); + for (oTeamList::iterator it = ce.Teams.begin(); it != ce.Teams.end(); ++it) { + if (!it->skip()) { + targetTeams.push_back(&*it); + } + } + + calculateTeamResults(set(), ResultType::TotalResult); + // Lookup by id + for (size_t k = 0; k < targetTeams.size(); k++) { + pTeam it = targetTeams[k]; + pTeam t = getTeam(it->Id); + if (!t) + continue; + + __int64 id1 = t->getExtIdentifier(); + __int64 id2 = it->getExtIdentifier(); + + if (id1>0 && id2>0 && id1 != id2) + continue; + + if ((id1>0 && id1 == id2) || (it->sName == t->sName && it->getClub() == t->getClub())) { + processed.insert(it->Id, 1); + used.insert(t->Id, 1); + it->setInputData(*t); + //checkTargetClass(it, r, ce.Classes, targetVacant, changedClass); + } + } + + int v = -1; + + // Store remaining runners + vector remainingTeams; + for (oTeamList::iterator it2 = Teams.begin(); it2 != Teams.end(); ++it2) { + if (it2->skip() || used.lookup(it2->Id, v)) + continue; + if (it2->isVacant()) + continue; // Ignore vacancies on source side + + remainingTeams.push_back(&*it2); + } + + if (processed.size() < int(targetTeams.size()) && !remainingTeams.empty()) { + // Lookup by name / ext id + vector cnd; + for (size_t k = 0; k < targetTeams.size(); k++) { + pTeam it = targetTeams[k]; + if (processed.lookup(it->Id, v)) + continue; + + __int64 id1 = it->getExtIdentifier(); + + cnd.clear(); + for (size_t j = 0; j < remainingTeams.size(); j++) { + pTeam src = remainingTeams[j]; + if (!src) + continue; + + if (id1 > 0) { + __int64 id2 = src->getExtIdentifier(); + if (id2 == id1) { + cnd.clear(); + cnd.push_back(j); + break; //This is the one, if they have the same Id there will be a unique match below + } + } + + if (it->sName == src->sName && it->getClub() == src->getClub()) + cnd.push_back(j); + } + + if (cnd.size() == 1) { + pTeam &src = remainingTeams[cnd[0]]; + processed.insert(it->Id, 1); + used.insert(src->Id, 1); + it->setInputData(*src); + //checkTargetClass(it, src, ce.Classes, targetVacant, changedClass); + src = 0; + } + else if (cnd.size() > 0) { // More than one candidate + int winnerIx = -1; + int point = -1; + for (size_t j = 0; j < cnd.size(); j++) { + pTeam src = remainingTeams[cnd[j]]; + int p = 0; + if (src->getClass(false) == it->getClass(false)) + p += 1; + if (p > point) { + winnerIx = cnd[j]; + point = p; + } + } + + if (winnerIx != -1) { + processed.insert(it->Id, 1); + pTeam winner = remainingTeams[winnerIx]; + remainingTeams[winnerIx] = 0; + + used.insert(winner->Id, 1); + it->setInputData(*winner); + //checkTargetClass(it, winner, ce.Classes, targetVacant, changedClass); + } + } + } + } + /* + // Transfer vacancies + for (size_t k = 0; k < remainingRunners.size(); k++) { + pRunner src = remainingRunners[k]; + if (!src || used.lookup(src->Id, v)) + continue; + + pRunner targetVacant = ce.getRunner(src->getId(), 0); + if (targetVacant && targetVacant->isVacant() && compareClassName(targetVacant->getClass(), src->getClass()) ) { + targetVacant->setName(src->getName()); + targetVacant->setClub(src->getClub()); + targetVacant->setCardNo(src->getCardNo(), false); + targetVacant->cloneData(src); + assignedVacant.push_back(targetVacant); + } + else { + pClass dstClass = ce.getClass(src->getClassId()); + if (dstClass && compareClassName(dstClass->getName(), src->getClass())) { + if (allowNewEntries.count(src->getClassId())) { + if (src->getClubId() > 0) + ce.getClubCreate(src->getClubId(), src->getClub()); + pRunner dst = ce.addRunner(src->getName(), src->getClubId(), src->getClassId(), + src->getCardNo(), src->getBirthYear(), true); + dst->cloneData(src); + dst->setInputData(*src); + newEntries.push_back(dst); + } + else if (transferAllNoCompete) { + if (src->getClubId() > 0) + ce.getClubCreate(src->getClubId(), src->getClub()); + pRunner dst = ce.addRunner(src->getName(), src->getClubId(), src->getClassId(), + 0, src->getBirthYear(), true); + dst->cloneData(src); + dst->setInputData(*src); + dst->setStatus(StatusNotCompetiting); + notTransfered.push_back(dst); + } + else + notTransfered.push_back(src); + } + } + } + + // Runners on target side not assigned a result + for (size_t k = 0; k < targetRunners.size(); k++) { + if (targetRunners[k] && !processed.count(targetRunners[k]->Id)) { + noAssignmentTarget.push_back(targetRunners[k]); + if (targetRunners[k]->inputStatus == StatusUnknown || + (targetRunners[k]->inputStatus == StatusOK && targetRunners[k]->inputTime == 0)) { + targetRunners[k]->inputStatus = StatusNotCompetiting; + } + } + + }*/ +} + +void oAbstractRunner::merge(const oBase &input) { + const oAbstractRunner &src = dynamic_cast(input); + setName(src.sName, false); + + setStartTime(src.startTime, true, ChangeType::Update, false); + setFinishTime(src.FinishTime); + setStatus(src.status, true, ChangeType::Update); + setStartNo(src.StartNo, ChangeType::Update); + setClubId(src.getClubId()); + setClassId(src.getClassId(false), false); + + setInputPlace(src.inputPlace); + if (inputTime != src.inputTime) { + inputTime = src.inputTime; + updateChanged(); + } + setInputStatus(src.inputStatus); + setInputPoints(src.inputPoints); +} + +void oRunner::merge(const oBase &input) { + oAbstractRunner::merge(input); + + const oRunner &src = dynamic_cast(input); + setCourseId(src.getCourseId()); + if (src.getCardId() != 0) + setCard(src.getCardId()); + setCardNo(src.getCardNo(), false); + + if (memcmp(oData, src.oData, sizeof(oData)) != 0) { + memcpy(oData, src.oData, sizeof(oData)); + updateChanged(); + } + synchronize(true); +} + +void oTeam::merge(const oBase &input) { + oAbstractRunner::merge(input); + + const oTeam &src = dynamic_cast(input); + + bool same = src.Runners.size() == Runners.size(); + vector r(src.Runners.size()); + for (size_t i = 0; i < src.Runners.size(); i++) { + if (src.Runners[i]) { + r[i] = src.Runners[i]->Id; + src.Runners[i]->tInTeam = nullptr; + } + if (same) { + int rc = Runners[i] ? Runners[i]->Id : 0; + if (rc != r[i]) + same = false; + } + } + + importRunners(r); + if (!same) + updateChanged(); + + if (memcmp(oData, src.oData, sizeof(oData)) != 0) { + memcpy(oData, src.oData, sizeof(oData)); + updateChanged(); + } + synchronize(true); +} + +void oControl::merge(const oBase &input) { + const oControl &src = dynamic_cast(input); + if (src.Name.length() > 0) + setName(src.Name); + setNumbers(src.codeNumbers()); + setStatus(src.getStatus()); + if (memcmp(oData, src.oData, sizeof(oData)) != 0) { + memcpy(oData, src.oData, sizeof(oData)); + updateChanged(); + } + synchronize(true); +} + +void oCourse::merge(const oBase &input) { + const oCourse &src = dynamic_cast(input); + + if (src.Name.length() > 0) + setName(src.Name); + setLength(src.Length); + importControls(src.getControls(), true, false); + importLegLengths(src.getLegLengths(), true); + + if (memcmp(oData, src.oData, sizeof(oData)) != 0) { + memcpy(oData, src.oData, sizeof(oData)); + updateChanged(); + } + synchronize(true); +} + +void oClass::merge(const oBase &input) { + const oClass &src = dynamic_cast(input); + + if (src.Name.length() > 0) + setName(src.Name, true); + setCourse(oe->getCourse(src.getCourseId())); + + if (src.MultiCourse.size() > 0) { + vector> mcCopy = MultiCourse; + set cid; + vector< vector > multi; + parseCourses(src.codeMultiCourse(), multi, cid); + importCourses(multi); + + if (mcCopy != MultiCourse) + updateChanged(); + } + else { + setNumStages(0); + } + + if (src.legInfo.size() > 0) { + if (codeLegMethod() != src.codeLegMethod()) { + importLegMethod(src.codeLegMethod()); + updateChanged(); + } + } + + if (memcmp(oData, src.oData, sizeof(oData)) != 0) { + memcpy(oData, src.oData, sizeof(oData)); + updateChanged(); + } + synchronize(true); +} + +void oClub::merge(const oBase &input) { + const oClub &src = dynamic_cast(input); + + setName(src.getName()); + if (memcmp(oData, src.oData, sizeof(oData)) != 0) { + memcpy(oData, src.oData, sizeof(oData)); + updateChanged(); + } + synchronize(true); +} + +void oCard::merge(const oBase &input) { + const oCard &src = dynamic_cast(input); + + setCardNo(src.getCardNo()); + if (readId != src.readId) { + readId = src.readId; + updateChanged(); + } + if (getPunchString() != src.getPunchString()) { + importPunches(src.getPunchString()); + updateChanged(); + } + synchronize(true); +} + +void oPunch::merge(const oBase &input) { + const oPunch &src = dynamic_cast(input); +// Not implemented +} + + +void oFreePunch::merge(const oBase &input) { + const oFreePunch &src = dynamic_cast(input); + // Not implemented +} + + +void oEvent::merge(const oBase &srcIn) { +} + + +wstring oEvent::getMergeTag(bool forceReset) { + wstring sm = getDCI().getString("MergeTag"); + if (sm.empty() || forceReset) { + static const char alphanum[] = + "0123456789" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz"; + + random_device r; + mt19937 e1(r()); + + wchar_t s[13]; + for (int i = 0; i < 12; ++i) { + s[i] = alphanum[rand() % (sizeof(alphanum) - 1)]; + } + s[12] = 0; + getDI().setString("MergeTag", s); + synchronize(true); + sm = s; + } + return sm; +} + +wstring oEvent::getMergeInfo(const wstring &tag) const { + wstring mv = getDCI().getString("MergeInfo"); + vector mvv; + split(mv, L":", mvv); + for (size_t j = 0; j + 1 < mvv.size(); j += 2) { + if (mvv[j] == tag) + return mvv[j + 1]; + } + return L""; +} + +void oEvent::addMergeInfo(const wstring &tag, const wstring &version) { + wstring mv = getDCI().getString("MergeInfo"); + vector mvv; + split(mv, L":", mvv); + bool ok = false; + for (size_t j = 0; j + 1 < mvv.size(); j += 2) { + if (mvv[j] == tag) + mvv[j + 1] = version, ok = true; + } + if (!ok) { + mvv.push_back(tag); + mvv.push_back(version); + } + unsplit(mvv, L":", mv); + getDI().setString("MergeInfo", mv); +} + +string oEvent::getLastModified() const { + string s; + + auto maxModified = [&s](auto &cnt) { + for (auto &b : cnt) + if (!b.isRemoved() && b.getStamp() > s) + s = b.getStamp(); + }; + + maxModified(Controls); + maxModified(Courses); + maxModified(Classes); + maxModified(Cards); + maxModified(Clubs); + maxModified(Runners); + maxModified(Teams); + // maxModified(punches); xxx ignored + + return s; +} diff --git a/code/swedish.lng b/code/swedish.lng index 769601e..8e0ad77 100644 --- a/code/swedish.lng +++ b/code/swedish.lng @@ -2501,3 +2501,29 @@ Lottat = Lottat Sist = Sist Fakturadatum = Fakturadatum Youth Cup X = Ungdomscup X +Ny startgrupp = Ny startgrupp +Slut = Slut +Startgrupper = Startgrupper +help:startgroup = Startgrupper används för att styra lottningen. Deltagare i en viss grupp börjar starta vid startgruppens starttid +Tips: ställ in rätt tid innan du lägger till fler grupper = Tips: ställ in rätt tid innan du lägger till fler grupper +Familj = Familj +Startgrupp = Startgrupp +Slå ihop tävlingar = Slå ihop tävlingar +help:merge = Det är möjligt att slå ihop tävlingar och resultat, givet att de är grundade på samma uppsättning banor och kontroller. Olika grupper deltagare kan genomföra tävlingen vid olika tillfällen och sedan kan de olika tävlingarna slås ihop till en tävling med gemensam resultatlista. En annan möjlighet är att ha olika TC för olika klasser. Om det inte är möjliget att att sätt upp ett gemensamt nätverk, kan man med jämna mellanrum utbyta tävlingsfiler för att införliva ändringar.\n\n1. Förbered hela tävlingen.\n2. Spara en kopia och importera den på de utlokaliserade datorerna (eller lokala nätverken).\n3. För att överföra ändringar, exportera tävlingen från den utlokaliserade datorn (eller datorerna) och slå ihop den med denna funktion. Exportera sedan en kopia från huvuddatorn och gör motsvarande import på de utlokaliserade datorerna.\n4 Proceduren kan upprepas flera gånger för att kontinuerligt överföra resultaten. \n\n Observera: Om du gör ändringar i (t.ex.) samma deltagare på flera ställen, blir vissa av ändringarna överskrivna utan varning. Se till att varje utlokaliserat ställe endast ändrar i sin del av tävlingen.\n\nTips: Gör en överföring så snart de utlokaliserade tävlingarna är startade innan någon ändring utförts, för att testa att allt blivit rätt uppsatt. +Denna datakälla är aldrig tidigare infogad = Denna datakälla är aldrig tidigare infogad +Fel: Denna tävlingsversion är redan infogad = Fel: Denna tävlingsversion är redan infogad +Infoga version: X = Infoga version: X +Samma bastävling = Samma bastävling +Sammanslagning fungerar om samma uppsättning banor/kontroller används = Sammanslagning fungerar om samma uppsättning banor/kontroller används +Varning: Olika bastävlingar = Varning: Olika bastävlingar +Borttagna: X = Borttagna: X +Fel: En tävling kan inte slås ihop med sig själv = Fel: En tävling kan inte slås ihop med sig själv +Sammanfattning, uppdateradet poster = Sammanfattning, uppdateradet poster +Sammanslagning klar = Sammanslagning klar +Skapade lokal säkerhetskopia (X) innan sammanslagning = Skapade lokal säkerhetskopia (X) innan sammanslagning +Tillagda: X = Tillagda: X +Uppdaterade: X = Uppdaterade: X +Tjänstebeställningar (IOF XML) = Tjänstebeställningar (IOF XML) +Tjänster (IOF XML) = Tjänster (IOF XML) +Flytta deltagare från överfulla grupper = Flytta deltagare från överfulla grupper +Lotta med startgrupper = Lotta med startgrupper