diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/SeriesCreationParametersDTO.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/SeriesCreationParametersDTO.java index 94ba4e935b4..fbc9ac70b83 100755 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/SeriesCreationParametersDTO.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/SeriesCreationParametersDTO.java @@ -1,58 +1,65 @@ -package com.sap.sailing.domain.common.dto; - -import java.io.Serializable; -import java.util.List; - -public class SeriesCreationParametersDTO implements Serializable { - private static final long serialVersionUID = -3205172230707607515L; - - private List fleets; - - private boolean medal; - - private boolean startsWithZero; - - private boolean firstColumnIsNonDiscardableCarryForward; - - private int[] discardingThresholds; - - private boolean hasSplitFleetContiguousScoring; - - SeriesCreationParametersDTO() {} - - public SeriesCreationParametersDTO(List fleets, boolean medal, boolean startsWithZero, boolean firstColumnIsNonDiscardableCarryForward, - int[] discardingThresholds, boolean hasSplitFleetContiguousScoring) { - super(); - this.fleets = fleets; - this.medal = medal; - this.startsWithZero = startsWithZero; - this.hasSplitFleetContiguousScoring = hasSplitFleetContiguousScoring; - this.firstColumnIsNonDiscardableCarryForward = firstColumnIsNonDiscardableCarryForward; - this.discardingThresholds = discardingThresholds; - } - - public List getFleets() { - return fleets; - } - - public boolean isMedal() { - return medal; - } - - public boolean isStartsWithZero() { - return startsWithZero; - } - - public boolean hasSplitFleetContiguousScoring() { - return hasSplitFleetContiguousScoring; - } - - public int[] getDiscardingThresholds() { - return discardingThresholds; - } - - public boolean isFirstColumnIsNonDiscardableCarryForward() { - return firstColumnIsNonDiscardableCarryForward; - } - -} +package com.sap.sailing.domain.common.dto; + +import java.io.Serializable; +import java.util.List; + +public class SeriesCreationParametersDTO implements Serializable { + private static final long serialVersionUID = -3205172230707607515L; + + private List fleets; + + private boolean isMedal; + + private boolean isFleetsCanRunInParallel; + + private boolean isStartsWithZeroScore; + + private boolean firstColumnIsNonDiscardableCarryForward; + + private int[] discardingThresholds; + + private boolean hasSplitFleetContiguousScoring; + + SeriesCreationParametersDTO() {} + + public SeriesCreationParametersDTO(List fleets, boolean isMedal, boolean isFleetsCanRunInParallel, boolean isStartsWithZeroScore, boolean firstColumnIsNonDiscardableCarryForward, + int[] discardingThresholds, boolean hasSplitFleetContiguousScoring) { + super(); + this.fleets = fleets; + this.isMedal = isMedal; + this.isFleetsCanRunInParallel = isFleetsCanRunInParallel; + this.isStartsWithZeroScore = isStartsWithZeroScore; + this.hasSplitFleetContiguousScoring = hasSplitFleetContiguousScoring; + this.firstColumnIsNonDiscardableCarryForward = firstColumnIsNonDiscardableCarryForward; + this.discardingThresholds = discardingThresholds; + } + + public List getFleets() { + return fleets; + } + + public boolean isMedal() { + return isMedal; + } + + public boolean isStartsWithZero() { + return isStartsWithZeroScore; + } + + public boolean isFleetsCanRunInParallel() { + return isFleetsCanRunInParallel; + } + + public boolean hasSplitFleetContiguousScoring() { + return hasSplitFleetContiguousScoring; + } + + public int[] getDiscardingThresholds() { + return discardingThresholds; + } + + public boolean isFirstColumnIsNonDiscardableCarryForward() { + return firstColumnIsNonDiscardableCarryForward; + } + +} diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/RaceLogRaceStatus.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/RaceLogRaceStatus.java index a617030d43e..9b7a97cb8d2 100644 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/RaceLogRaceStatus.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/RaceLogRaceStatus.java @@ -14,6 +14,11 @@ public enum RaceLogRaceStatus { RaceLogRaceStatus(int orderNumber) { this.orderNumber = orderNumber; } + + public static boolean isPreRunning(RaceLogRaceStatus status) { + return status != null && + (status.equals(SCHEDULED) || status.equals(STARTPHASE)); + } public static boolean isActive(RaceLogRaceStatus status) { return status != null && @@ -21,6 +26,11 @@ public enum RaceLogRaceStatus { } + public static boolean isRunningOrFinished(RaceLogRaceStatus status) { + return status != null && + (status.equals(STARTPHASE) || status.equals(RUNNING) || status.equals(FINISHING) || status.equals(FINISHED)); + } + public int getOrderNumber() { return orderNumber; } diff --git a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/DomainObjectFactoryImpl.java b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/DomainObjectFactoryImpl.java index 2680b9b4441..13be7d77169 100644 --- a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/DomainObjectFactoryImpl.java +++ b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/DomainObjectFactoryImpl.java @@ -1189,6 +1189,12 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory { private Series loadSeries(DBObject dbSeries, TrackedRegattaRegistry trackedRegattaRegistry) { String name = (String) dbSeries.get(FieldNames.SERIES_NAME.name()); boolean isMedal = (Boolean) dbSeries.get(FieldNames.SERIES_IS_MEDAL.name()); + // isFleetCanRunInParallel is a new field -> set to true when it does not exist in older db versions + boolean isFleetsCanRunInParallel = true; + Object isFleetCanRunInParallelObject = dbSeries.get(FieldNames.SERIES_IS_FLEETS_CAN_RUN_IN_PARALLEL.name()); + if (isFleetCanRunInParallelObject != null) { + isFleetsCanRunInParallel = (Boolean) dbSeries.get(FieldNames.SERIES_IS_FLEETS_CAN_RUN_IN_PARALLEL.name()); + } Boolean startsWithZeroScore = (Boolean) dbSeries.get(FieldNames.SERIES_STARTS_WITH_ZERO_SCORE.name()); Boolean hasSplitFleetContiguousScoring = (Boolean) dbSeries.get(FieldNames.SERIES_HAS_SPLIT_FLEET_CONTIGUOUS_SCORING.name()); Boolean firstColumnIsNonDiscardableCarryForward = (Boolean) dbSeries.get(FieldNames.SERIES_STARTS_WITH_NON_DISCARDABLE_CARRY_FORWARD.name()); @@ -1196,7 +1202,7 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory { List fleets = loadFleets(dbFleets); BasicDBList dbRaceColumns = (BasicDBList) dbSeries.get(FieldNames.SERIES_RACE_COLUMNS.name()); Iterable raceColumnNames = loadRaceColumnNames(dbRaceColumns); - Series series = new SeriesImpl(name, isMedal, fleets, raceColumnNames, trackedRegattaRegistry); + Series series = new SeriesImpl(name, isMedal, isFleetsCanRunInParallel, fleets, raceColumnNames, trackedRegattaRegistry); if (dbSeries.get(FieldNames.SERIES_DISCARDING_THRESHOLDS.name()) != null) { ThresholdBasedResultDiscardingRule resultDiscardingRule = loadResultDiscardingRule(dbSeries, FieldNames.SERIES_DISCARDING_THRESHOLDS); series.setResultDiscardingRule(resultDiscardingRule); diff --git a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/FieldNames.java b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/FieldNames.java index ab6560670e6..b94aec6fa43 100644 --- a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/FieldNames.java +++ b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/FieldNames.java @@ -36,6 +36,7 @@ public enum FieldNames { REGATTA_USE_START_TIME_INFERENCE, SERIES_NAME, SERIES_IS_MEDAL, + SERIES_IS_FLEETS_CAN_RUN_IN_PARALLEL, SERIES_STARTS_WITH_ZERO_SCORE, SERIES_HAS_SPLIT_FLEET_CONTIGUOUS_SCORING, SERIES_STARTS_WITH_NON_DISCARDABLE_CARRY_FORWARD, diff --git a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoObjectFactoryImpl.java b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoObjectFactoryImpl.java index 9942e04dbd7..46900f28ed4 100644 --- a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoObjectFactoryImpl.java +++ b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoObjectFactoryImpl.java @@ -674,6 +674,7 @@ public class MongoObjectFactoryImpl implements MongoObjectFactory { DBObject dbSeries = new BasicDBObject(); dbSeries.put(FieldNames.SERIES_NAME.name(), s.getName()); dbSeries.put(FieldNames.SERIES_IS_MEDAL.name(), s.isMedal()); + dbSeries.put(FieldNames.SERIES_IS_FLEETS_CAN_RUN_IN_PARALLEL.name(), s.isFleetsCanRunInParallel()); dbSeries.put(FieldNames.SERIES_HAS_SPLIT_FLEET_CONTIGUOUS_SCORING.name(), s.hasSplitFleetContiguousScoring()); dbSeries.put(FieldNames.SERIES_STARTS_WITH_ZERO_SCORE.name(), s.isStartsWithZeroScore()); dbSeries.put(FieldNames.SERIES_STARTS_WITH_NON_DISCARDABLE_CARRY_FORWARD.name(), s.isFirstColumnIsNonDiscardableCarryForward()); diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/impl/CreateAndTrackWithRaceLogTest.java b/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/impl/CreateAndTrackWithRaceLogTest.java index e70ac72f818..b5f231b00f3 100644 --- a/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/impl/CreateAndTrackWithRaceLogTest.java +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/impl/CreateAndTrackWithRaceLogTest.java @@ -76,7 +76,7 @@ public class CreateAndTrackWithRaceLogTest { gpsFixStore = service.getGPSFixStore(); service.getMongoObjectFactory().getDatabase().dropDatabase(); author = service.getServerAuthor(); - Series series = new SeriesImpl("series", false, Collections.singletonList(fleet), Collections.emptySet(), + Series series = new SeriesImpl("series", /* isMedal */ false, /* isFleetsCanRunInParallel */ true, Collections.singletonList(fleet), Collections.emptySet(), service); regatta = service.createRegatta(RegattaImpl.getDefaultName("regatta", "Laser"), "Laser", /* startDate */null, /* endDate */null, UUID.randomUUID(), Collections. singletonList(series), false, diff --git a/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/base/racegroup/impl/CurrentRaceFilterImplTest.java b/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/base/racegroup/impl/CurrentRaceFilterImplTest.java new file mode 100644 index 00000000000..64055e71e16 --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/base/racegroup/impl/CurrentRaceFilterImplTest.java @@ -0,0 +1,395 @@ +package com.sap.sailing.domain.base.racegroup.impl; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.junit.Before; +import org.junit.Test; + +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.impl.BoatClassImpl; +import com.sap.sailing.domain.base.impl.FleetImpl; +import com.sap.sailing.domain.base.racegroup.CurrentRaceFilter; +import com.sap.sailing.domain.base.racegroup.RaceCell; +import com.sap.sailing.domain.base.racegroup.RaceGroup; +import com.sap.sailing.domain.base.racegroup.RaceGroupSeriesFleetRaceColumn; +import com.sap.sailing.domain.base.racegroup.RaceRow; +import com.sap.sailing.domain.base.racegroup.SeriesWithRows; +import com.sap.sailing.domain.common.BoatClassMasterdata; +import com.sap.sailing.domain.common.racelog.RaceLogRaceStatus; + +public class CurrentRaceFilterImplTest { + + private CurrentRaceFilter fixture; + private RaceGroup _505; + private RaceGroup yes; + private RaceGroup isaf; + private RaceGroup league; + private RaceGroup ess; + private Map races; + + @Before + public void setUp() { + races = new HashMap<>(); + // five regattas ("RaceGroup"s): + // - simple single-series, single fleet set-up (505 style) + // - single-series, two fleets (Yellow / Blue, as used by YES) + // - qualification with Yellow / Blue; final with Gold / Silver; Medal + // - league set-up with single series with three fleets Red/Green/Blue + _505 = create505Regatta(); + races.putAll(getAllRaces(_505)); + yes = createYesRegatta(); + races.putAll(getAllRaces(yes)); + isaf = createIsafRegatta(); + races.putAll(getAllRaces(isaf)); + league = createLeagueRegatta(); + races.putAll(getAllRaces(league)); + ess = createEssRegatta(); + races.putAll(getAllRaces(ess)); + fixture = new CurrentRaceFilterImpl<>(races.values()); + } + + private RaceGroup create505Regatta() { + final List _505Series = new ArrayList<>(); + final List _505RaceRows = new ArrayList<>(); + final RaceRow _505RaceRow = createRaceRow(/* firstRaceColumnNumber */ 1, /* numberOfRaces */ 9, /* raceColumnNamePrefix */ "R", /* fleetName */ "Default"); + _505RaceRows.add(_505RaceRow); + _505Series.add(new SeriesWithRowsImpl("Default", /* isMedal */ false, /* isFleetsCanRunInParallel */ false, _505RaceRows)); + final RaceGroup _505 = new RaceGroupImpl("505", /* displayName */ null, + new BoatClassImpl(BoatClassMasterdata._5O5.getDisplayName(), BoatClassMasterdata._5O5), + /* courseArea */ null, _505Series, /* regattaConfiguration */ null); + return _505; + } + + private RaceGroup createYesRegatta() { + final List yesSeries = new ArrayList<>(); + final List yesRaceRows = Arrays.asList(createRaceRow(1, 9, "R", "Yellow"), createRaceRow(1, 9, "R", "Blue")); + yesSeries.add(new SeriesWithRowsImpl("Default", /* isMedal */ false, /* isFleetsCanRunInParallel */ true, yesRaceRows)); + final RaceGroup yes = new RaceGroupImpl("YES", /* displayName */ null, + new BoatClassImpl(BoatClassMasterdata._470.getDisplayName(), BoatClassMasterdata._470), + /* courseArea */ null, yesSeries, /* regattaConfiguration */ null); + return yes; + } + + private RaceGroup createIsafRegatta() { + final List isafSeries = new ArrayList<>(); + final List isafQualificationRaceRows = Arrays.asList(createRaceRow(1, 5, "Q", "Yellow"), createRaceRow(1, 5, "Q", "Blue")); + isafSeries.add(new SeriesWithRowsImpl("Qualification", /* isMedal */ false, /* isFleetsCanRunInParallel */ true, isafQualificationRaceRows)); + final List isafFinalRaceRows = Arrays.asList(createRaceRow(6, 5, "F", "Gold"), createRaceRow(6, 5, "F", "Silver")); + isafSeries.add(new SeriesWithRowsImpl("Final", /* isMedal */ false, /* isFleetsCanRunInParallel */ true, isafFinalRaceRows)); + final List isafMedalRaceRows = Arrays.asList(createRaceRow(1, 1, "M", "Medal")); + isafSeries.add(new SeriesWithRowsImpl("Medal", /* isMedal */ true, /* isFleetsCanRunInParallel */ true, isafMedalRaceRows)); + final RaceGroup isaf = new RaceGroupImpl("ISAF", /* displayName */ null, + new BoatClassImpl(BoatClassMasterdata._470.getDisplayName(), BoatClassMasterdata._470), + /* courseArea */ null, isafSeries, /* regattaConfiguration */ null); + return isaf; + } + + private RaceGroup createEssRegatta() { + final List essSeries = new ArrayList<>(); + final List essDay1RaceRows = Arrays.asList(createRaceRow(1, 5, "R", "Default")); + essSeries.add(new SeriesWithRowsImpl("Day 1", /* isMedal */ false, /* isFleetsCanRunInParallel */ true, essDay1RaceRows)); + final List essDay2RaceRows = Arrays.asList(createRaceRow(6, 5, "R", "Default")); + essSeries.add(new SeriesWithRowsImpl("Day 2", /* isMedal */ false, /* isFleetsCanRunInParallel */ true, essDay2RaceRows)); + final List essKnockoutQualificationRaceRows = Arrays.asList(createRaceRow(1, 1, "Q", "Yellow"), createRaceRow(1, 1, "Q", "Blue")); + essSeries.add(new SeriesWithRowsImpl("Knockout Qualification", /* isMedal */ false, /* isFleetsCanRunInParallel */ true, essKnockoutQualificationRaceRows)); + final List essKnockoutScoringRaceRows = Arrays.asList(createRaceRow(1, 1, "F", "Gold"), createRaceRow(1, 1, "F", "Silver")); + essSeries.add(new SeriesWithRowsImpl("Knockout Finals", /* isMedal */ false, /* isFleetsCanRunInParallel */ false, essKnockoutScoringRaceRows)); + final List essDay3RaceRows = Arrays.asList(createRaceRow(11, 5, "R", "Default")); + essSeries.add(new SeriesWithRowsImpl("Day 3", /* isMedal */ false, /* isFleetsCanRunInParallel */ true, essDay3RaceRows)); + final List essDay4RaceRows = Arrays.asList(createRaceRow(16, 5, "R", "Default")); + essSeries.add(new SeriesWithRowsImpl("Day 4", /* isMedal */ false, /* isFleetsCanRunInParallel */ true, essDay4RaceRows)); + final RaceGroup ess = new RaceGroupImpl("ESS", /* displayName */ null, + new BoatClassImpl(BoatClassMasterdata.EXTREME_40.getDisplayName(), BoatClassMasterdata.EXTREME_40), + /* courseArea */ null, essSeries, /* regattaConfiguration */ null); + return ess; + } + + private RaceGroup createLeagueRegatta() { + final List leagueSeries = new ArrayList<>(); + final List leagueRaceRows = Arrays.asList(createRaceRow(1, 15, "F", "Red"), createRaceRow(1, 15, "F", "Green"), createRaceRow(1, 15, "F", "Blue")); + leagueSeries.add(new SeriesWithRowsImpl("Default", /* isMedal */ false, /* isFleetsCanRunInParallel */ false, leagueRaceRows)); + final RaceGroup league = new RaceGroupImpl("League", /* displayName */ null, + new BoatClassImpl(BoatClassMasterdata.J70.getDisplayName(), BoatClassMasterdata.J70), + /* courseArea */ null, leagueSeries, /* regattaConfiguration */ null); + return league; + } + + private Map getAllRaces(RaceGroup raceGroup) { + final Map result = new HashMap<>(); + int zeroBasedSeriesIndex = 0; + for (SeriesWithRows series : raceGroup.getSeries()) { + for (final RaceRow raceRow : series.getRaceRows()) { + int zeroBasedIndexInFleet = 0; + for (final RaceCell raceCell : raceRow.getCells()) { + final SimpleFilterableRace race = new SimpleFilterableRace(raceGroup, series, raceRow.getFleet(), zeroBasedIndexInFleet, zeroBasedSeriesIndex, raceCell.getName()); + result.put(new RaceGroupSeriesFleetRaceColumn(race), race); + zeroBasedIndexInFleet++; + } + } + zeroBasedSeriesIndex++; + } + return result; + } + + @Test + public void testRaceAccessAndStatusUpdate() { + SimpleFilterableRace isafQ4Yellow = get(isaf, "F7", "Gold"); + assertNotNull(isafQ4Yellow); + assertEquals("F7", isafQ4Yellow.getRaceColumnName()); + assertEquals("Gold", isafQ4Yellow.getFleet().getName()); + assertEquals(1, isafQ4Yellow.getZeroBasedIndexInFleet()); + assertEquals(1, isafQ4Yellow.getZeroBasedSeriesIndex()); + assertEquals(RaceLogRaceStatus.UNSCHEDULED, isafQ4Yellow.getStatus()); + isafQ4Yellow.setStatus(RaceLogRaceStatus.SCHEDULED); + assertEquals(RaceLogRaceStatus.SCHEDULED, isafQ4Yellow.getStatus()); + } + + @Test + public void testBasicFilteringForFirstUnscheduledRaces() { + // with all races unscheduled we can expect the first race of each regatta's first series to show + final Set currentRaces = fixture.getCurrentRaces(); + + { + final Set current505Races = currentRaces.stream().filter(r->r.getRaceGroup() == _505).collect(Collectors.toSet()); + assertEquals(1, current505Races.size()); + final SimpleFilterableRace firstRace = current505Races.iterator().next(); + assertEquals("R1", firstRace.getRaceColumnName()); + } + { + final Set currentYesRaces = currentRaces.stream().filter(r->r.getRaceGroup() == yes).collect(Collectors.toSet()); + assertEquals(2, currentYesRaces.size()); + for (final SimpleFilterableRace currentYesRace : currentYesRaces) { + assertEquals("R1", currentYesRace.getRaceColumnName()); + } + } + { + final Set currentIsafRaces = currentRaces.stream().filter(r->r.getRaceGroup() == isaf).collect(Collectors.toSet()); + assertEquals(2, currentIsafRaces.size()); + for (final SimpleFilterableRace currentYesRace : currentIsafRaces) { + assertEquals("Q1", currentYesRace.getRaceColumnName()); + } + } + { + final Set currentLeagueRaces = currentRaces.stream().filter(r->r.getRaceGroup() == league).collect(Collectors.toSet()); + assertEquals(1, currentLeagueRaces.size()); + final SimpleFilterableRace firstRace = currentLeagueRaces.iterator().next(); + assertEquals("Red", firstRace.getFleet().getName()); + assertEquals("F1", firstRace.getRaceColumnName()); + } + { + final Set currentEssRaces = currentRaces.stream().filter(r->r.getRaceGroup() == ess).collect(Collectors.toSet()); + assertEquals(1, currentEssRaces.size()); + final SimpleFilterableRace firstRace = currentEssRaces.iterator().next(); + assertEquals("Default", firstRace.getFleet().getName()); + assertEquals("R1", firstRace.getRaceColumnName()); + } + } + + @Test + public void testSchedulingFirst505Race() { + // with all races unscheduled we can expect the first race of each regatta's first series to show + get(_505, "R1", "Default").setStatus(RaceLogRaceStatus.SCHEDULED); + final Set currentRaces = fixture.getCurrentRaces(); + final Set current505Races = currentRaces.stream().filter(r->r.getRaceGroup() == _505).collect(Collectors.toSet()); + assertEquals(2, current505Races.size()); + assertTrue(current505Races.contains(get(_505, "R1", "Default"))); // it's scheduled, so it shows + assertTrue(current505Races.contains(get(_505, "R2", "Default"))); // it's unscheduled and has a scheduled immediate predecessor + } + + @Test + public void testSchedulingFirstIsafRace() { + // with all races unscheduled we can expect the first race of each regatta's first series to show + get(isaf, "Q1", "Yellow").setStatus(RaceLogRaceStatus.SCHEDULED); + final Set currentRaces = fixture.getCurrentRaces(); + final Set currentEssRaces = currentRaces.stream().filter(r->r.getRaceGroup() == isaf).collect(Collectors.toSet()); + assertEquals(3, currentEssRaces.size()); + assertTrue(currentEssRaces.contains(get(isaf, "Q1", "Yellow"))); // it's scheduled, so it shows + assertTrue(currentEssRaces.contains(get(isaf, "Q1", "Blue"))); // it's unscheduled, first in fleet + assertTrue(currentEssRaces.contains(get(isaf, "Q2", "Yellow"))); // predecessor Q1/Yellow is scheduled, so show this as next in fleet + } + + @Test + public void testSchedulingFirstEssRace() { + // with all races unscheduled we can expect the first race of each regatta's first series to show + get(ess, "R1", "Default").setStatus(RaceLogRaceStatus.SCHEDULED); + final Set currentRaces = fixture.getCurrentRaces(); + final Set currentEssRaces = currentRaces.stream().filter(r->r.getRaceGroup() == ess).collect(Collectors.toSet()); + assertEquals(2, currentEssRaces.size()); + assertTrue(currentEssRaces.contains(get(ess, "R1", "Default"))); // it's scheduled, so it shows + assertTrue(currentEssRaces.contains(get(ess, "R2", "Default"))); // it's unscheduled, first after last scheduled + } + + @Test + public void testFinishingAllEssRacesBeforeKnockoutQualification() { + for (int i=1; i<=10; i++) { + get(ess, "R"+i, "Default").setStatus(RaceLogRaceStatus.FINISHED); + } + final Set currentRaces = fixture.getCurrentRaces(); + final Set currentIsaEssRaces = currentRaces.stream().filter(r->r.getRaceGroup() == ess).collect(Collectors.toSet()); + assertEquals(2, currentIsaEssRaces.size()); + assertTrue(currentIsaEssRaces.contains(get(ess, "Q1", "Yellow"))); + assertTrue(currentIsaEssRaces.contains(get(ess, "Q1", "Blue"))); + } + + @Test + public void testFinishingAllEssRacesBeforeKnockoutQualificationAndSchedulingFirstKnockout() { + for (int i=1; i<=10; i++) { + get(ess, "R"+i, "Default").setStatus(RaceLogRaceStatus.FINISHED); + } + get(ess, "Q1", "Yellow").setStatus(RaceLogRaceStatus.SCHEDULED); + final Set currentRaces = fixture.getCurrentRaces(); + final Set currentIsaEssRaces = currentRaces.stream().filter(r->r.getRaceGroup() == ess).collect(Collectors.toSet()); + assertEquals(3, currentIsaEssRaces.size()); + assertTrue(currentIsaEssRaces.contains(get(ess, "Q1", "Yellow"))); // scheduled + assertTrue(currentIsaEssRaces.contains(get(ess, "Q1", "Blue"))); // unscheduled + assertTrue(currentIsaEssRaces.contains(get(ess, "F1", "Gold"))); // first unscheduled in series; last column in preceding series has scheduled race + // F1/Silver remains invisible because the knockout finals runs its fleets in sequence + } + + @Test + public void testFinishingAllEssRacesBeforeKnockoutFinals() { + for (int i=1; i<=10; i++) { + get(ess, "R"+i, "Default").setStatus(RaceLogRaceStatus.FINISHED); + } + get(ess, "Q1", "Yellow").setStatus(RaceLogRaceStatus.FINISHED); + get(ess, "Q1", "Blue").setStatus(RaceLogRaceStatus.FINISHED); + final Set currentRaces = fixture.getCurrentRaces(); + final Set currentIsaEssRaces = currentRaces.stream().filter(r->r.getRaceGroup() == ess).collect(Collectors.toSet()); + assertEquals(1, currentIsaEssRaces.size()); + assertTrue(currentIsaEssRaces.contains(get(ess, "F1", "Gold"))); // first unscheduled in series; last column in preceding series has scheduled race + // F1/Silver remains invisible because the knockout finals runs its fleets in sequence + } + + @Test + public void testFinishingAllEssRacesBeforeKnockoutFinalsAndGoldRaceStarted() { + for (int i=1; i<=10; i++) { + get(ess, "R"+i, "Default").setStatus(RaceLogRaceStatus.FINISHED); + } + get(ess, "Q1", "Yellow").setStatus(RaceLogRaceStatus.FINISHED); + get(ess, "Q1", "Blue").setStatus(RaceLogRaceStatus.FINISHED); + get(ess, "F1", "Gold").setStatus(RaceLogRaceStatus.RUNNING); + final Set currentRaces = fixture.getCurrentRaces(); + final Set currentIsaEssRaces = currentRaces.stream().filter(r->r.getRaceGroup() == ess).collect(Collectors.toSet()); + assertEquals(2, currentIsaEssRaces.size()); + assertTrue(currentIsaEssRaces.contains(get(ess, "F1", "Gold"))); // running + assertTrue(currentIsaEssRaces.contains(get(ess, "F1", "Silver"))); // first unscheduled with a predecessor that's running + } + + @Test + public void testFinishingAllEssRacesBeforeKnockoutFinalsAndGoldRaceFinishedAndSilverRaceScheduled() { + for (int i=1; i<=10; i++) { + get(ess, "R"+i, "Default").setStatus(RaceLogRaceStatus.FINISHED); + } + get(ess, "Q1", "Yellow").setStatus(RaceLogRaceStatus.FINISHED); + get(ess, "Q1", "Blue").setStatus(RaceLogRaceStatus.FINISHED); + get(ess, "F1", "Gold").setStatus(RaceLogRaceStatus.FINISHED); + get(ess, "F1", "Silver").setStatus(RaceLogRaceStatus.SCHEDULED); + final Set currentRaces = fixture.getCurrentRaces(); + final Set currentIsaEssRaces = currentRaces.stream().filter(r->r.getRaceGroup() == ess).collect(Collectors.toSet()); + assertEquals(2, currentIsaEssRaces.size()); + assertTrue(currentIsaEssRaces.contains(get(ess, "F1", "Silver"))); // scheduled + assertTrue(currentIsaEssRaces.contains(get(ess, "R11", "Default"))); // first unscheduled in series with the last race in the preceding series scheduled + } + + @Test + public void testSchedulingAllIsafQualificationRacesButOne() { + for (int i=1; i<=4; i++) { + get(isaf, "Q"+i, "Yellow").setStatus(RaceLogRaceStatus.FINISHED); + get(isaf, "Q"+i, "Blue").setStatus(RaceLogRaceStatus.FINISHED); + } + get(isaf, "Q5", "Blue").setStatus(RaceLogRaceStatus.SCHEDULED); + final Set currentRaces = fixture.getCurrentRaces(); + final Set currentIsafRaces = currentRaces.stream().filter(r->r.getRaceGroup() == isaf).collect(Collectors.toSet()); + assertEquals(4, currentIsafRaces.size()); + assertTrue(currentIsafRaces.contains(get(isaf, "Q5", "Yellow"))); // it's unscheduled; its predecessor Q4/Yellow is FINISHED; show it + assertTrue(currentIsafRaces.contains(get(isaf, "Q5", "Blue"))); // it's scheduled + assertTrue(currentIsafRaces.contains(get(isaf, "F6", "Gold"))); // Q5/Blue is a scheduled immediate predecessor + assertTrue(currentIsafRaces.contains(get(isaf, "F6", "Silver"))); // Q5/Blue is a scheduled immediate predecessor + } + + @Test + public void testFinishingAllIsafQualificationAndFinalRaces() { + // the medal race shall be the only one remaining + for (int i=1; i<=5; i++) { + get(isaf, "Q"+i, "Yellow").setStatus(RaceLogRaceStatus.FINISHED); + get(isaf, "Q"+i, "Blue").setStatus(RaceLogRaceStatus.FINISHED); + get(isaf, "F"+(i+5), "Gold").setStatus(RaceLogRaceStatus.FINISHED); + get(isaf, "F"+(i+5), "Silver").setStatus(RaceLogRaceStatus.FINISHED); + } + final Set currentRaces = fixture.getCurrentRaces(); + final Set currentIsafRaces = currentRaces.stream().filter(r->r.getRaceGroup() == isaf).collect(Collectors.toSet()); + assertEquals(1, currentIsafRaces.size()); + assertTrue(currentIsafRaces.contains(get(isaf, "M1", "Medal"))); + } + + @Test + public void testFinishingFirstLeagueRace() { + // with all races unscheduled we can expect the first race of each regatta's first series to show + get(league, "F1", "Red").setStatus(RaceLogRaceStatus.FINISHED); + final Set currentRaces = fixture.getCurrentRaces(); + final Set currentLeagueRaces = currentRaces.stream().filter(r->r.getRaceGroup() == league).collect(Collectors.toSet()); + assertEquals(1, currentLeagueRaces.size()); + assertTrue(currentLeagueRaces.contains(get(league, "F1", "Green"))); // Red is finished, don't show it anymore + } + + @Test + public void testSkippingSecondLeagueRace() { + // with all races unscheduled we can expect the first race of each regatta's first series to show + get(league, "F1", "Red").setStatus(RaceLogRaceStatus.FINISHED); + get(league, "F1", "Blue").setStatus(RaceLogRaceStatus.SCHEDULED); + final Set currentRaces = fixture.getCurrentRaces(); + final Set currentLeagueRaces = currentRaces.stream().filter(r->r.getRaceGroup() == league).collect(Collectors.toSet()); + assertEquals(3, currentLeagueRaces.size()); + assertTrue(currentLeagueRaces.contains(get(league, "F1", "Green"))); // Red is finished, don't show it anymore; show the next one instead which is Green + assertTrue(currentLeagueRaces.contains(get(league, "F1", "Blue"))); // show a scheduled race + assertTrue(currentLeagueRaces.contains(get(league, "F2", "Red"))); // This is the next one after the scheduled F1/Blue + } + + private SimpleFilterableRace get(RaceGroup raceGroup, String raceColumnName, String fleetName) { + for (final SeriesWithRows series : raceGroup.getSeries()) { + final Fleet fleet = getFleet(series, fleetName); + if (fleet != null) { + final RaceRow raceRow = series.getRaceRow(fleet); + if (raceRow != null) { + for (final RaceCell raceCell : raceRow.getCells()) { + if (raceCell.getName().equals(raceColumnName)) { + return races.get(new RaceGroupSeriesFleetRaceColumn(raceGroup, series, fleet, raceColumnName)); + } + } + } + } + } + return null; + } + + private Fleet getFleet(SeriesWithRows series, String fleetName) { + for (final Fleet fleet : series.getFleets()) { + if (fleet.getName().equals(fleetName)) { + return fleet; + } + } + return null; + } + + private RaceRow createRaceRow(final int firstRaceColumnNumber, final int numberOfRaces, + final String raceColumnNamePrefix, final String fleetName) { + final List _505RaceCells = new ArrayList<>(); + for (int i=0; i < numberOfRaces; i++) { + _505RaceCells.add(new RaceCellImpl(raceColumnNamePrefix+(i+firstRaceColumnNumber), + /* race log */ null, /* factor */ firstRaceColumnNumber, /* explicitFactor */ null, + /* zeroBasedIndexInFleet */ i)); + } + final RaceRowImpl _505RaceRow = new RaceRowImpl(new FleetImpl(fleetName), _505RaceCells); + return _505RaceRow; + } +} diff --git a/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/base/racegroup/impl/SimpleFilterableRace.java b/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/base/racegroup/impl/SimpleFilterableRace.java new file mode 100755 index 00000000000..92390ccf061 --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/base/racegroup/impl/SimpleFilterableRace.java @@ -0,0 +1,75 @@ +package com.sap.sailing.domain.base.racegroup.impl; + +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.SeriesBase; +import com.sap.sailing.domain.base.racegroup.FilterableRace; +import com.sap.sailing.domain.base.racegroup.RaceGroup; +import com.sap.sailing.domain.common.racelog.RaceLogRaceStatus; + +public class SimpleFilterableRace implements FilterableRace { + private final RaceGroup raceGroup; + private final SeriesBase series; + private final Fleet fleet; + private RaceLogRaceStatus status; + private final int zeroBasedIndexInFleet; + private final int zeroBasedSeriesIndex; + private final String raceColumnName; + + public SimpleFilterableRace(RaceGroup raceGroup, SeriesBase series, Fleet fleet, int zeroBasedIndexInFleet, + int zeroBasedSeriesIndex, String raceColumnName) { + super(); + this.raceGroup = raceGroup; + this.series = series; + this.fleet = fleet; + this.zeroBasedIndexInFleet = zeroBasedIndexInFleet; + this.zeroBasedSeriesIndex = zeroBasedSeriesIndex; + this.raceColumnName = raceColumnName; + this.status = RaceLogRaceStatus.UNSCHEDULED; + } + + @Override + public RaceGroup getRaceGroup() { + return raceGroup; + } + + @Override + public SeriesBase getSeries() { + return series; + } + + @Override + public Fleet getFleet() { + return fleet; + } + + @Override + public RaceLogRaceStatus getStatus() { + return status; + } + + public void setStatus(RaceLogRaceStatus status) { + this.status = status; + } + + @Override + public int getZeroBasedIndexInFleet() { + return zeroBasedIndexInFleet; + } + + @Override + public int getZeroBasedSeriesIndex() { + return zeroBasedSeriesIndex; + } + + @Override + public String getRaceColumnName() { + return raceColumnName; + } + + @Override + public String toString() { + return "SimpleFilterableRace ["+getRaceGroup().getName() + " - " + getSeries().getName() + " - " + + getFleet().getName() + " - " + getRaceColumnName() + "]"; + } + +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/analyzing/impl/RaceStatusAnalyzer.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/analyzing/impl/RaceStatusAnalyzer.java index 823694ac179..889c5b9d27e 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/analyzing/impl/RaceStatusAnalyzer.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/analyzing/impl/RaceStatusAnalyzer.java @@ -44,9 +44,10 @@ public class RaceStatusAnalyzer extends RaceLogAnalyzer { @Override protected RaceLogRaceStatus performAnalysis() { - ArrayListNavigableSet statusEvents = new ArrayListNavigableSet<>(RaceLogRaceStatusEventComparator.INSTANCE); - for(RaceLogEvent event: getPassEvents()) { - if(event instanceof RaceLogRaceStatusEvent) { + ArrayListNavigableSet statusEvents = new ArrayListNavigableSet<>( + RaceLogRaceStatusEventComparator.INSTANCE); + for (RaceLogEvent event : getPassEvents()) { + if (event instanceof RaceLogRaceStatusEvent) { statusEvents.add((RaceLogRaceStatusEvent) event); } } @@ -54,7 +55,6 @@ public class RaceStatusAnalyzer extends RaceLogAnalyzer { event.accept(eventDispatcher); return eventDispatcher.nextStatus; } - return RaceLogRaceStatus.UNSCHEDULED; } @@ -79,15 +79,15 @@ public class RaceStatusAnalyzer extends RaceLogAnalyzer { private void setRaceLogStatusBasedOnStartTime(TimePoint startTime) { if (startTime == null) { nextStatus = RaceLogRaceStatus.PRESCHEDULED; - return; - } - TimePoint now = clock.now(); - if (racingProcedure.isStartphaseActive(startTime, now)) { - nextStatus = RaceLogRaceStatus.STARTPHASE; - } else if (now.before(startTime)) { - nextStatus = RaceLogRaceStatus.SCHEDULED; } else { - nextStatus = RaceLogRaceStatus.RUNNING; + TimePoint now = clock.now(); + if (racingProcedure.isStartphaseActive(startTime, now)) { + nextStatus = RaceLogRaceStatus.STARTPHASE; + } else if (now.before(startTime)) { + nextStatus = RaceLogRaceStatus.SCHEDULED; + } else { + nextStatus = RaceLogRaceStatus.RUNNING; + } } } @@ -101,6 +101,6 @@ public class RaceStatusAnalyzer extends RaceLogAnalyzer { DependentStartTimeResolver startTimeResolver = new DependentStartTimeResolver(resolver); TimePoint startTime = startTimeResolver.resolve(event).getStartTime(); setRaceLogStatusBasedOnStartTime(startTime); - }; - }; + } + } } \ No newline at end of file diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/SeriesBase.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/SeriesBase.java index 726eebefb64..6dbb0f7f1f8 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/SeriesBase.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/SeriesBase.java @@ -33,5 +33,5 @@ public interface SeriesBase extends Renamable { * made except that within a fleet the races are usually run in the order as defined by the {@link #getRaceColumns() * race columns}. */ - boolean isFleetsRunInParallel(); + boolean isFleetsCanRunInParallel(); } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/CurrentRaceComparator.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/CurrentRaceComparator.java new file mode 100644 index 00000000000..c80dcaa0149 --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/CurrentRaceComparator.java @@ -0,0 +1,50 @@ +package com.sap.sailing.domain.base.racegroup; + +import java.util.Comparator; + +import com.sap.sse.common.Util; +import com.sap.sse.common.util.NaturalComparator; + +/** + * When current races of a regatta are to be displayed to a user, a reasonable ordering is key to usability. This + * comparator sorts by their series index (day 1 before day 2; qualification before final, etc.), then by their index + * within the fleet (R1 before R2, etc.) and lastly by their fleet index (keeping the ordering as set on the server; not + * to be confused with the ranking ordering set for the fleet; so if on the server silver is placed before gold then + * that order is kept, regardless of the 1 and 2 values of these fleets). For a typical regatta with qualification, + * final and medal series, with race columns Q1..Q5, F6..F10, M and fleets Yellow/Blue and Silver/Gold, this would result + * in an ordering Q1-Yellow, Q1-Blue, ..., Q5-Yellow, Q5-Blue, F6-Silver, F6-Gold, ..., F10-Silver, F10-Gold, Medal. + * + * @author Axel Uhl (d043530) + * + */ +public class CurrentRaceComparator implements Comparator { + private final NaturalComparator naturalComparator = new NaturalComparator(); + + @Override + public int compare(FilterableRace o1, FilterableRace o2) { + int result = getRaceGroupDisplayName(o1).compareTo(getRaceGroupDisplayName(o2)); + if (result == 0) { + result = o1.getZeroBasedSeriesIndex() - o2.getZeroBasedSeriesIndex(); + if (result == 0) { + result = o1.getZeroBasedIndexInFleet() - o2.getZeroBasedIndexInFleet(); + if (result == 0) { + result = naturalComparator.compare(o1.getRaceColumnName(), o2.getRaceColumnName()); + if (result == 0) { + result = Util.indexOf(o1.getSeries().getFleets(), o1.getFleet()) - Util.indexOf(o2.getSeries().getFleets(), o2.getFleet()); + } + } + } + } + return result; + } + + private String getRaceGroupDisplayName(FilterableRace r) { + final String result; + if (r.getRaceGroup().getDisplayName() != null) { + result = r.getRaceGroup().getDisplayName(); + } else { + result = r.getRaceGroup().getName(); + } + return result; + } +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/CurrentRaceFilter.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/CurrentRaceFilter.java new file mode 100644 index 00000000000..1dcd61afa97 --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/CurrentRaceFilter.java @@ -0,0 +1,17 @@ +package com.sap.sailing.domain.base.racegroup; + +import java.util.Set; + +import com.sap.sailing.domain.abstractlog.race.state.RaceState; + +/** + * Given a set of races organized in fleets, series and regattas, this filter determines those races + * relevant for a race officer based on the races' {@link RaceState state} and their place in the + * regatta structure. + * + * @author Charlotte Proeller (D062347) + * @author Axel Uhl (d043530) + */ +public interface CurrentRaceFilter { + Set getCurrentRaces(); +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/FilterableRace.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/FilterableRace.java new file mode 100644 index 00000000000..e510c87c421 --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/FilterableRace.java @@ -0,0 +1,45 @@ +package com.sap.sailing.domain.base.racegroup; + +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.SeriesBase; +import com.sap.sailing.domain.common.racelog.RaceLogRaceStatus; +import com.sap.sse.common.Util; + +/** + * Describes the properties by which races may be filtered. This encompasses, in particular, the race's place + * in the regatta structure + * @author Axel Uhl (d043530) + * + */ +public interface FilterableRace { + RaceGroup getRaceGroup(); + + SeriesBase getSeries(); + + Fleet getFleet(); + + /** + * @return the status of the race's state. + */ + RaceLogRaceStatus getStatus(); + + /** + * A Series offers a sequence of RaceColumns, each of them split according to the Fleets modeled for the Series. A + * {@link RaceCell} describes a "slot" in this grid, defined by the series, the fleet and the race column. While + * this object's {@link #getName() name} represents the race column's name, this doesn't tell anything about the + * "horizontal" position in the "grid" or in other words what the index is of the race column in which this cell + * lies. + *

+ * + * Indices returned by this method start with zero, meaning the first race column in the series. This corresponds to + * what one would get by asking {@link Util#indexOf(Iterable, Object) Util.indexOf(series.getRaceColumns(), + * thisCellsRaceColumn)}, except in case the first race column is a "virtual" one that holds a non-discardable + * carry-forward result. In this case, the second Race Column, which is the first "non-virtual" one, receives + * index 0. + */ + int getZeroBasedIndexInFleet(); + + int getZeroBasedSeriesIndex(); + + String getRaceColumnName(); +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/IsFleetFragment.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/IsFleetFragment.java new file mode 100644 index 00000000000..725dc991b19 --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/IsFleetFragment.java @@ -0,0 +1,7 @@ +package com.sap.sailing.domain.base.racegroup; + +import com.sap.sailing.domain.base.SeriesBase; + +public interface IsFleetFragment extends RaceGroupFragment { + SeriesBase getSeries(); +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/IsRaceFragment.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/IsRaceFragment.java new file mode 100644 index 00000000000..d92746d3e3a --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/IsRaceFragment.java @@ -0,0 +1,7 @@ +package com.sap.sailing.domain.base.racegroup; + +import com.sap.sailing.domain.common.racelog.RaceLogRaceStatus; + +public interface IsRaceFragment extends RaceGroupFragment { + RaceLogRaceStatus getCurrentStatus(); +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/RaceCell.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/RaceCell.java index c3dc9f542bd..833f0a6fac2 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/RaceCell.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/RaceCell.java @@ -2,9 +2,10 @@ package com.sap.sailing.domain.base.racegroup; import com.sap.sailing.domain.abstractlog.race.RaceLog; import com.sap.sse.common.Named; +import com.sap.sse.common.Util; /** - * A "race". + * A "race". Its {@link #getName() name} represents the Race Column name coming from the leaderboard / {@link RaceGroup}. *

* * Because this and all other {@link RaceGroup} interfaces are used for communication with the Android applications a @@ -16,4 +17,20 @@ public interface RaceCell extends Named { double getFactor(); Double getExplicitFactor(); + + /** + * A Series offers a sequence of RaceColumns, each of them split according to the Fleets modeled for the Series. A + * {@link RaceCell} describes a "slot" in this grid, defined by the series, the fleet and the race column. While + * this object's {@link #getName() name} represents the race column's name, this doesn't tell anything about the + * "horizontal" position in the "grid" or in other words what the index is of the race column in which this cell + * lies. + *

+ * + * Indices returned by this method start with zero, meaning the first race column in the series. This corresponds to + * what one would get by asking {@link Util#indexOf(Iterable, Object) Util.indexOf(series.getRaceColumns(), + * thisCellsRaceColumn)}, except in case the first race column is a "virtual" one that holds a non-discardable + * carry-forward result. In this case, the second Race Column, which is the first "non-virtual" one, receives + * index 0. + */ + int getZeroBasedIndexInFleet(); } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/RaceGroup.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/RaceGroup.java index d68e0b4e04c..e260a6f5cd5 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/RaceGroup.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/RaceGroup.java @@ -1,5 +1,6 @@ package com.sap.sailing.domain.base.racegroup; +import com.sap.sailing.domain.abstractlog.race.state.RaceState; import com.sap.sailing.domain.abstractlog.race.state.racingprocedure.RacingProcedure; import com.sap.sailing.domain.base.BoatClass; import com.sap.sailing.domain.base.CourseArea; @@ -17,7 +18,9 @@ import com.sap.sse.common.Named; * * To align with the semantics of the Android application a {@link RaceGroup} consists of race rows rather than race * columns. A {@link RaceRow} is a set of races for one {@link Fleet}. A specific "race" of a row is called - * {@link RaceCell}. + * {@link RaceCell}. (Note that as described by bug 3011 this view is by now obsolete. The grouping by fleet is + * no longer desired by users; instead, grouping is desired only by regatta / {@link RaceGroup}, whereas ordering + * and filtering is based on series and fleet pertinence and {@link RaceState race state}, respectively.) *

* * For example: if you have 2 fleets (blue and yellow) there will be 2 {@link RaceRow}s in your {@link RaceGroup} @@ -39,7 +42,8 @@ public interface RaceGroup extends Named { public BoatClass getBoatClass(); /** - * Gets the collection of series. + * Gets the collection of series, for a {@link RaceGroup} representing a regatta in the same order as + * the {@code Regatta.getSeries()} call returns them. */ public Iterable getSeries(); diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/RaceGroupFragment.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/RaceGroupFragment.java new file mode 100644 index 00000000000..6e1a58c52d1 --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/RaceGroupFragment.java @@ -0,0 +1,5 @@ +package com.sap.sailing.domain.base.racegroup; + +public interface RaceGroupFragment { + +} diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/domain/impl/RaceGroupSeriesFleet.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/RaceGroupSeries.java old mode 100644 new mode 100755 similarity index 52% rename from mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/domain/impl/RaceGroupSeriesFleet.java rename to java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/RaceGroupSeries.java index 194ba051180..6df02d05669 --- a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/domain/impl/RaceGroupSeriesFleet.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/RaceGroupSeries.java @@ -1,33 +1,36 @@ -package com.sap.sailing.racecommittee.app.domain.impl; +package com.sap.sailing.domain.base.racegroup; -import android.text.TextUtils; - -import com.sap.sailing.domain.base.Fleet; import com.sap.sailing.domain.base.SeriesBase; -import com.sap.sailing.domain.base.racegroup.RaceGroup; import com.sap.sailing.domain.common.LeaderboardNameConstants; -import com.sap.sailing.racecommittee.app.domain.ManagedRace; import com.sap.sse.common.Util; -public class RaceGroupSeriesFleet { +/** + * Represents a pair of {@link RaceGroup} (representing a regatta or a flexible leaderboard) and {@link SeriesBase + * series}. An instance can be constructed for a {@link ManagedRace} which then extracts these two properties from the + * race. Note that equal objects of this type can result for different races as long as they are in the equal + * {@link RaceGroup} and {@link SeriesBase series}. + * + * @author Axel Uhl (d043530) + * + */ +public class RaceGroupSeries { - private RaceGroup raceGroup; - private Fleet fleet; - private SeriesBase series; - private int seriesOrder; - private int fleetOrder; + private final RaceGroup raceGroup; + private final SeriesBase series; + private final int seriesOrder; - public RaceGroupSeriesFleet(ManagedRace race) { - raceGroup = race.getRaceGroup(); - series = race.getSeries(); - fleet = race.getFleet(); - - seriesOrder = getSeriesIndex(race, series); - fleetOrder = getFleetIndex(series.getFleets(), race.getFleet()); + public RaceGroupSeries(FilterableRace race) { + this(race.getRaceGroup(), race.getSeries()); } - private static int getSeriesIndex(ManagedRace race, SeriesBase series) { - return Util.indexOf(race.getRaceGroup().getSeries(), series); + public RaceGroupSeries(RaceGroup raceGroup, SeriesBase series) { + this.raceGroup = raceGroup; + this.series = series; + seriesOrder = getSeriesIndex(raceGroup, series); + } + + private static int getSeriesIndex(RaceGroup raceGroup, SeriesBase series) { + return Util.indexOf(raceGroup.getSeries(), series); } public RaceGroup getRaceGroup() { @@ -38,10 +41,6 @@ public class RaceGroupSeriesFleet { return series; } - public Fleet getFleet() { - return fleet; - } - public String getRaceGroupName() { return raceGroup.getName(); } @@ -50,25 +49,18 @@ public class RaceGroupSeriesFleet { return series.getName(); } - public String getFleetName() { - return fleet.getName(); - } - public String getDisplayName() { return getDisplayName(false); } public String getDisplayName(boolean useDisplayName) { String name = raceGroup.getDisplayName(); - if (!useDisplayName || TextUtils.isEmpty(name)) { + if (!useDisplayName || name == null || name.length() == 0) { name = raceGroup.getName(); } if (series != null && !series.getName().equals(LeaderboardNameConstants.DEFAULT_SERIES_NAME)) { name += " - " + series.getName(); } - if (fleet != null && !fleet.getName().equals(LeaderboardNameConstants.DEFAULT_FLEET_NAME)) { - name += " - " + fleet.getName(); - } return name; } @@ -76,19 +68,13 @@ public class RaceGroupSeriesFleet { return seriesOrder; } - public int getFleetOrder() { - return fleetOrder; - } - @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((raceGroup == null) ? 0 : raceGroup.hashCode()); result = prime * result + ((series == null) ? 0 : series.hashCode()); - result = prime * result + ((fleet == null) ? 0 : fleet.hashCode()); result = prime * result + seriesOrder; - result = prime * result + fleetOrder; return result; } @@ -100,7 +86,7 @@ public class RaceGroupSeriesFleet { return false; if (getClass() != obj.getClass()) return false; - RaceGroupSeriesFleet other = (RaceGroupSeriesFleet) obj; + RaceGroupSeries other = (RaceGroupSeries) obj; if (raceGroup == null) { if (other.raceGroup != null) { return false; @@ -108,13 +94,6 @@ public class RaceGroupSeriesFleet { } else if (!raceGroup.equals(other.raceGroup)) { return false; } - if (fleet == null) { - if (other.fleet != null) { - return false; - } - } else if (!fleet.equals(other.fleet)) { - return false; - } if (series == null) { if (other.series != null) { return false; @@ -125,13 +104,6 @@ public class RaceGroupSeriesFleet { if (seriesOrder != other.seriesOrder) { return false; } - if (fleetOrder != other.fleetOrder) { - return false; - } return true; } - - private int getFleetIndex(Iterable fleets, Fleet fleet) { - return Util.indexOf(fleets, fleet); - } } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/RaceGroupSeriesComparator.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/RaceGroupSeriesComparator.java new file mode 100644 index 00000000000..39a8c732d99 --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/RaceGroupSeriesComparator.java @@ -0,0 +1,29 @@ +package com.sap.sailing.domain.base.racegroup; + +import java.util.Comparator; + +/** + * When current races of a regatta are to be displayed to a user, a reasonable ordering is key to usability. This + * comparator sorts by their series index (day 1 before day 2; qualification before final, etc.), then by their index + * within the fleet (R1 before R2, etc.) and lastly by their fleet index (keeping the ordering as set on the server; not + * to be confused with the ranking ordering set for the fleet; so if on the server silver is placed before gold then + * that order is kept, regardless of the 1 and 2 values of these fleets). For a typical regatta with qualification, + * final and medal series, with race columns Q1..Q5, F6..F10, M and fleets Yellow/Blue and Silver/Gold, this would result + * in an ordering Q1-Yellow, Q1-Blue, ..., Q5-Yellow, Q5-Blue, F6-Silver, F6-Gold, ..., F10-Silver, F10-Gold, Medal. + * + * @author Axel Uhl (d043530) + * + */ +public class RaceGroupSeriesComparator implements Comparator { + @Override + public int compare(RaceGroupSeries o1, RaceGroupSeries o2) { + int result = o1.getRaceGroup().getName().compareTo(o2.getRaceGroup().getName()); + if (result == 0) { + result = o1.getSeriesOrder() - o2.getSeriesOrder(); + if (result == 0) { + result = o1.getDisplayName().compareTo(o2.getDisplayName()); + } + } + return result; + } +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/RaceGroupSeriesFleet.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/RaceGroupSeriesFleet.java new file mode 100644 index 00000000000..69848124e6d --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/RaceGroupSeriesFleet.java @@ -0,0 +1,88 @@ +package com.sap.sailing.domain.base.racegroup; + +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.SeriesBase; +import com.sap.sailing.domain.common.LeaderboardNameConstants; +import com.sap.sse.common.Util; + +/** + * Represents a triple of {@link RaceGroup} (representing a regatta or a flexible leaderboard), a {@link SeriesBase series} + * and a {@link Fleet}. An instance can be constructed for a {@link ManagedRace} which then extracts these three properties + * from the race. Note that equal objects of this type can result for different races as long as they are in the equal + * {@link RaceGroup}, {@link SeriesBase series} and {@link Fleet}. + * + * @author Axel Uhl (d043530) + * + */ +public class RaceGroupSeriesFleet extends RaceGroupSeries { + + private final Fleet fleet; + private final int fleetOrder; + + public RaceGroupSeriesFleet(FilterableRace race) { + this(race.getRaceGroup(), race.getSeries(), race.getFleet()); + } + + public RaceGroupSeriesFleet(RaceGroup raceGroup, SeriesBase series, Fleet fleet) { + super(raceGroup, series); + this.fleet = fleet; + fleetOrder = getFleetIndex(series.getFleets(), fleet); + } + + private int getFleetIndex(Iterable fleets, Fleet fleet) { + return Util.indexOf(fleets, fleet); + } + + public Fleet getFleet() { + return fleet; + } + + public String getFleetName() { + return fleet.getName(); + } + + public String getDisplayName() { + return getDisplayName(false); + } + + public String getDisplayName(boolean useDisplayName) { + String name = super.getDisplayName(useDisplayName); + if (fleet != null && !fleet.getName().equals(LeaderboardNameConstants.DEFAULT_FLEET_NAME)) { + name += " - " + fleet.getName(); + } + return name; + } + + public int getFleetOrder() { + return fleetOrder; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((fleet == null) ? 0 : fleet.hashCode()); + result = prime * result + fleetOrder; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!super.equals(obj)) + return false; + if (getClass() != obj.getClass()) + return false; + RaceGroupSeriesFleet other = (RaceGroupSeriesFleet) obj; + if (fleet == null) { + if (other.fleet != null) + return false; + } else if (!fleet.equals(other.fleet)) + return false; + if (fleetOrder != other.fleetOrder) + return false; + return true; + } + +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/RaceGroupSeriesFleetRaceColumn.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/RaceGroupSeriesFleetRaceColumn.java new file mode 100644 index 00000000000..660da25f19c --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/RaceGroupSeriesFleetRaceColumn.java @@ -0,0 +1,61 @@ +package com.sap.sailing.domain.base.racegroup; + +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.SeriesBase; + +/** + * Represents a quadruple of {@link RaceGroup} (representing a regatta or a flexible leaderboard), a {@link SeriesBase + * series}, a {@link Fleet} and a race column, identified by its unique name within the leaderboard. An instance can be + * constructed for a {@link FilterableRace} which then extracts these four properties from the race. Equal + * objects of this type can only result for equal races because they identify a "race slot" in a regatta. + * + * @author Axel Uhl (d043530) + * + */ +public class RaceGroupSeriesFleetRaceColumn extends RaceGroupSeriesFleet { + private final String raceColumnName; + + public RaceGroupSeriesFleetRaceColumn(FilterableRace race) { + super(race); + raceColumnName = race.getRaceColumnName(); + } + + public RaceGroupSeriesFleetRaceColumn(RaceGroup raceGroup, SeriesBase series, Fleet fleet, String raceColumnName) { + super(raceGroup, series, fleet); + this.raceColumnName = raceColumnName; + } + + public String getRaceColumnName() { + return raceColumnName; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((raceColumnName == null) ? 0 : raceColumnName.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!super.equals(obj)) + return false; + if (getClass() != obj.getClass()) + return false; + RaceGroupSeriesFleetRaceColumn other = (RaceGroupSeriesFleetRaceColumn) obj; + if (raceColumnName == null) { + if (other.raceColumnName != null) + return false; + } else if (!raceColumnName.equals(other.raceColumnName)) + return false; + return true; + } + + @Override + public String toString() { + return getRaceGroupName() + " - " + getSeriesName() + " - " + getFleetName() + " - " + getRaceColumnName(); + } +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/SeriesWithRows.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/SeriesWithRows.java index 2e1604bf79a..90dbbc216fd 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/SeriesWithRows.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/SeriesWithRows.java @@ -1,5 +1,6 @@ package com.sap.sailing.domain.base.racegroup; +import com.sap.sailing.domain.base.Fleet; import com.sap.sailing.domain.base.SeriesBase; /** @@ -11,5 +12,7 @@ public interface SeriesWithRows extends SeriesBase { /** * All {@link RaceRow}s of this series. */ - public Iterable getRaceRows(); + Iterable getRaceRows(); + + RaceRow getRaceRow(Fleet fleet); } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/impl/CurrentRaceFilterImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/impl/CurrentRaceFilterImpl.java new file mode 100644 index 00000000000..582fa77816f --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/impl/CurrentRaceFilterImpl.java @@ -0,0 +1,193 @@ +package com.sap.sailing.domain.base.racegroup.impl; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.SeriesBase; +import com.sap.sailing.domain.base.racegroup.CurrentRaceFilter; +import com.sap.sailing.domain.base.racegroup.FilterableRace; +import com.sap.sailing.domain.base.racegroup.RaceCell; +import com.sap.sailing.domain.base.racegroup.RaceGroup; +import com.sap.sailing.domain.base.racegroup.RaceGroupSeriesFleetRaceColumn; +import com.sap.sailing.domain.base.racegroup.RaceRow; +import com.sap.sailing.domain.base.racegroup.SeriesWithRows; +import com.sap.sailing.domain.common.racelog.RaceLogRaceStatus; +import com.sap.sse.common.Util; + +/** + * Determines a subset from a set of {@link FilterableRace} objects based on a definition of "being current." Current + * races are interesting for a race officer to deal with. For example, the next set of races that are eligible for being + * scheduled are part of this set; furthermore all running races. See {@link #getCurrentRaces()} for a more formal + * definition what this filter considers to be a "current" race. + *

+ * + * An instance of this type is initialized with the full set of races out of which the "current" subset shall be + * extracted by the {@link #getCurrentRaces()} method. The full set of races is expected to skip no race columns and no + * fleets and no series, or in other words, hold all races that form the regatta structure. This is important for the + * "predecessor" relation which uses the fleet and series indices, such as + * {@link FilterableRace#getZeroBasedIndexInFleet()} and {@link FilterableRace#getZeroBasedSeriesIndex()}. + * + * @author Axel Uhl (d043530) + * + * @param + */ +public class CurrentRaceFilterImpl implements CurrentRaceFilter { + private final Iterable allRaces; + private final Map racesByRaceGroupSeriesFleet; + + /** + * Constructs a filter that can determine "current races" from a set of races representing all races of a set of + * regattas / race groups. A copy of {@code allRaces} is constructed here, so later outside modifications to + * {@code allRaces} will not have any effect on this filter. Clients need to construct a new instance if the set of + * all races has changed. + */ + public CurrentRaceFilterImpl(Iterable allRaces) { + final Collection copyOfAllRaces = new HashSet<>(); + Util.addAll(allRaces, copyOfAllRaces); + this.allRaces = copyOfAllRaces; + this.racesByRaceGroupSeriesFleet = new HashMap<>(); + for (final T race : allRaces) { + racesByRaceGroupSeriesFleet.put(new RaceGroupSeriesFleetRaceColumn(race), race); + } + } + + /** + * A race is considered "current" and shall be shown if its status is between (inclusive) + * {@link RaceLogRaceStatus#SCHEDULED} and {@link RaceLogRaceStatus#FINISHING}, or if the race + * has no predecessor in the regatta / leaderboard and therefore could start the regatta, + * or if the race has an immediate predecessor (a race A that must precede the race in question, say X, + * such that there is no other race B that also must precede X with A having to precede B) which + * is at least in state {@link RaceLogRaceStatus#SCHEDULED}. + */ + @Override + public Set getCurrentRaces() { + final Set filteredRaces = new HashSet<>(); + for (final T race : allRaces) { + if (race.getStatus().getOrderNumber() <= RaceLogRaceStatus.FINISHING.getOrderNumber()) { // don't show finished races + if (race.getStatus().getOrderNumber() >= RaceLogRaceStatus.SCHEDULED.getOrderNumber()) { + filteredRaces.add(race); + } else { + final boolean hasNoPredecessor=hasNoPredecessor(race); + if ((hasNoPredecessor && race.getStatus() == RaceLogRaceStatus.UNSCHEDULED) || + (!hasNoPredecessor && hasImmediatePredecessorThatIsAtLeastScheduled(race))) { + // show the race + filteredRaces.add(race); + } + } + } + } + return filteredRaces; + } + + /** + * For a race to have no predecessor in the regatta structure it has to be located in the first race column in the first series. + * If the races of the different fleets in the series may {@link SeriesBase#isFleetsRunInParallel() run in parallel} then this is + * already sufficient. If the races of the different fleets in the series are run in sequence, the race must additionally be + * in the first fleet of the series. + */ + private boolean hasNoPredecessor(FilterableRace race) { + return race.getZeroBasedSeriesIndex() == 0 && race.getZeroBasedIndexInFleet() == 0 && + (race.getSeries().isFleetsCanRunInParallel() || Util.indexOf(race.getSeries().getFleets(), race.getFleet()) == 0); + } + + /** + * Two cases need to be distinguished: the races in different fleets of the same series may be run + * in parallel or not.

+ * + * Parallel fleets in series: the immediate predecessor is the race in the same fleet in the immediately preceding + * race column; or if {@code race} is in the first column of its series, all races in the last column of the immediately + * preceding series.

+ * + * Sequential fleets in series: the immediate predecessor is the race that immediately precedes {@code race} in + * the list of races constructed by looping over all columns of the series and for each column looping over all + * fleets. If {@code race} is the first in this sequence of its series, then the immediate predecessors is/are the + * last race(s) (multiple if the previous series runs its fleets in parallel) of the immediately preceding series.

+ * + * Precondition: {@code !hasNoPredecessor(race)} + * + * @return whether any of the predecessors (of which there is least one because of the precondition) have a + * {@link RaceLogRaceStatus race status} whose {@link RaceLogRaceStatus#getOrderNumber() order number} is at least + * {@link RaceLogRaceStatus#SCHEDULED SCHEDULED.getOrderNumber()}. + */ + private boolean hasImmediatePredecessorThatIsAtLeastScheduled(FilterableRace race) { + final Set immediatePredecessors = new HashSet<>(); + final RaceGroup raceGroup = race.getRaceGroup(); + if (race.getSeries().isFleetsCanRunInParallel()) { + if (race.getZeroBasedIndexInFleet() == 0) { + // it's in the first column; for fleets that may run in parallel, the immediate predecessors are + // in the last column of the immediately preceding series: + Util.addAll(getLastRacesFromImmediatelyPrecedingSeries(race, raceGroup), immediatePredecessors); + } else { + // not the first race column in its fleet; for parallel fleets, only the immediate predecessor + // in the same fleet is an immediate predecessor for race + RaceRow raceRow = Util.get(raceGroup.getSeries(), race.getZeroBasedSeriesIndex()).getRaceRow(race.getFleet()); + RaceCell previousCellInFleet = Util.get(raceRow.getCells(), race.getZeroBasedIndexInFleet()-1); + immediatePredecessors.add(racesByRaceGroupSeriesFleet.get( + new RaceGroupSeriesFleetRaceColumn(raceGroup, race.getSeries(), race.getFleet(), previousCellInFleet.getName()))); + } + } else { + // race's series is run sequentially, within one race column one fleet after the other, in the order of the + // fleets as specified by the series; the immediate predecessor is within this series the previous element in this + // ordering, or the last race(s) from the immediately preceding series if race is in the first column's first fleet. + final int zeroBasedIndexOfFleetInSeries = Util.indexOf(race.getSeries().getFleets(), race.getFleet()); + if (race.getZeroBasedIndexInFleet() == 0 && zeroBasedIndexOfFleetInSeries == 0) { + Util.addAll(getLastRacesFromImmediatelyPrecedingSeries(race, raceGroup), immediatePredecessors); + } else { + final Fleet fleetOfImmediatelyPrecedingRace; + final int zeroBasedIndexOfImmediatelyPrecedingRaceInFleet; + if (zeroBasedIndexOfFleetInSeries == 0) { + // race is in first fleet; wrap around to last fleet in immediately preceding column + fleetOfImmediatelyPrecedingRace = Util.last(race.getSeries().getFleets()); + zeroBasedIndexOfImmediatelyPrecedingRaceInFleet = race.getZeroBasedIndexInFleet() - 1; + } else { + fleetOfImmediatelyPrecedingRace = Util.get(race.getSeries().getFleets(), zeroBasedIndexOfFleetInSeries-1); + zeroBasedIndexOfImmediatelyPrecedingRaceInFleet = race.getZeroBasedIndexInFleet(); + } + final RaceGroupSeriesFleetRaceColumn identifierOfImmediatelyPrecedingRace = + new RaceGroupSeriesFleetRaceColumn(raceGroup, race.getSeries(), fleetOfImmediatelyPrecedingRace, + Util.get(Util.get(raceGroup.getSeries(), race.getZeroBasedSeriesIndex()).getRaceRow( + fleetOfImmediatelyPrecedingRace).getCells(), zeroBasedIndexOfImmediatelyPrecedingRaceInFleet).getName()); + immediatePredecessors.add(racesByRaceGroupSeriesFleet.get(identifierOfImmediatelyPrecedingRace)); + } + } + final Iterator i = immediatePredecessors.iterator(); + boolean result = false; + while (!result && i.hasNext()) { + final RaceLogRaceStatus nextStatus = i.next().getStatus(); + result = nextStatus.getOrderNumber() >= RaceLogRaceStatus.SCHEDULED.getOrderNumber(); + } + return result; + } + + private Iterable getLastRacesFromImmediatelyPrecedingSeries(FilterableRace race, final RaceGroup raceGroup) { + final Set lastRacesInImmediatelyPrecedingSeries = new HashSet<>(); + final SeriesWithRows immediatelyPrecedingSeries = Util.get(raceGroup.getSeries(), race.getZeroBasedSeriesIndex()-1); + if (immediatelyPrecedingSeries.isFleetsCanRunInParallel()) { + // add all last races of all fleets in immediately preceding series because they may be run in parallel + for (final RaceRow rowInImmediatelyPrecedingSeries : immediatelyPrecedingSeries.getRaceRows()) { + final T lastRaceInRow = getLastRaceInRow(raceGroup, immediatelyPrecedingSeries, rowInImmediatelyPrecedingSeries); + lastRacesInImmediatelyPrecedingSeries.add(lastRaceInRow); + } + } else { + // add only the last race of the last fleet in the immediately preceding series + final RaceRow lastRowInImmediatelyPrecedingSeries = Util.last(immediatelyPrecedingSeries.getRaceRows()); + lastRacesInImmediatelyPrecedingSeries.add(getLastRaceInRow(raceGroup, immediatelyPrecedingSeries, lastRowInImmediatelyPrecedingSeries)); + } + return lastRacesInImmediatelyPrecedingSeries; + } + + private T getLastRaceInRow(final RaceGroup raceGroup, final SeriesWithRows immediatelyPrecedingSeries, + final RaceRow rowInImmediatelyPrecedingSeries) { + final RaceCell lastCell = Util.last(rowInImmediatelyPrecedingSeries.getCells()); + RaceGroupSeriesFleetRaceColumn identifierOfALastRaceInImmediatelyPrecedingSeries = + new RaceGroupSeriesFleetRaceColumn(raceGroup, immediatelyPrecedingSeries, + rowInImmediatelyPrecedingSeries.getFleet(), lastCell.getName()); + final T lastRaceInRow = racesByRaceGroupSeriesFleet.get(identifierOfALastRaceInImmediatelyPrecedingSeries); + return lastRaceInRow; + } +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/impl/RaceCellImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/impl/RaceCellImpl.java index fb56c588657..cb69b6d7b83 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/impl/RaceCellImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/impl/RaceCellImpl.java @@ -2,6 +2,7 @@ package com.sap.sailing.domain.base.racegroup.impl; import com.sap.sailing.domain.abstractlog.race.RaceLog; import com.sap.sailing.domain.base.racegroup.RaceCell; +import com.sap.sse.common.Util; import com.sap.sse.common.impl.NamedImpl; public class RaceCellImpl extends NamedImpl implements RaceCell { @@ -18,12 +19,27 @@ public class RaceCellImpl extends NamedImpl implements RaceCell { * See {@code RaceColumn.getExplicitFactor()} */ private final Double explicitFactor; + + private final int zeroBasedIndexInFleet; - public RaceCellImpl(String name, RaceLog raceLog, double factor, Double explicitFactor) { - super(name); + /** + * @param zeroBasedIndexInFleet + * A Series offers a sequence of RaceColumns, each of them split according to the Fleets modeled for the + * Series. A {@link RaceCell} describes a "slot" in this grid, defined by the series, the fleet and the + * race column. While this object's {@link #getName() name} represents the race column's name, this + * doesn't tell anything about the "horizontal" position in the "grid" or in other words what the index + * is of the race column in which this cell lies. Indices returned by this method start with zero, + * meaning the first race column in the series. This corresponds to what one would get by asking + * {@link Util#indexOf(Iterable, Object) Util.indexOf(series.getRaceColumns(), thisCellsRaceColumn)}, + * except in case the first race column is a "virtual" one that holds a non-discardable carry-forward + * result. In this case, the second Race Column, which is the first "non-virtual" one, receives index 0. + */ + public RaceCellImpl(String raceColumnName, RaceLog raceLog, double factor, Double explicitFactor, int zeroBasedIndexInFleet) { + super(raceColumnName); this.raceLog = raceLog; this.factor = factor; this.explicitFactor = explicitFactor; + this.zeroBasedIndexInFleet = zeroBasedIndexInFleet; } @Override @@ -48,4 +64,15 @@ public class RaceCellImpl extends NamedImpl implements RaceCell { public Double getExplicitFactor() { return explicitFactor; } + + @Override + public int getZeroBasedIndexInFleet() { + return zeroBasedIndexInFleet; + } + + @Override + public String toString() { + return "RaceCellImpl [factor=" + factor + ", explicitFactor=" + explicitFactor + ", zeroBasedIndexInFleet=" + + zeroBasedIndexInFleet + ", getName()=" + getName() + "]"; + } } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/impl/RaceGroupImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/impl/RaceGroupImpl.java index cb3a9e84d9f..98cb087fbe1 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/impl/RaceGroupImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/impl/RaceGroupImpl.java @@ -17,6 +17,11 @@ public class RaceGroupImpl extends NamedImpl implements RaceGroup { private final Iterable series; private final RegattaConfiguration regattaConfiguration; + /** + * @param series + * the series; for a regatta they are expected to be in the same order as {@code Regatta.getSeries()} + * would deliver them. + */ public RaceGroupImpl(String name, String displayName, BoatClass boatClass, CourseArea courseArea, Iterable series, RegattaConfiguration regattaConfiguration) { super(name); diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/impl/RaceRowImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/impl/RaceRowImpl.java index 16a4d2a7d38..6a9ca58b07b 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/impl/RaceRowImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/impl/RaceRowImpl.java @@ -9,6 +9,10 @@ public class RaceRowImpl implements RaceRow { private Fleet fleet; private Iterable races; + /** + * @param races + * expected to be provided in the order in which the race columns appear in the series / leaderboard + */ public RaceRowImpl(Fleet fleet, Iterable races) { this.fleet = fleet; this.races = races; @@ -24,4 +28,9 @@ public class RaceRowImpl implements RaceRow { return races; } + @Override + public String toString() { + return "RaceRowImpl [fleet=" + fleet + ", races=" + races + "]"; + } + } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/impl/SeriesWithRowsImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/impl/SeriesWithRowsImpl.java index fa5aa6c7126..fad62d6b128 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/impl/SeriesWithRowsImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/racegroup/impl/SeriesWithRowsImpl.java @@ -13,25 +13,41 @@ public class SeriesWithRowsImpl implements SeriesWithRows { private String name; private Iterable raceRows; private boolean isMedal; - - public SeriesWithRowsImpl(String name, boolean isMedal, Iterable raceRows) { + private boolean isFleetsCanRunInParallel; + + public SeriesWithRowsImpl(String name, boolean isMedal, boolean isFleetsCanRunInParallel, Iterable raceRows) { this.name = name; this.raceRows = raceRows; this.isMedal = isMedal; + this.isFleetsCanRunInParallel = isFleetsCanRunInParallel; } + @Override public String getName() { return name; } + @Override public boolean isMedal() { return isMedal; } + @Override public Iterable getRaceRows() { return raceRows; } + @Override + public RaceRow getRaceRow(Fleet fleet) { + for (final RaceRow row : getRaceRows()) { + if (row.getFleet() == fleet) { + return row; + } + } + return null; + } + + @Override public Iterable getFleets() { Collection fleets = new ArrayList(); for (RaceRow row : raceRows) { @@ -46,8 +62,13 @@ public class SeriesWithRowsImpl implements SeriesWithRows { } @Override - public boolean isFleetsRunInParallel() { - // TODO This is a default implementation so far, allowing for parallel fleet races to not restrict generality. This shall become a configurable series property. See bug 3532 - return true; + public boolean isFleetsCanRunInParallel() { + return isFleetsCanRunInParallel; + } + + @Override + public String toString() { + return "SeriesWithRowsImpl [name=" + name + ", raceRows=" + raceRows + ", isMedal=" + isMedal + + ", isFleetsCanRunInParallel=" + isFleetsCanRunInParallel + "]"; } } diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/CompetitorProviderCacheInvalidationTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/CompetitorProviderCacheInvalidationTest.java index b9a5c66198f..b47d2282365 100644 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/CompetitorProviderCacheInvalidationTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/CompetitorProviderCacheInvalidationTest.java @@ -78,7 +78,7 @@ public class CompetitorProviderCacheInvalidationTest extends AbstractLeaderboard null, /* endDate */null, /* trackedRegattaRegistry */null, new LowPoint(), UUID.randomUUID(), courseArea); regattaLeaderboard = new RegattaLeaderboardImpl(regatta, new ThresholdBasedResultDiscardingRuleImpl(new int[0])); - regatta.addSeries(new SeriesImpl("Test Series", /* isMedal */false, Arrays.asList(new FleetImpl("Yellow"), + regatta.addSeries(new SeriesImpl("Test Series", /* isMedal */false, /* isFleetsCanRunInParallel */ true, Arrays.asList(new FleetImpl("Yellow"), new FleetImpl("Blue")), Arrays.asList("R1", "R2", "R3"), /* trackedRegattaRegistry */null)); for (int l = 0; l < NUMBER_OF_COMP_LISTS; l++) { compLists[l] = new ArrayList(); diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/LeaderboardCourseChangeTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/LeaderboardCourseChangeTest.java index b43c350c0e5..94f362b2691 100644 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/LeaderboardCourseChangeTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/LeaderboardCourseChangeTest.java @@ -175,7 +175,7 @@ public class LeaderboardCourseChangeTest { Set fleets = new HashSet<>(); fleets.add(fleet); - Series series = new SeriesImpl("TestSeries", false, fleets, new HashSet(), trackedRegattaRegistry); + Series series = new SeriesImpl("TestSeries", false, true, fleets, new HashSet(), trackedRegattaRegistry); return series; } diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/LeaderboardScoringAndRankingTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/LeaderboardScoringAndRankingTest.java index ace5830c7fe..62ee7421832 100644 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/LeaderboardScoringAndRankingTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/LeaderboardScoringAndRankingTest.java @@ -1709,7 +1709,7 @@ public class LeaderboardScoringAndRankingTest extends AbstractLeaderboardTest { for (int i = 1; i <= numberOfQualifyingRaces; i++) { qualifyingRaceColumnNames.add("Q" + i); } - Series qualifyingSeries = new SeriesImpl("Qualifying", /* isMedal */false, qualifyingFleets, + Series qualifyingSeries = new SeriesImpl("Qualifying", /* isMedal */false, /* isFleetsCanRunInParallel */ true, qualifyingFleets, qualifyingRaceColumnNames, /* trackedRegattaRegistry */null); series.add(qualifyingSeries); } @@ -1725,7 +1725,7 @@ public class LeaderboardScoringAndRankingTest extends AbstractLeaderboardTest { for (int i = 1; i <= numberOfFinalRaces; i++) { finalRaceColumnNames.add("F" + i); } - Series finalSeries = new SeriesImpl("Final", /* isMedal */false, finalFleets, finalRaceColumnNames, /* trackedRegattaRegistry */ null); + Series finalSeries = new SeriesImpl("Final", /* isMedal */false, /* isFleetsCanRunInParallel */ true, finalFleets, finalRaceColumnNames, /* trackedRegattaRegistry */ null); series.add(finalSeries); } @@ -1735,7 +1735,7 @@ public class LeaderboardScoringAndRankingTest extends AbstractLeaderboardTest { medalFleets.add(new FleetImpl("Medal")); List medalRaceColumnNames = new ArrayList(); medalRaceColumnNames.add("M"); - Series medalSeries = new SeriesImpl("Medal", /* isMedal */true, medalFleets, medalRaceColumnNames, /* trackedRegattaRegistry */ null); + Series medalSeries = new SeriesImpl("Medal", /* isMedal */true, /* isFleetsCanRunInParallel */ true, medalFleets, medalRaceColumnNames, /* trackedRegattaRegistry */ null); series.add(medalSeries); } @@ -1774,7 +1774,7 @@ public class LeaderboardScoringAndRankingTest extends AbstractLeaderboardTest { } else { roundName = "Round "+roundNumber; } - Series seriesForRound = new SeriesImpl("E"+elimination+" "+roundName, /* isMedal */ false, fleetsInRound, + Series seriesForRound = new SeriesImpl("E"+elimination+" "+roundName, /* isMedal */ false, /* isFleetsCanRunInParallel */ true, fleetsInRound, raceColumnNameForRound, /* trackedRegattaRegistry */null); if (isFinalRound) { // last "Final" round; here, the fleets are contiguously scored @@ -1808,7 +1808,7 @@ public class LeaderboardScoringAndRankingTest extends AbstractLeaderboardTest { for (int i = 1; i <= numberOfBeforeRaces; i++) { beforeRaceColumnNames.add("R" + i); } - Series qualifyingSeries = new SeriesImpl("Before", /* isMedal */false, beforeFleets, + Series qualifyingSeries = new SeriesImpl("Before", /* isMedal */false, /* isFleetsCanRunInParallel */ true, beforeFleets, beforeRaceColumnNames, /* trackedRegattaRegistry */null); series.add(qualifyingSeries); } @@ -1820,7 +1820,7 @@ public class LeaderboardScoringAndRankingTest extends AbstractLeaderboardTest { } List qualificationRaceColumnNames = new ArrayList(); qualificationRaceColumnNames.add("Q"); - Series qualificationSeries = new SeriesImpl("Qualification", /* isMedal */false, qualificationFleets, qualificationRaceColumnNames, /* trackedRegattaRegistry */ null); + Series qualificationSeries = new SeriesImpl("Qualification", /* isMedal */false, /* isFleetsCanRunInParallel */ true, qualificationFleets, qualificationRaceColumnNames, /* trackedRegattaRegistry */ null); // discard the one and only qualification race; it doesn't score qualificationSeries.setResultDiscardingRule(new ThresholdBasedResultDiscardingRuleImpl(new int[] { 1 })); series.add(qualificationSeries); @@ -1835,7 +1835,7 @@ public class LeaderboardScoringAndRankingTest extends AbstractLeaderboardTest { } List finalRaceColumnNames = new ArrayList(); finalRaceColumnNames.add("F"); - Series finalSeries = new SeriesImpl("Final", /* isMedal */false, finalFleets, finalRaceColumnNames, /* trackedRegattaRegistry */ null); + Series finalSeries = new SeriesImpl("Final", /* isMedal */false, /* isFleetsCanRunInParallel */ true, finalFleets, finalRaceColumnNames, /* trackedRegattaRegistry */ null); finalSeries.setSplitFleetContiguousScoring(true); series.add(finalSeries); } @@ -1847,7 +1847,7 @@ public class LeaderboardScoringAndRankingTest extends AbstractLeaderboardTest { for (int i = numberOfBeforeRaces+1; i <= numberOfBeforeRaces+numberOfFinalRaces; i++) { finalRaceColumnNames.add("R" + i); } - Series finalSeries = new SeriesImpl("After", /* isMedal */false, afterFleets, finalRaceColumnNames, /* trackedRegattaRegistry */ null); + Series finalSeries = new SeriesImpl("After", /* isMedal */false, /* isFleetsCanRunInParallel */ true, afterFleets, finalRaceColumnNames, /* trackedRegattaRegistry */ null); series.add(finalSeries); } final BoatClass boatClass = DomainFactory.INSTANCE.getOrCreateBoatClass("Extreme40", /* typicallyStartsUpwind */ false); diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/RaceExecutionOrderProvdiderAttachDetachTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/RaceExecutionOrderProvdiderAttachDetachTest.java index 13784b461aa..fbb03acad0b 100644 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/RaceExecutionOrderProvdiderAttachDetachTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/RaceExecutionOrderProvdiderAttachDetachTest.java @@ -1,293 +1,293 @@ -package com.sap.sailing.domain.test; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; - -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; -import java.util.UUID; - -import org.junit.Test; - -import com.sap.sailing.domain.abstractlog.race.RaceLog; -import com.sap.sailing.domain.base.BoatClass; -import com.sap.sailing.domain.base.Competitor; -import com.sap.sailing.domain.base.Fleet; -import com.sap.sailing.domain.base.RaceColumn; -import com.sap.sailing.domain.base.RaceColumnInSeries; -import com.sap.sailing.domain.base.Regatta; -import com.sap.sailing.domain.base.Series; -import com.sap.sailing.domain.base.impl.BoatClassImpl; -import com.sap.sailing.domain.base.impl.FleetImpl; -import com.sap.sailing.domain.base.impl.RegattaImpl; -import com.sap.sailing.domain.base.impl.SeriesImpl; -import com.sap.sailing.domain.base.impl.TrackedRaces; -import com.sap.sailing.domain.common.TimingConstants; -import com.sap.sailing.domain.common.Wind; -import com.sap.sailing.domain.common.impl.DegreeBearingImpl; -import com.sap.sailing.domain.common.impl.DegreePosition; -import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl; -import com.sap.sailing.domain.common.impl.WindImpl; -import com.sap.sailing.domain.leaderboard.FlexibleLeaderboard; -import com.sap.sailing.domain.leaderboard.ScoringScheme; -import com.sap.sailing.domain.leaderboard.impl.FlexibleLeaderboardImpl; -import com.sap.sailing.domain.leaderboard.impl.LowPoint; -import com.sap.sailing.domain.leaderboard.impl.ThresholdBasedResultDiscardingRuleImpl; -import com.sap.sailing.domain.ranking.OneDesignRankingMetric; -import com.sap.sailing.domain.tracking.DynamicTrackedRace; -import com.sap.sailing.domain.tracking.RaceExecutionOrderProvider; -import com.sap.sailing.domain.tracking.TrackedRace; -import com.sap.sailing.domain.tracking.impl.DynamicTrackedRaceImpl; -import com.sap.sailing.domain.tracking.impl.TrackedRaceImpl; -import com.sap.sse.common.Duration; -import com.sap.sse.common.TimePoint; -import com.sap.sse.common.impl.MillisecondsTimePoint; - -/** - * Covers the linking and unlinking between {@link RaceColumn}s and {@link TrackedRaces}s - * and tests whether {@link RaceExecutionOrderProvider}s get correctly attached and detached. Also tests - * late attaching and detaching of {@link RaceExecutionOrderProvider}s in case the {@link RaceExecutionOrderProvider} - * was null when {@link RaceColumn} was linked to {@link TrackedRace}. - * - * @author Alexander Ries (D062114) - * @author Axel Uhl (D043530) - * - */ -public class RaceExecutionOrderProvdiderAttachDetachTest extends TrackBasedTest { - private FlexibleLeaderboard flexibleLeaderboard; - private RaceColumnInSeries raceColumnInSeries; - private Fleet fleet; - private TrackedRaceImpl trackedRace; - private Regatta regatta; - private Series series; - - private final String REGATTA = "TestRegatta"; - private final String RACE = "TestRace"; - private final String FLEET = "TestFleet"; - private final String BOATCLASS = "TestClass"; - private final String SERIES = "TestSeries"; - private final String FLEXIBLELEADERBOARD = "TestFlexibleLeaderboard"; - private final String RACECOLUMN_SERIES = "TestSeriesRaceColumn"; - private final String RACECOLUMN_FLEXIBLELEADERBOARD = "TestFlexibleLeaderboardRaceColumn"; - - @Test - public void testRaceExecutionOrderProviderAttachDetachWithRaceColumn() { - trackedRace = createTestTrackedRace(REGATTA, RACE, BOATCLASS, Collections. emptyList(), - MillisecondsTimePoint.now()); - flexibleLeaderboard = new FlexibleLeaderboardImpl(FLEXIBLELEADERBOARD, - new ThresholdBasedResultDiscardingRuleImpl(new int[] { 3, 6 }), new LowPoint(), null); - flexibleLeaderboard.addRace(trackedRace, RACECOLUMN_FLEXIBLELEADERBOARD, false); - assertTrue(trackedRace.hasRaceExecutionOrderProvidersAttached()); - flexibleLeaderboard.removeRaceColumn(RACECOLUMN_FLEXIBLELEADERBOARD); - assertFalse(trackedRace.hasRaceExecutionOrderProvidersAttached()); - } - - @Test - public void testWindInRegularIntervalWithPreviousRaceStillTracking() { - final TimePoint startOfFirstRace = MillisecondsTimePoint.now(); - final TimePoint startOfSecondRace = startOfFirstRace.plus(Duration.ONE_MINUTE.times(5)); - DynamicTrackedRaceImpl previousTrackedRace = createTestTrackedRace(REGATTA, RACE, BOATCLASS, Collections. emptyList(), startOfFirstRace); - previousTrackedRace.setStartOfTrackingReceived(startOfFirstRace); - trackedRace = createTestTrackedRace(REGATTA, "TestRace2", BOATCLASS, Collections. emptyList(), startOfSecondRace); - flexibleLeaderboard = new FlexibleLeaderboardImpl(FLEXIBLELEADERBOARD, - new ThresholdBasedResultDiscardingRuleImpl(new int[] { 3, 6 }), new LowPoint(), null); - flexibleLeaderboard.addRace(previousTrackedRace, RACECOLUMN_FLEXIBLELEADERBOARD+"1", false); - flexibleLeaderboard.addRace(trackedRace, RACECOLUMN_FLEXIBLELEADERBOARD+"2", false); - Wind wind = new WindImpl(new DegreePosition(12, 13), startOfSecondRace.plus(Duration.ONE_MINUTE), new KnotSpeedWithBearingImpl( - /* speedInKnots */18, new DegreeBearingImpl(185))); - assertTrue(previousTrackedRace.takesWindFixWithTimePoint(wind.getTimePoint())); // previous race has tracking still open and takes the fix - assertTrue(trackedRace.takesWindFixWithTimePoint(wind.getTimePoint())); // tracked race also needs to take the fix as it falls into the regular tracking interval - } - - @Test - public void testWindInExtendedLeadIntervalWithPreviousRaceStillTracking() { - final TimePoint startOfFirstRace = MillisecondsTimePoint.now(); - final TimePoint endOfFirstRace = startOfFirstRace.plus(Duration.ONE_SECOND); - final TimePoint startOfSecondRace = startOfFirstRace.plus(Duration.ONE_HOUR); - DynamicTrackedRaceImpl previousTrackedRace = createTestTrackedRace(REGATTA, RACE, BOATCLASS, Collections. emptyList(), startOfFirstRace); - previousTrackedRace.setStartOfTrackingReceived(startOfFirstRace); - previousTrackedRace.setEndOfTrackingReceived(endOfFirstRace); // a very short race... - trackedRace = createTestTrackedRace(REGATTA, "TestRace2", BOATCLASS, Collections. emptyList(), startOfSecondRace); - flexibleLeaderboard = new FlexibleLeaderboardImpl(FLEXIBLELEADERBOARD, - new ThresholdBasedResultDiscardingRuleImpl(new int[] { 3, 6 }), new LowPoint(), null); - flexibleLeaderboard.addRace(previousTrackedRace, RACECOLUMN_FLEXIBLELEADERBOARD+"1", false); - flexibleLeaderboard.addRace(trackedRace, RACECOLUMN_FLEXIBLELEADERBOARD+"2", false); - // the wind fix is after the grace period of the first race's end, so won't be accepted by it, but within the extended - // time range before the second race, so shall be accepted by it. - Wind wind = new WindImpl(new DegreePosition(12, 13), - endOfFirstRace.plus(TimingConstants.IS_LIVE_GRACE_PERIOD_IN_MILLIS).plus(Duration.ONE_MINUTE), // fix is after the grace period - new KnotSpeedWithBearingImpl(/* speedInKnots */18, new DegreeBearingImpl(185))); - assertFalse(previousTrackedRace.takesWindFixWithTimePoint(wind.getTimePoint())); // previous race has tracking closed one second after it started and doesn't accept the fix - assertTrue(trackedRace.takesWindFixWithTimePoint(wind.getTimePoint())); // tracked race also needs to take the fix as it falls into the regular tracking interval - } - - @Test - public void testWindInExtendedLeadIntervalWithNoPreviousRace() { - final TimePoint startOfFirstRace = MillisecondsTimePoint.now(); - final TimePoint endOfFirstRace = startOfFirstRace.plus(Duration.ONE_SECOND); - final TimePoint startOfSecondRace = startOfFirstRace.plus(Duration.ONE_HOUR); - trackedRace = createTestTrackedRace(REGATTA, "TestRace2", BOATCLASS, Collections. emptyList(), startOfSecondRace); - flexibleLeaderboard = new FlexibleLeaderboardImpl(FLEXIBLELEADERBOARD, - new ThresholdBasedResultDiscardingRuleImpl(new int[] { 3, 6 }), new LowPoint(), null); - flexibleLeaderboard.addRace(trackedRace, RACECOLUMN_FLEXIBLELEADERBOARD, false); - // the wind fix is within the extended time range before the second race, and there is no previous race, so shall be accepted by it - Wind wind = new WindImpl(new DegreePosition(12, 13), - endOfFirstRace.plus(TimingConstants.IS_LIVE_GRACE_PERIOD_IN_MILLIS).plus(Duration.ONE_MINUTE), // fix is after the grace period - new KnotSpeedWithBearingImpl(/* speedInKnots */18, new DegreeBearingImpl(185))); - assertTrue(trackedRace.takesWindFixWithTimePoint(wind.getTimePoint())); - } - - @Test - public void testWindInExtendedLeadIntervalWithPreviousRaceLongAgo() { - final TimePoint startOfFirstRace = MillisecondsTimePoint.now(); - final TimePoint endOfFirstRace = startOfFirstRace.plus(Duration.ONE_SECOND); - final TimePoint startOfSecondRace = startOfFirstRace.plus(TrackedRaceImpl.EXTRA_LONG_TIME_BEFORE_START_TO_TRACK_WIND_MILLIS.times(2)); - DynamicTrackedRaceImpl previousTrackedRace = createTestTrackedRace(REGATTA, RACE, BOATCLASS, Collections. emptyList(), startOfFirstRace); - previousTrackedRace.setStartOfTrackingReceived(startOfFirstRace); - previousTrackedRace.setEndOfTrackingReceived(endOfFirstRace); // a very short race... - trackedRace = createTestTrackedRace(REGATTA, "TestRace2", BOATCLASS, Collections. emptyList(), startOfSecondRace); - flexibleLeaderboard = new FlexibleLeaderboardImpl(FLEXIBLELEADERBOARD, - new ThresholdBasedResultDiscardingRuleImpl(new int[] { 3, 6 }), new LowPoint(), null); - flexibleLeaderboard.addRace(previousTrackedRace, RACECOLUMN_FLEXIBLELEADERBOARD+"1", false); - flexibleLeaderboard.addRace(trackedRace, RACECOLUMN_FLEXIBLELEADERBOARD+"2", false); - // the wind fix is outside the extended time range before the second race, and the previous race doesn't accept it; shall not be accepted - Wind wind = new WindImpl(new DegreePosition(12, 13), - endOfFirstRace.plus(TimingConstants.IS_LIVE_GRACE_PERIOD_IN_MILLIS).plus(Duration.ONE_MINUTE), // fix is after the grace period - new KnotSpeedWithBearingImpl(/* speedInKnots */18, new DegreeBearingImpl(185))); - assertFalse(previousTrackedRace.takesWindFixWithTimePoint(wind.getTimePoint())); // fix is after first race's end plus grace period - assertFalse(trackedRace.takesWindFixWithTimePoint(wind.getTimePoint())); - } - - @Test - public void testWindInExtendedLeadIntervalButStillRecordedByPreviousRace() { - final TimePoint startOfFirstRace = MillisecondsTimePoint.now(); - final TimePoint endOfFirstRace = startOfFirstRace.plus(Duration.ONE_SECOND); - final TimePoint startOfSecondRace = startOfFirstRace.plus(TrackedRaceImpl.EXTRA_LONG_TIME_BEFORE_START_TO_TRACK_WIND_MILLIS.divide(2)); - DynamicTrackedRaceImpl previousTrackedRace = createTestTrackedRace(REGATTA, RACE, BOATCLASS, Collections. emptyList(), startOfFirstRace); - previousTrackedRace.setStartOfTrackingReceived(startOfFirstRace); - previousTrackedRace.setEndOfTrackingReceived(endOfFirstRace); // a very short race... - trackedRace = createTestTrackedRace(REGATTA, "TestRace2", BOATCLASS, Collections. emptyList(), startOfSecondRace); - flexibleLeaderboard = new FlexibleLeaderboardImpl(FLEXIBLELEADERBOARD, - new ThresholdBasedResultDiscardingRuleImpl(new int[] { 3, 6 }), new LowPoint(), null); - flexibleLeaderboard.addRace(previousTrackedRace, RACECOLUMN_FLEXIBLELEADERBOARD+"1", false); - flexibleLeaderboard.addRace(trackedRace, RACECOLUMN_FLEXIBLELEADERBOARD+"2", false); - // the wind fix is inside the extended time range before the second race, but the previous race still accepts it; shall not be accepted - Wind wind = new WindImpl(new DegreePosition(12, 13), - startOfFirstRace.plus(Duration.ONE_SECOND), // fix is recorded by previous race - new KnotSpeedWithBearingImpl(/* speedInKnots */18, new DegreeBearingImpl(185))); - assertTrue(previousTrackedRace.takesWindFixWithTimePoint(wind.getTimePoint())); // fix is after first race's end plus grace period - assertFalse(trackedRace.takesWindFixWithTimePoint(wind.getTimePoint())); - } - - @Test - public void testRaceExecutionOrderProviderAttachDetachWithRaceCollumnInSeries() { - createTestSetupWithRegattaAndSeries(/* linkSeriesToRegatta */true); - raceColumnInSeries.setTrackedRace(fleet, trackedRace); - assertTrue(trackedRace.hasRaceExecutionOrderProvidersAttached()); - raceColumnInSeries.releaseTrackedRace(fleet); - assertFalse(trackedRace.hasRaceExecutionOrderProvidersAttached()); - } - - @Test - public void testRaceExecutionOrderProviderAttachDetachWhenSeriesRegattaIsSetAndRemovedAfterTrackedRaceHaveBeenSetToRaceColumns() { - createTestSetupWithRegattaAndSeries(/* linkSeriesToRegatta */false); - raceColumnInSeries.setTrackedRace(fleet, trackedRace); - assertFalse(trackedRace.hasRaceExecutionOrderProvidersAttached()); - series.setRegatta(regatta); - assertTrue(trackedRace.hasRaceExecutionOrderProvidersAttached()); - series.setRegatta(null); - assertFalse(trackedRace.hasRaceExecutionOrderProvidersAttached()); - } - - @Test - public void testThatTrackedRaceReceivesRaceLogWhenSeriesIsLinkedToRegatta() { - createTestSetupWithRegattaAndSeries(/* linkSeriesToRegatta */false); - raceColumnInSeries.setTrackedRace(fleet, trackedRace); - assertNull(raceColumnInSeries.getRaceLog(fleet)); - regatta.addSeries(series); - final RaceLog raceLog = raceColumnInSeries.getRaceLog(fleet); - assertNotNull(raceLog); - assertNotNull(trackedRace.getRaceLog(raceLog.getId())); - } - - private void createTestSetupWithRegattaAndSeries(boolean linkSeriesToRegatta) { - trackedRace = createTestTrackedRace(REGATTA, RACE, BOATCLASS, Collections. emptyList(), - MillisecondsTimePoint.now()); - fleet = new FleetImpl(FLEET); - Set fleets = new HashSet<>(); - fleets.add(fleet); - series = new SeriesImpl(SERIES, false, fleets, new HashSet(), null); - Set seriesSet = new HashSet<>(); - if (linkSeriesToRegatta) { - seriesSet.add(series); - } - BoatClass boatClass = new BoatClassImpl(BOATCLASS, true); - raceColumnInSeries = series.addRaceColumn(RACECOLUMN_SERIES, null); - ScoringScheme scoringScheme = new LowPoint(); - regatta = new RegattaImpl(RegattaImpl.getDefaultName(REGATTA, boatClass.getName()), boatClass, - /* startDate */null, /* endDate */null, seriesSet, false, scoringScheme, UUID.randomUUID(), null, - OneDesignRankingMetric::new); - } - - /** - * See bug 3173. When a race is linked more than once to a fleet in a series, the "previous race" - * definition's transitive closure may become cyclic, and a race may be considered its own - * direct or transitive predecessor. This can lead to endless recursion in the {@link TrackedRace#takesWindFix(Wind)} - * implementation which traverses a race's predecessors recursively. - */ - @Test - public void testCyclicPreviousRacesSequence() { - createTestSetupWithRegattaAndSeries(/* linkSeriesToRegatta */ true); - final RaceColumnInSeries r1 = series.addRaceColumn("R1", /* trackedRegattaRegistry */ null); - final RaceColumnInSeries r2 = series.addRaceColumn("R2", /* trackedRegattaRegistry */ null); - final RaceColumnInSeries r3 = series.addRaceColumn("R2", /* trackedRegattaRegistry */ null); - final RaceColumnInSeries r4 = series.addRaceColumn("R2", /* trackedRegattaRegistry */ null); - final TimePoint startOfFirstRace = MillisecondsTimePoint.now(); - final TimePoint endOfFirstRace = startOfFirstRace.plus(Duration.ONE_MINUTE); - final TimePoint startOfSecondRace = endOfFirstRace.plus(Duration.ONE_MINUTE); - final TimePoint endOfSecondRace = startOfSecondRace.plus(Duration.ONE_MINUTE); - final TimePoint startOfThirdRace = endOfSecondRace.plus(Duration.ONE_MINUTE); - final TimePoint endOfThirdRace = startOfThirdRace.plus(Duration.ONE_MINUTE); - final TrackedRace tr1 = createTrackedRace("FirstRace", startOfFirstRace, endOfFirstRace); - final TrackedRace tr2 = createTrackedRace("SecondRace", startOfSecondRace, endOfSecondRace); - final TrackedRace tr3 = createTrackedRace("ThirdRace", startOfThirdRace, endOfThirdRace); - r1.setTrackedRace(fleet, tr3); - r2.setTrackedRace(fleet, tr1); - r3.setTrackedRace(fleet, tr2); - r4.setTrackedRace(fleet, tr3); // produce a cycle because now transitively tr1 has itself as a predecessor - assertTrue(tr3.takesWindFixWithTimePoint(new WindImpl(new DegreePosition(12, 13), - startOfThirdRace.minus(TrackedRaceImpl.EXTRA_LONG_TIME_BEFORE_START_TO_TRACK_WIND_MILLIS.divide(2)), - new KnotSpeedWithBearingImpl(/* speedInKnots */18, new DegreeBearingImpl(185))).getTimePoint())); - } - - /** - * See bug 3173. When a race is linked more than once to a fleet in a series, the "previous race" - * definition's transitive closure may become cyclic, and a race may be considered its own - * direct or transitive predecessor. This can lead to endless recursion in the {@link TrackedRace#takesWindFix(Wind)} - * implementation which traverses a race's predecessors recursively. - */ - @Test - public void testCyclicPreviousRacesSequenceUsingSingleRace() { - createTestSetupWithRegattaAndSeries(/* linkSeriesToRegatta */ true); - final RaceColumnInSeries r1 = series.addRaceColumn("R1", /* trackedRegattaRegistry */ null); - final RaceColumnInSeries r2 = series.addRaceColumn("R2", /* trackedRegattaRegistry */ null); - final TimePoint startOfFirstRace = MillisecondsTimePoint.now(); - final TimePoint endOfFirstRace = startOfFirstRace.plus(Duration.ONE_MINUTE); - final TrackedRace tr1 = createTrackedRace("FirstRace", startOfFirstRace, endOfFirstRace); - r1.setTrackedRace(fleet, tr1); - r2.setTrackedRace(fleet, tr1); - assertTrue(tr1.takesWindFixWithTimePoint(new WindImpl(new DegreePosition(12, 13), - startOfFirstRace.minus(TrackedRaceImpl.EXTRA_LONG_TIME_BEFORE_START_TO_TRACK_WIND_MILLIS.divide(2)), - new KnotSpeedWithBearingImpl(/* speedInKnots */18, new DegreeBearingImpl(185))).getTimePoint())); - } - - private DynamicTrackedRace createTrackedRace(final String name, final TimePoint startOfRace, final TimePoint endOfRace) { - DynamicTrackedRace trackedRace = createTestTrackedRace(REGATTA, name, BOATCLASS, Collections. emptyList(), startOfRace); - trackedRace.setStartOfTrackingReceived(startOfRace); - trackedRace.setEndOfTrackingReceived(endOfRace); - return trackedRace; - } -} +package com.sap.sailing.domain.test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +import org.junit.Test; + +import com.sap.sailing.domain.abstractlog.race.RaceLog; +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.RaceColumn; +import com.sap.sailing.domain.base.RaceColumnInSeries; +import com.sap.sailing.domain.base.Regatta; +import com.sap.sailing.domain.base.Series; +import com.sap.sailing.domain.base.impl.BoatClassImpl; +import com.sap.sailing.domain.base.impl.FleetImpl; +import com.sap.sailing.domain.base.impl.RegattaImpl; +import com.sap.sailing.domain.base.impl.SeriesImpl; +import com.sap.sailing.domain.base.impl.TrackedRaces; +import com.sap.sailing.domain.common.TimingConstants; +import com.sap.sailing.domain.common.Wind; +import com.sap.sailing.domain.common.impl.DegreeBearingImpl; +import com.sap.sailing.domain.common.impl.DegreePosition; +import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl; +import com.sap.sailing.domain.common.impl.WindImpl; +import com.sap.sailing.domain.leaderboard.FlexibleLeaderboard; +import com.sap.sailing.domain.leaderboard.ScoringScheme; +import com.sap.sailing.domain.leaderboard.impl.FlexibleLeaderboardImpl; +import com.sap.sailing.domain.leaderboard.impl.LowPoint; +import com.sap.sailing.domain.leaderboard.impl.ThresholdBasedResultDiscardingRuleImpl; +import com.sap.sailing.domain.ranking.OneDesignRankingMetric; +import com.sap.sailing.domain.tracking.DynamicTrackedRace; +import com.sap.sailing.domain.tracking.RaceExecutionOrderProvider; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sailing.domain.tracking.impl.DynamicTrackedRaceImpl; +import com.sap.sailing.domain.tracking.impl.TrackedRaceImpl; +import com.sap.sse.common.Duration; +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.impl.MillisecondsTimePoint; + +/** + * Covers the linking and unlinking between {@link RaceColumn}s and {@link TrackedRaces}s + * and tests whether {@link RaceExecutionOrderProvider}s get correctly attached and detached. Also tests + * late attaching and detaching of {@link RaceExecutionOrderProvider}s in case the {@link RaceExecutionOrderProvider} + * was null when {@link RaceColumn} was linked to {@link TrackedRace}. + * + * @author Alexander Ries (D062114) + * @author Axel Uhl (D043530) + * + */ +public class RaceExecutionOrderProvdiderAttachDetachTest extends TrackBasedTest { + private FlexibleLeaderboard flexibleLeaderboard; + private RaceColumnInSeries raceColumnInSeries; + private Fleet fleet; + private TrackedRaceImpl trackedRace; + private Regatta regatta; + private Series series; + + private final String REGATTA = "TestRegatta"; + private final String RACE = "TestRace"; + private final String FLEET = "TestFleet"; + private final String BOATCLASS = "TestClass"; + private final String SERIES = "TestSeries"; + private final String FLEXIBLELEADERBOARD = "TestFlexibleLeaderboard"; + private final String RACECOLUMN_SERIES = "TestSeriesRaceColumn"; + private final String RACECOLUMN_FLEXIBLELEADERBOARD = "TestFlexibleLeaderboardRaceColumn"; + + @Test + public void testRaceExecutionOrderProviderAttachDetachWithRaceColumn() { + trackedRace = createTestTrackedRace(REGATTA, RACE, BOATCLASS, Collections. emptyList(), + MillisecondsTimePoint.now()); + flexibleLeaderboard = new FlexibleLeaderboardImpl(FLEXIBLELEADERBOARD, + new ThresholdBasedResultDiscardingRuleImpl(new int[] { 3, 6 }), new LowPoint(), null); + flexibleLeaderboard.addRace(trackedRace, RACECOLUMN_FLEXIBLELEADERBOARD, false); + assertTrue(trackedRace.hasRaceExecutionOrderProvidersAttached()); + flexibleLeaderboard.removeRaceColumn(RACECOLUMN_FLEXIBLELEADERBOARD); + assertFalse(trackedRace.hasRaceExecutionOrderProvidersAttached()); + } + + @Test + public void testWindInRegularIntervalWithPreviousRaceStillTracking() { + final TimePoint startOfFirstRace = MillisecondsTimePoint.now(); + final TimePoint startOfSecondRace = startOfFirstRace.plus(Duration.ONE_MINUTE.times(5)); + DynamicTrackedRaceImpl previousTrackedRace = createTestTrackedRace(REGATTA, RACE, BOATCLASS, Collections. emptyList(), startOfFirstRace); + previousTrackedRace.setStartOfTrackingReceived(startOfFirstRace); + trackedRace = createTestTrackedRace(REGATTA, "TestRace2", BOATCLASS, Collections. emptyList(), startOfSecondRace); + flexibleLeaderboard = new FlexibleLeaderboardImpl(FLEXIBLELEADERBOARD, + new ThresholdBasedResultDiscardingRuleImpl(new int[] { 3, 6 }), new LowPoint(), null); + flexibleLeaderboard.addRace(previousTrackedRace, RACECOLUMN_FLEXIBLELEADERBOARD+"1", false); + flexibleLeaderboard.addRace(trackedRace, RACECOLUMN_FLEXIBLELEADERBOARD+"2", false); + Wind wind = new WindImpl(new DegreePosition(12, 13), startOfSecondRace.plus(Duration.ONE_MINUTE), new KnotSpeedWithBearingImpl( + /* speedInKnots */18, new DegreeBearingImpl(185))); + assertTrue(previousTrackedRace.takesWindFixWithTimePoint(wind.getTimePoint())); // previous race has tracking still open and takes the fix + assertTrue(trackedRace.takesWindFixWithTimePoint(wind.getTimePoint())); // tracked race also needs to take the fix as it falls into the regular tracking interval + } + + @Test + public void testWindInExtendedLeadIntervalWithPreviousRaceStillTracking() { + final TimePoint startOfFirstRace = MillisecondsTimePoint.now(); + final TimePoint endOfFirstRace = startOfFirstRace.plus(Duration.ONE_SECOND); + final TimePoint startOfSecondRace = startOfFirstRace.plus(Duration.ONE_HOUR); + DynamicTrackedRaceImpl previousTrackedRace = createTestTrackedRace(REGATTA, RACE, BOATCLASS, Collections. emptyList(), startOfFirstRace); + previousTrackedRace.setStartOfTrackingReceived(startOfFirstRace); + previousTrackedRace.setEndOfTrackingReceived(endOfFirstRace); // a very short race... + trackedRace = createTestTrackedRace(REGATTA, "TestRace2", BOATCLASS, Collections. emptyList(), startOfSecondRace); + flexibleLeaderboard = new FlexibleLeaderboardImpl(FLEXIBLELEADERBOARD, + new ThresholdBasedResultDiscardingRuleImpl(new int[] { 3, 6 }), new LowPoint(), null); + flexibleLeaderboard.addRace(previousTrackedRace, RACECOLUMN_FLEXIBLELEADERBOARD+"1", false); + flexibleLeaderboard.addRace(trackedRace, RACECOLUMN_FLEXIBLELEADERBOARD+"2", false); + // the wind fix is after the grace period of the first race's end, so won't be accepted by it, but within the extended + // time range before the second race, so shall be accepted by it. + Wind wind = new WindImpl(new DegreePosition(12, 13), + endOfFirstRace.plus(TimingConstants.IS_LIVE_GRACE_PERIOD_IN_MILLIS).plus(Duration.ONE_MINUTE), // fix is after the grace period + new KnotSpeedWithBearingImpl(/* speedInKnots */18, new DegreeBearingImpl(185))); + assertFalse(previousTrackedRace.takesWindFixWithTimePoint(wind.getTimePoint())); // previous race has tracking closed one second after it started and doesn't accept the fix + assertTrue(trackedRace.takesWindFixWithTimePoint(wind.getTimePoint())); // tracked race also needs to take the fix as it falls into the regular tracking interval + } + + @Test + public void testWindInExtendedLeadIntervalWithNoPreviousRace() { + final TimePoint startOfFirstRace = MillisecondsTimePoint.now(); + final TimePoint endOfFirstRace = startOfFirstRace.plus(Duration.ONE_SECOND); + final TimePoint startOfSecondRace = startOfFirstRace.plus(Duration.ONE_HOUR); + trackedRace = createTestTrackedRace(REGATTA, "TestRace2", BOATCLASS, Collections. emptyList(), startOfSecondRace); + flexibleLeaderboard = new FlexibleLeaderboardImpl(FLEXIBLELEADERBOARD, + new ThresholdBasedResultDiscardingRuleImpl(new int[] { 3, 6 }), new LowPoint(), null); + flexibleLeaderboard.addRace(trackedRace, RACECOLUMN_FLEXIBLELEADERBOARD, false); + // the wind fix is within the extended time range before the second race, and there is no previous race, so shall be accepted by it + Wind wind = new WindImpl(new DegreePosition(12, 13), + endOfFirstRace.plus(TimingConstants.IS_LIVE_GRACE_PERIOD_IN_MILLIS).plus(Duration.ONE_MINUTE), // fix is after the grace period + new KnotSpeedWithBearingImpl(/* speedInKnots */18, new DegreeBearingImpl(185))); + assertTrue(trackedRace.takesWindFixWithTimePoint(wind.getTimePoint())); + } + + @Test + public void testWindInExtendedLeadIntervalWithPreviousRaceLongAgo() { + final TimePoint startOfFirstRace = MillisecondsTimePoint.now(); + final TimePoint endOfFirstRace = startOfFirstRace.plus(Duration.ONE_SECOND); + final TimePoint startOfSecondRace = startOfFirstRace.plus(TrackedRaceImpl.EXTRA_LONG_TIME_BEFORE_START_TO_TRACK_WIND_MILLIS.times(2)); + DynamicTrackedRaceImpl previousTrackedRace = createTestTrackedRace(REGATTA, RACE, BOATCLASS, Collections. emptyList(), startOfFirstRace); + previousTrackedRace.setStartOfTrackingReceived(startOfFirstRace); + previousTrackedRace.setEndOfTrackingReceived(endOfFirstRace); // a very short race... + trackedRace = createTestTrackedRace(REGATTA, "TestRace2", BOATCLASS, Collections. emptyList(), startOfSecondRace); + flexibleLeaderboard = new FlexibleLeaderboardImpl(FLEXIBLELEADERBOARD, + new ThresholdBasedResultDiscardingRuleImpl(new int[] { 3, 6 }), new LowPoint(), null); + flexibleLeaderboard.addRace(previousTrackedRace, RACECOLUMN_FLEXIBLELEADERBOARD+"1", false); + flexibleLeaderboard.addRace(trackedRace, RACECOLUMN_FLEXIBLELEADERBOARD+"2", false); + // the wind fix is outside the extended time range before the second race, and the previous race doesn't accept it; shall not be accepted + Wind wind = new WindImpl(new DegreePosition(12, 13), + endOfFirstRace.plus(TimingConstants.IS_LIVE_GRACE_PERIOD_IN_MILLIS).plus(Duration.ONE_MINUTE), // fix is after the grace period + new KnotSpeedWithBearingImpl(/* speedInKnots */18, new DegreeBearingImpl(185))); + assertFalse(previousTrackedRace.takesWindFixWithTimePoint(wind.getTimePoint())); // fix is after first race's end plus grace period + assertFalse(trackedRace.takesWindFixWithTimePoint(wind.getTimePoint())); + } + + @Test + public void testWindInExtendedLeadIntervalButStillRecordedByPreviousRace() { + final TimePoint startOfFirstRace = MillisecondsTimePoint.now(); + final TimePoint endOfFirstRace = startOfFirstRace.plus(Duration.ONE_SECOND); + final TimePoint startOfSecondRace = startOfFirstRace.plus(TrackedRaceImpl.EXTRA_LONG_TIME_BEFORE_START_TO_TRACK_WIND_MILLIS.divide(2)); + DynamicTrackedRaceImpl previousTrackedRace = createTestTrackedRace(REGATTA, RACE, BOATCLASS, Collections. emptyList(), startOfFirstRace); + previousTrackedRace.setStartOfTrackingReceived(startOfFirstRace); + previousTrackedRace.setEndOfTrackingReceived(endOfFirstRace); // a very short race... + trackedRace = createTestTrackedRace(REGATTA, "TestRace2", BOATCLASS, Collections. emptyList(), startOfSecondRace); + flexibleLeaderboard = new FlexibleLeaderboardImpl(FLEXIBLELEADERBOARD, + new ThresholdBasedResultDiscardingRuleImpl(new int[] { 3, 6 }), new LowPoint(), null); + flexibleLeaderboard.addRace(previousTrackedRace, RACECOLUMN_FLEXIBLELEADERBOARD+"1", false); + flexibleLeaderboard.addRace(trackedRace, RACECOLUMN_FLEXIBLELEADERBOARD+"2", false); + // the wind fix is inside the extended time range before the second race, but the previous race still accepts it; shall not be accepted + Wind wind = new WindImpl(new DegreePosition(12, 13), + startOfFirstRace.plus(Duration.ONE_SECOND), // fix is recorded by previous race + new KnotSpeedWithBearingImpl(/* speedInKnots */18, new DegreeBearingImpl(185))); + assertTrue(previousTrackedRace.takesWindFixWithTimePoint(wind.getTimePoint())); // fix is after first race's end plus grace period + assertFalse(trackedRace.takesWindFixWithTimePoint(wind.getTimePoint())); + } + + @Test + public void testRaceExecutionOrderProviderAttachDetachWithRaceCollumnInSeries() { + createTestSetupWithRegattaAndSeries(/* linkSeriesToRegatta */true); + raceColumnInSeries.setTrackedRace(fleet, trackedRace); + assertTrue(trackedRace.hasRaceExecutionOrderProvidersAttached()); + raceColumnInSeries.releaseTrackedRace(fleet); + assertFalse(trackedRace.hasRaceExecutionOrderProvidersAttached()); + } + + @Test + public void testRaceExecutionOrderProviderAttachDetachWhenSeriesRegattaIsSetAndRemovedAfterTrackedRaceHaveBeenSetToRaceColumns() { + createTestSetupWithRegattaAndSeries(/* linkSeriesToRegatta */false); + raceColumnInSeries.setTrackedRace(fleet, trackedRace); + assertFalse(trackedRace.hasRaceExecutionOrderProvidersAttached()); + series.setRegatta(regatta); + assertTrue(trackedRace.hasRaceExecutionOrderProvidersAttached()); + series.setRegatta(null); + assertFalse(trackedRace.hasRaceExecutionOrderProvidersAttached()); + } + + @Test + public void testThatTrackedRaceReceivesRaceLogWhenSeriesIsLinkedToRegatta() { + createTestSetupWithRegattaAndSeries(/* linkSeriesToRegatta */false); + raceColumnInSeries.setTrackedRace(fleet, trackedRace); + assertNull(raceColumnInSeries.getRaceLog(fleet)); + regatta.addSeries(series); + final RaceLog raceLog = raceColumnInSeries.getRaceLog(fleet); + assertNotNull(raceLog); + assertNotNull(trackedRace.getRaceLog(raceLog.getId())); + } + + private void createTestSetupWithRegattaAndSeries(boolean linkSeriesToRegatta) { + trackedRace = createTestTrackedRace(REGATTA, RACE, BOATCLASS, Collections. emptyList(), + MillisecondsTimePoint.now()); + fleet = new FleetImpl(FLEET); + Set fleets = new HashSet<>(); + fleets.add(fleet); + series = new SeriesImpl(SERIES, false, /* isFleetsCanRunInParallel */ true, fleets, new HashSet(), null); + Set seriesSet = new HashSet<>(); + if (linkSeriesToRegatta) { + seriesSet.add(series); + } + BoatClass boatClass = new BoatClassImpl(BOATCLASS, true); + raceColumnInSeries = series.addRaceColumn(RACECOLUMN_SERIES, null); + ScoringScheme scoringScheme = new LowPoint(); + regatta = new RegattaImpl(RegattaImpl.getDefaultName(REGATTA, boatClass.getName()), boatClass, + /* startDate */null, /* endDate */null, seriesSet, false, scoringScheme, UUID.randomUUID(), null, + OneDesignRankingMetric::new); + } + + /** + * See bug 3173. When a race is linked more than once to a fleet in a series, the "previous race" + * definition's transitive closure may become cyclic, and a race may be considered its own + * direct or transitive predecessor. This can lead to endless recursion in the {@link TrackedRace#takesWindFix(Wind)} + * implementation which traverses a race's predecessors recursively. + */ + @Test + public void testCyclicPreviousRacesSequence() { + createTestSetupWithRegattaAndSeries(/* linkSeriesToRegatta */ true); + final RaceColumnInSeries r1 = series.addRaceColumn("R1", /* trackedRegattaRegistry */ null); + final RaceColumnInSeries r2 = series.addRaceColumn("R2", /* trackedRegattaRegistry */ null); + final RaceColumnInSeries r3 = series.addRaceColumn("R2", /* trackedRegattaRegistry */ null); + final RaceColumnInSeries r4 = series.addRaceColumn("R2", /* trackedRegattaRegistry */ null); + final TimePoint startOfFirstRace = MillisecondsTimePoint.now(); + final TimePoint endOfFirstRace = startOfFirstRace.plus(Duration.ONE_MINUTE); + final TimePoint startOfSecondRace = endOfFirstRace.plus(Duration.ONE_MINUTE); + final TimePoint endOfSecondRace = startOfSecondRace.plus(Duration.ONE_MINUTE); + final TimePoint startOfThirdRace = endOfSecondRace.plus(Duration.ONE_MINUTE); + final TimePoint endOfThirdRace = startOfThirdRace.plus(Duration.ONE_MINUTE); + final TrackedRace tr1 = createTrackedRace("FirstRace", startOfFirstRace, endOfFirstRace); + final TrackedRace tr2 = createTrackedRace("SecondRace", startOfSecondRace, endOfSecondRace); + final TrackedRace tr3 = createTrackedRace("ThirdRace", startOfThirdRace, endOfThirdRace); + r1.setTrackedRace(fleet, tr3); + r2.setTrackedRace(fleet, tr1); + r3.setTrackedRace(fleet, tr2); + r4.setTrackedRace(fleet, tr3); // produce a cycle because now transitively tr1 has itself as a predecessor + assertTrue(tr3.takesWindFixWithTimePoint(new WindImpl(new DegreePosition(12, 13), + startOfThirdRace.minus(TrackedRaceImpl.EXTRA_LONG_TIME_BEFORE_START_TO_TRACK_WIND_MILLIS.divide(2)), + new KnotSpeedWithBearingImpl(/* speedInKnots */18, new DegreeBearingImpl(185))).getTimePoint())); + } + + /** + * See bug 3173. When a race is linked more than once to a fleet in a series, the "previous race" + * definition's transitive closure may become cyclic, and a race may be considered its own + * direct or transitive predecessor. This can lead to endless recursion in the {@link TrackedRace#takesWindFix(Wind)} + * implementation which traverses a race's predecessors recursively. + */ + @Test + public void testCyclicPreviousRacesSequenceUsingSingleRace() { + createTestSetupWithRegattaAndSeries(/* linkSeriesToRegatta */ true); + final RaceColumnInSeries r1 = series.addRaceColumn("R1", /* trackedRegattaRegistry */ null); + final RaceColumnInSeries r2 = series.addRaceColumn("R2", /* trackedRegattaRegistry */ null); + final TimePoint startOfFirstRace = MillisecondsTimePoint.now(); + final TimePoint endOfFirstRace = startOfFirstRace.plus(Duration.ONE_MINUTE); + final TrackedRace tr1 = createTrackedRace("FirstRace", startOfFirstRace, endOfFirstRace); + r1.setTrackedRace(fleet, tr1); + r2.setTrackedRace(fleet, tr1); + assertTrue(tr1.takesWindFixWithTimePoint(new WindImpl(new DegreePosition(12, 13), + startOfFirstRace.minus(TrackedRaceImpl.EXTRA_LONG_TIME_BEFORE_START_TO_TRACK_WIND_MILLIS.divide(2)), + new KnotSpeedWithBearingImpl(/* speedInKnots */18, new DegreeBearingImpl(185))).getTimePoint())); + } + + private DynamicTrackedRace createTrackedRace(final String name, final TimePoint startOfRace, final TimePoint endOfRace) { + DynamicTrackedRace trackedRace = createTestTrackedRace(REGATTA, name, BOATCLASS, Collections. emptyList(), startOfRace); + trackedRace.setStartOfTrackingReceived(startOfRace); + trackedRace.setEndOfTrackingReceived(endOfRace); + return trackedRace; + } +} diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/RegattaLogEventNotificationForwardingTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/RegattaLogEventNotificationForwardingTest.java index 31d96c7cb22..6555282c269 100644 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/RegattaLogEventNotificationForwardingTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/RegattaLogEventNotificationForwardingTest.java @@ -178,7 +178,8 @@ public class RegattaLogEventNotificationForwardingTest extends AbstractSerializa } private Regatta createRegatta() { - Series series = new SeriesImpl("Test Series", /* isMedal */ false, /* fleets */ Collections.singleton(new FleetImpl("Default")), + Series series = new SeriesImpl("Test Series", /* isMedal */ false, /* isFleetsCanRunInParallel */ true, + /* fleets */ Collections.singleton(new FleetImpl("Default")), Collections.singleton("R1"), /* trackedRegattaRegistry */ null); Regatta regatta = new RegattaImpl("test", null, null, null, Collections.singleton(series), false, new LowPoint(), "test", null, OneDesignRankingMetric::new); diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/markpassing/AbstractMockedRaceMarkPassingTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/markpassing/AbstractMockedRaceMarkPassingTest.java index 1f6023f282d..fedbd2f437a 100755 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/markpassing/AbstractMockedRaceMarkPassingTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/markpassing/AbstractMockedRaceMarkPassingTest.java @@ -68,7 +68,7 @@ public class AbstractMockedRaceMarkPassingTest { Waypoint w5 = new WaypointImpl(cp, PassingInstruction.Line); waypoints = Arrays.asList(w1, w2, w3, w4, w5); Regatta r = new RegattaImpl(RegattaImpl.getDefaultName("regatta", boatClass.getName()), boatClass, - /*startDate*/ null, /*endDate*/ null, Arrays.asList(new SeriesImpl("Series", true, Arrays.asList(new FleetImpl("fleet")), + /*startDate*/ null, /*endDate*/ null, Arrays.asList(new SeriesImpl("Series", true, /* isFleetsCanRunInParallel */ true, Arrays.asList(new FleetImpl("fleet")), new ArrayList(), null)), true, new HighPoint(), "ID", new CourseAreaImpl("area", new UUID(5, 5)), OneDesignRankingMetric::new); Course course = new CourseImpl("course", waypoints); RaceDefinition raceDef = new RaceDefinitionImpl("Performance Race", course, boatClass, Arrays.asList(ron, tom, ben)); diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/RaceColumnListener.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/RaceColumnListener.java index d096c29fcb4..ac4bea25d40 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/RaceColumnListener.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/RaceColumnListener.java @@ -14,7 +14,9 @@ public interface RaceColumnListener extends Serializable { void trackedRaceUnlinked(RaceColumn raceColumn, Fleet fleet, TrackedRace trackedRace); void isMedalRaceChanged(RaceColumn raceColumn, boolean newIsMedalRace); - + + void isFleetsCanRunInParallelChanged(RaceColumn raceColumn, boolean newIsFleetsCanRunInParallel); + void isStartsWithZeroScoreChanged(RaceColumn raceColumn, boolean newIsStartsWithZeroScore); void isFirstColumnIsNonDiscardableCarryForwardChanged(RaceColumn raceColumn, boolean firstColumnIsNonDiscardableCarryForward); diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/Series.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/Series.java index 5f61acb1989..02bf40878bc 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/Series.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/Series.java @@ -41,6 +41,8 @@ public interface Series extends SeriesBase { void setIsMedal(boolean isMedal); + void setIsFleetsCanRunInParallel(boolean isFleetsCanRunInParallel); + Fleet getFleetByName(String fleetName); /** diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/RaceColumnListenerWithDefaultAction.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/RaceColumnListenerWithDefaultAction.java index 6c154e3c3cd..2454b024231 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/RaceColumnListenerWithDefaultAction.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/RaceColumnListenerWithDefaultAction.java @@ -1,107 +1,112 @@ -package com.sap.sailing.domain.base.impl; - -import com.sap.sailing.domain.abstractlog.race.RaceLogEvent; -import com.sap.sailing.domain.abstractlog.regatta.RegattaLogEvent; -import com.sap.sailing.domain.base.Competitor; -import com.sap.sailing.domain.base.Fleet; -import com.sap.sailing.domain.base.RaceColumn; -import com.sap.sailing.domain.base.RaceColumnListener; -import com.sap.sailing.domain.leaderboard.ResultDiscardingRule; -import com.sap.sailing.domain.racelog.RaceLogIdentifier; -import com.sap.sailing.domain.tracking.TrackedRace; - -/** - * Delegates all listener operations to a default action which is implemented here to do nothing. This way, subclasses - * can easily provide a default action to be executed for all callback methods except for maybe a few which then need to - * be explicitly overridden. If not most methods need to perform the same action then instead of implementing a default - * action subclasses should rather override the {@link RaceColumnListener} operations individually. - * - * @author Alexander Ries (D062114) - * - */ -public interface RaceColumnListenerWithDefaultAction extends RaceColumnListener { - /** - * This implementation of the default action to which all other operations delegate does nothing. Subclasses can - * choose to override this method to provide this as the default behavior for all other methods and then still - * override individual {@link RaceColumnListener} methods, or they can leave the default action empty and just - * override {@link RaceColumnListener} methods. - */ - void defaultAction(); - - @Override - default void trackedRaceLinked(RaceColumn raceColumn, Fleet fleet, TrackedRace trackedRace) { - defaultAction(); - } - - @Override - default void trackedRaceUnlinked(RaceColumn raceColumn, Fleet fleet, TrackedRace trackedRace) { - defaultAction(); - } - - @Override - default void isMedalRaceChanged(RaceColumn raceColumn, boolean newIsMedalRace) { - defaultAction(); - } - - @Override - default void isStartsWithZeroScoreChanged(RaceColumn raceColumn, boolean newIsStartsWithZeroScore) { - defaultAction(); - } - - @Override - default void isFirstColumnIsNonDiscardableCarryForwardChanged(RaceColumn raceColumn, - boolean firstColumnIsNonDiscardableCarryForward) { - defaultAction(); - } - - @Override - default void hasSplitFleetContiguousScoringChanged(RaceColumn raceColumn, boolean hasSplitFleetContiguousScoring) { - defaultAction(); - } - - @Override - default void raceColumnAddedToContainer(RaceColumn raceColumn) { - defaultAction(); - } - - @Override - default void raceColumnRemovedFromContainer(RaceColumn raceColumn) { - defaultAction(); - } - - @Override - default void raceColumnMoved(RaceColumn raceColumn, int newIndex) { - defaultAction(); - } - - @Override - default void factorChanged(RaceColumn raceColumn, Double oldFactor, Double newFactor) { - defaultAction(); - } - - @Override - default void competitorDisplayNameChanged(Competitor competitor, String oldDisplayName, String displayName) { - defaultAction(); - } - - @Override - default void resultDiscardingRuleChanged(ResultDiscardingRule oldDiscardingRule, - ResultDiscardingRule newDiscardingRule) { - defaultAction(); - } - - @Override - default void raceLogEventAdded(RaceColumn raceColumn, RaceLogIdentifier raceLogIdentifier, RaceLogEvent event) { - defaultAction(); - } - - @Override - default void regattaLogEventAdded(RegattaLogEvent event) { - defaultAction(); - } - - @Override - default boolean isTransient() { - return false; - } -} +package com.sap.sailing.domain.base.impl; + +import com.sap.sailing.domain.abstractlog.race.RaceLogEvent; +import com.sap.sailing.domain.abstractlog.regatta.RegattaLogEvent; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.RaceColumn; +import com.sap.sailing.domain.base.RaceColumnListener; +import com.sap.sailing.domain.leaderboard.ResultDiscardingRule; +import com.sap.sailing.domain.racelog.RaceLogIdentifier; +import com.sap.sailing.domain.tracking.TrackedRace; + +/** + * Delegates all listener operations to a default action which is implemented here to do nothing. This way, subclasses + * can easily provide a default action to be executed for all callback methods except for maybe a few which then need to + * be explicitly overridden. If not most methods need to perform the same action then instead of implementing a default + * action subclasses should rather override the {@link RaceColumnListener} operations individually. + * + * @author Alexander Ries (D062114) + * + */ +public interface RaceColumnListenerWithDefaultAction extends RaceColumnListener { + /** + * This implementation of the default action to which all other operations delegate does nothing. Subclasses can + * choose to override this method to provide this as the default behavior for all other methods and then still + * override individual {@link RaceColumnListener} methods, or they can leave the default action empty and just + * override {@link RaceColumnListener} methods. + */ + void defaultAction(); + + @Override + default void trackedRaceLinked(RaceColumn raceColumn, Fleet fleet, TrackedRace trackedRace) { + defaultAction(); + } + + @Override + default void trackedRaceUnlinked(RaceColumn raceColumn, Fleet fleet, TrackedRace trackedRace) { + defaultAction(); + } + + @Override + default void isMedalRaceChanged(RaceColumn raceColumn, boolean newIsMedalRace) { + defaultAction(); + } + + @Override + default void isFleetsCanRunInParallelChanged(RaceColumn raceColumn, boolean newIsFleetsCanRunInParallel) { + defaultAction(); + } + + @Override + default void isStartsWithZeroScoreChanged(RaceColumn raceColumn, boolean newIsStartsWithZeroScore) { + defaultAction(); + } + + @Override + default void isFirstColumnIsNonDiscardableCarryForwardChanged(RaceColumn raceColumn, + boolean firstColumnIsNonDiscardableCarryForward) { + defaultAction(); + } + + @Override + default void hasSplitFleetContiguousScoringChanged(RaceColumn raceColumn, boolean hasSplitFleetContiguousScoring) { + defaultAction(); + } + + @Override + default void raceColumnAddedToContainer(RaceColumn raceColumn) { + defaultAction(); + } + + @Override + default void raceColumnRemovedFromContainer(RaceColumn raceColumn) { + defaultAction(); + } + + @Override + default void raceColumnMoved(RaceColumn raceColumn, int newIndex) { + defaultAction(); + } + + @Override + default void factorChanged(RaceColumn raceColumn, Double oldFactor, Double newFactor) { + defaultAction(); + } + + @Override + default void competitorDisplayNameChanged(Competitor competitor, String oldDisplayName, String displayName) { + defaultAction(); + } + + @Override + default void resultDiscardingRuleChanged(ResultDiscardingRule oldDiscardingRule, + ResultDiscardingRule newDiscardingRule) { + defaultAction(); + } + + @Override + default void raceLogEventAdded(RaceColumn raceColumn, RaceLogIdentifier raceLogIdentifier, RaceLogEvent event) { + defaultAction(); + } + + @Override + default void regattaLogEventAdded(RegattaLogEvent event) { + defaultAction(); + } + + @Override + default boolean isTransient() { + return false; + } +} diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/RegattaImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/RegattaImpl.java index 6c39ac72caa..a665090fd58 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/RegattaImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/RegattaImpl.java @@ -182,7 +182,7 @@ public class RegattaImpl extends NamedImpl implements Regatta, RaceColumnListene RankingMetricConstructor rankingMetricConstructor) { this(raceLogStore, regattaLogStore, name, boatClass, startDate, endDate, Collections .singletonList(new SeriesImpl(LeaderboardNameConstants.DEFAULT_SERIES_NAME, - /* isMedal */false, Collections + /* isMedal */false, /* isFleetsCanRunInParallel */ true, Collections .singletonList(new FleetImpl(LeaderboardNameConstants.DEFAULT_FLEET_NAME)), /* race column names */new ArrayList(), trackedRegattaRegistry)), /* persistent */false, scoringScheme, id, courseArea, /* useStartTimeInference */true, rankingMetricConstructor); @@ -429,6 +429,11 @@ public class RegattaImpl extends NamedImpl implements Regatta, RaceColumnListene raceColumnListeners.notifyListenersAboutIsMedalRaceChanged(raceColumn, newIsMedalRace); } + @Override + public void isFleetsCanRunInParallelChanged(RaceColumn raceColumn, boolean newIsFleetsCanRunInParallel) { + raceColumnListeners.notifyListenersAboutIsFleetsCanRunInParallelChanged(raceColumn, newIsFleetsCanRunInParallel); + } + @Override public void isStartsWithZeroScoreChanged(RaceColumn raceColumn, boolean newIsStartsWithZeroScore) { raceColumnListeners.notifyListenersAboutIsStartsWithZeroScoreChanged(raceColumn, newIsStartsWithZeroScore); diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/SeriesImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/SeriesImpl.java index 64b283d2476..80cd55b1260 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/SeriesImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/SeriesImpl.java @@ -36,6 +36,7 @@ public class SeriesImpl extends RenamableImpl implements Series, RaceColumnListe private final List fleetsInAscendingOrder; private final List raceColumns; private boolean isMedal; + private boolean isFleetsCanRunInParallel; private Regatta regatta; private final RaceColumnListeners raceColumnListeners; private ThresholdBasedResultDiscardingRule resultDiscardingRule; @@ -71,7 +72,7 @@ public class SeriesImpl extends RenamableImpl implements Series, RaceColumnListe * this column's series {@link Regatta}, respectively. If null, the re-association won't be * carried out. */ - public SeriesImpl(String name, boolean isMedal, Iterable fleets, Iterable raceColumnNames, + public SeriesImpl(String name, boolean isMedal, boolean isFleetsCanRunInParallel, Iterable fleets, Iterable raceColumnNames, TrackedRegattaRegistry trackedRegattaRegistry) { super(name); if (fleets == null || Util.isEmpty(fleets)) { @@ -86,6 +87,7 @@ public class SeriesImpl extends RenamableImpl implements Series, RaceColumnListe Collections.sort(fleetsInAscendingOrder); this.raceColumns = new ArrayList(); this.isMedal = isMedal; + this.isFleetsCanRunInParallel = isFleetsCanRunInParallel; this.raceColumnListeners = new RaceColumnListeners(); for (String raceColumnName : raceColumnNames) { addRaceColumn(raceColumnName, trackedRegattaRegistry); @@ -284,6 +286,22 @@ public class SeriesImpl extends RenamableImpl implements Series, RaceColumnListe } } + @Override + public boolean isFleetsCanRunInParallel() { + return isFleetsCanRunInParallel; + } + + @Override + public void setIsFleetsCanRunInParallel(boolean isFleetsCanRunInParallel) { + boolean oldIsFleetsCanRunInParallel = this.isFleetsCanRunInParallel; + this.isFleetsCanRunInParallel = isFleetsCanRunInParallel; + if (oldIsFleetsCanRunInParallel != isFleetsCanRunInParallel) { + for (RaceColumn raceColumn : getRaceColumns()) { + raceColumnListeners.notifyListenersAboutIsFleetsCanRunInParallelChanged(raceColumn, isFleetsCanRunInParallel); + } + } + } + @Override public void trackedRaceLinked(RaceColumn raceColumn, Fleet fleet, TrackedRace trackedRace) { raceColumnListeners.notifyListenersAboutTrackedRaceLinked(raceColumn, fleet, trackedRace); @@ -299,6 +317,10 @@ public class SeriesImpl extends RenamableImpl implements Series, RaceColumnListe raceColumnListeners.notifyListenersAboutIsMedalRaceChanged(raceColumn, newIsMedalRace); } + @Override + public void isFleetsCanRunInParallelChanged(RaceColumn raceColumn, boolean newIsFleetsCanRunInParallel) { + raceColumnListeners.notifyListenersAboutIsFleetsCanRunInParallelChanged(raceColumn, newIsFleetsCanRunInParallel); + } @Override public void isStartsWithZeroScoreChanged(RaceColumn raceColumn, boolean newIsStartsWithZeroScore) { raceColumnListeners.notifyListenersAboutIsStartsWithZeroScoreChanged(raceColumn, newIsStartsWithZeroScore); @@ -434,10 +456,4 @@ public class SeriesImpl extends RenamableImpl implements Series, RaceColumnListe public boolean hasSplitFleetContiguousScoring() { return hasSplitFleetContiguousScoring; } - - @Override - public boolean isFleetsRunInParallel() { - // TODO This is a default implementation so far, allowing for parallel fleet races to not restrict generality. This shall become a configurable series property. See bug 3532 - return true; - } } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractSimpleLeaderboardImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractSimpleLeaderboardImpl.java index c55d25d5ba6..76a200f12f6 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractSimpleLeaderboardImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractSimpleLeaderboardImpl.java @@ -1,1943 +1,1948 @@ -package com.sap.sailing.domain.leaderboard.impl; - -import java.io.IOException; -import java.io.ObjectInputStream; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.NavigableSet; -import java.util.NoSuchElementException; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.FutureTask; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.RunnableFuture; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import java.util.logging.Logger; - -import com.sap.sailing.domain.abstractlog.race.InvalidatesLeaderboardCache; -import com.sap.sailing.domain.abstractlog.race.RaceLogEvent; -import com.sap.sailing.domain.abstractlog.regatta.RegattaLogEvent; -import com.sap.sailing.domain.base.Competitor; -import com.sap.sailing.domain.base.Course; -import com.sap.sailing.domain.base.DomainFactory; -import com.sap.sailing.domain.base.Fleet; -import com.sap.sailing.domain.base.Leg; -import com.sap.sailing.domain.base.RaceColumn; -import com.sap.sailing.domain.base.RaceColumnInSeries; -import com.sap.sailing.domain.base.RaceColumnListener; -import com.sap.sailing.domain.base.Waypoint; -import com.sap.sailing.domain.common.Distance; -import com.sap.sailing.domain.common.LeaderboardType; -import com.sap.sailing.domain.common.LegType; -import com.sap.sailing.domain.common.ManeuverType; -import com.sap.sailing.domain.common.MaxPointsReason; -import com.sap.sailing.domain.common.NoWindError; -import com.sap.sailing.domain.common.NoWindException; -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; -import com.sap.sailing.domain.common.RegattaNameAndRaceName; -import com.sap.sailing.domain.common.Speed; -import com.sap.sailing.domain.common.SpeedWithBearing; -import com.sap.sailing.domain.common.dto.BasicRaceDTO; -import com.sap.sailing.domain.common.dto.CompetitorDTO; -import com.sap.sailing.domain.common.dto.FleetDTO; -import com.sap.sailing.domain.common.dto.LeaderboardDTO; -import com.sap.sailing.domain.common.dto.LeaderboardEntryDTO; -import com.sap.sailing.domain.common.dto.LeaderboardRowDTO; -import com.sap.sailing.domain.common.dto.LegEntryDTO; -import com.sap.sailing.domain.common.dto.MetaLeaderboardRaceColumnDTO; -import com.sap.sailing.domain.common.dto.RaceColumnDTO; -import com.sap.sailing.domain.common.dto.RaceDTO; -import com.sap.sailing.domain.common.tracking.GPSFixMoving; -import com.sap.sailing.domain.leaderboard.Leaderboard; -import com.sap.sailing.domain.leaderboard.NumberOfCompetitorsInLeaderboardFetcher; -import com.sap.sailing.domain.leaderboard.ResultDiscardingRule; -import com.sap.sailing.domain.leaderboard.ScoreCorrection.Result; -import com.sap.sailing.domain.leaderboard.SettableScoreCorrection; -import com.sap.sailing.domain.leaderboard.ThresholdBasedResultDiscardingRule; -import com.sap.sailing.domain.leaderboard.caching.LeaderboardDTOCache; -import com.sap.sailing.domain.leaderboard.caching.LeaderboardDTOCalculationReuseCache; -import com.sap.sailing.domain.leaderboard.caching.LiveLeaderboardUpdater; -import com.sap.sailing.domain.leaderboard.meta.MetaLeaderboardColumn; -import com.sap.sailing.domain.racelog.RaceLogIdentifier; -import com.sap.sailing.domain.ranking.RankingMetric.CompetitorRankingInfo; -import com.sap.sailing.domain.ranking.RankingMetric.RankingInfo; -import com.sap.sailing.domain.tracking.GPSFixTrack; -import com.sap.sailing.domain.tracking.Maneuver; -import com.sap.sailing.domain.tracking.MarkPassing; -import com.sap.sailing.domain.tracking.MarkPassingManeuver; -import com.sap.sailing.domain.tracking.RaceChangeListener; -import com.sap.sailing.domain.tracking.TrackedLeg; -import com.sap.sailing.domain.tracking.TrackedLegOfCompetitor; -import com.sap.sailing.domain.tracking.TrackedRace; -import com.sap.sailing.domain.tracking.TrackedRegattaRegistry; -import com.sap.sailing.domain.tracking.WindLegTypeAndLegBearingCache; -import com.sap.sailing.domain.tracking.WindPositionMode; -import com.sap.sailing.domain.tracking.impl.AbstractRaceChangeListener; -import com.sap.sailing.util.impl.RaceColumnListeners; -import com.sap.sse.common.Duration; -import com.sap.sse.common.TimePoint; -import com.sap.sse.common.Util; -import com.sap.sse.common.impl.MillisecondsTimePoint; -import com.sap.sse.concurrent.LockUtil; -import com.sap.sse.concurrent.NamedReentrantReadWriteLock; -import com.sap.sse.util.impl.ThreadFactoryWithPriority; - -/** - * Base implementation for various types of leaderboards. The {@link RaceColumnListener} implementation forwards events - * received to all {@link RaceColumnListener} subscribed with this leaderboard. To which objects this leaderboard subscribes - * as {@link RaceColumnListener} is left to the concrete subclasses to implement, but the race columns seem like useful - * candidates. - * - * @author Axel Uhl (d043530) - * - */ -public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, RaceColumnListener { - private static final long serialVersionUID = 330156778603279333L; - - private static final Logger logger = Logger.getLogger(AbstractSimpleLeaderboardImpl.class.getName()); - - static final Double DOUBLE_0 = new Double(0); - - private final SettableScoreCorrection scoreCorrection; - - private ThresholdBasedResultDiscardingRule crossLeaderboardResultDiscardingRule; - - /** - * The optional display name mappings for competitors. This allows a user to override the tracking-provided - * competitor names for display in a leaderboard. - */ - private final Map displayNames; - - /** the display name of the leaderboard */ - private String displayName; - - /** - * Backs the {@link #getCarriedPoints(Competitor)} API with data. Can be used to prime this leaderboard - * with aggregated results of races not tracked / displayed by this leaderboard in detail. The points - * provided by this map are considered by {@link #getTotalPoints(Competitor, TimePoint)}. - */ - private final Map carriedPoints; - - private final RaceColumnListeners raceColumnListeners; - - /** - * A set that manages the difference between {@link #getCompetitors()} and {@link #getAllCompetitors()}. Access - * is controlled by the {@link #suppressedCompetitorsLock} lock. - */ - private final Set suppressedCompetitors; - private final NamedReentrantReadWriteLock suppressedCompetitorsLock; - - private transient Map, RunnableFuture> raceDetailsAtEndOfTrackingCache; - - /** - * This executor needs to be a different one than {@link #executor} because the tasks run by {@link #executor} - * can depend on the results of the tasks run by {@link #raceDetailsExecutor}, and an {@link Executor} doesn't - * move a task that is blocked by waiting for another {@link FutureTask} to the side but blocks permanently, - * ending in a deadlock (one that cannot easily be detected by the Eclipse debugger either). - */ - private transient Executor raceDetailsExecutor; - - /** - * Used to remove all these listeners from their tracked races when this servlet is {@link #destroy() destroyed}. - */ - private transient Set cacheInvalidationListeners; - - private transient ThreadPoolExecutor executor; - - private transient LiveLeaderboardUpdater liveLeaderboardUpdater; - - private transient LeaderboardDTOCache leaderboardDTOCache; - - /** - * A leaderboard entry representing a snapshot of a cell at a given time point for a single race/competitor. - * - * @author Axel Uhl (d043530) - * - */ - class EntryImpl implements Entry { - private final Callable trackedRankProvider; - private final Double netPoints; - private final Callable netPointsUncorrectedProvider; - private final boolean isNetPointsCorrected; - private final Double totalPoints; - private final MaxPointsReason maxPointsReason; - private final boolean discarded; - private final Fleet fleet; - - private EntryImpl(Callable trackedRankProvider, Double netPoints, - Callable netPointsUncorrectedProvider, boolean isNetPointsCorrected, - Double totalPoints, MaxPointsReason maxPointsReason, boolean discarded, Fleet fleet) { - super(); - this.trackedRankProvider = trackedRankProvider; - this.netPoints = netPoints; - this.netPointsUncorrectedProvider = netPointsUncorrectedProvider; - this.isNetPointsCorrected = isNetPointsCorrected; - this.totalPoints = totalPoints; - this.maxPointsReason = maxPointsReason; - this.discarded = discarded; - this.fleet = fleet; - } - @Override - public int getTrackedRank() { - try { - return trackedRankProvider.call(); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - @Override - public Double getNetPoints() { - return netPoints; - } - @Override - public boolean isNetPointsCorrected() { - return isNetPointsCorrected; - } - @Override - public Double getTotalPoints() { - return totalPoints; - } - @Override - public MaxPointsReason getMaxPointsReason() { - return maxPointsReason; - } - @Override - public boolean isDiscarded() { - return discarded; - } - @Override - public Fleet getFleet() { - return fleet; - } - @Override - public Double getNetPointsUncorrected() { - try { - return netPointsUncorrectedProvider.call(); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - } - - /** - * Computing the competitors can be a bit expensive, particularly if the fleet is large and there may be suppressed - * competitors, and the leaderboard may be a meta-leaderboard that refers to other leaderboards which each have - * several tracked races attached from where the competitors need to be retrieved. Ideally, the competitors list - * would be cached, but that is again difficult because we would have to monitor all changes in all dependent - * leaderboards and columns and tracked races properly. - *

- * - * As it turns out, one of the most frequent uses of the {@link AbstractSimpleLeaderboardImpl#getCompetitors} - * competitors list is to determine their number which in turn is only required for high-point scoring systems and - * for computing the default score for penalties. Again, the most frequently used low-point family of scoring schemes - * does not require this number. Yet, the scoring scheme requires an argument for polymorphic use by those that - * need it. Instead of computing it for each call, this interface lets us defer the actual calculation until the - * point when it's really needed. Once asked, this object will cache the result. Therefore, a new one should be - * constructed each time the number shall be computed. - * - * @author Axel Uhl (D043530) - * - */ - public class NumberOfCompetitorsFetcherImpl implements NumberOfCompetitorsInLeaderboardFetcher { - private int numberOfCompetitors = -1; - - @Override - public int getNumberOfCompetitorsInLeaderboard() { - if (numberOfCompetitors == -1) { - numberOfCompetitors = Util.size(getCompetitors()); - } - return numberOfCompetitors; - } - } - - /** - * Handles the invalidation of the {@link SailingServiceImpl#raceDetailsAtEndOfTrackingCache} entries if the tracked - * race changes in any way. In particular, for {@link #statusChanged}, when the status changes away from LOADING, - * calculations may start or resume, making it necessary to clear the cache. - * - * @author Axel Uhl (D043530) - * - */ - private class CacheInvalidationListener extends AbstractRaceChangeListener { - private final TrackedRace trackedRace; - private final Competitor competitor; - - public CacheInvalidationListener(TrackedRace trackedRace, Competitor competitor) { - this.trackedRace = trackedRace; - this.competitor = competitor; - } - - public TrackedRace getTrackedRace() { - return trackedRace; - } - - public void removeFromTrackedRace() { - trackedRace.removeListener(this); - } - - private void invalidateCacheAndRemoveThisListenerFromTrackedRace() { - synchronized (raceDetailsAtEndOfTrackingCache) { - raceDetailsAtEndOfTrackingCache.remove(new com.sap.sse.common.Util.Pair(trackedRace, competitor)); - removeFromTrackedRace(); - } - } - - @Override - protected void defaultAction() { - invalidateCacheAndRemoveThisListenerFromTrackedRace(); - } - } - - private static class UUIDGenerator implements LeaderboardDTO.UUIDGenerator { - @Override - public String generateRandomUUID() { - return UUID.randomUUID().toString(); - } - } - - public AbstractSimpleLeaderboardImpl(ThresholdBasedResultDiscardingRule resultDiscardingRule) { - this.carriedPoints = new HashMap(); - this.scoreCorrection = createScoreCorrection(); - this.displayNames = new HashMap(); - this.crossLeaderboardResultDiscardingRule = resultDiscardingRule; - this.suppressedCompetitors = new HashSet(); - this.suppressedCompetitorsLock = new NamedReentrantReadWriteLock("suppressedCompetitorsLock", /* fair */ false); - this.raceColumnListeners = new RaceColumnListeners(); - this.raceDetailsAtEndOfTrackingCache = new HashMap, RunnableFuture>(); - initTransientFields(); - } - - /** - * Produces the score correction object to use in this leaderboard. Used by the constructor. Subclasses may override - * this method to create a more specific type of score correction. This implementation produces an object of type - * {@link ScoreCorrectionImpl}. - */ - protected SettableScoreCorrection createScoreCorrection() { - return new ScoreCorrectionImpl(this); - } - - private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException { - ois.defaultReadObject(); - initTransientFields(); - } - - private void initTransientFields() { - this.raceDetailsAtEndOfTrackingCache = new HashMap, RunnableFuture>(); - this.raceDetailsExecutor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); - this.cacheInvalidationListeners = new HashSet(); - // When many updates are triggered in a short period of time by a single thread, ensure that the single thread - // providing the updates is not outperformed by all the re-calculations happening here. Leave at least one - // core to other things, but by using at least three threads ensure that no simplistic deadlocks may occur. - final int THREAD_POOL_SIZE = Math.max(Runtime.getRuntime().availableProcessors(), 3); - executor = new ThreadPoolExecutor(/* corePoolSize */ THREAD_POOL_SIZE, - /* maximumPoolSize */ THREAD_POOL_SIZE, - /* keepAliveTime */ 60, TimeUnit.SECONDS, - /* workQueue */ new LinkedBlockingQueue(), new ThreadFactoryWithPriority(Thread.NORM_PRIORITY-1, /* daemon */ true)); - } - - @Override - public void destroy() { - for (CacheInvalidationListener cacheInvalidationListener : cacheInvalidationListeners) { - cacheInvalidationListener.removeFromTrackedRace(); - } - } - - @Override - public SettableScoreCorrection getScoreCorrection() { - return scoreCorrection; - } - - @Override - public String getDisplayName(Competitor competitor) { - return displayNames.get(competitor); - } - - @Override - public String getDisplayName() { - return displayName; - } - - @Override - public void setDisplayName(String displayName) { - this.displayName = displayName; - } - - @Override - public ResultDiscardingRule getResultDiscardingRule() { - return crossLeaderboardResultDiscardingRule; - } - - @Override - public void setCarriedPoints(Competitor competitor, double carriedPoints) { - Double oldCarriedPoints = this.carriedPoints.put(competitor, carriedPoints); - if (!Util.equalsWithNull(oldCarriedPoints, carriedPoints)) { - getScoreCorrection().notifyListenersAboutCarriedPointsChange(competitor, oldCarriedPoints, carriedPoints); - } - } - - @Override - public double getCarriedPoints(Competitor competitor) { - Double result = carriedPoints.get(competitor); - return result == null ? 0 : result; - } - - @Override - public Map getCompetitorsForWhichThereAreCarriedPoints() { - return Collections.unmodifiableMap(carriedPoints); - } - - @Override - public void unsetCarriedPoints(Competitor competitor) { - Double oldCarriedPoints = carriedPoints.remove(competitor); - if (oldCarriedPoints != null) { - getScoreCorrection().notifyListenersAboutCarriedPointsChange(competitor, oldCarriedPoints, null); - } - } - - @Override - public boolean hasCarriedPoints() { - return !carriedPoints.isEmpty(); - } - - @Override - public boolean hasCarriedPoints(Competitor competitor) { - return carriedPoints.containsKey(competitor); - } - - @Override - public void setDisplayName(Competitor competitor, String displayName) { - String oldDisplayName = displayNames.get(competitor); - displayNames.put(competitor, displayName); - if (!Util.equalsWithNull(oldDisplayName, displayName)) { - getRaceColumnListeners().notifyListenersAboutCompetitorDisplayNameChanged(competitor, oldDisplayName, displayName); - } - } - - @Override - public void setCrossLeaderboardResultDiscardingRule(ThresholdBasedResultDiscardingRule discardingRule) { - ResultDiscardingRule oldDiscardingRule = getResultDiscardingRule(); - this.crossLeaderboardResultDiscardingRule = discardingRule; - getRaceColumnListeners().notifyListenersAboutResultDiscardingRuleChanged(oldDiscardingRule, discardingRule); - } - - @Override - public Double getNetPoints(final Competitor competitor, final RaceColumn raceColumn, final TimePoint timePoint) { - return getScoreCorrection().getCorrectedScore( - new Callable() { - public Integer call() throws NoWindException { - return getTrackedRank(competitor, raceColumn, timePoint); - } - }, competitor, - raceColumn, timePoint, new NumberOfCompetitorsFetcherImpl(), getScoringScheme()).getCorrectedScore(); - } - - @Override - public MaxPointsReason getMaxPointsReason(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint) { - return getScoreCorrection().getMaxPointsReason(competitor, raceColumn, timePoint); - } - - @Override - public boolean isDiscarded(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint) { - return isDiscarded(competitor, raceColumn, getRaceColumns(), timePoint); - } - - private boolean isDiscarded(Competitor competitor, RaceColumn raceColumn, - Iterable raceColumnsToConsider, TimePoint timePoint) { - final Set discardedRaceColumns = getResultDiscardingRule() - .getDiscardedRaceColumns(competitor, this, raceColumnsToConsider, timePoint); - return isDiscarded(competitor, raceColumn, timePoint, discardedRaceColumns); - } - - /** - * Same as {@link #isDiscarded(Competitor, RaceColumn, TimePoint)}, only that the set of discarded race columns can - * be specified which is useful when total points are to be computed for more than one column for the same - * competitor because then the calculation of discards (which requires looking at all columns) only needs to be done - * once and not again for each column (which would lead to quadratic effort). - * - * @param discardedRaceColumns - * expected to be the result of what we would get if we called {@link #getResultDiscardingRule()}. - * {@link ResultDiscardingRule#getDiscardedRaceColumns(Competitor, Leaderboard, Iterable, TimePoint) - * getDiscardedRaceColumns(competitor, this, raceColumnsToConsider, timePoint)}. - */ - private boolean isDiscarded(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint, - final Set discardedRaceColumns) { - return !raceColumn.isMedalRace() - && getMaxPointsReason(competitor, raceColumn, timePoint).isDiscardable() - && discardedRaceColumns.contains(raceColumn); - } - - @Override - public Double getTotalPoints(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint) throws NoWindException { - return getTotalPoints(competitor, raceColumn, getRaceColumns(), timePoint); - } - - @Override - public Double getTotalPoints(Competitor competitor, RaceColumn raceColumn, - Iterable raceColumnsToConsider, TimePoint timePoint) throws NoWindException { - final Set discardedRaceColumns = getResultDiscardingRule() - .getDiscardedRaceColumns(competitor, this, raceColumnsToConsider, timePoint); - return getTotalPoints(competitor, raceColumn, timePoint, discardedRaceColumns); - } - - /** - * Same as {@link #getTotalPoints(Competitor, RaceColumn, Iterable, TimePoint)}, only that the set of discarded race columns can - * be specified which is useful when total points are to be computed for more than one column for the same - * competitor because then the calculation of discards (which requires looking at all columns) only needs to be done - * once and not again for each column (which would lead to quadratic effort). - * - * @param discardedRaceColumns - * expected to be the result of what we would get if we called {@link #getResultDiscardingRule()}. - * {@link ResultDiscardingRule#getDiscardedRaceColumns(Competitor, Leaderboard, Iterable, TimePoint) - * getDiscardedRaceColumns(competitor, this, raceColumnsToConsider, timePoint)}. - */ - @Override - public Double getTotalPoints(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint, Set discardedRaceColumns) { - Double result; - if (isDiscarded(competitor, raceColumn, timePoint, discardedRaceColumns)) { - result = 0.0; - } else { - final Double netPoints = getNetPoints(competitor, raceColumn, timePoint); - if (netPoints == null) { - result = null; - } else { - result = raceColumn.getFactor() * netPoints; - } - } - return result; - } - - @Override - public Double getTotalPoints(Competitor competitor, TimePoint timePoint) { - return getTotalPoints(competitor, getRaceColumns(), timePoint); - } - - @Override - public Double getTotalPoints(Competitor competitor, final Iterable raceColumnsToConsider, - TimePoint timePoint) { - // when a column with isStartsWithZeroScore() is found, only reset score if the competitor scored in any race from there on - boolean needToResetScoreUponNextNonEmptyEntry = false; - double result = getCarriedPoints(competitor); - final Set discardedRaceColumns = getResultDiscardingRule() - .getDiscardedRaceColumns(competitor, this, raceColumnsToConsider, timePoint); - for (RaceColumn raceColumn : raceColumnsToConsider) { - if (raceColumn.isStartsWithZeroScore()) { - needToResetScoreUponNextNonEmptyEntry = true; - } - if (getScoringScheme().isValidInTotalScore(this, raceColumn, competitor, timePoint)) { - final Double totalPoints = getTotalPoints(competitor, raceColumn, timePoint, discardedRaceColumns); - if (totalPoints != null) { - if (needToResetScoreUponNextNonEmptyEntry) { - result = 0; - needToResetScoreUponNextNonEmptyEntry = false; - } - result += totalPoints; - } - } - } - return result; - } - - /** - * All competitors with non-null net points are added to the result which is then sorted by net points in ascending - * order. The fleet, if ordered, is the primary ordering criterion, followed by the net points. - */ - @Override - public List getCompetitorsFromBestToWorst(final RaceColumn raceColumn, TimePoint timePoint) throws NoWindException { - final Map> netPointsAndFleet = new HashMap>(); - for (Competitor competitor : getCompetitors()) { - Double netPoints = getNetPoints(competitor, raceColumn, timePoint); - if (netPoints != null) { - netPointsAndFleet.put(competitor, new com.sap.sse.common.Util.Pair(netPoints, raceColumn.getFleetOfCompetitor(competitor))); - } - } - List result = new ArrayList(netPointsAndFleet.keySet()); - Collections.sort(result, new Comparator() { - @Override - public int compare(Competitor o1, Competitor o2) { - int comparisonResult; - if (o1 == o2) { - comparisonResult = 0; - } else { - if (raceColumn.hasSplitFleets() && !raceColumn.hasSplitFleetContiguousScoring()) { - // only check fleets if there are more than one and the column is not to be contiguously scored even in case - // of split fleets - final Fleet o1Fleet = netPointsAndFleet.get(o1).getB(); - final Fleet o2Fleet = netPointsAndFleet.get(o2).getB(); - if (o1Fleet == null) { - if (o2Fleet == null) { - comparisonResult = 0; - } else { - comparisonResult = 1; // o1 ranks "worse" because it doesn't have a fleet set while o2 - // has - } - } else { - if (o2Fleet == null) { - comparisonResult = -1; // o1 ranks "better" because it has a fleet set while o2 hasn't - } else { - comparisonResult = o1Fleet.compareTo(o2Fleet); - } - } - } else { - // either there are no split fleets or the split isn't relevant for scoring as for ordered fleets - // the scoring runs contiguously from top to bottom - comparisonResult = 0; - } - if (comparisonResult == 0) { - comparisonResult = getScoringScheme().getScoreComparator(/* nullScoresAreBetter */ false).compare( - netPointsAndFleet.get(o1).getA(), netPointsAndFleet.get(o2).getA()); - } - } - return comparisonResult; - } - }); - return result; - } - - @Override - public List getCompetitorsFromBestToWorst(TimePoint timePoint) { - return getCompetitorsFromBestToWorst(getRaceColumns(), timePoint); - } - - private List getCompetitorsFromBestToWorst(Iterable raceColumnsToConsider, TimePoint timePoint) { - List result = new ArrayList(); - for (Competitor competitor : getCompetitors()) { - result.add(competitor); - } - Collections.sort(result, getTotalRankComparator(raceColumnsToConsider, timePoint)); - return result; - } - - @Override - public int getTotalRankOfCompetitor(Competitor competitor, TimePoint timePoint) throws NoWindException { - List competitorsFromBestToWorst = getCompetitorsFromBestToWorst(timePoint); - return competitorsFromBestToWorst.indexOf(competitor) + 1; - } - - protected Comparator getTotalRankComparator(Iterable raceColumnsToConsider, TimePoint timePoint) { - return new LeaderboardTotalRankComparator(this, timePoint, getScoringScheme(), /* nullScoresAreBetter */ false, raceColumnsToConsider); - } - - @Override - public RaceColumn getRaceColumnByName(String columnName) { - RaceColumn result = null; - for (RaceColumn r : getRaceColumns()) { - if (r.getName().equals(columnName)) { - result = r; - break; - } - } - return result; - } - - @Override - public Competitor getCompetitorByName(String competitorName) { - for (Competitor competitor : getAllCompetitors()) { - if (competitor.getName().equals(competitorName)) { - return competitor; - } - } - return null; - } - - @Override - public boolean countRaceForComparisonWithDiscardingThresholds(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint) { - TrackedRace trackedRaceForCompetitorInColumn; - return getScoringScheme().isValidInTotalScore(this, raceColumn, competitor, timePoint) && - (getScoreCorrection().isScoreCorrected(competitor, raceColumn, timePoint) || - ((trackedRaceForCompetitorInColumn=raceColumn.getTrackedRace(competitor)) != null && - trackedRaceForCompetitorInColumn.hasStarted(timePoint) && - trackedRaceForCompetitorInColumn.getRank(competitor, timePoint) != 0)); - } - - @Override - public void addRaceColumnListener(RaceColumnListener listener) { - getRaceColumnListeners().addRaceColumnListener(listener); - } - - @Override - public void removeRaceColumnListener(RaceColumnListener listener) { - getRaceColumnListeners().removeRaceColumnListener(listener); - } - - @Override - public Entry getEntry(final Competitor competitor, final RaceColumn race, final TimePoint timePoint) throws NoWindException { - final Set discardedRaceColumns = getResultDiscardingRule().getDiscardedRaceColumns(competitor, this, getRaceColumns(), timePoint); - return getEntry(competitor, race, timePoint, discardedRaceColumns); - } - - @Override - public Entry getEntry(final Competitor competitor, final RaceColumn race, final TimePoint timePoint, - Set discardedRaceColumns) throws NoWindException { - Callable trackedRankProvider = new Callable() { - public Integer call() throws NoWindException { - return getTrackedRank(competitor, race, timePoint); - } - }; - final Result correctedResults = getScoreCorrection().getCorrectedScore(trackedRankProvider, competitor, race, - timePoint, new NumberOfCompetitorsFetcherImpl(), getScoringScheme()); - boolean discarded = isDiscarded(competitor, race, timePoint, discardedRaceColumns); - final Double correctedScore = correctedResults.getCorrectedScore(); - return new EntryImpl(trackedRankProvider, correctedScore, new Callable() { - @Override - public Double call() { - return correctedResults.getUncorrectedScore(); - } - }, correctedResults.isCorrected(), discarded ? DOUBLE_0 : correctedScore == null ? null - : Double.valueOf(correctedScore * race.getFactor()), correctedResults.getMaxPointsReason(), discarded, - race.getFleetOfCompetitor(competitor)); - } - - @Override - public Map> getRankedCompetitorsFromBestToWorstAfterEachRaceColumn(TimePoint timePoint) throws NoWindException { - Map> result = new LinkedHashMap<>(); - List raceColumnsToConsider = new ArrayList<>(); - for (RaceColumn raceColumn : getRaceColumns()) { - raceColumnsToConsider.add(raceColumn); - result.put(raceColumn, getCompetitorsFromBestToWorst(raceColumnsToConsider, timePoint)); - } - return result; - } - - @Override - public Map> getTotalPointsSumAfterRaceColumn(final TimePoint timePoint) - throws NoWindException { - final Map> result = new LinkedHashMap<>(); - List raceColumnsToConsider = new ArrayList<>(); - Map>> futures = new HashMap<>(); - for (final RaceColumn raceColumn : getRaceColumns()) { - raceColumnsToConsider.add(raceColumn); - final Iterable finalRaceColumnsToConsider = new ArrayList<>(raceColumnsToConsider); - futures.put(raceColumn, executor.submit(new Callable>() { - @Override - public Map call() { - Map totalPointsSumPerCompetitorInColumn = new HashMap<>(); - for (Competitor competitor : getCompetitors()) { - totalPointsSumPerCompetitorInColumn.put(competitor, getTotalPoints(competitor, finalRaceColumnsToConsider, timePoint)); - } - synchronized (result) { - return totalPointsSumPerCompetitorInColumn; - } - } - })); - } - for (RaceColumn raceColumn : getRaceColumns()) { - try { - result.put(raceColumn, futures.get(raceColumn).get()); - } catch (InterruptedException | ExecutionException e) { - if (e.getCause() instanceof NoWindError) { - throw ((NoWindError) e.getCause()).getCause(); - } else { - throw new RuntimeException(e); // no caught exceptions occur in the futures executed - } - } - } - return result; - } - - @Override - public Map, Entry> getContent(final TimePoint timePoint) throws NoWindException { - Map, Entry> result = new HashMap, Entry>(); - Map> discardedRaces = new HashMap>(); - for (final RaceColumn raceColumn : getRaceColumns()) { - for (final Competitor competitor : getCompetitors()) { - Callable trackedRankProvider = new Callable() { - @Override - public Integer call() throws Exception { - return getTrackedRank(competitor, raceColumn, timePoint); - } - }; - final Result correctedResults = getScoreCorrection().getCorrectedScore(trackedRankProvider, competitor, raceColumn, - timePoint, new NumberOfCompetitorsFetcherImpl(), getScoringScheme()); - Set discardedRacesForCompetitor = discardedRaces.get(competitor); - if (discardedRacesForCompetitor == null) { - discardedRacesForCompetitor = getResultDiscardingRule().getDiscardedRaceColumns(competitor, this, getRaceColumns(), timePoint); - discardedRaces.put(competitor, discardedRacesForCompetitor); - } - boolean discarded = discardedRacesForCompetitor.contains(raceColumn); - final Double correctedScore = correctedResults.getCorrectedScore(); - Entry entry = new EntryImpl(trackedRankProvider, correctedScore, - new Callable() { @Override public Double call() { return correctedResults.getUncorrectedScore(); } }, - correctedResults.isCorrected(), - discarded ? DOUBLE_0 : (correctedScore==null?null: - Double.valueOf((correctedScore * raceColumn.getFactor()))), correctedResults.getMaxPointsReason(), - discarded, raceColumn.getFleetOfCompetitor(competitor)); - result.put(new com.sap.sse.common.Util.Pair(competitor, raceColumn), entry); - } - } - return result; - } - - @Override - public void trackedRaceLinked(RaceColumn raceColumn, Fleet fleet, TrackedRace trackedRace) { - getRaceColumnListeners().notifyListenersAboutTrackedRaceLinked(raceColumn, fleet, trackedRace); - } - - @Override - public void trackedRaceUnlinked(RaceColumn raceColumn, Fleet fleet, TrackedRace trackedRace) { - getRaceColumnListeners().notifyListenersAboutTrackedRaceUnlinked(raceColumn, fleet, trackedRace); - // It's generally possible that a leaderboard links to the same tracked race in multiple columns / fleets; - // only if it no longer references the trackedRace currently unlinked from one column/fleet, also unlink - // all cache invalidation listeners for said trackedRace - if (!Util.contains(getTrackedRaces(), trackedRace)) { - synchronized (cacheInvalidationListeners) { - for (Iterator cacheInvalidationListenerIter=cacheInvalidationListeners.iterator(); - cacheInvalidationListenerIter.hasNext(); ) { - CacheInvalidationListener cacheInvalidationListener = cacheInvalidationListenerIter.next(); - if (cacheInvalidationListener.getTrackedRace() == trackedRace) { - cacheInvalidationListener.removeFromTrackedRace(); - cacheInvalidationListenerIter.remove(); - } - } - } - } - } - - @Override - public void isMedalRaceChanged(RaceColumn raceColumn, boolean newIsMedalRace) { - getRaceColumnListeners().notifyListenersAboutIsMedalRaceChanged(raceColumn, newIsMedalRace); - } - - @Override - public void isStartsWithZeroScoreChanged(RaceColumn raceColumn, boolean newIsStartsWithZeroScore) { - getRaceColumnListeners().notifyListenersAboutIsStartsWithZeroScoreChanged(raceColumn, newIsStartsWithZeroScore); - } - - @Override - public void isFirstColumnIsNonDiscardableCarryForwardChanged(RaceColumn raceColumn, boolean firstColumnIsNonDiscardableCarryForward) { - getRaceColumnListeners().notifyListenersAboutIsFirstColumnIsNonDiscardableCarryForwardChanged(raceColumn, firstColumnIsNonDiscardableCarryForward); - } - - @Override - public void hasSplitFleetContiguousScoringChanged(RaceColumn raceColumn, boolean hasSplitFleetContiguousScoring) { - getRaceColumnListeners().notifyListenersAboutHasSplitFleetContiguousScoringChanged(raceColumn, hasSplitFleetContiguousScoring); - } - - @Override - public void raceColumnMoved(RaceColumn raceColumn, int newIndex) { - getRaceColumnListeners().notifyListenersAboutRaceColumnMoved(raceColumn, newIndex); - } - - @Override - public void factorChanged(RaceColumn raceColumn, Double oldFactor, Double newFactor) { - getRaceColumnListeners().notifyListenersAboutFactorChanged(raceColumn, oldFactor, newFactor); - } - - /** - * A leaderboard will only accept the addition of a race column if the column's name is unique across the leaderboard. - */ - @Override - public boolean canAddRaceColumnToContainer(RaceColumn newRaceColumn) { - boolean result = true; - for (RaceColumn raceColumn : getRaceColumns()) { - if (raceColumn.getName().equals(newRaceColumn.getName())) { - result = false; - break; - } - } - return result; - } - - @Override - public void raceColumnAddedToContainer(RaceColumn raceColumn) { - getRaceColumnListeners().notifyListenersAboutRaceColumnAddedToContainer(raceColumn); - } - - @Override - public void raceColumnRemovedFromContainer(RaceColumn raceColumn) { - getRaceColumnListeners().notifyListenersAboutRaceColumnRemovedFromContainer(raceColumn); - } - - @Override - public void competitorDisplayNameChanged(Competitor competitor, String oldDisplayName, String displayName) { - getRaceColumnListeners().notifyListenersAboutCompetitorDisplayNameChanged(competitor, oldDisplayName, displayName); - } - - @Override - public void resultDiscardingRuleChanged(ResultDiscardingRule oldDiscardingRule, ResultDiscardingRule newDiscardingRule) { - getRaceColumnListeners().notifyListenersAboutResultDiscardingRuleChanged(oldDiscardingRule, newDiscardingRule); - } - - @Override - public boolean isTransient() { - return false; - } - - /** - * Finds out the time point when any of the {@link Leaderboard#getTrackedRaces() tracked races currently attached to - * the leaderboard} and the {@link Leaderboard#getScoreCorrection() score corrections} have last been - * modified. If no tracked race is attached and no time-stamped score corrections have been applied to the leaderboard, - * null is returned. The time point computed this way is a good choice for normalizing queries for later time - * points in an attempt to achieve more cache hits.

- * - * Note, however, that the result does not tell about structural changes to the leaderboard and therefore cannot be used - * to determine the need for cache invalidation. For example, if a column is added to a leaderboard after the time point - * returned by this method but that column's attached tracked race has finished before the time point returned by this method, - * the result of this method won't change. Still, the contents of the leaderboard will change by a change in column structure. - * A different means to determine the possibility of changes that happened to this leaderboard must be used for cache - * management. Such a facility has to listen for score correction changes, tracked races being attached or detached and - * the column structure changing. - * - * @see TrackedRace#getTimePointOfNewestEvent() - * @see SettableScoreCorrection#getTimePointOfLastCorrectionsValidity() - */ - @Override - public TimePoint getTimePointOfLatestModification() { - TimePoint result = null; - for (TrackedRace trackedRace : getTrackedRaces()) { - if (result == null || (trackedRace.getTimePointOfNewestEvent() != null && trackedRace.getTimePointOfNewestEvent().after(result))) { - result = trackedRace.getTimePointOfNewestEvent(); - } - } - TimePoint timePointOfLastScoreCorrection = getScoreCorrection().getTimePointOfLastCorrectionsValidity(); - if (timePointOfLastScoreCorrection != null && (result == null || timePointOfLastScoreCorrection.after(result))) { - result = timePointOfLastScoreCorrection; - } - return result; - } - - @Override - public com.sap.sse.common.Util.Pair getMaximumSpeedOverGround(Competitor competitor, TimePoint timePoint) { - com.sap.sse.common.Util.Pair result = null; - // TODO should we ensure that competitor participated in all race columns? - for (TrackedRace trackedRace : getTrackedRaces()) { - if (Util.contains(trackedRace.getRace().getCompetitors(), competitor)) { - NavigableSet markPassings = trackedRace.getMarkPassings(competitor); - if (!markPassings.isEmpty()) { - TimePoint from = markPassings.first().getTimePoint(); - TimePoint to; - if (timePoint.after(markPassings.last().getTimePoint()) && - markPassings.last().getWaypoint() == trackedRace.getRace().getCourse().getLastWaypoint()) { - // stop counting when competitor finished the race - to = markPassings.last().getTimePoint(); - } else { - to = timePoint; - } - com.sap.sse.common.Util.Pair maxSpeed = trackedRace.getTrack(competitor).getMaximumSpeedOverGround(from, to); - if (result == null || result.getB() == null || - (maxSpeed != null && maxSpeed.getB() != null && maxSpeed.getB().compareTo(result.getB()) > 0)) { - result = maxSpeed; - } - } - } - } - return result; - } - - @Override - public Speed getAverageSpeedOverGround(Competitor competitor, TimePoint timePoint) { - Speed result = null; - for (TrackedRace trackedRace : getTrackedRaces()) { - if (Util.contains(trackedRace.getRace().getCompetitors(), competitor)) { - NavigableSet markPassings = trackedRace.getMarkPassings(competitor); - if (!markPassings.isEmpty()) { - TimePoint from = markPassings.first().getTimePoint(); - TimePoint to; - if (timePoint.after(markPassings.last().getTimePoint()) && - markPassings.last().getWaypoint() == trackedRace.getRace().getCourse().getLastWaypoint()) { - // stop counting when competitor finished the race - to = markPassings.last().getTimePoint(); - } else { - if (markPassings.last().getWaypoint() != trackedRace.getRace().getCourse().getLastWaypoint() && - timePoint.after(markPassings.last().getTimePoint())) { - result = null; - break; - } - to = timePoint; - } - Distance distanceTraveled = trackedRace.getDistanceTraveled(competitor, timePoint); - if (distanceTraveled != null) { - result = distanceTraveled.inTime(to.asMillis()-from.asMillis()); - } - } - } - } - return result; - } - - @Override - public Duration getTotalTimeSailedInLegType(Competitor competitor, LegType legType, TimePoint timePoint) throws NoWindException { - return getTotalTimeSailedInLegType(competitor, legType, timePoint, new HashMap()); - } - - private Duration getTotalTimeSailedInLegType(Competitor competitor, LegType legType, TimePoint timePoint, Map legTypeCache) throws NoWindException { - Duration result = null; - // TODO should we ensure that competitor participated in all race columns? - outerLoop: - for (TrackedRace trackedRace : getTrackedRaces()) { - if (Util.contains(trackedRace.getRace().getCompetitors(), competitor)) { - trackedRace.getRace().getCourse().lockForRead(); - try { - for (Leg leg : trackedRace.getRace().getCourse().getLegs()) { - TrackedLegOfCompetitor trackedLegOfCompetitor = trackedRace.getTrackedLeg(competitor, leg); - if (trackedLegOfCompetitor.hasStartedLeg(timePoint)) { - // find out leg type at the time the competitor started the leg - try { - final TrackedLeg trackedLeg = trackedRace.getTrackedLeg(leg); - LegType trackedLegType = legTypeCache.get(trackedLeg); - if (trackedLegType == null) { - final TimePoint startTime = trackedLegOfCompetitor.getStartTime(); - TimePoint finishTime = trackedLegOfCompetitor.getFinishTime(); - if (finishTime == null) { - finishTime = timePoint; - } - trackedLegType = trackedLeg.getLegType(startTime.plus(startTime.until(finishTime).divide(2))); // middle of the leg - legTypeCache.put(trackedLeg, trackedLegType); - } - if (legType == trackedLegType) { - Duration timeSpentOnDownwind = trackedLegOfCompetitor.getTime(timePoint); - if (timeSpentOnDownwind != null) { - if (result == null) { - result = timeSpentOnDownwind; - } else { - result = result.plus(timeSpentOnDownwind); - } - } else { - // Although the competitor has started the leg, no value was produced. This - // means that the competitor didn't finish the leg before tracking ended. No useful value - // can be obtained for this competitor anymore. - result = null; - break outerLoop; - } - } - } catch (NoWindException nwe) { - // without wind there is no leg type and hence there is no reasonable value for this: - result = null; - break outerLoop; - } - } - } - } finally { - trackedRace.getRace().getCourse().unlockAfterRead(); - } - } - } - return result; - } - - @Override - public Duration getTotalTimeSailed(Competitor competitor, TimePoint timePoint) { - Duration result = null; - for (TrackedRace trackedRace : getTrackedRaces()) { - if (Util.contains(trackedRace.getRace().getCompetitors(), competitor)) { - NavigableSet markPassings = trackedRace.getMarkPassings(competitor); - if (!markPassings.isEmpty()) { - TimePoint from = trackedRace.getStartOfRace(); // start counting at race start, not when the competitor passed the line - if (from != null && !timePoint.before(from)) { // but only if the race started after timePoint - TimePoint to; - if (timePoint.after(markPassings.last().getTimePoint()) - && markPassings.last().getWaypoint() == trackedRace.getRace().getCourse() - .getLastWaypoint()) { - // stop counting when competitor finished the race - to = markPassings.last().getTimePoint(); - } else { - if (trackedRace.getEndOfTracking() != null - && timePoint.after(trackedRace.getEndOfTracking())) { - result = null; // race not finished until end of tracking; no reasonable value can be - // computed for competitor - break; - } else { - to = timePoint; - } - } - Duration timeSpent = from.until(to); - if (result == null) { - result = timeSpent; - } else { - result=result.plus(timeSpent); - } - } - } - } - } - return result; - } - - @Override - public Distance getTotalDistanceTraveled(Competitor competitor, TimePoint timePoint) { - Distance result = null; - for (TrackedRace trackedRace : getTrackedRaces()) { - TimePoint startOfRace; - if (Util.contains(trackedRace.getRace().getCompetitors(), competitor) && - (startOfRace=trackedRace.getStartOfRace()) != null && - !startOfRace.after(timePoint)) { - Distance distanceSailedInRace = trackedRace.getDistanceTraveled(competitor, timePoint); - if (distanceSailedInRace != null) { - if (result == null) { - result = distanceSailedInRace; - } else { - result = result.add(distanceSailedInRace); - } - } else { - // if competitor has not finished one single race in the whole - // series then we can not return a meaningful value for all - // all races - return null; - } - } - } - return result; - } - - protected RaceColumnListeners getRaceColumnListeners() { - return raceColumnListeners; - } - - @Override - public Iterable getCompetitors() { - final Iterable result; - // mostly the set of suppressed competitors is empty; in this case, avoid having to loop over the - // potentially large set of competitors - if (Util.isEmpty(getSuppressedCompetitors())) { - result = getAllCompetitors(); - } else { - final Iterable allCompetitors = getAllCompetitors(); - final Set suppressed = new HashSet<>(); - Util.addAll(getSuppressedCompetitors(), suppressed); - result = getCompetitorIterableSkippingSuppressed(allCompetitors, suppressed); - } - return result; - } - - @Override - public Iterable getCompetitors(RaceColumn raceColumn, Fleet fleet) { - return getCompetitorIterableSkippingSuppressed(getAllCompetitors(raceColumn, fleet), getSuppressedCompetitors()); - } - - /** - * return an iterable with a smart iterator that filters out the suppressed elements on demand - */ - protected Iterable getCompetitorIterableSkippingSuppressed(final Iterable allCompetitors, - final Iterable suppressed) { - final Iterable result; - result = new Iterable() { - @Override - public Iterator iterator() { - return new Iterator() { - private final Iterator allIter = allCompetitors.iterator(); - private Competitor next = advance(); - - private Competitor advance() { - next = null; - while (allIter.hasNext() && next == null) { - next = allIter.next(); - if (Util.contains(suppressed, next)) { - next = null; - } - } - return next; - } - - @Override - public boolean hasNext() { - return next != null; - } - - @Override - public Competitor next() { - if (next == null) { - throw new NoSuchElementException(); - } - final Competitor result = next; - advance(); - return result; - } - }; - } - }; - return result; - } - - @Override - public Iterable getSuppressedCompetitors() { - LockUtil.lockForRead(suppressedCompetitorsLock); - try { - return new HashSet(suppressedCompetitors); - } finally { - LockUtil.unlockAfterRead(suppressedCompetitorsLock); - } - } - - @Override - public void setSuppressed(Competitor competitor, boolean suppressed) { - LockUtil.lockForWrite(suppressedCompetitorsLock); - try { - if (suppressed) { - suppressedCompetitors.add(competitor); - } else { - suppressedCompetitors.remove(competitor); - } - } finally { - LockUtil.unlockAfterWrite(suppressedCompetitorsLock); - } - getScoreCorrection().notifyListenersAboutIsSuppressedChange(competitor, suppressed); - } - - @Override - public TimePoint getNowMinusDelay() { - final TimePoint now = MillisecondsTimePoint.now(); - final Long delayToLiveInMillis = getDelayToLiveInMillis(); - TimePoint timePoint = delayToLiveInMillis == null ? now : now.minus(delayToLiveInMillis); - return timePoint; - } - - @Override - public void raceLogEventAdded(RaceColumn raceColumn, RaceLogIdentifier raceLogIdentifier, RaceLogEvent event) { - getRaceColumnListeners().notifyListenersAboutRaceLogEventAdded(raceColumn, raceLogIdentifier, event); - if (event instanceof InvalidatesLeaderboardCache) { - // make sure to invalidate the cache as this event indicates that - // it changes values the cache could still hold - if (leaderboardDTOCache != null) { - leaderboardDTOCache.invalidate(this); - } - } - } - - @Override - public void regattaLogEventAdded(RegattaLogEvent event) { - getRaceColumnListeners().notifyListenersAboutRegattaLogEventAdded(event); - } - - @Override - public LeaderboardDTO computeDTO(final TimePoint timePoint, - final Collection namesOfRaceColumnsForWhichToLoadLegDetails, boolean addOverallDetails, - final boolean waitForLatestAnalyses, TrackedRegattaRegistry trackedRegattaRegistry, final DomainFactory baseDomainFactory, - final boolean fillNetPointsUncorrected) - throws NoWindException { - long startOfRequestHandling = System.currentTimeMillis(); - final LeaderboardDTOCalculationReuseCache cache = new LeaderboardDTOCalculationReuseCache(timePoint); - final LeaderboardDTO result = new LeaderboardDTO(this.getScoreCorrection().getTimePointOfLastCorrectionsValidity() == null ? null - : this.getScoreCorrection().getTimePointOfLastCorrectionsValidity().asDate(), - this.getScoreCorrection() == null ? null : this.getScoreCorrection().getComment(), - this.getScoringScheme() == null ? null : this.getScoringScheme().getType(), this - .getScoringScheme().isHigherBetter(), new UUIDGenerator(), addOverallDetails); - result.type = getLeaderboardType(); - result.competitors = new ArrayList(); - result.name = this.getName(); - result.competitorDisplayNames = new HashMap(); - for (Competitor suppressedCompetitor : this.getSuppressedCompetitors()) { - result.setSuppressed(baseDomainFactory.convertToCompetitorDTO(suppressedCompetitor), true); - } - // Now create the race columns and, as a future task, set their competitorsFromBestToWorst, then wait for all these - // futures to finish: - Map>> competitorsFromBestToWorstTasks = new HashMap<>(); - for (final RaceColumn raceColumn : this.getRaceColumns()) { - boolean isMetaLeaderboardColumn = raceColumn instanceof MetaLeaderboardColumn; - RaceColumnDTO raceColumnDTO = result.createEmptyRaceColumn(raceColumn.getName(), raceColumn.isMedalRace(), - raceColumn instanceof RaceColumnInSeries ? ((RaceColumnInSeries) raceColumn).getRegatta().getName() : null, - raceColumn instanceof RaceColumnInSeries ? ((RaceColumnInSeries) raceColumn).getSeries().getName() : null, - isMetaLeaderboardColumn); - if (isMetaLeaderboardColumn && raceColumnDTO instanceof MetaLeaderboardRaceColumnDTO) { - calculateRacesMetadata((MetaLeaderboardColumn) raceColumn, (MetaLeaderboardRaceColumnDTO) raceColumnDTO, - trackedRegattaRegistry, baseDomainFactory); - } - for (Fleet fleet : raceColumn.getFleets()) { - RegattaAndRaceIdentifier raceIdentifier = null; - RaceDTO race = null; - TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); - final FleetDTO fleetDTO = baseDomainFactory.convertToFleetDTO(fleet); - if (trackedRace != null) { - raceIdentifier = new RegattaNameAndRaceName(trackedRace.getTrackedRegatta().getRegatta().getName(), - trackedRace.getRace().getName()); - race = baseDomainFactory.createRaceDTO(trackedRegattaRegistry, /* withGeoLocationData */ false, raceIdentifier, trackedRace); - } - // Note: the RaceColumnDTO won't be created by the following addRace call because it has been created - // above by the result.createEmptyRaceColumn call - result.addRace(raceColumn.getName(), raceColumn.getExplicitFactor(), raceColumn.getFactor(), - raceColumn instanceof RaceColumnInSeries ? ((RaceColumnInSeries) raceColumn).getRegatta().getName() : null, - raceColumn instanceof RaceColumnInSeries ? ((RaceColumnInSeries) raceColumn).getSeries().getName() : null, - fleetDTO, raceColumn.isMedalRace(), raceIdentifier, race, isMetaLeaderboardColumn); - } - FutureTask> task = new FutureTask>( - () -> baseDomainFactory.getCompetitorDTOList(AbstractSimpleLeaderboardImpl.this.getCompetitorsFromBestToWorst(raceColumn, timePoint))); - executor.execute(task); - competitorsFromBestToWorstTasks.put(raceColumn, task); - } - // wait for the competitor orderings to have been computed for all race columns before continuing; subsequent tasks may depend on these data - for (Map.Entry>> raceColumnAndTaskToJoin : competitorsFromBestToWorstTasks.entrySet()) { - try { - result.setCompetitorsFromBestToWorst(raceColumnAndTaskToJoin.getKey().getName(), raceColumnAndTaskToJoin.getValue().get()); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } catch (ExecutionException e) { - // See also bug 1371: for stability reasons, don't let the exception percolate but rather accept - // null values. - // If new evidence is provided, a re-calculation of the leaderboard will be triggered anyway. So - // this helps robustness from a user's perspective. - logger.log( - Level.SEVERE, - AbstractSimpleLeaderboardImpl.class.getName() + ".computeDTO(" + this.getName() + ", " - + timePoint + ", " + namesOfRaceColumnsForWhichToLoadLegDetails+", addOverallDetails="+addOverallDetails - + "): exception during computing competitor ordering for race column "+raceColumnAndTaskToJoin.getKey().getName(), e); - } - } - result.setDelayToLiveInMillisForLatestRace(this.getDelayToLiveInMillis()); - result.rows = new HashMap(); - result.hasCarriedPoints = this.hasCarriedPoints(); - if (this.getResultDiscardingRule() instanceof ThresholdBasedResultDiscardingRule) { - result.discardThresholds = ((ThresholdBasedResultDiscardingRule) this.getResultDiscardingRule()) - .getDiscardIndexResultsStartingWithHowManyRaces(); - } else { - result.discardThresholds = null; - } - // Computing the competitor leg ranks is expensive, especially in live mode, in case new events keep - // invalidating the ranks cache in TrackedLegImpl. The problem then is that the sorting based on wind data is repeated for - // each competitor, leading to square effort. We therefore need to compute the leg ranks for those races where leg - // details are requested only once and pass them into getLeaderboardEntryDTO - final Map> legRanksCache = new HashMap>(); - for (final RaceColumn raceColumn : this.getRaceColumns()) { - // if details for the column are requested, cache the leg's ranks - if (namesOfRaceColumnsForWhichToLoadLegDetails != null - && namesOfRaceColumnsForWhichToLoadLegDetails.contains(raceColumn.getName())) { - for (Fleet fleet : raceColumn.getFleets()) { - TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); - if (trackedRace != null) { - trackedRace.getRace().getCourse().lockForRead(); - try { - for (TrackedLeg trackedLeg : trackedRace.getTrackedLegs()) { - legRanksCache.put(trackedLeg.getLeg(), trackedLeg.getRanks(timePoint, cache)); - } - } finally { - trackedRace.getRace().getCourse().unlockAfterRead(); - } - } - } - } - } - for (final Competitor competitor : this.getCompetitorsFromBestToWorst(timePoint)) { - CompetitorDTO competitorDTO = baseDomainFactory.convertToCompetitorDTO(competitor); - LeaderboardRowDTO row = new LeaderboardRowDTO(); - row.competitor = competitorDTO; - row.fieldsByRaceColumnName = new HashMap(); - row.carriedPoints = this.hasCarriedPoints(competitor) ? this.getCarriedPoints(competitor) : null; - row.totalPoints = this.getTotalPoints(competitor, timePoint); - if (addOverallDetails) { - addOverallDetailsToRow(timePoint, competitor, row); - } - result.competitors.add(competitorDTO); - Map> futuresForColumnName = new HashMap>(); - final Set discardedRaceColumns = getResultDiscardingRule().getDiscardedRaceColumns(competitor, this, getRaceColumns(), timePoint); - for (final RaceColumn raceColumn : this.getRaceColumns()) { - RunnableFuture future = new FutureTask(() -> { - Entry entry = AbstractSimpleLeaderboardImpl.this.getEntry(competitor, raceColumn, timePoint, discardedRaceColumns); - return getLeaderboardEntryDTO(entry, raceColumn, competitor, timePoint, - namesOfRaceColumnsForWhichToLoadLegDetails != null - && namesOfRaceColumnsForWhichToLoadLegDetails.contains(raceColumn - .getName()), waitForLatestAnalyses, legRanksCache, baseDomainFactory, - fillNetPointsUncorrected, cache); - }); - executor.execute(future); - futuresForColumnName.put(raceColumn.getName(), future); - } - for (Map.Entry> raceColumnNameAndFuture : futuresForColumnName.entrySet()) { - try { - row.fieldsByRaceColumnName.put(raceColumnNameAndFuture.getKey(), raceColumnNameAndFuture.getValue().get()); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } catch (ExecutionException e) { - // See also bug 1371: for stability reasons, don't let the exception percolate but rather accept - // null values. - // If new evidence is provided, a re-calculation of the leaderboard will be triggered anyway. So - // this helps robustness from a user's perspective. - logger.log( - Level.SEVERE, - AbstractSimpleLeaderboardImpl.class.getName() + ".computeDTO(" + this.getName() + ", " - + timePoint + ", " + namesOfRaceColumnsForWhichToLoadLegDetails+", addOverallDetails="+addOverallDetails - + "): exception during computing leaderboard entry for competitor " - + competitor.getName() + " in race column " + raceColumnNameAndFuture.getKey() - + ". Leaving empty.", e); - } - } - result.rows.put(competitorDTO, row); - String displayName = this.getDisplayName(competitor); - if (displayName != null) { - result.competitorDisplayNames.put(competitorDTO, displayName); - } - } - logger.info("computeLeaderboardByName(" + this.getName() + ", " + timePoint + ", " - + namesOfRaceColumnsForWhichToLoadLegDetails + ", addOverallDetails=" + addOverallDetails + ") took " - + (System.currentTimeMillis() - startOfRequestHandling) + "ms"); - return result; - } - - private void calculateRacesMetadata(MetaLeaderboardColumn metaLeaderboardColumn, MetaLeaderboardRaceColumnDTO columnDTO, - TrackedRegattaRegistry trackedRegattaRegistry, final DomainFactory baseDomainFactory) { - for (final RaceColumn raceColumn : metaLeaderboardColumn.getLeaderboard().getRaceColumns()) { - for (Fleet fleet : raceColumn.getFleets()) { - TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); - if (trackedRace != null) { - String regattaName = trackedRace.getTrackedRegatta().getRegatta().getName(); - String raceName = trackedRace.getRace().getName(); - RegattaAndRaceIdentifier raceIdentifier = new RegattaNameAndRaceName(regattaName, raceName); - columnDTO.addRace(new BasicRaceDTO(raceIdentifier, baseDomainFactory.createTrackedRaceDTO(trackedRace))); - } - } - } - } - - private void addOverallDetailsToRow(final TimePoint timePoint, - final Competitor competitor, LeaderboardRowDTO row) throws NoWindException { - final com.sap.sse.common.Util.Pair maximumSpeedOverGround = this.getMaximumSpeedOverGround(competitor, timePoint); - if (maximumSpeedOverGround != null && maximumSpeedOverGround.getB() != null) { - row.maximumSpeedOverGroundInKnots = maximumSpeedOverGround.getB().getKnots(); - row.whenMaximumSpeedOverGroundWasAchieved = maximumSpeedOverGround.getA().getTimePoint().asDate(); - } - Map legTypeCache = new HashMap<>(); - final Duration totalTimeSailedDownwind = this.getTotalTimeSailedInLegType(competitor, LegType.DOWNWIND, timePoint, legTypeCache); - row.totalTimeSailedDownwindInSeconds = totalTimeSailedDownwind==null?null:totalTimeSailedDownwind.asSeconds(); - final Duration totalTimeSailedUpwind = this.getTotalTimeSailedInLegType(competitor, LegType.UPWIND, timePoint, legTypeCache); - row.totalTimeSailedUpwindInSeconds = totalTimeSailedUpwind==null?null:totalTimeSailedUpwind.asSeconds(); - final Duration totalTimeSailedReaching = this.getTotalTimeSailedInLegType(competitor, LegType.REACHING, timePoint, legTypeCache); - row.totalTimeSailedReachingInSeconds = totalTimeSailedReaching==null?null:totalTimeSailedReaching.asSeconds(); - final Duration totalTimeSailed = this.getTotalTimeSailed(competitor, timePoint); - row.totalTimeSailedInSeconds = totalTimeSailed==null?null:totalTimeSailed.asSeconds(); - final Distance totalDistanceTraveledInMeters = this.getTotalDistanceTraveled(competitor, timePoint); - row.totalDistanceTraveledInMeters = totalDistanceTraveledInMeters==null?null:totalDistanceTraveledInMeters.getMeters(); - } - - /** - * @param waitForLatestAnalyses - * if false, this method is allowed to read the maneuver analysis results from a cache that - * may not reflect all data already received; otherwise, the method will always block for the latest - * cache updates to have happened before returning. - * @param fillNetPointsUncorrected - * tells if {@link LeaderboardEntryDTO#netPointsUncorrected} shall be filled; filling it is rather - * expensive, especially when compared to simply retrieving a score correction, and particularly if in a - * larger fleet a number of competitors haven't properly finished the race. This should only be used for - * leaderboard editing where a user needs to see what the uncorrected score was that would be used when - * the correction was removed. - */ - private LeaderboardEntryDTO getLeaderboardEntryDTO(Entry entry, RaceColumn raceColumn, Competitor competitor, - TimePoint timePoint, boolean addLegDetails, boolean waitForLatestAnalyses, - Map> legRanksCache, DomainFactory baseDomainFactory, - boolean fillNetPointsUncorrected, WindLegTypeAndLegBearingCache cache) { - LeaderboardEntryDTO entryDTO = new LeaderboardEntryDTO(); - TrackedRace trackedRace = raceColumn.getTrackedRace(competitor); - entryDTO.race = trackedRace == null ? null : trackedRace.getRaceIdentifier(); - entryDTO.netPoints = entry.getNetPoints(); - if (fillNetPointsUncorrected) { - entryDTO.netPointsUncorrected = entry.getNetPointsUncorrected(); - } - entryDTO.netPointsCorrected = entry.isNetPointsCorrected(); - entryDTO.totalPoints = entry.getTotalPoints(); - entryDTO.reasonForMaxPoints = entry.getMaxPointsReason(); - entryDTO.discarded = entry.isDiscarded(); - final GPSFixTrack track = trackedRace == null ? null : trackedRace.getTrack(competitor); - if (trackedRace != null) { - Date timePointOfLastPositionFixAtOrBeforeQueryTimePoint = getTimePointOfLastFixAtOrBefore(competitor, trackedRace, timePoint); - if (track != null) { - entryDTO.averageSamplingInterval = track.getAverageIntervalBetweenRawFixes(); - } - if (timePointOfLastPositionFixAtOrBeforeQueryTimePoint != null) { - long timeDifferenceInMs = timePoint.asMillis() - timePointOfLastPositionFixAtOrBeforeQueryTimePoint.getTime(); - entryDTO.timeSinceLastPositionFixInSeconds = timeDifferenceInMs == 0 ? 0.0 : timeDifferenceInMs / 1000.0; - } else { - entryDTO.timeSinceLastPositionFixInSeconds = null; - } - } - if (addLegDetails && trackedRace != null) { - try { - final RankingInfo rankingInfo = trackedRace.getRankingMetric().getRankingInfo(timePoint, cache); - RaceDetails raceDetails = getRaceDetails(trackedRace, competitor, timePoint, waitForLatestAnalyses, - legRanksCache, rankingInfo, cache); - entryDTO.legDetails = raceDetails.getLegDetails(); - entryDTO.windwardDistanceToCompetitorFarthestAheadInMeters = raceDetails.getWindwardDistanceToCompetitorFarthestAhead() == null ? null - : raceDetails.getWindwardDistanceToCompetitorFarthestAhead().getMeters(); - entryDTO.gapToLeaderInOwnTime = trackedRace.getRankingMetric().getGapToLeaderInOwnTime(rankingInfo, competitor, cache); - entryDTO.averageAbsoluteCrossTrackErrorInMeters = raceDetails.getAverageAbsoluteCrossTrackError() == null ? null - : raceDetails.getAverageAbsoluteCrossTrackError().getMeters(); - entryDTO.averageSignedCrossTrackErrorInMeters = raceDetails.getAverageSignedCrossTrackError() == null ? null - : raceDetails.getAverageSignedCrossTrackError().getMeters(); - entryDTO.calculatedTime = raceDetails.getCorrectedTime(); - entryDTO.calculatedTimeAtEstimatedArrivalAtCompetitorFarthestAhead = raceDetails.getCorrectedTimeAtEstimatedArrivalAtCompetitorFarthestAhead(); - entryDTO.gapToLeaderInOwnTime = raceDetails.getGapToLeaderInOwnTime(); - final TimePoint startOfRace = trackedRace.getStartOfRace(); - if (startOfRace != null) { - Waypoint startWaypoint = trackedRace.getRace().getCourse().getFirstWaypoint(); - NavigableSet competitorMarkPassings = trackedRace.getMarkPassings(competitor); - trackedRace.lockForRead(competitorMarkPassings); - try { - if (!competitorMarkPassings.isEmpty()) { - final MarkPassing firstMarkPassing = competitorMarkPassings.iterator().next(); - if (firstMarkPassing.getWaypoint() == startWaypoint) { - Distance distanceToStartLineFiveSecondsBeforeStartOfRace = trackedRace.getDistanceToStartLine(competitor, /*milliseconds before start*/ 5000); - entryDTO.distanceToStartLineFiveSecondsBeforeStartInMeters = distanceToStartLineFiveSecondsBeforeStartOfRace == null ? null - : distanceToStartLineFiveSecondsBeforeStartOfRace.getMeters(); - Speed speedFiveSecondsBeforeStartOfRace = trackedRace.getSpeed(competitor, /*milliseconds before start*/ 5000); - entryDTO.speedOverGroundFiveSecondsBeforeStartInKnots = speedFiveSecondsBeforeStartOfRace == null ? null - : speedFiveSecondsBeforeStartOfRace.getKnots(); - Distance distanceToStartLineAtStartOfRace = trackedRace.getDistanceToStartLine( - competitor, startOfRace); - entryDTO.distanceToStartLineAtStartOfRaceInMeters = distanceToStartLineAtStartOfRace == null ? null - : distanceToStartLineAtStartOfRace.getMeters(); - Speed speedAtStartTime = track == null ? null : track.getEstimatedSpeed(startOfRace); - entryDTO.speedOverGroundAtStartOfRaceInKnots = speedAtStartTime == null ? null - : speedAtStartTime.getKnots(); - TimePoint competitorStartTime = firstMarkPassing.getTimePoint(); - entryDTO.timeBetweenRaceStartAndCompetitorStartInSeconds = startOfRace.until(competitorStartTime).asSeconds(); - Speed competitorSpeedWhenPassingStart = track == null ? null : track - .getEstimatedSpeed(competitorStartTime); - entryDTO.speedOverGroundAtPassingStartWaypointInKnots = competitorSpeedWhenPassingStart == null ? null - : competitorSpeedWhenPassingStart.getKnots(); - try { - entryDTO.startTack = trackedRace.getTack(competitor, competitorStartTime); - } catch (NoWindException nwe) { - entryDTO.startTack = null; // leave empty in case no wind information is available - } - Distance distanceFromStarboardSideOfStartLineWhenPassingStart = trackedRace - .getDistanceFromStarboardSideOfStartLineWhenPassingStart(competitor); - entryDTO.distanceToStarboardSideOfStartLineInMeters = distanceFromStarboardSideOfStartLineWhenPassingStart == null ? null - : distanceFromStarboardSideOfStartLineWhenPassingStart.getMeters(); - } - } - } finally { - trackedRace.unlockAfterRead(competitorMarkPassings); - } - } - } catch (InterruptedException e) { - throw new RuntimeException(e); - } catch (ExecutionException e) { - throw new RuntimeException(e); // the future used to calculate the leg details was interrupted; escalate as runtime exception - } - } - final Fleet fleet = entry.getFleet(); - entryDTO.fleet = fleet == null ? null : baseDomainFactory.convertToFleetDTO(fleet); - return entryDTO; - } - - /** - * Determines the time point of the last raw fix (with outliers not removed) for competitor in - * trackedRace. If the competitor's track is null or empty, null is returned. - * @param trackedRace must not be null - * @param atOrBefore find the last fix at or before the time point specified - */ - private Date getTimePointOfLastFixAtOrBefore(Competitor competitor, TrackedRace trackedRace, TimePoint atOrBefore) { - assert trackedRace != null; - final Date timePointOfLastPositionFix; - GPSFixTrack track = trackedRace.getTrack(competitor); - if (track == null) { - timePointOfLastPositionFix = null; - } else { - GPSFixMoving lastFix = track.getLastFixAtOrBefore(atOrBefore); - if (lastFix == null) { - timePointOfLastPositionFix = null; - } else { - timePointOfLastPositionFix = lastFix.getTimePoint().asDate(); - } - } - return timePointOfLastPositionFix; - } - - private static class RaceDetails { - private final List legDetails; - private final Distance windwardDistanceToCompetitorFarthestAhead; - private final Distance averageAbsoluteCrossTrackError; - private final Distance averageSignedCrossTrackError; - private final Duration gapToLeaderInOwnTime; - private final Duration correctedTime; - private final Duration correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead; - - public RaceDetails(List legDetails, Distance windwardDistanceToCompetitorFarthestAhead, - Distance averageAbsoluteCrossTrackError, Distance averageSignedCrossTrackError, - Duration gapToLeaderInOwnTime, Duration correctedTime, - Duration correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead) { - super(); - this.legDetails = legDetails; - this.windwardDistanceToCompetitorFarthestAhead = windwardDistanceToCompetitorFarthestAhead; - this.averageAbsoluteCrossTrackError = averageAbsoluteCrossTrackError; - this.averageSignedCrossTrackError = averageSignedCrossTrackError; - this.gapToLeaderInOwnTime = gapToLeaderInOwnTime; - this.correctedTime = correctedTime; - this.correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead = correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead; - } - public List getLegDetails() { - return legDetails; - } - public Distance getWindwardDistanceToCompetitorFarthestAhead() { - return windwardDistanceToCompetitorFarthestAhead; - } - public Distance getAverageAbsoluteCrossTrackError() { - return averageAbsoluteCrossTrackError; - } - public Distance getAverageSignedCrossTrackError() { - return averageSignedCrossTrackError; - } - public Duration getGapToLeaderInOwnTime() { - return gapToLeaderInOwnTime; - } - public Duration getCorrectedTime() { - return correctedTime; - } - public Duration getCorrectedTimeAtEstimatedArrivalAtCompetitorFarthestAhead() { - return correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead; - } - } - - /** - * If timePoint is after the end of the race's tracking the query will be adjusted to obtain the values - * at the end of the {@link TrackedRace#getEndOfTracking() race's tracking time}. If the time point adjusted this - * way equals the end of the tracking time, the query results will be looked up in a cache first and if not found, - * they will be stored to the cache after calculating them. A cache invalidation {@link RaceChangeListener listener} - * will be registered with the race which will be triggered for any event received by the race. - * @param waitForLatestAnalyses - * if false, this method is allowed to read the maneuver analysis results from a cache that - * may not reflect all data already received; otherwise, the method will always block for the latest - * cache updates to have happened before returning. - */ - private RaceDetails getRaceDetails(TrackedRace trackedRace, Competitor competitor, TimePoint timePoint, - boolean waitForLatestAnalyses, Map> legRanksCache, - RankingInfo rankingInfo, WindLegTypeAndLegBearingCache cache) throws InterruptedException, ExecutionException { - final RaceDetails raceDetails; - if (trackedRace.getEndOfTracking() != null && trackedRace.getEndOfTracking().compareTo(timePoint) < 0) { - raceDetails = getRaceDetailsForEndOfTrackingFromCacheOrCalculateAndCache(trackedRace, competitor, legRanksCache, rankingInfo, cache); - } else { - raceDetails = calculateRaceDetails(trackedRace, competitor, timePoint, waitForLatestAnalyses, legRanksCache, cache, rankingInfo); - } - return raceDetails; - } - - private RaceDetails getRaceDetailsForEndOfTrackingFromCacheOrCalculateAndCache(final TrackedRace trackedRace, - final Competitor competitor, final Map> legRanksCache, - RankingInfo rankingInfo, final WindLegTypeAndLegBearingCache cache) throws InterruptedException, ExecutionException { - final com.sap.sse.common.Util.Pair key = new com.sap.sse.common.Util.Pair(trackedRace, competitor); - RunnableFuture raceDetails; - synchronized (raceDetailsAtEndOfTrackingCache) { - raceDetails = raceDetailsAtEndOfTrackingCache.get(key); - if (raceDetails == null) { - raceDetails = new FutureTask(new Callable() { - @Override - public RaceDetails call() throws Exception { - TimePoint end = trackedRace.getEndOfRace(); - if (end == null) { - end = trackedRace.getEndOfTracking(); - } - return calculateRaceDetails(trackedRace, competitor, end, - // TODO see bug 1358: for now, use waitForLatest==false until we've switched to optimistic locking for the course read lock - /* TODO old comment when it was still true: "because this is done only once after end of tracking" */ - /* waitForLatestAnalyses (maneuver and cross track error) */ false, - legRanksCache, cache, rankingInfo); - } - }); - raceDetailsExecutor.execute(raceDetails); - raceDetailsAtEndOfTrackingCache.put(key, raceDetails); - final CacheInvalidationListener cacheInvalidationListener = new CacheInvalidationListener(trackedRace, competitor); - trackedRace.addListener(cacheInvalidationListener); - cacheInvalidationListeners.add(cacheInvalidationListener); - } - } - return raceDetails.get(); - } - - private RaceDetails calculateRaceDetails(TrackedRace trackedRace, Competitor competitor, TimePoint timePoint, - boolean waitForLatestAnalyses, Map> legRanksCache, - WindLegTypeAndLegBearingCache cache, final RankingInfo rankingInfo) { - final List legDetails = new ArrayList(); - final Course course = trackedRace.getRace().getCourse(); - course.lockForRead(); // hold back any course re-configurations while looping over the legs - try { - for (Leg leg : course.getLegs()) { - LegEntryDTO legEntry; - // We loop over a copy of the course's legs; during a course change, legs may become "stale," even with - // regard to the leg/trackedLeg structures inside the tracked race which is updated by the course change - // immediately. That's why we've acquired a read lock for the course above. - TrackedLegOfCompetitor trackedLeg = trackedRace.getTrackedLeg(competitor, leg); - if (trackedLeg != null && trackedLeg.hasStartedLeg(timePoint)) { - legEntry = createLegEntry(trackedLeg, timePoint, waitForLatestAnalyses, legRanksCache, rankingInfo, cache); - } else { - legEntry = null; - } - legDetails.add(legEntry); - } - final Distance windwardDistanceToCompetitorFarthestAhead = trackedRace == null ? null : trackedRace - .getWindwardDistanceToCompetitorFarthestAhead(competitor, timePoint, WindPositionMode.LEG_MIDDLE, rankingInfo, cache); - Distance averageAbsoluteCrossTrackError; - try { - averageAbsoluteCrossTrackError = trackedRace == null ? null : trackedRace.getAverageAbsoluteCrossTrackError( - competitor, timePoint, waitForLatestAnalyses, cache); - } catch (NoWindException nwe) { - // without wind information, use null meaning "unknown" - averageAbsoluteCrossTrackError = null; - } - Distance averageSignedCrossTrackError; - try { - averageSignedCrossTrackError = trackedRace == null ? null : trackedRace.getAverageSignedCrossTrackError( - competitor, timePoint, waitForLatestAnalyses, cache); - } catch (NoWindException nwe) { - // without wind information, use null meaning "unknown" - averageSignedCrossTrackError = null; - } - final CompetitorRankingInfo competitorRankingInfo = rankingInfo.getCompetitorRankingInfo().apply(competitor); - return new RaceDetails(legDetails, windwardDistanceToCompetitorFarthestAhead, averageAbsoluteCrossTrackError, averageSignedCrossTrackError, - trackedRace.getRankingMetric().getGapToLeaderInOwnTime(rankingInfo, competitor, cache), - trackedRace.getRankingMetric().getCorrectedTime(competitor, timePoint), - competitorRankingInfo == null ? null : competitorRankingInfo.getCorrectedTimeAtEstimatedArrivalAtCompetitorFarthestAhead()); - } finally { - course.unlockAfterRead(); - } - } - - private LegEntryDTO createLegEntry(TrackedLegOfCompetitor trackedLeg, TimePoint timePoint, - boolean waitForLatestAnalyses, Map> legRanksCache, - RankingInfo rankingInfo, WindLegTypeAndLegBearingCache cache) { - LegEntryDTO result; - final Duration time = trackedLeg.getTime(timePoint); - if (trackedLeg == null || time == null) { - result = null; - } else { - result = new LegEntryDTO(); - try { - result.legType = trackedLeg.getTrackedLeg().getLegType(timePoint); - } catch (NoWindException nwe) { - result.legType = null; // can't determine leg type without wind data - } - final Speed averageSpeedOverGround = trackedLeg.getAverageSpeedOverGround(timePoint); - result.averageSpeedOverGroundInKnots = averageSpeedOverGround == null ? null : averageSpeedOverGround.getKnots(); - Distance averageAbsoluteCrossTrackError; - try { - averageAbsoluteCrossTrackError = trackedLeg.getAverageAbsoluteCrossTrackError(timePoint, waitForLatestAnalyses); - } catch (NoWindException nwe) { - // leave averageAbsoluteCrossTrackError as null, meaning "unknown" - averageAbsoluteCrossTrackError = null; - } - result.averageAbsoluteCrossTrackErrorInMeters = averageAbsoluteCrossTrackError == null ? null : averageAbsoluteCrossTrackError.getMeters(); - Distance averageSignedCrossTrackError; - try { - averageSignedCrossTrackError = trackedLeg.getAverageSignedCrossTrackError(timePoint, waitForLatestAnalyses); - } catch (NoWindException nwe) { - // leave averageSignedCrossTrackError as null, meaning "unknown" - averageSignedCrossTrackError = null; - } - result.averageSignedCrossTrackErrorInMeters = averageSignedCrossTrackError == null ? null : averageSignedCrossTrackError.getMeters(); - Double speedOverGroundInKnots; - if (trackedLeg.hasFinishedLeg(timePoint)) { - speedOverGroundInKnots = averageSpeedOverGround == null ? null : averageSpeedOverGround.getKnots(); - } else { - final SpeedWithBearing speedOverGround = trackedLeg.getSpeedOverGround(timePoint); - speedOverGroundInKnots = speedOverGround == null ? null : speedOverGround.getKnots(); - } - result.currentSpeedOverGroundInKnots = speedOverGroundInKnots == null ? null : speedOverGroundInKnots; - Distance distanceTraveled = trackedLeg.getDistanceTraveled(timePoint); - result.distanceTraveledInMeters = distanceTraveled == null ? null : distanceTraveled.getMeters(); - Distance distanceTraveledConsideringGateStart = trackedLeg.getDistanceTraveledConsideringGateStart(timePoint); - result.distanceTraveledIncludingGateStartInMeters = distanceTraveledConsideringGateStart == null ? null : distanceTraveledConsideringGateStart.getMeters(); - final Duration estimatedTimeToNextMarkInSeconds = trackedLeg.getEstimatedTimeToNextMark(timePoint, WindPositionMode.EXACT, cache); - result.estimatedTimeToNextWaypointInSeconds = estimatedTimeToNextMarkInSeconds==null?null:estimatedTimeToNextMarkInSeconds.asSeconds(); - result.timeInMilliseconds = time.asMillis(); - result.finished = trackedLeg.hasFinishedLeg(timePoint); - final TimePoint legFinishTime = trackedLeg.getFinishTime(); - result.correctedTotalTime = trackedLeg.hasStartedLeg(timePoint) ? trackedLeg.getTrackedLeg().getTrackedRace().getRankingMetric().getCorrectedTime(trackedLeg.getCompetitor(), - trackedLeg.hasFinishedLeg(timePoint) ? legFinishTime : timePoint, cache) : null; - // fetch the leg gap in own corrected time from the ranking metric - final Duration gapToLeaderInOwnTime = trackedLeg.getTrackedLeg().getTrackedRace().getRankingMetric(). - getLegGapToLegLeaderInOwnTime(trackedLeg, timePoint, rankingInfo, cache); - result.gapToLeaderInSeconds = gapToLeaderInOwnTime == null ? null : gapToLeaderInOwnTime.asSeconds(); - if (result.gapToLeaderInSeconds != null) { - final Duration gapAtEndOfPreviousLeg = getGapAtEndOfPreviousLeg(trackedLeg, rankingInfo, cache); - if (gapAtEndOfPreviousLeg != null) { - result.gapChangeSinceLegStartInSeconds = result.gapToLeaderInSeconds - gapAtEndOfPreviousLeg.asSeconds(); - } - } - LinkedHashMap legRanks = legRanksCache.get(trackedLeg.getLeg()); - if (legRanks != null) { - result.rank = legRanks.get(trackedLeg.getCompetitor()); - } else { - result.rank = trackedLeg.getRank(timePoint, cache); - } - result.started = trackedLeg.hasStartedLeg(timePoint); - Speed velocityMadeGood; - if (trackedLeg.hasFinishedLeg(timePoint)) { - velocityMadeGood = trackedLeg.getAverageVelocityMadeGood(timePoint); - } else { - velocityMadeGood = trackedLeg.getVelocityMadeGood(timePoint, WindPositionMode.EXACT); - } - result.velocityMadeGoodInKnots = velocityMadeGood == null ? null : velocityMadeGood.getKnots(); - Distance windwardDistanceToGo = trackedLeg.getWindwardDistanceToGo(timePoint, WindPositionMode.LEG_MIDDLE); - result.windwardDistanceToGoInMeters = windwardDistanceToGo == null ? null : windwardDistanceToGo - .getMeters(); - final TimePoint startOfRace = trackedLeg.getTrackedLeg().getTrackedRace().getStartOfRace(); - if (startOfRace != null && trackedLeg.hasStartedLeg(timePoint)) { - // not using trackedLeg.getManeuvers(...) because it may not catch the mark passing maneuver starting this leg - // because that may have been detected as slightly before the mark passing time, hence associated with the previous leg - List maneuvers = trackedLeg.getTrackedLeg().getTrackedRace() - .getManeuvers(trackedLeg.getCompetitor(), startOfRace, timePoint, waitForLatestAnalyses); - if (maneuvers != null) { - result.numberOfManeuvers = new HashMap(); - result.numberOfManeuvers.put(ManeuverType.TACK, 0); - result.numberOfManeuvers.put(ManeuverType.JIBE, 0); - result.numberOfManeuvers.put(ManeuverType.PENALTY_CIRCLE, 0); - Map totalManeuverLossInMeters = new HashMap(); - totalManeuverLossInMeters.put(ManeuverType.TACK, 0.0); - totalManeuverLossInMeters.put(ManeuverType.JIBE, 0.0); - totalManeuverLossInMeters.put(ManeuverType.PENALTY_CIRCLE, 0.0); - TimePoint startOfLeg = trackedLeg.getStartTime(); - for (Maneuver maneuver : maneuvers) { - // don't count maneuvers that were in previous legs - switch (maneuver.getType()) { - case TACK: - case JIBE: - case PENALTY_CIRCLE: - if (!maneuver.getTimePoint().before(startOfLeg) && (legFinishTime == null || legFinishTime.after(timePoint) || - maneuver.getTimePoint().before(legFinishTime))) { - if (maneuver.getManeuverLoss() != null) { - result.numberOfManeuvers.put(maneuver.getType(), - result.numberOfManeuvers.get(maneuver.getType()) + 1); - totalManeuverLossInMeters.put(maneuver.getType(), - totalManeuverLossInMeters.get(maneuver.getType()) - + maneuver.getManeuverLoss().getMeters()); - } - } - break; - case MARK_PASSING: - // analyze all mark passings, not only those after this leg's start, to catch the mark passing - // maneuver starting this leg, even if its time point is slightly before the mark passing starting this leg - MarkPassingManeuver mpm = (MarkPassingManeuver) maneuver; - if (mpm.getWaypointPassed() == trackedLeg.getLeg().getFrom()) { - result.sideToWhichMarkAtLegStartWasRounded = mpm.getSide(); - } - break; - default: - /* Do nothing here. - * Throwing an exception destroys the toggling (and maybe other behaviour) of the leaderboard. - */ - } - } - result.averageManeuverLossInMeters = new HashMap(); - for (ManeuverType maneuverType : new ManeuverType[] { ManeuverType.TACK, ManeuverType.JIBE, - ManeuverType.PENALTY_CIRCLE }) { - if (result.numberOfManeuvers.get(maneuverType) != 0) { - result.averageManeuverLossInMeters.put( - maneuverType, - totalManeuverLossInMeters.get(maneuverType) - / result.numberOfManeuvers.get(maneuverType)); - } - } - } - } - } - return result; - } - - private Duration getGapAtEndOfPreviousLeg(TrackedLegOfCompetitor trackedLeg, final RankingInfo rankingInfo, WindLegTypeAndLegBearingCache cache) { - final Duration result; - final Course course = trackedLeg.getTrackedLeg().getTrackedRace().getRace().getCourse(); - // if trackedLeg is the first leg, compute the gap at the start of this leg; otherwise, compute gap - // at the end of the previous leg - final TimePoint timePoint = trackedLeg.getStartTime(); - final TrackedLegOfCompetitor tloc; - if (course.getFirstWaypoint() == trackedLeg.getLeg().getFrom()) { - tloc = trackedLeg; - } else { - tloc = trackedLeg.getTrackedLeg().getTrackedRace().getTrackedLegFinishingAt(trackedLeg.getLeg().getFrom()) - .getTrackedLeg(trackedLeg.getCompetitor()); - } - result = trackedLeg.getTrackedLeg().getTrackedRace().getRankingMetric().getLegGapToLegLeaderInOwnTime(tloc, timePoint, rankingInfo, cache); - return result; - } - - private LeaderboardDTO getLiveLeaderboard(Collection namesOfRaceColumnsForWhichToLoadLegDetails, - boolean addOverallDetails, TrackedRegattaRegistry trackedRegattaRegistry, DomainFactory baseDomainFactory) throws NoWindException, ExecutionException { - LiveLeaderboardUpdater liveLeaderboardUpdater = getLiveLeaderboardUpdater(trackedRegattaRegistry, - baseDomainFactory); - return liveLeaderboardUpdater.getLiveLeaderboard(namesOfRaceColumnsForWhichToLoadLegDetails, addOverallDetails); - } - - private LiveLeaderboardUpdater getLiveLeaderboardUpdater(TrackedRegattaRegistry trackedRegattaRegistry, - DomainFactory baseDomainFactory) { - LiveLeaderboardUpdater result = this.liveLeaderboardUpdater; - if (result == null) { - synchronized (this) { - result = this.liveLeaderboardUpdater; - if (result == null) { - this.liveLeaderboardUpdater = new LiveLeaderboardUpdater(this, trackedRegattaRegistry, baseDomainFactory); - result = this.liveLeaderboardUpdater; - } - } - } - return result; - } - - private LeaderboardDTOCache getLeaderboardDTOCache() { - LeaderboardDTOCache result = this.leaderboardDTOCache; - if (result == null) { - synchronized (this) { - result = this.leaderboardDTOCache; - if (result == null) { - // The leaderboard cache is invalidated upon all competitor and mark position changes; some analyzes - // are pretty expensive, such as the maneuver re-calculation. Waiting for the latest analysis after only a - // single fix was updated is too expensive if users use the replay feature while a race is still running. - // Therefore, using waitForLatestAnalyses==false seems appropriate here. - this.leaderboardDTOCache = new LeaderboardDTOCache(/* waitForLatestAnalyses */false, this); - result = this.leaderboardDTOCache; - } - } - } - return result; - } - - @Override - public LeaderboardDTO getLeaderboardDTO(TimePoint timePoint, - Collection namesOfRaceColumnsForWhichToLoadLegDetails, boolean addOverallDetails, - TrackedRegattaRegistry trackedRegattaRegistry, DomainFactory baseDomainFactory, boolean fillNetPointsUncorrected) throws NoWindException, - InterruptedException, ExecutionException { - LeaderboardDTO result = null; - if (timePoint == null) { - // timePoint==null means live mode; however, if we're after the end of all races and after all score - // corrections, don't use the live leaderboard updater which would keep re-calculating over and over again, but map - // this to a usual non-live call which uses the regular LeaderboardDTOCache which is invalidated properly - // when the tracked race associations or score corrections or tracked race contents changes: - final TimePoint nowMinusDelay = this.getNowMinusDelay(); - final TimePoint timePointOfLatestModification = this.getTimePointOfLatestModification(); - if (fillNetPointsUncorrected || (timePointOfLatestModification != null && !nowMinusDelay.before(timePointOfLatestModification))) { - // if there hasn't been any modification to the leaderboard since nowMinusDelay, use non-live mode - // and pull the result from the regular leaderboard cache: - timePoint = timePointOfLatestModification; - } else { - // don't use the regular leaderboard cache; the race still seems to be on; use the live leaderboard updater instead: - timePoint = null; - result = this.getLiveLeaderboard(namesOfRaceColumnsForWhichToLoadLegDetails, addOverallDetails, trackedRegattaRegistry, baseDomainFactory); - } - } - if (timePoint != null) { - if (fillNetPointsUncorrected) { - // explicitly filling the uncorrected net points requires uncached recalculation - result = computeDTO(timePoint, namesOfRaceColumnsForWhichToLoadLegDetails, addOverallDetails, /* waitForLatestAnalyses */ true, - trackedRegattaRegistry, baseDomainFactory, fillNetPointsUncorrected); - } else { - // in replay we'd like up-to-date results; they are still cached - // which is OK because the cache is invalidated whenever any of the tracked races attached to the - // leaderboard changes. - result = getLeaderboardDTOCache().getLeaderboardByName(timePoint, - namesOfRaceColumnsForWhichToLoadLegDetails, addOverallDetails, baseDomainFactory, - trackedRegattaRegistry); - } - } - return result; - } - - public String toString() { - return getName() + " " + (getDefaultCourseArea() != null ? getDefaultCourseArea().getName() : "") + " " + (getScoringScheme() != null ? getScoringScheme().getType().name() : ""); - } - - @Override - public NumberOfCompetitorsInLeaderboardFetcher getNumberOfCompetitorsInLeaderboardFetcher() { - return new NumberOfCompetitorsFetcherImpl(); - } - - protected abstract LeaderboardType getLeaderboardType(); - -} +package com.sap.sailing.domain.leaderboard.impl; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.NavigableSet; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.RunnableFuture; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.sap.sailing.domain.abstractlog.race.InvalidatesLeaderboardCache; +import com.sap.sailing.domain.abstractlog.race.RaceLogEvent; +import com.sap.sailing.domain.abstractlog.regatta.RegattaLogEvent; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.Course; +import com.sap.sailing.domain.base.DomainFactory; +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.Leg; +import com.sap.sailing.domain.base.RaceColumn; +import com.sap.sailing.domain.base.RaceColumnInSeries; +import com.sap.sailing.domain.base.RaceColumnListener; +import com.sap.sailing.domain.base.Waypoint; +import com.sap.sailing.domain.common.Distance; +import com.sap.sailing.domain.common.LeaderboardType; +import com.sap.sailing.domain.common.LegType; +import com.sap.sailing.domain.common.ManeuverType; +import com.sap.sailing.domain.common.MaxPointsReason; +import com.sap.sailing.domain.common.NoWindError; +import com.sap.sailing.domain.common.NoWindException; +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.domain.common.RegattaNameAndRaceName; +import com.sap.sailing.domain.common.Speed; +import com.sap.sailing.domain.common.SpeedWithBearing; +import com.sap.sailing.domain.common.dto.BasicRaceDTO; +import com.sap.sailing.domain.common.dto.CompetitorDTO; +import com.sap.sailing.domain.common.dto.FleetDTO; +import com.sap.sailing.domain.common.dto.LeaderboardDTO; +import com.sap.sailing.domain.common.dto.LeaderboardEntryDTO; +import com.sap.sailing.domain.common.dto.LeaderboardRowDTO; +import com.sap.sailing.domain.common.dto.LegEntryDTO; +import com.sap.sailing.domain.common.dto.MetaLeaderboardRaceColumnDTO; +import com.sap.sailing.domain.common.dto.RaceColumnDTO; +import com.sap.sailing.domain.common.dto.RaceDTO; +import com.sap.sailing.domain.common.tracking.GPSFixMoving; +import com.sap.sailing.domain.leaderboard.Leaderboard; +import com.sap.sailing.domain.leaderboard.NumberOfCompetitorsInLeaderboardFetcher; +import com.sap.sailing.domain.leaderboard.ResultDiscardingRule; +import com.sap.sailing.domain.leaderboard.ScoreCorrection.Result; +import com.sap.sailing.domain.leaderboard.SettableScoreCorrection; +import com.sap.sailing.domain.leaderboard.ThresholdBasedResultDiscardingRule; +import com.sap.sailing.domain.leaderboard.caching.LeaderboardDTOCache; +import com.sap.sailing.domain.leaderboard.caching.LeaderboardDTOCalculationReuseCache; +import com.sap.sailing.domain.leaderboard.caching.LiveLeaderboardUpdater; +import com.sap.sailing.domain.leaderboard.meta.MetaLeaderboardColumn; +import com.sap.sailing.domain.racelog.RaceLogIdentifier; +import com.sap.sailing.domain.ranking.RankingMetric.CompetitorRankingInfo; +import com.sap.sailing.domain.ranking.RankingMetric.RankingInfo; +import com.sap.sailing.domain.tracking.GPSFixTrack; +import com.sap.sailing.domain.tracking.Maneuver; +import com.sap.sailing.domain.tracking.MarkPassing; +import com.sap.sailing.domain.tracking.MarkPassingManeuver; +import com.sap.sailing.domain.tracking.RaceChangeListener; +import com.sap.sailing.domain.tracking.TrackedLeg; +import com.sap.sailing.domain.tracking.TrackedLegOfCompetitor; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sailing.domain.tracking.TrackedRegattaRegistry; +import com.sap.sailing.domain.tracking.WindLegTypeAndLegBearingCache; +import com.sap.sailing.domain.tracking.WindPositionMode; +import com.sap.sailing.domain.tracking.impl.AbstractRaceChangeListener; +import com.sap.sailing.util.impl.RaceColumnListeners; +import com.sap.sse.common.Duration; +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.Util; +import com.sap.sse.common.impl.MillisecondsTimePoint; +import com.sap.sse.concurrent.LockUtil; +import com.sap.sse.concurrent.NamedReentrantReadWriteLock; +import com.sap.sse.util.impl.ThreadFactoryWithPriority; + +/** + * Base implementation for various types of leaderboards. The {@link RaceColumnListener} implementation forwards events + * received to all {@link RaceColumnListener} subscribed with this leaderboard. To which objects this leaderboard subscribes + * as {@link RaceColumnListener} is left to the concrete subclasses to implement, but the race columns seem like useful + * candidates. + * + * @author Axel Uhl (d043530) + * + */ +public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, RaceColumnListener { + private static final long serialVersionUID = 330156778603279333L; + + private static final Logger logger = Logger.getLogger(AbstractSimpleLeaderboardImpl.class.getName()); + + static final Double DOUBLE_0 = new Double(0); + + private final SettableScoreCorrection scoreCorrection; + + private ThresholdBasedResultDiscardingRule crossLeaderboardResultDiscardingRule; + + /** + * The optional display name mappings for competitors. This allows a user to override the tracking-provided + * competitor names for display in a leaderboard. + */ + private final Map displayNames; + + /** the display name of the leaderboard */ + private String displayName; + + /** + * Backs the {@link #getCarriedPoints(Competitor)} API with data. Can be used to prime this leaderboard + * with aggregated results of races not tracked / displayed by this leaderboard in detail. The points + * provided by this map are considered by {@link #getTotalPoints(Competitor, TimePoint)}. + */ + private final Map carriedPoints; + + private final RaceColumnListeners raceColumnListeners; + + /** + * A set that manages the difference between {@link #getCompetitors()} and {@link #getAllCompetitors()}. Access + * is controlled by the {@link #suppressedCompetitorsLock} lock. + */ + private final Set suppressedCompetitors; + private final NamedReentrantReadWriteLock suppressedCompetitorsLock; + + private transient Map, RunnableFuture> raceDetailsAtEndOfTrackingCache; + + /** + * This executor needs to be a different one than {@link #executor} because the tasks run by {@link #executor} + * can depend on the results of the tasks run by {@link #raceDetailsExecutor}, and an {@link Executor} doesn't + * move a task that is blocked by waiting for another {@link FutureTask} to the side but blocks permanently, + * ending in a deadlock (one that cannot easily be detected by the Eclipse debugger either). + */ + private transient Executor raceDetailsExecutor; + + /** + * Used to remove all these listeners from their tracked races when this servlet is {@link #destroy() destroyed}. + */ + private transient Set cacheInvalidationListeners; + + private transient ThreadPoolExecutor executor; + + private transient LiveLeaderboardUpdater liveLeaderboardUpdater; + + private transient LeaderboardDTOCache leaderboardDTOCache; + + /** + * A leaderboard entry representing a snapshot of a cell at a given time point for a single race/competitor. + * + * @author Axel Uhl (d043530) + * + */ + class EntryImpl implements Entry { + private final Callable trackedRankProvider; + private final Double netPoints; + private final Callable netPointsUncorrectedProvider; + private final boolean isNetPointsCorrected; + private final Double totalPoints; + private final MaxPointsReason maxPointsReason; + private final boolean discarded; + private final Fleet fleet; + + private EntryImpl(Callable trackedRankProvider, Double netPoints, + Callable netPointsUncorrectedProvider, boolean isNetPointsCorrected, + Double totalPoints, MaxPointsReason maxPointsReason, boolean discarded, Fleet fleet) { + super(); + this.trackedRankProvider = trackedRankProvider; + this.netPoints = netPoints; + this.netPointsUncorrectedProvider = netPointsUncorrectedProvider; + this.isNetPointsCorrected = isNetPointsCorrected; + this.totalPoints = totalPoints; + this.maxPointsReason = maxPointsReason; + this.discarded = discarded; + this.fleet = fleet; + } + @Override + public int getTrackedRank() { + try { + return trackedRankProvider.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + @Override + public Double getNetPoints() { + return netPoints; + } + @Override + public boolean isNetPointsCorrected() { + return isNetPointsCorrected; + } + @Override + public Double getTotalPoints() { + return totalPoints; + } + @Override + public MaxPointsReason getMaxPointsReason() { + return maxPointsReason; + } + @Override + public boolean isDiscarded() { + return discarded; + } + @Override + public Fleet getFleet() { + return fleet; + } + @Override + public Double getNetPointsUncorrected() { + try { + return netPointsUncorrectedProvider.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + /** + * Computing the competitors can be a bit expensive, particularly if the fleet is large and there may be suppressed + * competitors, and the leaderboard may be a meta-leaderboard that refers to other leaderboards which each have + * several tracked races attached from where the competitors need to be retrieved. Ideally, the competitors list + * would be cached, but that is again difficult because we would have to monitor all changes in all dependent + * leaderboards and columns and tracked races properly. + *

+ * + * As it turns out, one of the most frequent uses of the {@link AbstractSimpleLeaderboardImpl#getCompetitors} + * competitors list is to determine their number which in turn is only required for high-point scoring systems and + * for computing the default score for penalties. Again, the most frequently used low-point family of scoring schemes + * does not require this number. Yet, the scoring scheme requires an argument for polymorphic use by those that + * need it. Instead of computing it for each call, this interface lets us defer the actual calculation until the + * point when it's really needed. Once asked, this object will cache the result. Therefore, a new one should be + * constructed each time the number shall be computed. + * + * @author Axel Uhl (D043530) + * + */ + public class NumberOfCompetitorsFetcherImpl implements NumberOfCompetitorsInLeaderboardFetcher { + private int numberOfCompetitors = -1; + + @Override + public int getNumberOfCompetitorsInLeaderboard() { + if (numberOfCompetitors == -1) { + numberOfCompetitors = Util.size(getCompetitors()); + } + return numberOfCompetitors; + } + } + + /** + * Handles the invalidation of the {@link SailingServiceImpl#raceDetailsAtEndOfTrackingCache} entries if the tracked + * race changes in any way. In particular, for {@link #statusChanged}, when the status changes away from LOADING, + * calculations may start or resume, making it necessary to clear the cache. + * + * @author Axel Uhl (D043530) + * + */ + private class CacheInvalidationListener extends AbstractRaceChangeListener { + private final TrackedRace trackedRace; + private final Competitor competitor; + + public CacheInvalidationListener(TrackedRace trackedRace, Competitor competitor) { + this.trackedRace = trackedRace; + this.competitor = competitor; + } + + public TrackedRace getTrackedRace() { + return trackedRace; + } + + public void removeFromTrackedRace() { + trackedRace.removeListener(this); + } + + private void invalidateCacheAndRemoveThisListenerFromTrackedRace() { + synchronized (raceDetailsAtEndOfTrackingCache) { + raceDetailsAtEndOfTrackingCache.remove(new com.sap.sse.common.Util.Pair(trackedRace, competitor)); + removeFromTrackedRace(); + } + } + + @Override + protected void defaultAction() { + invalidateCacheAndRemoveThisListenerFromTrackedRace(); + } + } + + private static class UUIDGenerator implements LeaderboardDTO.UUIDGenerator { + @Override + public String generateRandomUUID() { + return UUID.randomUUID().toString(); + } + } + + public AbstractSimpleLeaderboardImpl(ThresholdBasedResultDiscardingRule resultDiscardingRule) { + this.carriedPoints = new HashMap(); + this.scoreCorrection = createScoreCorrection(); + this.displayNames = new HashMap(); + this.crossLeaderboardResultDiscardingRule = resultDiscardingRule; + this.suppressedCompetitors = new HashSet(); + this.suppressedCompetitorsLock = new NamedReentrantReadWriteLock("suppressedCompetitorsLock", /* fair */ false); + this.raceColumnListeners = new RaceColumnListeners(); + this.raceDetailsAtEndOfTrackingCache = new HashMap, RunnableFuture>(); + initTransientFields(); + } + + /** + * Produces the score correction object to use in this leaderboard. Used by the constructor. Subclasses may override + * this method to create a more specific type of score correction. This implementation produces an object of type + * {@link ScoreCorrectionImpl}. + */ + protected SettableScoreCorrection createScoreCorrection() { + return new ScoreCorrectionImpl(this); + } + + private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException { + ois.defaultReadObject(); + initTransientFields(); + } + + private void initTransientFields() { + this.raceDetailsAtEndOfTrackingCache = new HashMap, RunnableFuture>(); + this.raceDetailsExecutor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); + this.cacheInvalidationListeners = new HashSet(); + // When many updates are triggered in a short period of time by a single thread, ensure that the single thread + // providing the updates is not outperformed by all the re-calculations happening here. Leave at least one + // core to other things, but by using at least three threads ensure that no simplistic deadlocks may occur. + final int THREAD_POOL_SIZE = Math.max(Runtime.getRuntime().availableProcessors(), 3); + executor = new ThreadPoolExecutor(/* corePoolSize */ THREAD_POOL_SIZE, + /* maximumPoolSize */ THREAD_POOL_SIZE, + /* keepAliveTime */ 60, TimeUnit.SECONDS, + /* workQueue */ new LinkedBlockingQueue(), new ThreadFactoryWithPriority(Thread.NORM_PRIORITY-1, /* daemon */ true)); + } + + @Override + public void destroy() { + for (CacheInvalidationListener cacheInvalidationListener : cacheInvalidationListeners) { + cacheInvalidationListener.removeFromTrackedRace(); + } + } + + @Override + public SettableScoreCorrection getScoreCorrection() { + return scoreCorrection; + } + + @Override + public String getDisplayName(Competitor competitor) { + return displayNames.get(competitor); + } + + @Override + public String getDisplayName() { + return displayName; + } + + @Override + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + @Override + public ResultDiscardingRule getResultDiscardingRule() { + return crossLeaderboardResultDiscardingRule; + } + + @Override + public void setCarriedPoints(Competitor competitor, double carriedPoints) { + Double oldCarriedPoints = this.carriedPoints.put(competitor, carriedPoints); + if (!Util.equalsWithNull(oldCarriedPoints, carriedPoints)) { + getScoreCorrection().notifyListenersAboutCarriedPointsChange(competitor, oldCarriedPoints, carriedPoints); + } + } + + @Override + public double getCarriedPoints(Competitor competitor) { + Double result = carriedPoints.get(competitor); + return result == null ? 0 : result; + } + + @Override + public Map getCompetitorsForWhichThereAreCarriedPoints() { + return Collections.unmodifiableMap(carriedPoints); + } + + @Override + public void unsetCarriedPoints(Competitor competitor) { + Double oldCarriedPoints = carriedPoints.remove(competitor); + if (oldCarriedPoints != null) { + getScoreCorrection().notifyListenersAboutCarriedPointsChange(competitor, oldCarriedPoints, null); + } + } + + @Override + public boolean hasCarriedPoints() { + return !carriedPoints.isEmpty(); + } + + @Override + public boolean hasCarriedPoints(Competitor competitor) { + return carriedPoints.containsKey(competitor); + } + + @Override + public void setDisplayName(Competitor competitor, String displayName) { + String oldDisplayName = displayNames.get(competitor); + displayNames.put(competitor, displayName); + if (!Util.equalsWithNull(oldDisplayName, displayName)) { + getRaceColumnListeners().notifyListenersAboutCompetitorDisplayNameChanged(competitor, oldDisplayName, displayName); + } + } + + @Override + public void setCrossLeaderboardResultDiscardingRule(ThresholdBasedResultDiscardingRule discardingRule) { + ResultDiscardingRule oldDiscardingRule = getResultDiscardingRule(); + this.crossLeaderboardResultDiscardingRule = discardingRule; + getRaceColumnListeners().notifyListenersAboutResultDiscardingRuleChanged(oldDiscardingRule, discardingRule); + } + + @Override + public Double getNetPoints(final Competitor competitor, final RaceColumn raceColumn, final TimePoint timePoint) { + return getScoreCorrection().getCorrectedScore( + new Callable() { + public Integer call() throws NoWindException { + return getTrackedRank(competitor, raceColumn, timePoint); + } + }, competitor, + raceColumn, timePoint, new NumberOfCompetitorsFetcherImpl(), getScoringScheme()).getCorrectedScore(); + } + + @Override + public MaxPointsReason getMaxPointsReason(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint) { + return getScoreCorrection().getMaxPointsReason(competitor, raceColumn, timePoint); + } + + @Override + public boolean isDiscarded(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint) { + return isDiscarded(competitor, raceColumn, getRaceColumns(), timePoint); + } + + private boolean isDiscarded(Competitor competitor, RaceColumn raceColumn, + Iterable raceColumnsToConsider, TimePoint timePoint) { + final Set discardedRaceColumns = getResultDiscardingRule() + .getDiscardedRaceColumns(competitor, this, raceColumnsToConsider, timePoint); + return isDiscarded(competitor, raceColumn, timePoint, discardedRaceColumns); + } + + /** + * Same as {@link #isDiscarded(Competitor, RaceColumn, TimePoint)}, only that the set of discarded race columns can + * be specified which is useful when total points are to be computed for more than one column for the same + * competitor because then the calculation of discards (which requires looking at all columns) only needs to be done + * once and not again for each column (which would lead to quadratic effort). + * + * @param discardedRaceColumns + * expected to be the result of what we would get if we called {@link #getResultDiscardingRule()}. + * {@link ResultDiscardingRule#getDiscardedRaceColumns(Competitor, Leaderboard, Iterable, TimePoint) + * getDiscardedRaceColumns(competitor, this, raceColumnsToConsider, timePoint)}. + */ + private boolean isDiscarded(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint, + final Set discardedRaceColumns) { + return !raceColumn.isMedalRace() + && getMaxPointsReason(competitor, raceColumn, timePoint).isDiscardable() + && discardedRaceColumns.contains(raceColumn); + } + + @Override + public Double getTotalPoints(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint) throws NoWindException { + return getTotalPoints(competitor, raceColumn, getRaceColumns(), timePoint); + } + + @Override + public Double getTotalPoints(Competitor competitor, RaceColumn raceColumn, + Iterable raceColumnsToConsider, TimePoint timePoint) throws NoWindException { + final Set discardedRaceColumns = getResultDiscardingRule() + .getDiscardedRaceColumns(competitor, this, raceColumnsToConsider, timePoint); + return getTotalPoints(competitor, raceColumn, timePoint, discardedRaceColumns); + } + + /** + * Same as {@link #getTotalPoints(Competitor, RaceColumn, Iterable, TimePoint)}, only that the set of discarded race columns can + * be specified which is useful when total points are to be computed for more than one column for the same + * competitor because then the calculation of discards (which requires looking at all columns) only needs to be done + * once and not again for each column (which would lead to quadratic effort). + * + * @param discardedRaceColumns + * expected to be the result of what we would get if we called {@link #getResultDiscardingRule()}. + * {@link ResultDiscardingRule#getDiscardedRaceColumns(Competitor, Leaderboard, Iterable, TimePoint) + * getDiscardedRaceColumns(competitor, this, raceColumnsToConsider, timePoint)}. + */ + @Override + public Double getTotalPoints(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint, Set discardedRaceColumns) { + Double result; + if (isDiscarded(competitor, raceColumn, timePoint, discardedRaceColumns)) { + result = 0.0; + } else { + final Double netPoints = getNetPoints(competitor, raceColumn, timePoint); + if (netPoints == null) { + result = null; + } else { + result = raceColumn.getFactor() * netPoints; + } + } + return result; + } + + @Override + public Double getTotalPoints(Competitor competitor, TimePoint timePoint) { + return getTotalPoints(competitor, getRaceColumns(), timePoint); + } + + @Override + public Double getTotalPoints(Competitor competitor, final Iterable raceColumnsToConsider, + TimePoint timePoint) { + // when a column with isStartsWithZeroScore() is found, only reset score if the competitor scored in any race from there on + boolean needToResetScoreUponNextNonEmptyEntry = false; + double result = getCarriedPoints(competitor); + final Set discardedRaceColumns = getResultDiscardingRule() + .getDiscardedRaceColumns(competitor, this, raceColumnsToConsider, timePoint); + for (RaceColumn raceColumn : raceColumnsToConsider) { + if (raceColumn.isStartsWithZeroScore()) { + needToResetScoreUponNextNonEmptyEntry = true; + } + if (getScoringScheme().isValidInTotalScore(this, raceColumn, competitor, timePoint)) { + final Double totalPoints = getTotalPoints(competitor, raceColumn, timePoint, discardedRaceColumns); + if (totalPoints != null) { + if (needToResetScoreUponNextNonEmptyEntry) { + result = 0; + needToResetScoreUponNextNonEmptyEntry = false; + } + result += totalPoints; + } + } + } + return result; + } + + /** + * All competitors with non-null net points are added to the result which is then sorted by net points in ascending + * order. The fleet, if ordered, is the primary ordering criterion, followed by the net points. + */ + @Override + public List getCompetitorsFromBestToWorst(final RaceColumn raceColumn, TimePoint timePoint) throws NoWindException { + final Map> netPointsAndFleet = new HashMap>(); + for (Competitor competitor : getCompetitors()) { + Double netPoints = getNetPoints(competitor, raceColumn, timePoint); + if (netPoints != null) { + netPointsAndFleet.put(competitor, new com.sap.sse.common.Util.Pair(netPoints, raceColumn.getFleetOfCompetitor(competitor))); + } + } + List result = new ArrayList(netPointsAndFleet.keySet()); + Collections.sort(result, new Comparator() { + @Override + public int compare(Competitor o1, Competitor o2) { + int comparisonResult; + if (o1 == o2) { + comparisonResult = 0; + } else { + if (raceColumn.hasSplitFleets() && !raceColumn.hasSplitFleetContiguousScoring()) { + // only check fleets if there are more than one and the column is not to be contiguously scored even in case + // of split fleets + final Fleet o1Fleet = netPointsAndFleet.get(o1).getB(); + final Fleet o2Fleet = netPointsAndFleet.get(o2).getB(); + if (o1Fleet == null) { + if (o2Fleet == null) { + comparisonResult = 0; + } else { + comparisonResult = 1; // o1 ranks "worse" because it doesn't have a fleet set while o2 + // has + } + } else { + if (o2Fleet == null) { + comparisonResult = -1; // o1 ranks "better" because it has a fleet set while o2 hasn't + } else { + comparisonResult = o1Fleet.compareTo(o2Fleet); + } + } + } else { + // either there are no split fleets or the split isn't relevant for scoring as for ordered fleets + // the scoring runs contiguously from top to bottom + comparisonResult = 0; + } + if (comparisonResult == 0) { + comparisonResult = getScoringScheme().getScoreComparator(/* nullScoresAreBetter */ false).compare( + netPointsAndFleet.get(o1).getA(), netPointsAndFleet.get(o2).getA()); + } + } + return comparisonResult; + } + }); + return result; + } + + @Override + public List getCompetitorsFromBestToWorst(TimePoint timePoint) { + return getCompetitorsFromBestToWorst(getRaceColumns(), timePoint); + } + + private List getCompetitorsFromBestToWorst(Iterable raceColumnsToConsider, TimePoint timePoint) { + List result = new ArrayList(); + for (Competitor competitor : getCompetitors()) { + result.add(competitor); + } + Collections.sort(result, getTotalRankComparator(raceColumnsToConsider, timePoint)); + return result; + } + + @Override + public int getTotalRankOfCompetitor(Competitor competitor, TimePoint timePoint) throws NoWindException { + List competitorsFromBestToWorst = getCompetitorsFromBestToWorst(timePoint); + return competitorsFromBestToWorst.indexOf(competitor) + 1; + } + + protected Comparator getTotalRankComparator(Iterable raceColumnsToConsider, TimePoint timePoint) { + return new LeaderboardTotalRankComparator(this, timePoint, getScoringScheme(), /* nullScoresAreBetter */ false, raceColumnsToConsider); + } + + @Override + public RaceColumn getRaceColumnByName(String columnName) { + RaceColumn result = null; + for (RaceColumn r : getRaceColumns()) { + if (r.getName().equals(columnName)) { + result = r; + break; + } + } + return result; + } + + @Override + public Competitor getCompetitorByName(String competitorName) { + for (Competitor competitor : getAllCompetitors()) { + if (competitor.getName().equals(competitorName)) { + return competitor; + } + } + return null; + } + + @Override + public boolean countRaceForComparisonWithDiscardingThresholds(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint) { + TrackedRace trackedRaceForCompetitorInColumn; + return getScoringScheme().isValidInTotalScore(this, raceColumn, competitor, timePoint) && + (getScoreCorrection().isScoreCorrected(competitor, raceColumn, timePoint) || + ((trackedRaceForCompetitorInColumn=raceColumn.getTrackedRace(competitor)) != null && + trackedRaceForCompetitorInColumn.hasStarted(timePoint) && + trackedRaceForCompetitorInColumn.getRank(competitor, timePoint) != 0)); + } + + @Override + public void addRaceColumnListener(RaceColumnListener listener) { + getRaceColumnListeners().addRaceColumnListener(listener); + } + + @Override + public void removeRaceColumnListener(RaceColumnListener listener) { + getRaceColumnListeners().removeRaceColumnListener(listener); + } + + @Override + public Entry getEntry(final Competitor competitor, final RaceColumn race, final TimePoint timePoint) throws NoWindException { + final Set discardedRaceColumns = getResultDiscardingRule().getDiscardedRaceColumns(competitor, this, getRaceColumns(), timePoint); + return getEntry(competitor, race, timePoint, discardedRaceColumns); + } + + @Override + public Entry getEntry(final Competitor competitor, final RaceColumn race, final TimePoint timePoint, + Set discardedRaceColumns) throws NoWindException { + Callable trackedRankProvider = new Callable() { + public Integer call() throws NoWindException { + return getTrackedRank(competitor, race, timePoint); + } + }; + final Result correctedResults = getScoreCorrection().getCorrectedScore(trackedRankProvider, competitor, race, + timePoint, new NumberOfCompetitorsFetcherImpl(), getScoringScheme()); + boolean discarded = isDiscarded(competitor, race, timePoint, discardedRaceColumns); + final Double correctedScore = correctedResults.getCorrectedScore(); + return new EntryImpl(trackedRankProvider, correctedScore, new Callable() { + @Override + public Double call() { + return correctedResults.getUncorrectedScore(); + } + }, correctedResults.isCorrected(), discarded ? DOUBLE_0 : correctedScore == null ? null + : Double.valueOf(correctedScore * race.getFactor()), correctedResults.getMaxPointsReason(), discarded, + race.getFleetOfCompetitor(competitor)); + } + + @Override + public Map> getRankedCompetitorsFromBestToWorstAfterEachRaceColumn(TimePoint timePoint) throws NoWindException { + Map> result = new LinkedHashMap<>(); + List raceColumnsToConsider = new ArrayList<>(); + for (RaceColumn raceColumn : getRaceColumns()) { + raceColumnsToConsider.add(raceColumn); + result.put(raceColumn, getCompetitorsFromBestToWorst(raceColumnsToConsider, timePoint)); + } + return result; + } + + @Override + public Map> getTotalPointsSumAfterRaceColumn(final TimePoint timePoint) + throws NoWindException { + final Map> result = new LinkedHashMap<>(); + List raceColumnsToConsider = new ArrayList<>(); + Map>> futures = new HashMap<>(); + for (final RaceColumn raceColumn : getRaceColumns()) { + raceColumnsToConsider.add(raceColumn); + final Iterable finalRaceColumnsToConsider = new ArrayList<>(raceColumnsToConsider); + futures.put(raceColumn, executor.submit(new Callable>() { + @Override + public Map call() { + Map totalPointsSumPerCompetitorInColumn = new HashMap<>(); + for (Competitor competitor : getCompetitors()) { + totalPointsSumPerCompetitorInColumn.put(competitor, getTotalPoints(competitor, finalRaceColumnsToConsider, timePoint)); + } + synchronized (result) { + return totalPointsSumPerCompetitorInColumn; + } + } + })); + } + for (RaceColumn raceColumn : getRaceColumns()) { + try { + result.put(raceColumn, futures.get(raceColumn).get()); + } catch (InterruptedException | ExecutionException e) { + if (e.getCause() instanceof NoWindError) { + throw ((NoWindError) e.getCause()).getCause(); + } else { + throw new RuntimeException(e); // no caught exceptions occur in the futures executed + } + } + } + return result; + } + + @Override + public Map, Entry> getContent(final TimePoint timePoint) throws NoWindException { + Map, Entry> result = new HashMap, Entry>(); + Map> discardedRaces = new HashMap>(); + for (final RaceColumn raceColumn : getRaceColumns()) { + for (final Competitor competitor : getCompetitors()) { + Callable trackedRankProvider = new Callable() { + @Override + public Integer call() throws Exception { + return getTrackedRank(competitor, raceColumn, timePoint); + } + }; + final Result correctedResults = getScoreCorrection().getCorrectedScore(trackedRankProvider, competitor, raceColumn, + timePoint, new NumberOfCompetitorsFetcherImpl(), getScoringScheme()); + Set discardedRacesForCompetitor = discardedRaces.get(competitor); + if (discardedRacesForCompetitor == null) { + discardedRacesForCompetitor = getResultDiscardingRule().getDiscardedRaceColumns(competitor, this, getRaceColumns(), timePoint); + discardedRaces.put(competitor, discardedRacesForCompetitor); + } + boolean discarded = discardedRacesForCompetitor.contains(raceColumn); + final Double correctedScore = correctedResults.getCorrectedScore(); + Entry entry = new EntryImpl(trackedRankProvider, correctedScore, + new Callable() { @Override public Double call() { return correctedResults.getUncorrectedScore(); } }, + correctedResults.isCorrected(), + discarded ? DOUBLE_0 : (correctedScore==null?null: + Double.valueOf((correctedScore * raceColumn.getFactor()))), correctedResults.getMaxPointsReason(), + discarded, raceColumn.getFleetOfCompetitor(competitor)); + result.put(new com.sap.sse.common.Util.Pair(competitor, raceColumn), entry); + } + } + return result; + } + + @Override + public void trackedRaceLinked(RaceColumn raceColumn, Fleet fleet, TrackedRace trackedRace) { + getRaceColumnListeners().notifyListenersAboutTrackedRaceLinked(raceColumn, fleet, trackedRace); + } + + @Override + public void trackedRaceUnlinked(RaceColumn raceColumn, Fleet fleet, TrackedRace trackedRace) { + getRaceColumnListeners().notifyListenersAboutTrackedRaceUnlinked(raceColumn, fleet, trackedRace); + // It's generally possible that a leaderboard links to the same tracked race in multiple columns / fleets; + // only if it no longer references the trackedRace currently unlinked from one column/fleet, also unlink + // all cache invalidation listeners for said trackedRace + if (!Util.contains(getTrackedRaces(), trackedRace)) { + synchronized (cacheInvalidationListeners) { + for (Iterator cacheInvalidationListenerIter=cacheInvalidationListeners.iterator(); + cacheInvalidationListenerIter.hasNext(); ) { + CacheInvalidationListener cacheInvalidationListener = cacheInvalidationListenerIter.next(); + if (cacheInvalidationListener.getTrackedRace() == trackedRace) { + cacheInvalidationListener.removeFromTrackedRace(); + cacheInvalidationListenerIter.remove(); + } + } + } + } + } + + @Override + public void isMedalRaceChanged(RaceColumn raceColumn, boolean newIsMedalRace) { + getRaceColumnListeners().notifyListenersAboutIsMedalRaceChanged(raceColumn, newIsMedalRace); + } + + @Override + public void isFleetsCanRunInParallelChanged(RaceColumn raceColumn, boolean newIsFleetsCanRunInParallel) { + getRaceColumnListeners().notifyListenersAboutIsFleetsCanRunInParallelChanged(raceColumn, newIsFleetsCanRunInParallel); + } + + @Override + public void isStartsWithZeroScoreChanged(RaceColumn raceColumn, boolean newIsStartsWithZeroScore) { + getRaceColumnListeners().notifyListenersAboutIsStartsWithZeroScoreChanged(raceColumn, newIsStartsWithZeroScore); + } + + @Override + public void isFirstColumnIsNonDiscardableCarryForwardChanged(RaceColumn raceColumn, boolean firstColumnIsNonDiscardableCarryForward) { + getRaceColumnListeners().notifyListenersAboutIsFirstColumnIsNonDiscardableCarryForwardChanged(raceColumn, firstColumnIsNonDiscardableCarryForward); + } + + @Override + public void hasSplitFleetContiguousScoringChanged(RaceColumn raceColumn, boolean hasSplitFleetContiguousScoring) { + getRaceColumnListeners().notifyListenersAboutHasSplitFleetContiguousScoringChanged(raceColumn, hasSplitFleetContiguousScoring); + } + + @Override + public void raceColumnMoved(RaceColumn raceColumn, int newIndex) { + getRaceColumnListeners().notifyListenersAboutRaceColumnMoved(raceColumn, newIndex); + } + + @Override + public void factorChanged(RaceColumn raceColumn, Double oldFactor, Double newFactor) { + getRaceColumnListeners().notifyListenersAboutFactorChanged(raceColumn, oldFactor, newFactor); + } + + /** + * A leaderboard will only accept the addition of a race column if the column's name is unique across the leaderboard. + */ + @Override + public boolean canAddRaceColumnToContainer(RaceColumn newRaceColumn) { + boolean result = true; + for (RaceColumn raceColumn : getRaceColumns()) { + if (raceColumn.getName().equals(newRaceColumn.getName())) { + result = false; + break; + } + } + return result; + } + + @Override + public void raceColumnAddedToContainer(RaceColumn raceColumn) { + getRaceColumnListeners().notifyListenersAboutRaceColumnAddedToContainer(raceColumn); + } + + @Override + public void raceColumnRemovedFromContainer(RaceColumn raceColumn) { + getRaceColumnListeners().notifyListenersAboutRaceColumnRemovedFromContainer(raceColumn); + } + + @Override + public void competitorDisplayNameChanged(Competitor competitor, String oldDisplayName, String displayName) { + getRaceColumnListeners().notifyListenersAboutCompetitorDisplayNameChanged(competitor, oldDisplayName, displayName); + } + + @Override + public void resultDiscardingRuleChanged(ResultDiscardingRule oldDiscardingRule, ResultDiscardingRule newDiscardingRule) { + getRaceColumnListeners().notifyListenersAboutResultDiscardingRuleChanged(oldDiscardingRule, newDiscardingRule); + } + + @Override + public boolean isTransient() { + return false; + } + + /** + * Finds out the time point when any of the {@link Leaderboard#getTrackedRaces() tracked races currently attached to + * the leaderboard} and the {@link Leaderboard#getScoreCorrection() score corrections} have last been + * modified. If no tracked race is attached and no time-stamped score corrections have been applied to the leaderboard, + * null is returned. The time point computed this way is a good choice for normalizing queries for later time + * points in an attempt to achieve more cache hits.

+ * + * Note, however, that the result does not tell about structural changes to the leaderboard and therefore cannot be used + * to determine the need for cache invalidation. For example, if a column is added to a leaderboard after the time point + * returned by this method but that column's attached tracked race has finished before the time point returned by this method, + * the result of this method won't change. Still, the contents of the leaderboard will change by a change in column structure. + * A different means to determine the possibility of changes that happened to this leaderboard must be used for cache + * management. Such a facility has to listen for score correction changes, tracked races being attached or detached and + * the column structure changing. + * + * @see TrackedRace#getTimePointOfNewestEvent() + * @see SettableScoreCorrection#getTimePointOfLastCorrectionsValidity() + */ + @Override + public TimePoint getTimePointOfLatestModification() { + TimePoint result = null; + for (TrackedRace trackedRace : getTrackedRaces()) { + if (result == null || (trackedRace.getTimePointOfNewestEvent() != null && trackedRace.getTimePointOfNewestEvent().after(result))) { + result = trackedRace.getTimePointOfNewestEvent(); + } + } + TimePoint timePointOfLastScoreCorrection = getScoreCorrection().getTimePointOfLastCorrectionsValidity(); + if (timePointOfLastScoreCorrection != null && (result == null || timePointOfLastScoreCorrection.after(result))) { + result = timePointOfLastScoreCorrection; + } + return result; + } + + @Override + public com.sap.sse.common.Util.Pair getMaximumSpeedOverGround(Competitor competitor, TimePoint timePoint) { + com.sap.sse.common.Util.Pair result = null; + // TODO should we ensure that competitor participated in all race columns? + for (TrackedRace trackedRace : getTrackedRaces()) { + if (Util.contains(trackedRace.getRace().getCompetitors(), competitor)) { + NavigableSet markPassings = trackedRace.getMarkPassings(competitor); + if (!markPassings.isEmpty()) { + TimePoint from = markPassings.first().getTimePoint(); + TimePoint to; + if (timePoint.after(markPassings.last().getTimePoint()) && + markPassings.last().getWaypoint() == trackedRace.getRace().getCourse().getLastWaypoint()) { + // stop counting when competitor finished the race + to = markPassings.last().getTimePoint(); + } else { + to = timePoint; + } + com.sap.sse.common.Util.Pair maxSpeed = trackedRace.getTrack(competitor).getMaximumSpeedOverGround(from, to); + if (result == null || result.getB() == null || + (maxSpeed != null && maxSpeed.getB() != null && maxSpeed.getB().compareTo(result.getB()) > 0)) { + result = maxSpeed; + } + } + } + } + return result; + } + + @Override + public Speed getAverageSpeedOverGround(Competitor competitor, TimePoint timePoint) { + Speed result = null; + for (TrackedRace trackedRace : getTrackedRaces()) { + if (Util.contains(trackedRace.getRace().getCompetitors(), competitor)) { + NavigableSet markPassings = trackedRace.getMarkPassings(competitor); + if (!markPassings.isEmpty()) { + TimePoint from = markPassings.first().getTimePoint(); + TimePoint to; + if (timePoint.after(markPassings.last().getTimePoint()) && + markPassings.last().getWaypoint() == trackedRace.getRace().getCourse().getLastWaypoint()) { + // stop counting when competitor finished the race + to = markPassings.last().getTimePoint(); + } else { + if (markPassings.last().getWaypoint() != trackedRace.getRace().getCourse().getLastWaypoint() && + timePoint.after(markPassings.last().getTimePoint())) { + result = null; + break; + } + to = timePoint; + } + Distance distanceTraveled = trackedRace.getDistanceTraveled(competitor, timePoint); + if (distanceTraveled != null) { + result = distanceTraveled.inTime(to.asMillis()-from.asMillis()); + } + } + } + } + return result; + } + + @Override + public Duration getTotalTimeSailedInLegType(Competitor competitor, LegType legType, TimePoint timePoint) throws NoWindException { + return getTotalTimeSailedInLegType(competitor, legType, timePoint, new HashMap()); + } + + private Duration getTotalTimeSailedInLegType(Competitor competitor, LegType legType, TimePoint timePoint, Map legTypeCache) throws NoWindException { + Duration result = null; + // TODO should we ensure that competitor participated in all race columns? + outerLoop: + for (TrackedRace trackedRace : getTrackedRaces()) { + if (Util.contains(trackedRace.getRace().getCompetitors(), competitor)) { + trackedRace.getRace().getCourse().lockForRead(); + try { + for (Leg leg : trackedRace.getRace().getCourse().getLegs()) { + TrackedLegOfCompetitor trackedLegOfCompetitor = trackedRace.getTrackedLeg(competitor, leg); + if (trackedLegOfCompetitor.hasStartedLeg(timePoint)) { + // find out leg type at the time the competitor started the leg + try { + final TrackedLeg trackedLeg = trackedRace.getTrackedLeg(leg); + LegType trackedLegType = legTypeCache.get(trackedLeg); + if (trackedLegType == null) { + final TimePoint startTime = trackedLegOfCompetitor.getStartTime(); + TimePoint finishTime = trackedLegOfCompetitor.getFinishTime(); + if (finishTime == null) { + finishTime = timePoint; + } + trackedLegType = trackedLeg.getLegType(startTime.plus(startTime.until(finishTime).divide(2))); // middle of the leg + legTypeCache.put(trackedLeg, trackedLegType); + } + if (legType == trackedLegType) { + Duration timeSpentOnDownwind = trackedLegOfCompetitor.getTime(timePoint); + if (timeSpentOnDownwind != null) { + if (result == null) { + result = timeSpentOnDownwind; + } else { + result = result.plus(timeSpentOnDownwind); + } + } else { + // Although the competitor has started the leg, no value was produced. This + // means that the competitor didn't finish the leg before tracking ended. No useful value + // can be obtained for this competitor anymore. + result = null; + break outerLoop; + } + } + } catch (NoWindException nwe) { + // without wind there is no leg type and hence there is no reasonable value for this: + result = null; + break outerLoop; + } + } + } + } finally { + trackedRace.getRace().getCourse().unlockAfterRead(); + } + } + } + return result; + } + + @Override + public Duration getTotalTimeSailed(Competitor competitor, TimePoint timePoint) { + Duration result = null; + for (TrackedRace trackedRace : getTrackedRaces()) { + if (Util.contains(trackedRace.getRace().getCompetitors(), competitor)) { + NavigableSet markPassings = trackedRace.getMarkPassings(competitor); + if (!markPassings.isEmpty()) { + TimePoint from = trackedRace.getStartOfRace(); // start counting at race start, not when the competitor passed the line + if (from != null && !timePoint.before(from)) { // but only if the race started after timePoint + TimePoint to; + if (timePoint.after(markPassings.last().getTimePoint()) + && markPassings.last().getWaypoint() == trackedRace.getRace().getCourse() + .getLastWaypoint()) { + // stop counting when competitor finished the race + to = markPassings.last().getTimePoint(); + } else { + if (trackedRace.getEndOfTracking() != null + && timePoint.after(trackedRace.getEndOfTracking())) { + result = null; // race not finished until end of tracking; no reasonable value can be + // computed for competitor + break; + } else { + to = timePoint; + } + } + Duration timeSpent = from.until(to); + if (result == null) { + result = timeSpent; + } else { + result=result.plus(timeSpent); + } + } + } + } + } + return result; + } + + @Override + public Distance getTotalDistanceTraveled(Competitor competitor, TimePoint timePoint) { + Distance result = null; + for (TrackedRace trackedRace : getTrackedRaces()) { + TimePoint startOfRace; + if (Util.contains(trackedRace.getRace().getCompetitors(), competitor) && + (startOfRace=trackedRace.getStartOfRace()) != null && + !startOfRace.after(timePoint)) { + Distance distanceSailedInRace = trackedRace.getDistanceTraveled(competitor, timePoint); + if (distanceSailedInRace != null) { + if (result == null) { + result = distanceSailedInRace; + } else { + result = result.add(distanceSailedInRace); + } + } else { + // if competitor has not finished one single race in the whole + // series then we can not return a meaningful value for all + // all races + return null; + } + } + } + return result; + } + + protected RaceColumnListeners getRaceColumnListeners() { + return raceColumnListeners; + } + + @Override + public Iterable getCompetitors() { + final Iterable result; + // mostly the set of suppressed competitors is empty; in this case, avoid having to loop over the + // potentially large set of competitors + if (Util.isEmpty(getSuppressedCompetitors())) { + result = getAllCompetitors(); + } else { + final Iterable allCompetitors = getAllCompetitors(); + final Set suppressed = new HashSet<>(); + Util.addAll(getSuppressedCompetitors(), suppressed); + result = getCompetitorIterableSkippingSuppressed(allCompetitors, suppressed); + } + return result; + } + + @Override + public Iterable getCompetitors(RaceColumn raceColumn, Fleet fleet) { + return getCompetitorIterableSkippingSuppressed(getAllCompetitors(raceColumn, fleet), getSuppressedCompetitors()); + } + + /** + * return an iterable with a smart iterator that filters out the suppressed elements on demand + */ + protected Iterable getCompetitorIterableSkippingSuppressed(final Iterable allCompetitors, + final Iterable suppressed) { + final Iterable result; + result = new Iterable() { + @Override + public Iterator iterator() { + return new Iterator() { + private final Iterator allIter = allCompetitors.iterator(); + private Competitor next = advance(); + + private Competitor advance() { + next = null; + while (allIter.hasNext() && next == null) { + next = allIter.next(); + if (Util.contains(suppressed, next)) { + next = null; + } + } + return next; + } + + @Override + public boolean hasNext() { + return next != null; + } + + @Override + public Competitor next() { + if (next == null) { + throw new NoSuchElementException(); + } + final Competitor result = next; + advance(); + return result; + } + }; + } + }; + return result; + } + + @Override + public Iterable getSuppressedCompetitors() { + LockUtil.lockForRead(suppressedCompetitorsLock); + try { + return new HashSet(suppressedCompetitors); + } finally { + LockUtil.unlockAfterRead(suppressedCompetitorsLock); + } + } + + @Override + public void setSuppressed(Competitor competitor, boolean suppressed) { + LockUtil.lockForWrite(suppressedCompetitorsLock); + try { + if (suppressed) { + suppressedCompetitors.add(competitor); + } else { + suppressedCompetitors.remove(competitor); + } + } finally { + LockUtil.unlockAfterWrite(suppressedCompetitorsLock); + } + getScoreCorrection().notifyListenersAboutIsSuppressedChange(competitor, suppressed); + } + + @Override + public TimePoint getNowMinusDelay() { + final TimePoint now = MillisecondsTimePoint.now(); + final Long delayToLiveInMillis = getDelayToLiveInMillis(); + TimePoint timePoint = delayToLiveInMillis == null ? now : now.minus(delayToLiveInMillis); + return timePoint; + } + + @Override + public void raceLogEventAdded(RaceColumn raceColumn, RaceLogIdentifier raceLogIdentifier, RaceLogEvent event) { + getRaceColumnListeners().notifyListenersAboutRaceLogEventAdded(raceColumn, raceLogIdentifier, event); + if (event instanceof InvalidatesLeaderboardCache) { + // make sure to invalidate the cache as this event indicates that + // it changes values the cache could still hold + if (leaderboardDTOCache != null) { + leaderboardDTOCache.invalidate(this); + } + } + } + + @Override + public void regattaLogEventAdded(RegattaLogEvent event) { + getRaceColumnListeners().notifyListenersAboutRegattaLogEventAdded(event); + } + + @Override + public LeaderboardDTO computeDTO(final TimePoint timePoint, + final Collection namesOfRaceColumnsForWhichToLoadLegDetails, boolean addOverallDetails, + final boolean waitForLatestAnalyses, TrackedRegattaRegistry trackedRegattaRegistry, final DomainFactory baseDomainFactory, + final boolean fillNetPointsUncorrected) + throws NoWindException { + long startOfRequestHandling = System.currentTimeMillis(); + final LeaderboardDTOCalculationReuseCache cache = new LeaderboardDTOCalculationReuseCache(timePoint); + final LeaderboardDTO result = new LeaderboardDTO(this.getScoreCorrection().getTimePointOfLastCorrectionsValidity() == null ? null + : this.getScoreCorrection().getTimePointOfLastCorrectionsValidity().asDate(), + this.getScoreCorrection() == null ? null : this.getScoreCorrection().getComment(), + this.getScoringScheme() == null ? null : this.getScoringScheme().getType(), this + .getScoringScheme().isHigherBetter(), new UUIDGenerator(), addOverallDetails); + result.type = getLeaderboardType(); + result.competitors = new ArrayList(); + result.name = this.getName(); + result.competitorDisplayNames = new HashMap(); + for (Competitor suppressedCompetitor : this.getSuppressedCompetitors()) { + result.setSuppressed(baseDomainFactory.convertToCompetitorDTO(suppressedCompetitor), true); + } + // Now create the race columns and, as a future task, set their competitorsFromBestToWorst, then wait for all these + // futures to finish: + Map>> competitorsFromBestToWorstTasks = new HashMap<>(); + for (final RaceColumn raceColumn : this.getRaceColumns()) { + boolean isMetaLeaderboardColumn = raceColumn instanceof MetaLeaderboardColumn; + RaceColumnDTO raceColumnDTO = result.createEmptyRaceColumn(raceColumn.getName(), raceColumn.isMedalRace(), + raceColumn instanceof RaceColumnInSeries ? ((RaceColumnInSeries) raceColumn).getRegatta().getName() : null, + raceColumn instanceof RaceColumnInSeries ? ((RaceColumnInSeries) raceColumn).getSeries().getName() : null, + isMetaLeaderboardColumn); + if (isMetaLeaderboardColumn && raceColumnDTO instanceof MetaLeaderboardRaceColumnDTO) { + calculateRacesMetadata((MetaLeaderboardColumn) raceColumn, (MetaLeaderboardRaceColumnDTO) raceColumnDTO, + trackedRegattaRegistry, baseDomainFactory); + } + for (Fleet fleet : raceColumn.getFleets()) { + RegattaAndRaceIdentifier raceIdentifier = null; + RaceDTO race = null; + TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); + final FleetDTO fleetDTO = baseDomainFactory.convertToFleetDTO(fleet); + if (trackedRace != null) { + raceIdentifier = new RegattaNameAndRaceName(trackedRace.getTrackedRegatta().getRegatta().getName(), + trackedRace.getRace().getName()); + race = baseDomainFactory.createRaceDTO(trackedRegattaRegistry, /* withGeoLocationData */ false, raceIdentifier, trackedRace); + } + // Note: the RaceColumnDTO won't be created by the following addRace call because it has been created + // above by the result.createEmptyRaceColumn call + result.addRace(raceColumn.getName(), raceColumn.getExplicitFactor(), raceColumn.getFactor(), + raceColumn instanceof RaceColumnInSeries ? ((RaceColumnInSeries) raceColumn).getRegatta().getName() : null, + raceColumn instanceof RaceColumnInSeries ? ((RaceColumnInSeries) raceColumn).getSeries().getName() : null, + fleetDTO, raceColumn.isMedalRace(), raceIdentifier, race, isMetaLeaderboardColumn); + } + FutureTask> task = new FutureTask>( + () -> baseDomainFactory.getCompetitorDTOList(AbstractSimpleLeaderboardImpl.this.getCompetitorsFromBestToWorst(raceColumn, timePoint))); + executor.execute(task); + competitorsFromBestToWorstTasks.put(raceColumn, task); + } + // wait for the competitor orderings to have been computed for all race columns before continuing; subsequent tasks may depend on these data + for (Map.Entry>> raceColumnAndTaskToJoin : competitorsFromBestToWorstTasks.entrySet()) { + try { + result.setCompetitorsFromBestToWorst(raceColumnAndTaskToJoin.getKey().getName(), raceColumnAndTaskToJoin.getValue().get()); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } catch (ExecutionException e) { + // See also bug 1371: for stability reasons, don't let the exception percolate but rather accept + // null values. + // If new evidence is provided, a re-calculation of the leaderboard will be triggered anyway. So + // this helps robustness from a user's perspective. + logger.log( + Level.SEVERE, + AbstractSimpleLeaderboardImpl.class.getName() + ".computeDTO(" + this.getName() + ", " + + timePoint + ", " + namesOfRaceColumnsForWhichToLoadLegDetails+", addOverallDetails="+addOverallDetails + + "): exception during computing competitor ordering for race column "+raceColumnAndTaskToJoin.getKey().getName(), e); + } + } + result.setDelayToLiveInMillisForLatestRace(this.getDelayToLiveInMillis()); + result.rows = new HashMap(); + result.hasCarriedPoints = this.hasCarriedPoints(); + if (this.getResultDiscardingRule() instanceof ThresholdBasedResultDiscardingRule) { + result.discardThresholds = ((ThresholdBasedResultDiscardingRule) this.getResultDiscardingRule()) + .getDiscardIndexResultsStartingWithHowManyRaces(); + } else { + result.discardThresholds = null; + } + // Computing the competitor leg ranks is expensive, especially in live mode, in case new events keep + // invalidating the ranks cache in TrackedLegImpl. The problem then is that the sorting based on wind data is repeated for + // each competitor, leading to square effort. We therefore need to compute the leg ranks for those races where leg + // details are requested only once and pass them into getLeaderboardEntryDTO + final Map> legRanksCache = new HashMap>(); + for (final RaceColumn raceColumn : this.getRaceColumns()) { + // if details for the column are requested, cache the leg's ranks + if (namesOfRaceColumnsForWhichToLoadLegDetails != null + && namesOfRaceColumnsForWhichToLoadLegDetails.contains(raceColumn.getName())) { + for (Fleet fleet : raceColumn.getFleets()) { + TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); + if (trackedRace != null) { + trackedRace.getRace().getCourse().lockForRead(); + try { + for (TrackedLeg trackedLeg : trackedRace.getTrackedLegs()) { + legRanksCache.put(trackedLeg.getLeg(), trackedLeg.getRanks(timePoint, cache)); + } + } finally { + trackedRace.getRace().getCourse().unlockAfterRead(); + } + } + } + } + } + for (final Competitor competitor : this.getCompetitorsFromBestToWorst(timePoint)) { + CompetitorDTO competitorDTO = baseDomainFactory.convertToCompetitorDTO(competitor); + LeaderboardRowDTO row = new LeaderboardRowDTO(); + row.competitor = competitorDTO; + row.fieldsByRaceColumnName = new HashMap(); + row.carriedPoints = this.hasCarriedPoints(competitor) ? this.getCarriedPoints(competitor) : null; + row.totalPoints = this.getTotalPoints(competitor, timePoint); + if (addOverallDetails) { + addOverallDetailsToRow(timePoint, competitor, row); + } + result.competitors.add(competitorDTO); + Map> futuresForColumnName = new HashMap>(); + final Set discardedRaceColumns = getResultDiscardingRule().getDiscardedRaceColumns(competitor, this, getRaceColumns(), timePoint); + for (final RaceColumn raceColumn : this.getRaceColumns()) { + RunnableFuture future = new FutureTask(() -> { + Entry entry = AbstractSimpleLeaderboardImpl.this.getEntry(competitor, raceColumn, timePoint, discardedRaceColumns); + return getLeaderboardEntryDTO(entry, raceColumn, competitor, timePoint, + namesOfRaceColumnsForWhichToLoadLegDetails != null + && namesOfRaceColumnsForWhichToLoadLegDetails.contains(raceColumn + .getName()), waitForLatestAnalyses, legRanksCache, baseDomainFactory, + fillNetPointsUncorrected, cache); + }); + executor.execute(future); + futuresForColumnName.put(raceColumn.getName(), future); + } + for (Map.Entry> raceColumnNameAndFuture : futuresForColumnName.entrySet()) { + try { + row.fieldsByRaceColumnName.put(raceColumnNameAndFuture.getKey(), raceColumnNameAndFuture.getValue().get()); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } catch (ExecutionException e) { + // See also bug 1371: for stability reasons, don't let the exception percolate but rather accept + // null values. + // If new evidence is provided, a re-calculation of the leaderboard will be triggered anyway. So + // this helps robustness from a user's perspective. + logger.log( + Level.SEVERE, + AbstractSimpleLeaderboardImpl.class.getName() + ".computeDTO(" + this.getName() + ", " + + timePoint + ", " + namesOfRaceColumnsForWhichToLoadLegDetails+", addOverallDetails="+addOverallDetails + + "): exception during computing leaderboard entry for competitor " + + competitor.getName() + " in race column " + raceColumnNameAndFuture.getKey() + + ". Leaving empty.", e); + } + } + result.rows.put(competitorDTO, row); + String displayName = this.getDisplayName(competitor); + if (displayName != null) { + result.competitorDisplayNames.put(competitorDTO, displayName); + } + } + logger.info("computeLeaderboardByName(" + this.getName() + ", " + timePoint + ", " + + namesOfRaceColumnsForWhichToLoadLegDetails + ", addOverallDetails=" + addOverallDetails + ") took " + + (System.currentTimeMillis() - startOfRequestHandling) + "ms"); + return result; + } + + private void calculateRacesMetadata(MetaLeaderboardColumn metaLeaderboardColumn, MetaLeaderboardRaceColumnDTO columnDTO, + TrackedRegattaRegistry trackedRegattaRegistry, final DomainFactory baseDomainFactory) { + for (final RaceColumn raceColumn : metaLeaderboardColumn.getLeaderboard().getRaceColumns()) { + for (Fleet fleet : raceColumn.getFleets()) { + TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); + if (trackedRace != null) { + String regattaName = trackedRace.getTrackedRegatta().getRegatta().getName(); + String raceName = trackedRace.getRace().getName(); + RegattaAndRaceIdentifier raceIdentifier = new RegattaNameAndRaceName(regattaName, raceName); + columnDTO.addRace(new BasicRaceDTO(raceIdentifier, baseDomainFactory.createTrackedRaceDTO(trackedRace))); + } + } + } + } + + private void addOverallDetailsToRow(final TimePoint timePoint, + final Competitor competitor, LeaderboardRowDTO row) throws NoWindException { + final com.sap.sse.common.Util.Pair maximumSpeedOverGround = this.getMaximumSpeedOverGround(competitor, timePoint); + if (maximumSpeedOverGround != null && maximumSpeedOverGround.getB() != null) { + row.maximumSpeedOverGroundInKnots = maximumSpeedOverGround.getB().getKnots(); + row.whenMaximumSpeedOverGroundWasAchieved = maximumSpeedOverGround.getA().getTimePoint().asDate(); + } + Map legTypeCache = new HashMap<>(); + final Duration totalTimeSailedDownwind = this.getTotalTimeSailedInLegType(competitor, LegType.DOWNWIND, timePoint, legTypeCache); + row.totalTimeSailedDownwindInSeconds = totalTimeSailedDownwind==null?null:totalTimeSailedDownwind.asSeconds(); + final Duration totalTimeSailedUpwind = this.getTotalTimeSailedInLegType(competitor, LegType.UPWIND, timePoint, legTypeCache); + row.totalTimeSailedUpwindInSeconds = totalTimeSailedUpwind==null?null:totalTimeSailedUpwind.asSeconds(); + final Duration totalTimeSailedReaching = this.getTotalTimeSailedInLegType(competitor, LegType.REACHING, timePoint, legTypeCache); + row.totalTimeSailedReachingInSeconds = totalTimeSailedReaching==null?null:totalTimeSailedReaching.asSeconds(); + final Duration totalTimeSailed = this.getTotalTimeSailed(competitor, timePoint); + row.totalTimeSailedInSeconds = totalTimeSailed==null?null:totalTimeSailed.asSeconds(); + final Distance totalDistanceTraveledInMeters = this.getTotalDistanceTraveled(competitor, timePoint); + row.totalDistanceTraveledInMeters = totalDistanceTraveledInMeters==null?null:totalDistanceTraveledInMeters.getMeters(); + } + + /** + * @param waitForLatestAnalyses + * if false, this method is allowed to read the maneuver analysis results from a cache that + * may not reflect all data already received; otherwise, the method will always block for the latest + * cache updates to have happened before returning. + * @param fillNetPointsUncorrected + * tells if {@link LeaderboardEntryDTO#netPointsUncorrected} shall be filled; filling it is rather + * expensive, especially when compared to simply retrieving a score correction, and particularly if in a + * larger fleet a number of competitors haven't properly finished the race. This should only be used for + * leaderboard editing where a user needs to see what the uncorrected score was that would be used when + * the correction was removed. + */ + private LeaderboardEntryDTO getLeaderboardEntryDTO(Entry entry, RaceColumn raceColumn, Competitor competitor, + TimePoint timePoint, boolean addLegDetails, boolean waitForLatestAnalyses, + Map> legRanksCache, DomainFactory baseDomainFactory, + boolean fillNetPointsUncorrected, WindLegTypeAndLegBearingCache cache) { + LeaderboardEntryDTO entryDTO = new LeaderboardEntryDTO(); + TrackedRace trackedRace = raceColumn.getTrackedRace(competitor); + entryDTO.race = trackedRace == null ? null : trackedRace.getRaceIdentifier(); + entryDTO.netPoints = entry.getNetPoints(); + if (fillNetPointsUncorrected) { + entryDTO.netPointsUncorrected = entry.getNetPointsUncorrected(); + } + entryDTO.netPointsCorrected = entry.isNetPointsCorrected(); + entryDTO.totalPoints = entry.getTotalPoints(); + entryDTO.reasonForMaxPoints = entry.getMaxPointsReason(); + entryDTO.discarded = entry.isDiscarded(); + final GPSFixTrack track = trackedRace == null ? null : trackedRace.getTrack(competitor); + if (trackedRace != null) { + Date timePointOfLastPositionFixAtOrBeforeQueryTimePoint = getTimePointOfLastFixAtOrBefore(competitor, trackedRace, timePoint); + if (track != null) { + entryDTO.averageSamplingInterval = track.getAverageIntervalBetweenRawFixes(); + } + if (timePointOfLastPositionFixAtOrBeforeQueryTimePoint != null) { + long timeDifferenceInMs = timePoint.asMillis() - timePointOfLastPositionFixAtOrBeforeQueryTimePoint.getTime(); + entryDTO.timeSinceLastPositionFixInSeconds = timeDifferenceInMs == 0 ? 0.0 : timeDifferenceInMs / 1000.0; + } else { + entryDTO.timeSinceLastPositionFixInSeconds = null; + } + } + if (addLegDetails && trackedRace != null) { + try { + final RankingInfo rankingInfo = trackedRace.getRankingMetric().getRankingInfo(timePoint, cache); + RaceDetails raceDetails = getRaceDetails(trackedRace, competitor, timePoint, waitForLatestAnalyses, + legRanksCache, rankingInfo, cache); + entryDTO.legDetails = raceDetails.getLegDetails(); + entryDTO.windwardDistanceToCompetitorFarthestAheadInMeters = raceDetails.getWindwardDistanceToCompetitorFarthestAhead() == null ? null + : raceDetails.getWindwardDistanceToCompetitorFarthestAhead().getMeters(); + entryDTO.gapToLeaderInOwnTime = trackedRace.getRankingMetric().getGapToLeaderInOwnTime(rankingInfo, competitor, cache); + entryDTO.averageAbsoluteCrossTrackErrorInMeters = raceDetails.getAverageAbsoluteCrossTrackError() == null ? null + : raceDetails.getAverageAbsoluteCrossTrackError().getMeters(); + entryDTO.averageSignedCrossTrackErrorInMeters = raceDetails.getAverageSignedCrossTrackError() == null ? null + : raceDetails.getAverageSignedCrossTrackError().getMeters(); + entryDTO.calculatedTime = raceDetails.getCorrectedTime(); + entryDTO.calculatedTimeAtEstimatedArrivalAtCompetitorFarthestAhead = raceDetails.getCorrectedTimeAtEstimatedArrivalAtCompetitorFarthestAhead(); + entryDTO.gapToLeaderInOwnTime = raceDetails.getGapToLeaderInOwnTime(); + final TimePoint startOfRace = trackedRace.getStartOfRace(); + if (startOfRace != null) { + Waypoint startWaypoint = trackedRace.getRace().getCourse().getFirstWaypoint(); + NavigableSet competitorMarkPassings = trackedRace.getMarkPassings(competitor); + trackedRace.lockForRead(competitorMarkPassings); + try { + if (!competitorMarkPassings.isEmpty()) { + final MarkPassing firstMarkPassing = competitorMarkPassings.iterator().next(); + if (firstMarkPassing.getWaypoint() == startWaypoint) { + Distance distanceToStartLineFiveSecondsBeforeStartOfRace = trackedRace.getDistanceToStartLine(competitor, /*milliseconds before start*/ 5000); + entryDTO.distanceToStartLineFiveSecondsBeforeStartInMeters = distanceToStartLineFiveSecondsBeforeStartOfRace == null ? null + : distanceToStartLineFiveSecondsBeforeStartOfRace.getMeters(); + Speed speedFiveSecondsBeforeStartOfRace = trackedRace.getSpeed(competitor, /*milliseconds before start*/ 5000); + entryDTO.speedOverGroundFiveSecondsBeforeStartInKnots = speedFiveSecondsBeforeStartOfRace == null ? null + : speedFiveSecondsBeforeStartOfRace.getKnots(); + Distance distanceToStartLineAtStartOfRace = trackedRace.getDistanceToStartLine( + competitor, startOfRace); + entryDTO.distanceToStartLineAtStartOfRaceInMeters = distanceToStartLineAtStartOfRace == null ? null + : distanceToStartLineAtStartOfRace.getMeters(); + Speed speedAtStartTime = track == null ? null : track.getEstimatedSpeed(startOfRace); + entryDTO.speedOverGroundAtStartOfRaceInKnots = speedAtStartTime == null ? null + : speedAtStartTime.getKnots(); + TimePoint competitorStartTime = firstMarkPassing.getTimePoint(); + entryDTO.timeBetweenRaceStartAndCompetitorStartInSeconds = startOfRace.until(competitorStartTime).asSeconds(); + Speed competitorSpeedWhenPassingStart = track == null ? null : track + .getEstimatedSpeed(competitorStartTime); + entryDTO.speedOverGroundAtPassingStartWaypointInKnots = competitorSpeedWhenPassingStart == null ? null + : competitorSpeedWhenPassingStart.getKnots(); + try { + entryDTO.startTack = trackedRace.getTack(competitor, competitorStartTime); + } catch (NoWindException nwe) { + entryDTO.startTack = null; // leave empty in case no wind information is available + } + Distance distanceFromStarboardSideOfStartLineWhenPassingStart = trackedRace + .getDistanceFromStarboardSideOfStartLineWhenPassingStart(competitor); + entryDTO.distanceToStarboardSideOfStartLineInMeters = distanceFromStarboardSideOfStartLineWhenPassingStart == null ? null + : distanceFromStarboardSideOfStartLineWhenPassingStart.getMeters(); + } + } + } finally { + trackedRace.unlockAfterRead(competitorMarkPassings); + } + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } catch (ExecutionException e) { + throw new RuntimeException(e); // the future used to calculate the leg details was interrupted; escalate as runtime exception + } + } + final Fleet fleet = entry.getFleet(); + entryDTO.fleet = fleet == null ? null : baseDomainFactory.convertToFleetDTO(fleet); + return entryDTO; + } + + /** + * Determines the time point of the last raw fix (with outliers not removed) for competitor in + * trackedRace. If the competitor's track is null or empty, null is returned. + * @param trackedRace must not be null + * @param atOrBefore find the last fix at or before the time point specified + */ + private Date getTimePointOfLastFixAtOrBefore(Competitor competitor, TrackedRace trackedRace, TimePoint atOrBefore) { + assert trackedRace != null; + final Date timePointOfLastPositionFix; + GPSFixTrack track = trackedRace.getTrack(competitor); + if (track == null) { + timePointOfLastPositionFix = null; + } else { + GPSFixMoving lastFix = track.getLastFixAtOrBefore(atOrBefore); + if (lastFix == null) { + timePointOfLastPositionFix = null; + } else { + timePointOfLastPositionFix = lastFix.getTimePoint().asDate(); + } + } + return timePointOfLastPositionFix; + } + + private static class RaceDetails { + private final List legDetails; + private final Distance windwardDistanceToCompetitorFarthestAhead; + private final Distance averageAbsoluteCrossTrackError; + private final Distance averageSignedCrossTrackError; + private final Duration gapToLeaderInOwnTime; + private final Duration correctedTime; + private final Duration correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead; + + public RaceDetails(List legDetails, Distance windwardDistanceToCompetitorFarthestAhead, + Distance averageAbsoluteCrossTrackError, Distance averageSignedCrossTrackError, + Duration gapToLeaderInOwnTime, Duration correctedTime, + Duration correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead) { + super(); + this.legDetails = legDetails; + this.windwardDistanceToCompetitorFarthestAhead = windwardDistanceToCompetitorFarthestAhead; + this.averageAbsoluteCrossTrackError = averageAbsoluteCrossTrackError; + this.averageSignedCrossTrackError = averageSignedCrossTrackError; + this.gapToLeaderInOwnTime = gapToLeaderInOwnTime; + this.correctedTime = correctedTime; + this.correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead = correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead; + } + public List getLegDetails() { + return legDetails; + } + public Distance getWindwardDistanceToCompetitorFarthestAhead() { + return windwardDistanceToCompetitorFarthestAhead; + } + public Distance getAverageAbsoluteCrossTrackError() { + return averageAbsoluteCrossTrackError; + } + public Distance getAverageSignedCrossTrackError() { + return averageSignedCrossTrackError; + } + public Duration getGapToLeaderInOwnTime() { + return gapToLeaderInOwnTime; + } + public Duration getCorrectedTime() { + return correctedTime; + } + public Duration getCorrectedTimeAtEstimatedArrivalAtCompetitorFarthestAhead() { + return correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead; + } + } + + /** + * If timePoint is after the end of the race's tracking the query will be adjusted to obtain the values + * at the end of the {@link TrackedRace#getEndOfTracking() race's tracking time}. If the time point adjusted this + * way equals the end of the tracking time, the query results will be looked up in a cache first and if not found, + * they will be stored to the cache after calculating them. A cache invalidation {@link RaceChangeListener listener} + * will be registered with the race which will be triggered for any event received by the race. + * @param waitForLatestAnalyses + * if false, this method is allowed to read the maneuver analysis results from a cache that + * may not reflect all data already received; otherwise, the method will always block for the latest + * cache updates to have happened before returning. + */ + private RaceDetails getRaceDetails(TrackedRace trackedRace, Competitor competitor, TimePoint timePoint, + boolean waitForLatestAnalyses, Map> legRanksCache, + RankingInfo rankingInfo, WindLegTypeAndLegBearingCache cache) throws InterruptedException, ExecutionException { + final RaceDetails raceDetails; + if (trackedRace.getEndOfTracking() != null && trackedRace.getEndOfTracking().compareTo(timePoint) < 0) { + raceDetails = getRaceDetailsForEndOfTrackingFromCacheOrCalculateAndCache(trackedRace, competitor, legRanksCache, rankingInfo, cache); + } else { + raceDetails = calculateRaceDetails(trackedRace, competitor, timePoint, waitForLatestAnalyses, legRanksCache, cache, rankingInfo); + } + return raceDetails; + } + + private RaceDetails getRaceDetailsForEndOfTrackingFromCacheOrCalculateAndCache(final TrackedRace trackedRace, + final Competitor competitor, final Map> legRanksCache, + RankingInfo rankingInfo, final WindLegTypeAndLegBearingCache cache) throws InterruptedException, ExecutionException { + final com.sap.sse.common.Util.Pair key = new com.sap.sse.common.Util.Pair(trackedRace, competitor); + RunnableFuture raceDetails; + synchronized (raceDetailsAtEndOfTrackingCache) { + raceDetails = raceDetailsAtEndOfTrackingCache.get(key); + if (raceDetails == null) { + raceDetails = new FutureTask(new Callable() { + @Override + public RaceDetails call() throws Exception { + TimePoint end = trackedRace.getEndOfRace(); + if (end == null) { + end = trackedRace.getEndOfTracking(); + } + return calculateRaceDetails(trackedRace, competitor, end, + // TODO see bug 1358: for now, use waitForLatest==false until we've switched to optimistic locking for the course read lock + /* TODO old comment when it was still true: "because this is done only once after end of tracking" */ + /* waitForLatestAnalyses (maneuver and cross track error) */ false, + legRanksCache, cache, rankingInfo); + } + }); + raceDetailsExecutor.execute(raceDetails); + raceDetailsAtEndOfTrackingCache.put(key, raceDetails); + final CacheInvalidationListener cacheInvalidationListener = new CacheInvalidationListener(trackedRace, competitor); + trackedRace.addListener(cacheInvalidationListener); + cacheInvalidationListeners.add(cacheInvalidationListener); + } + } + return raceDetails.get(); + } + + private RaceDetails calculateRaceDetails(TrackedRace trackedRace, Competitor competitor, TimePoint timePoint, + boolean waitForLatestAnalyses, Map> legRanksCache, + WindLegTypeAndLegBearingCache cache, final RankingInfo rankingInfo) { + final List legDetails = new ArrayList(); + final Course course = trackedRace.getRace().getCourse(); + course.lockForRead(); // hold back any course re-configurations while looping over the legs + try { + for (Leg leg : course.getLegs()) { + LegEntryDTO legEntry; + // We loop over a copy of the course's legs; during a course change, legs may become "stale," even with + // regard to the leg/trackedLeg structures inside the tracked race which is updated by the course change + // immediately. That's why we've acquired a read lock for the course above. + TrackedLegOfCompetitor trackedLeg = trackedRace.getTrackedLeg(competitor, leg); + if (trackedLeg != null && trackedLeg.hasStartedLeg(timePoint)) { + legEntry = createLegEntry(trackedLeg, timePoint, waitForLatestAnalyses, legRanksCache, rankingInfo, cache); + } else { + legEntry = null; + } + legDetails.add(legEntry); + } + final Distance windwardDistanceToCompetitorFarthestAhead = trackedRace == null ? null : trackedRace + .getWindwardDistanceToCompetitorFarthestAhead(competitor, timePoint, WindPositionMode.LEG_MIDDLE, rankingInfo, cache); + Distance averageAbsoluteCrossTrackError; + try { + averageAbsoluteCrossTrackError = trackedRace == null ? null : trackedRace.getAverageAbsoluteCrossTrackError( + competitor, timePoint, waitForLatestAnalyses, cache); + } catch (NoWindException nwe) { + // without wind information, use null meaning "unknown" + averageAbsoluteCrossTrackError = null; + } + Distance averageSignedCrossTrackError; + try { + averageSignedCrossTrackError = trackedRace == null ? null : trackedRace.getAverageSignedCrossTrackError( + competitor, timePoint, waitForLatestAnalyses, cache); + } catch (NoWindException nwe) { + // without wind information, use null meaning "unknown" + averageSignedCrossTrackError = null; + } + final CompetitorRankingInfo competitorRankingInfo = rankingInfo.getCompetitorRankingInfo().apply(competitor); + return new RaceDetails(legDetails, windwardDistanceToCompetitorFarthestAhead, averageAbsoluteCrossTrackError, averageSignedCrossTrackError, + trackedRace.getRankingMetric().getGapToLeaderInOwnTime(rankingInfo, competitor, cache), + trackedRace.getRankingMetric().getCorrectedTime(competitor, timePoint), + competitorRankingInfo == null ? null : competitorRankingInfo.getCorrectedTimeAtEstimatedArrivalAtCompetitorFarthestAhead()); + } finally { + course.unlockAfterRead(); + } + } + + private LegEntryDTO createLegEntry(TrackedLegOfCompetitor trackedLeg, TimePoint timePoint, + boolean waitForLatestAnalyses, Map> legRanksCache, + RankingInfo rankingInfo, WindLegTypeAndLegBearingCache cache) { + LegEntryDTO result; + final Duration time = trackedLeg.getTime(timePoint); + if (trackedLeg == null || time == null) { + result = null; + } else { + result = new LegEntryDTO(); + try { + result.legType = trackedLeg.getTrackedLeg().getLegType(timePoint); + } catch (NoWindException nwe) { + result.legType = null; // can't determine leg type without wind data + } + final Speed averageSpeedOverGround = trackedLeg.getAverageSpeedOverGround(timePoint); + result.averageSpeedOverGroundInKnots = averageSpeedOverGround == null ? null : averageSpeedOverGround.getKnots(); + Distance averageAbsoluteCrossTrackError; + try { + averageAbsoluteCrossTrackError = trackedLeg.getAverageAbsoluteCrossTrackError(timePoint, waitForLatestAnalyses); + } catch (NoWindException nwe) { + // leave averageAbsoluteCrossTrackError as null, meaning "unknown" + averageAbsoluteCrossTrackError = null; + } + result.averageAbsoluteCrossTrackErrorInMeters = averageAbsoluteCrossTrackError == null ? null : averageAbsoluteCrossTrackError.getMeters(); + Distance averageSignedCrossTrackError; + try { + averageSignedCrossTrackError = trackedLeg.getAverageSignedCrossTrackError(timePoint, waitForLatestAnalyses); + } catch (NoWindException nwe) { + // leave averageSignedCrossTrackError as null, meaning "unknown" + averageSignedCrossTrackError = null; + } + result.averageSignedCrossTrackErrorInMeters = averageSignedCrossTrackError == null ? null : averageSignedCrossTrackError.getMeters(); + Double speedOverGroundInKnots; + if (trackedLeg.hasFinishedLeg(timePoint)) { + speedOverGroundInKnots = averageSpeedOverGround == null ? null : averageSpeedOverGround.getKnots(); + } else { + final SpeedWithBearing speedOverGround = trackedLeg.getSpeedOverGround(timePoint); + speedOverGroundInKnots = speedOverGround == null ? null : speedOverGround.getKnots(); + } + result.currentSpeedOverGroundInKnots = speedOverGroundInKnots == null ? null : speedOverGroundInKnots; + Distance distanceTraveled = trackedLeg.getDistanceTraveled(timePoint); + result.distanceTraveledInMeters = distanceTraveled == null ? null : distanceTraveled.getMeters(); + Distance distanceTraveledConsideringGateStart = trackedLeg.getDistanceTraveledConsideringGateStart(timePoint); + result.distanceTraveledIncludingGateStartInMeters = distanceTraveledConsideringGateStart == null ? null : distanceTraveledConsideringGateStart.getMeters(); + final Duration estimatedTimeToNextMarkInSeconds = trackedLeg.getEstimatedTimeToNextMark(timePoint, WindPositionMode.EXACT, cache); + result.estimatedTimeToNextWaypointInSeconds = estimatedTimeToNextMarkInSeconds==null?null:estimatedTimeToNextMarkInSeconds.asSeconds(); + result.timeInMilliseconds = time.asMillis(); + result.finished = trackedLeg.hasFinishedLeg(timePoint); + final TimePoint legFinishTime = trackedLeg.getFinishTime(); + result.correctedTotalTime = trackedLeg.hasStartedLeg(timePoint) ? trackedLeg.getTrackedLeg().getTrackedRace().getRankingMetric().getCorrectedTime(trackedLeg.getCompetitor(), + trackedLeg.hasFinishedLeg(timePoint) ? legFinishTime : timePoint, cache) : null; + // fetch the leg gap in own corrected time from the ranking metric + final Duration gapToLeaderInOwnTime = trackedLeg.getTrackedLeg().getTrackedRace().getRankingMetric(). + getLegGapToLegLeaderInOwnTime(trackedLeg, timePoint, rankingInfo, cache); + result.gapToLeaderInSeconds = gapToLeaderInOwnTime == null ? null : gapToLeaderInOwnTime.asSeconds(); + if (result.gapToLeaderInSeconds != null) { + final Duration gapAtEndOfPreviousLeg = getGapAtEndOfPreviousLeg(trackedLeg, rankingInfo, cache); + if (gapAtEndOfPreviousLeg != null) { + result.gapChangeSinceLegStartInSeconds = result.gapToLeaderInSeconds - gapAtEndOfPreviousLeg.asSeconds(); + } + } + LinkedHashMap legRanks = legRanksCache.get(trackedLeg.getLeg()); + if (legRanks != null) { + result.rank = legRanks.get(trackedLeg.getCompetitor()); + } else { + result.rank = trackedLeg.getRank(timePoint, cache); + } + result.started = trackedLeg.hasStartedLeg(timePoint); + Speed velocityMadeGood; + if (trackedLeg.hasFinishedLeg(timePoint)) { + velocityMadeGood = trackedLeg.getAverageVelocityMadeGood(timePoint); + } else { + velocityMadeGood = trackedLeg.getVelocityMadeGood(timePoint, WindPositionMode.EXACT); + } + result.velocityMadeGoodInKnots = velocityMadeGood == null ? null : velocityMadeGood.getKnots(); + Distance windwardDistanceToGo = trackedLeg.getWindwardDistanceToGo(timePoint, WindPositionMode.LEG_MIDDLE); + result.windwardDistanceToGoInMeters = windwardDistanceToGo == null ? null : windwardDistanceToGo + .getMeters(); + final TimePoint startOfRace = trackedLeg.getTrackedLeg().getTrackedRace().getStartOfRace(); + if (startOfRace != null && trackedLeg.hasStartedLeg(timePoint)) { + // not using trackedLeg.getManeuvers(...) because it may not catch the mark passing maneuver starting this leg + // because that may have been detected as slightly before the mark passing time, hence associated with the previous leg + List maneuvers = trackedLeg.getTrackedLeg().getTrackedRace() + .getManeuvers(trackedLeg.getCompetitor(), startOfRace, timePoint, waitForLatestAnalyses); + if (maneuvers != null) { + result.numberOfManeuvers = new HashMap(); + result.numberOfManeuvers.put(ManeuverType.TACK, 0); + result.numberOfManeuvers.put(ManeuverType.JIBE, 0); + result.numberOfManeuvers.put(ManeuverType.PENALTY_CIRCLE, 0); + Map totalManeuverLossInMeters = new HashMap(); + totalManeuverLossInMeters.put(ManeuverType.TACK, 0.0); + totalManeuverLossInMeters.put(ManeuverType.JIBE, 0.0); + totalManeuverLossInMeters.put(ManeuverType.PENALTY_CIRCLE, 0.0); + TimePoint startOfLeg = trackedLeg.getStartTime(); + for (Maneuver maneuver : maneuvers) { + // don't count maneuvers that were in previous legs + switch (maneuver.getType()) { + case TACK: + case JIBE: + case PENALTY_CIRCLE: + if (!maneuver.getTimePoint().before(startOfLeg) && (legFinishTime == null || legFinishTime.after(timePoint) || + maneuver.getTimePoint().before(legFinishTime))) { + if (maneuver.getManeuverLoss() != null) { + result.numberOfManeuvers.put(maneuver.getType(), + result.numberOfManeuvers.get(maneuver.getType()) + 1); + totalManeuverLossInMeters.put(maneuver.getType(), + totalManeuverLossInMeters.get(maneuver.getType()) + + maneuver.getManeuverLoss().getMeters()); + } + } + break; + case MARK_PASSING: + // analyze all mark passings, not only those after this leg's start, to catch the mark passing + // maneuver starting this leg, even if its time point is slightly before the mark passing starting this leg + MarkPassingManeuver mpm = (MarkPassingManeuver) maneuver; + if (mpm.getWaypointPassed() == trackedLeg.getLeg().getFrom()) { + result.sideToWhichMarkAtLegStartWasRounded = mpm.getSide(); + } + break; + default: + /* Do nothing here. + * Throwing an exception destroys the toggling (and maybe other behaviour) of the leaderboard. + */ + } + } + result.averageManeuverLossInMeters = new HashMap(); + for (ManeuverType maneuverType : new ManeuverType[] { ManeuverType.TACK, ManeuverType.JIBE, + ManeuverType.PENALTY_CIRCLE }) { + if (result.numberOfManeuvers.get(maneuverType) != 0) { + result.averageManeuverLossInMeters.put( + maneuverType, + totalManeuverLossInMeters.get(maneuverType) + / result.numberOfManeuvers.get(maneuverType)); + } + } + } + } + } + return result; + } + + private Duration getGapAtEndOfPreviousLeg(TrackedLegOfCompetitor trackedLeg, final RankingInfo rankingInfo, WindLegTypeAndLegBearingCache cache) { + final Duration result; + final Course course = trackedLeg.getTrackedLeg().getTrackedRace().getRace().getCourse(); + // if trackedLeg is the first leg, compute the gap at the start of this leg; otherwise, compute gap + // at the end of the previous leg + final TimePoint timePoint = trackedLeg.getStartTime(); + final TrackedLegOfCompetitor tloc; + if (course.getFirstWaypoint() == trackedLeg.getLeg().getFrom()) { + tloc = trackedLeg; + } else { + tloc = trackedLeg.getTrackedLeg().getTrackedRace().getTrackedLegFinishingAt(trackedLeg.getLeg().getFrom()) + .getTrackedLeg(trackedLeg.getCompetitor()); + } + result = trackedLeg.getTrackedLeg().getTrackedRace().getRankingMetric().getLegGapToLegLeaderInOwnTime(tloc, timePoint, rankingInfo, cache); + return result; + } + + private LeaderboardDTO getLiveLeaderboard(Collection namesOfRaceColumnsForWhichToLoadLegDetails, + boolean addOverallDetails, TrackedRegattaRegistry trackedRegattaRegistry, DomainFactory baseDomainFactory) throws NoWindException, ExecutionException { + LiveLeaderboardUpdater liveLeaderboardUpdater = getLiveLeaderboardUpdater(trackedRegattaRegistry, + baseDomainFactory); + return liveLeaderboardUpdater.getLiveLeaderboard(namesOfRaceColumnsForWhichToLoadLegDetails, addOverallDetails); + } + + private LiveLeaderboardUpdater getLiveLeaderboardUpdater(TrackedRegattaRegistry trackedRegattaRegistry, + DomainFactory baseDomainFactory) { + LiveLeaderboardUpdater result = this.liveLeaderboardUpdater; + if (result == null) { + synchronized (this) { + result = this.liveLeaderboardUpdater; + if (result == null) { + this.liveLeaderboardUpdater = new LiveLeaderboardUpdater(this, trackedRegattaRegistry, baseDomainFactory); + result = this.liveLeaderboardUpdater; + } + } + } + return result; + } + + private LeaderboardDTOCache getLeaderboardDTOCache() { + LeaderboardDTOCache result = this.leaderboardDTOCache; + if (result == null) { + synchronized (this) { + result = this.leaderboardDTOCache; + if (result == null) { + // The leaderboard cache is invalidated upon all competitor and mark position changes; some analyzes + // are pretty expensive, such as the maneuver re-calculation. Waiting for the latest analysis after only a + // single fix was updated is too expensive if users use the replay feature while a race is still running. + // Therefore, using waitForLatestAnalyses==false seems appropriate here. + this.leaderboardDTOCache = new LeaderboardDTOCache(/* waitForLatestAnalyses */false, this); + result = this.leaderboardDTOCache; + } + } + } + return result; + } + + @Override + public LeaderboardDTO getLeaderboardDTO(TimePoint timePoint, + Collection namesOfRaceColumnsForWhichToLoadLegDetails, boolean addOverallDetails, + TrackedRegattaRegistry trackedRegattaRegistry, DomainFactory baseDomainFactory, boolean fillNetPointsUncorrected) throws NoWindException, + InterruptedException, ExecutionException { + LeaderboardDTO result = null; + if (timePoint == null) { + // timePoint==null means live mode; however, if we're after the end of all races and after all score + // corrections, don't use the live leaderboard updater which would keep re-calculating over and over again, but map + // this to a usual non-live call which uses the regular LeaderboardDTOCache which is invalidated properly + // when the tracked race associations or score corrections or tracked race contents changes: + final TimePoint nowMinusDelay = this.getNowMinusDelay(); + final TimePoint timePointOfLatestModification = this.getTimePointOfLatestModification(); + if (fillNetPointsUncorrected || (timePointOfLatestModification != null && !nowMinusDelay.before(timePointOfLatestModification))) { + // if there hasn't been any modification to the leaderboard since nowMinusDelay, use non-live mode + // and pull the result from the regular leaderboard cache: + timePoint = timePointOfLatestModification; + } else { + // don't use the regular leaderboard cache; the race still seems to be on; use the live leaderboard updater instead: + timePoint = null; + result = this.getLiveLeaderboard(namesOfRaceColumnsForWhichToLoadLegDetails, addOverallDetails, trackedRegattaRegistry, baseDomainFactory); + } + } + if (timePoint != null) { + if (fillNetPointsUncorrected) { + // explicitly filling the uncorrected net points requires uncached recalculation + result = computeDTO(timePoint, namesOfRaceColumnsForWhichToLoadLegDetails, addOverallDetails, /* waitForLatestAnalyses */ true, + trackedRegattaRegistry, baseDomainFactory, fillNetPointsUncorrected); + } else { + // in replay we'd like up-to-date results; they are still cached + // which is OK because the cache is invalidated whenever any of the tracked races attached to the + // leaderboard changes. + result = getLeaderboardDTOCache().getLeaderboardByName(timePoint, + namesOfRaceColumnsForWhichToLoadLegDetails, addOverallDetails, baseDomainFactory, + trackedRegattaRegistry); + } + } + return result; + } + + public String toString() { + return getName() + " " + (getDefaultCourseArea() != null ? getDefaultCourseArea().getName() : "") + " " + (getScoringScheme() != null ? getScoringScheme().getType().name() : ""); + } + + @Override + public NumberOfCompetitorsInLeaderboardFetcher getNumberOfCompetitorsInLeaderboardFetcher() { + return new NumberOfCompetitorsFetcherImpl(); + } + + protected abstract LeaderboardType getLeaderboardType(); + +} diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/DelayedLeaderboardCorrectionsImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/DelayedLeaderboardCorrectionsImpl.java index 15e2db0a7cf..067c30d255d 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/DelayedLeaderboardCorrectionsImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/DelayedLeaderboardCorrectionsImpl.java @@ -278,6 +278,10 @@ public class DelayedLeaderboardCorrectionsImpl implements DelayedLeaderboardCorr public void isMedalRaceChanged(RaceColumn raceColumn, boolean newIsMedalRace) { } + @Override + public void isFleetsCanRunInParallelChanged(RaceColumn raceColumn, boolean newIsFleetsCanRunInParallel) { + } + @Override public void isStartsWithZeroScoreChanged(RaceColumn raceColumn, boolean newIsStartsWithZeroScore) { } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/MetaLeaderboardColumn.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/MetaLeaderboardColumn.java index f9016dbef70..85f2cfa40fe 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/MetaLeaderboardColumn.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/MetaLeaderboardColumn.java @@ -121,6 +121,11 @@ public class MetaLeaderboardColumn extends SimpleAbstractRaceColumn implements R getRaceColumnListeners().notifyListenersAboutIsMedalRaceChanged(raceColumn, newIsMedalRace); } + @Override + public void isFleetsCanRunInParallelChanged(RaceColumn raceColumn, boolean newIsFleetsCanRunInParallel) { + getRaceColumnListeners().notifyListenersAboutIsFleetsCanRunInParallelChanged(raceColumn, newIsFleetsCanRunInParallel); + } + @Override public void isStartsWithZeroScoreChanged(RaceColumn raceColumn, boolean newIsStartsWithZeroScore) { getRaceColumnListeners().notifyListenersAboutIsStartsWithZeroScoreChanged(raceColumn, newIsStartsWithZeroScore); diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/util/impl/RaceColumnListeners.java b/java/com.sap.sailing.domain/src/com/sap/sailing/util/impl/RaceColumnListeners.java index 92629e3aaca..c138fdf058b 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/util/impl/RaceColumnListeners.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/util/impl/RaceColumnListeners.java @@ -86,6 +86,12 @@ public class RaceColumnListeners implements Serializable { } } + public void notifyListenersAboutIsFleetsCanRunInParallelChanged(RaceColumn raceColumn, boolean newIsFleetsCanRunInParallel) { + for (RaceColumnListener listener : getRaceColumnListeners()) { + listener.isFleetsCanRunInParallelChanged(raceColumn, newIsFleetsCanRunInParallel); + } + } + public void notifyListenersAboutIsStartsWithZeroScoreChanged(RaceColumn raceColumn, boolean newIsStartsWithZeroScore) { for (RaceColumnListener listener : getRaceColumnListeners()) { listener.isStartsWithZeroScoreChanged(raceColumn, newIsStartsWithZeroScore); diff --git a/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/PingMarkViaRegattaLogTest.java b/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/PingMarkViaRegattaLogTest.java index 8809a8d1916..08a42409376 100644 --- a/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/PingMarkViaRegattaLogTest.java +++ b/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/PingMarkViaRegattaLogTest.java @@ -1,53 +1,53 @@ -package com.sap.sailing.gwt.ui.test; - -import java.util.Collections; -import java.util.UUID; - -import org.junit.Before; -import org.junit.Test; - -import com.sap.sailing.domain.base.Fleet; -import com.sap.sailing.domain.base.Regatta; -import com.sap.sailing.domain.base.Series; -import com.sap.sailing.domain.base.impl.FleetImpl; -import com.sap.sailing.domain.base.impl.RegattaImpl; -import com.sap.sailing.domain.base.impl.SeriesImpl; -import com.sap.sailing.domain.common.Position; -import com.sap.sailing.domain.common.RankingMetrics; -import com.sap.sailing.domain.common.impl.DegreePosition; -import com.sap.sailing.domain.common.racelog.tracking.DoesNotHaveRegattaLogException; -import com.sap.sailing.domain.leaderboard.RegattaLeaderboard; -import com.sap.sailing.domain.leaderboard.impl.HighPoint; -import com.sap.sailing.domain.ranking.RankingMetricsFactory; -import com.sap.sailing.gwt.ui.shared.MarkDTO; -import com.sap.sailing.server.RacingEventService; - -public class PingMarkViaRegattaLogTest { - private SailingServiceImplMock sailingService; - private RacingEventService service; - private final String columnName = "column"; - private final Fleet fleet = new FleetImpl("fleet"); - - @Before - public void prepare() { - sailingService = new SailingServiceImplMock(); - service = sailingService.getRacingEventService(); - } - - @Test - public void testPinging() throws DoesNotHaveRegattaLogException { - service.getMongoObjectFactory().getDatabase().dropDatabase(); - Series series = new SeriesImpl("series", false, Collections.singletonList(fleet), - Collections.singletonList(columnName), service); - Regatta regatta = service.createRegatta(RegattaImpl.getDefaultName("regatta", "Laser"), "Laser", /*startDate*/ null, /*endDate*/ null, - UUID.randomUUID(), Collections.singletonList(series), - false, new HighPoint(), UUID.randomUUID(), /* useStartTimeInference */ true, - RankingMetricsFactory.getRankingMetricConstructor(RankingMetrics.ONE_DESIGN)); - RegattaLeaderboard leaderboard = service.addRegattaLeaderboard(regatta.getRegattaIdentifier(), "RegattaLeaderboard", new int[] {}); - - MarkDTO mark = new MarkDTO("mark", "mark"); - Position position = new DegreePosition(30, 40); - - sailingService.pingMark(leaderboard.getName(), mark, position); - } -} +package com.sap.sailing.gwt.ui.test; + +import java.util.Collections; +import java.util.UUID; + +import org.junit.Before; +import org.junit.Test; + +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.Regatta; +import com.sap.sailing.domain.base.Series; +import com.sap.sailing.domain.base.impl.FleetImpl; +import com.sap.sailing.domain.base.impl.RegattaImpl; +import com.sap.sailing.domain.base.impl.SeriesImpl; +import com.sap.sailing.domain.common.Position; +import com.sap.sailing.domain.common.RankingMetrics; +import com.sap.sailing.domain.common.impl.DegreePosition; +import com.sap.sailing.domain.common.racelog.tracking.DoesNotHaveRegattaLogException; +import com.sap.sailing.domain.leaderboard.RegattaLeaderboard; +import com.sap.sailing.domain.leaderboard.impl.HighPoint; +import com.sap.sailing.domain.ranking.RankingMetricsFactory; +import com.sap.sailing.gwt.ui.shared.MarkDTO; +import com.sap.sailing.server.RacingEventService; + +public class PingMarkViaRegattaLogTest { + private SailingServiceImplMock sailingService; + private RacingEventService service; + private final String columnName = "column"; + private final Fleet fleet = new FleetImpl("fleet"); + + @Before + public void prepare() { + sailingService = new SailingServiceImplMock(); + service = sailingService.getRacingEventService(); + } + + @Test + public void testPinging() throws DoesNotHaveRegattaLogException { + service.getMongoObjectFactory().getDatabase().dropDatabase(); + Series series = new SeriesImpl("series", false, /* isFleetsCanRunInParallel */ true, Collections.singletonList(fleet), + Collections.singletonList(columnName), service); + Regatta regatta = service.createRegatta(RegattaImpl.getDefaultName("regatta", "Laser"), "Laser", /*startDate*/ null, /*endDate*/ null, + UUID.randomUUID(), Collections.singletonList(series), + false, new HighPoint(), UUID.randomUUID(), /* useStartTimeInference */ true, + RankingMetricsFactory.getRankingMetricConstructor(RankingMetrics.ONE_DESIGN)); + RegattaLeaderboard leaderboard = service.addRegattaLeaderboard(regatta.getRegattaIdentifier(), "RegattaLeaderboard", new int[] {}); + + MarkDTO mark = new MarkDTO("mark", "mark"); + Position position = new DegreePosition(30, 40); + + sailingService.pingMark(leaderboard.getName(), mark, position); + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/server/RaceContext.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/server/RaceContext.java index 08236de9e4b..edc8421f2b9 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/server/RaceContext.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/server/RaceContext.java @@ -293,7 +293,7 @@ public class RaceContext { case FINISHING: case RUNNING: case SCHEDULED: - case PRESCHEDULED: + case PRESCHEDULED: case STARTPHASE: case UNKNOWN: break; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CreateRegattaCallback.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CreateRegattaCallback.java index 628bb577a3c..0908b933709 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CreateRegattaCallback.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CreateRegattaCallback.java @@ -58,7 +58,7 @@ public class CreateRegattaCallback implements DialogCallback{ LinkedHashMap seriesStructure = new LinkedHashMap(); for (SeriesDTO seriesDTO : newRegatta.series) { SeriesCreationParametersDTO seriesPair = new SeriesCreationParametersDTO(seriesDTO.getFleets(), - seriesDTO.isMedal(), seriesDTO.isStartsWithZeroScore(), + seriesDTO.isMedal(), seriesDTO.isFleetsCanRunInParallel(), seriesDTO.isStartsWithZeroScore(), seriesDTO.isFirstColumnIsNonDiscardableCarryForward(), seriesDTO.getDiscardThresholds(), seriesDTO.hasSplitFleetContiguousScoring()); seriesStructure.put(seriesDTO.getName(), seriesPair); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaDetailsComposite.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaDetailsComposite.java index 7303c1479cc..cdf9d168b85 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaDetailsComposite.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaDetailsComposite.java @@ -120,41 +120,43 @@ public class RegattaDetailsComposite extends Composite { private CellTable createRegattaSeriesTable() { CellTable table = new BaseCelltable(/* pageSize */10000, tableRes); table.setWidth("100%"); - TextColumn seriesNameColumn = new TextColumn() { @Override public String getValue(SeriesDTO series) { return series.getName(); } }; - TextColumn isMedalSeriesColumn = new TextColumn() { @Override public String getValue(SeriesDTO series) { return series.isMedal() ? stringMessages.yes() : stringMessages.no(); } }; - TextColumn startsWithZeroScoreColumn = new TextColumn() { @Override public String getValue(SeriesDTO series) { return series.isStartsWithZeroScore() ? stringMessages.yes() : stringMessages.no(); } }; - TextColumn hasSplitFleetContiguousScoringColumn = new TextColumn() { @Override public String getValue(SeriesDTO series) { return series.hasSplitFleetContiguousScoring() ? stringMessages.yes() : stringMessages.no(); } }; - TextColumn isFirstColumnIsNonDiscardableCarryForwardColumn = new TextColumn() { @Override public String getValue(SeriesDTO series) { return series.isFirstColumnIsNonDiscardableCarryForward() ? stringMessages.yes() : stringMessages.no(); } }; + TextColumn isFleetsCanRunInParallelColumn = new TextColumn() { + @Override + public String getValue(SeriesDTO series) { + return series.isFleetsCanRunInParallel() ? stringMessages.yes() : stringMessages.no(); + } + }; + TextColumn racesColumn = new TextColumn() { @Override @@ -253,6 +255,7 @@ public class RegattaDetailsComposite extends Composite { table.addColumn(isFirstColumnIsNonDiscardableCarryForwardColumn, stringMessages.firstRaceIsNonDiscardableCarryForward()); table.addColumn(startsWithZeroScoreColumn, stringMessages.startsWithZeroScore()); table.addColumn(hasSplitFleetContiguousScoringColumn, stringMessages.hasSplitFleetContiguousScoring()); + table.addColumn(isFleetsCanRunInParallelColumn, stringMessages.canFleetsRunInParallel()); table.addColumn(seriesActionColumn, stringMessages.actions()); return table; @@ -287,6 +290,7 @@ public class RegattaDetailsComposite extends Composite { final SeriesDTO series = seriesDescriptor.getSeries(); final List newRaceColumns = seriesDescriptor.getRaces(); final boolean isMedalChanged = series.isMedal() != seriesDescriptor.isMedal(); + final boolean isFleetsCanRunInParallelChanged = series.isFleetsCanRunInParallel() != seriesDescriptor.isFleetsCanRunInParallel(); final boolean isStartsWithZeroScoreChanged = series.isStartsWithZeroScore() != seriesDescriptor.isStartsWithZeroScore(); final boolean isFirstColumnIsNonDiscardableCarryForwardChanged = series.isFirstColumnIsNonDiscardableCarryForward() != seriesDescriptor.isFirstColumnIsNonDiscardableCarryForward(); final boolean hasSplitFleetContiguousScoringChanged = series.hasSplitFleetContiguousScoring() != seriesDescriptor.hasSplitFleetContiguousScoring(); @@ -350,11 +354,11 @@ public class RegattaDetailsComposite extends Composite { }); } }); - if (isMedalChanged || seriesResultDiscardingThresholdsChanged || isStartsWithZeroScoreChanged + if (isMedalChanged || isFleetsCanRunInParallelChanged || seriesResultDiscardingThresholdsChanged || isStartsWithZeroScoreChanged || isFirstColumnIsNonDiscardableCarryForwardChanged || hasSplitFleetContiguousScoringChanged || seriesNameChanged) { sailingService.updateSeries(regattaIdentifier, series.getName(), seriesDescriptor.getSeriesName(), - seriesDescriptor.isMedal(), seriesDescriptor.getResultDiscardingThresholds(), + seriesDescriptor.isMedal(), seriesDescriptor.isFleetsCanRunInParallel(), seriesDescriptor.getResultDiscardingThresholds(), seriesDescriptor.isStartsWithZeroScore(), seriesDescriptor.isFirstColumnIsNonDiscardableCarryForward(), seriesDescriptor.hasSplitFleetContiguousScoring(), series.getFleets(), diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java index ba20696021a..1b2e45f1b51 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java @@ -309,7 +309,7 @@ public class RegattaListComposite extends Composite implements RegattasDisplayer if (seriesIter.hasNext()) { final SeriesDTO series = seriesIter.next(); sailingService.updateSeries(regattaName, series.getName(), series.getName(), series.isMedal(), - series.getDiscardThresholds(), series.isStartsWithZeroScore(), + series.isFleetsCanRunInParallel(), series.getDiscardThresholds(), series.isStartsWithZeroScore(), series.isFirstColumnIsNonDiscardableCarryForward(), series.hasSplitFleetContiguousScoring(), series.getFleets(), new MarkedAsyncCallback(new AsyncCallback() { @Override diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SeriesDescriptor.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SeriesDescriptor.java index 80bfa4ce806..8f2e0bf25f1 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SeriesDescriptor.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SeriesDescriptor.java @@ -1,69 +1,75 @@ -package com.sap.sailing.gwt.ui.adminconsole; - -import java.util.List; - -import com.sap.sailing.domain.common.dto.RaceColumnDTO; -import com.sap.sailing.gwt.ui.shared.SeriesDTO; - -/** - * Used by {@link SeriesEditDialog} to capture the result of the user's configuration changes, - * including the race columns that the series shall define, the medal status and the discarding rules, if any. - * - * @author Axel Uhl (d043530) - * - */ -public class SeriesDescriptor { - private final SeriesDTO series; - private final List races; - private final String seriesName; - private final boolean isMedal; - private final boolean startsWithZeroScore; - private final boolean hasSplitFleetContiguousScoring; - private final boolean firstColumnIsNonDiscardableCarryForward; - private final int[] resultDiscardingThresholds; - - public SeriesDescriptor(SeriesDTO series, String seriesName, List races, boolean isMedal, - int[] resultDiscardingThresholds, boolean startsWithZeroScore, - boolean firstColumnIsNonDiscardableCarryForward, boolean hasSplitFleetContiguousScoring) { - this.seriesName = seriesName; - this.series = series; - this.races = races; - this.isMedal = isMedal; - this.startsWithZeroScore = startsWithZeroScore; - this.hasSplitFleetContiguousScoring = hasSplitFleetContiguousScoring; - this.resultDiscardingThresholds = resultDiscardingThresholds; - this.firstColumnIsNonDiscardableCarryForward = firstColumnIsNonDiscardableCarryForward; - } - - public SeriesDTO getSeries() { - return series; - } - - public String getSeriesName() { - return seriesName; - } - - public List getRaces() { - return races; - } - - public boolean isMedal() { - return isMedal; - } - - public int[] getResultDiscardingThresholds() { - return resultDiscardingThresholds; - } - - public boolean isStartsWithZeroScore() { - return startsWithZeroScore; - } - - public boolean hasSplitFleetContiguousScoring() { - return hasSplitFleetContiguousScoring; - } - - public boolean isFirstColumnIsNonDiscardableCarryForward() { - return firstColumnIsNonDiscardableCarryForward; - } -} +package com.sap.sailing.gwt.ui.adminconsole; + +import java.util.List; + +import com.sap.sailing.domain.common.dto.RaceColumnDTO; +import com.sap.sailing.gwt.ui.shared.SeriesDTO; + +/** + * Used by {@link SeriesEditDialog} to capture the result of the user's configuration changes, + * including the race columns that the series shall define, the medal status and the discarding rules, if any. + * + * @author Axel Uhl (d043530) + * + */ +public class SeriesDescriptor { + private final SeriesDTO series; + private final List races; + private final String seriesName; + private final boolean isMedal; + private final boolean isFleetsCanRunInParallel; + private final boolean startsWithZeroScore; + private final boolean hasSplitFleetContiguousScoring; + private final boolean firstColumnIsNonDiscardableCarryForward; + private final int[] resultDiscardingThresholds; + + public SeriesDescriptor(SeriesDTO series, String seriesName, List races, boolean isMedal, boolean isFleetsCanRunInParallel, + int[] resultDiscardingThresholds, boolean startsWithZeroScore, + boolean firstColumnIsNonDiscardableCarryForward, boolean hasSplitFleetContiguousScoring) { + this.seriesName = seriesName; + this.series = series; + this.races = races; + this.isMedal = isMedal; + this.isFleetsCanRunInParallel = isFleetsCanRunInParallel; + this.startsWithZeroScore = startsWithZeroScore; + this.hasSplitFleetContiguousScoring = hasSplitFleetContiguousScoring; + this.resultDiscardingThresholds = resultDiscardingThresholds; + this.firstColumnIsNonDiscardableCarryForward = firstColumnIsNonDiscardableCarryForward; + } + + public SeriesDTO getSeries() { + return series; + } + + public String getSeriesName() { + return seriesName; + } + + public List getRaces() { + return races; + } + + public boolean isMedal() { + return isMedal; + } + + public boolean isFleetsCanRunInParallel() { + return isFleetsCanRunInParallel; + } + + public int[] getResultDiscardingThresholds() { + return resultDiscardingThresholds; + } + + public boolean isStartsWithZeroScore() { + return startsWithZeroScore; + } + + public boolean hasSplitFleetContiguousScoring() { + return hasSplitFleetContiguousScoring; + } + + public boolean isFirstColumnIsNonDiscardableCarryForward() { + return firstColumnIsNonDiscardableCarryForward; + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SeriesEditDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SeriesEditDialog.java index 5e5a0d1952c..aba2040308e 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SeriesEditDialog.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SeriesEditDialog.java @@ -39,6 +39,7 @@ import com.sap.sse.gwt.client.dialog.DataEntryDialog; public class SeriesEditDialog extends DataEntryDialog { private TextBox seriesNameTextBox; private CheckBox isMedalCheckbox; + private CheckBox fleetsCanRunInParallelCheckbox; private CheckBox startWithZeroScoreCheckbox; private CheckBox hasSplitFleetContiguousScoringCheckbox; private CheckBox firstColumnIsNonDiscardableCarryForwardCheckbox; @@ -133,6 +134,7 @@ public class SeriesEditDialog extends DataEntryDialog { races.add(raceColumnDTO); } return new SeriesDescriptor(selectedSeries, seriesNameTextBox.getValue(), races, isMedalCheckbox.getValue(), + fleetsCanRunInParallelCheckbox.getValue(), useSeriesResultDiscardingThresholdsCheckbox.getValue() ? discardThresholdBoxes.getDiscardThresholds() : null, startWithZeroScoreCheckbox.getValue(), firstColumnIsNonDiscardableCarryForwardCheckbox.getValue(), hasSplitFleetContiguousScoringCheckbox.getValue()); @@ -171,7 +173,12 @@ public class SeriesEditDialog extends DataEntryDialog { isMedalCheckbox.ensureDebugId("MedalSeriesCheckbox"); isMedalCheckbox.setValue(selectedSeries.isMedal()); additionalWidgetPanel.add(isMedalCheckbox); - + + fleetsCanRunInParallelCheckbox = createCheckbox(stringMessages.canFleetsRunInParallel()); + fleetsCanRunInParallelCheckbox.ensureDebugId("FleetsCanRaceInParallelSeriesCheckbox"); + fleetsCanRunInParallelCheckbox.setValue(selectedSeries.isFleetsCanRunInParallel()); + additionalWidgetPanel.add(fleetsCanRunInParallelCheckbox); + startWithZeroScoreCheckbox = createCheckbox(stringMessages.startsWithZeroScore()); startWithZeroScoreCheckbox.ensureDebugId("StartsWithZeroScoreCheckbox"); startWithZeroScoreCheckbox.setValue(selectedSeries.isStartsWithZeroScore()); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SeriesWithFleetsCreateDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SeriesWithFleetsCreateDialog.java index 1972066466d..abcfad84eb2 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SeriesWithFleetsCreateDialog.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SeriesWithFleetsCreateDialog.java @@ -34,6 +34,7 @@ public class SeriesWithFleetsCreateDialog extends DataEntryDialog { protected final TextBox nameEntryField; protected CheckBox isMedalSeriesCheckbox; + protected CheckBox fleetsCanRunInParallelCheckbox; protected CheckBox startsWithZeroScoreCheckbox; protected CheckBox hasSplitFleetContiguousScoringCheckbox; protected CheckBox firstColumnIsNonDiscardableCarryForwardCheckbox; @@ -122,6 +123,10 @@ public class SeriesWithFleetsCreateDialog extends DataEntryDialog { isMedalSeriesCheckbox = createCheckbox(stringMessages.medalSeries()); isMedalSeriesCheckbox.ensureDebugId("MedalSeriesCheckbox"); + fleetsCanRunInParallelCheckbox = createCheckbox(stringMessages.canFleetsRunInParallel()); + fleetsCanRunInParallelCheckbox.setValue(true); + fleetsCanRunInParallelCheckbox.ensureDebugId("FleetsCanRaceInParallelSeriesCheckbox"); + startsWithZeroScoreCheckbox = createCheckbox(stringMessages.startsWithZeroScore()); startsWithZeroScoreCheckbox.ensureDebugId("StartsWithZeroScoreCheckbox"); @@ -162,6 +167,7 @@ public class SeriesWithFleetsCreateDialog extends DataEntryDialog { protected SeriesDTO getResult() { series.setName(nameEntryField.getText()); series.setMedal(isMedalSeriesCheckbox.getValue()); + series.setFleetsCanRunInParallel(fleetsCanRunInParallelCheckbox.getValue()); series.setStartsWithZeroScore(startsWithZeroScoreCheckbox.getValue()); series.setSplitFleetContiguousScoring(hasSplitFleetContiguousScoringCheckbox.getValue()); series.setFirstColumnIsNonDiscardableCarryForward(firstColumnIsNonDiscardableCarryForwardCheckbox.getValue()); @@ -177,16 +183,17 @@ public class SeriesWithFleetsCreateDialog extends DataEntryDialog { if (additionalWidget != null) { panel.add(additionalWidget); } - Grid formGrid = new Grid(7, 2); + Grid formGrid = new Grid(8, 2); panel.add(formGrid); formGrid.setWidget(0, 0, new Label(stringMessages.name() + ":")); formGrid.setWidget(0, 1, nameEntryField); formGrid.setWidget(1, 1, isMedalSeriesCheckbox); - formGrid.setWidget(2, 1, startsWithZeroScoreCheckbox); - formGrid.setWidget(3, 1, hasSplitFleetContiguousScoringCheckbox); - formGrid.setWidget(4, 1, firstColumnIsNonDiscardableCarryForwardCheckbox); - formGrid.setWidget(5, 1, useSeriesResultDiscardingThresholdsCheckbox); - formGrid.setWidget(6, 1, discardThresholdBoxes.getWidget()); + formGrid.setWidget(2, 1, fleetsCanRunInParallelCheckbox); + formGrid.setWidget(3, 1, startsWithZeroScoreCheckbox); + formGrid.setWidget(4, 1, hasSplitFleetContiguousScoringCheckbox); + formGrid.setWidget(5, 1, firstColumnIsNonDiscardableCarryForwardCheckbox); + formGrid.setWidget(6, 1, useSeriesResultDiscardingThresholdsCheckbox); + formGrid.setWidget(7, 1, discardThresholdBoxes.getWidget()); TabPanel tabPanel = new TabPanel(); tabPanel.setWidth("100%"); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SeriesWithFleetsDefaultCreateDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SeriesWithFleetsDefaultCreateDialog.java index 2ba1ac26a8b..934143ba7bb 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SeriesWithFleetsDefaultCreateDialog.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SeriesWithFleetsDefaultCreateDialog.java @@ -32,6 +32,7 @@ public class SeriesWithFleetsDefaultCreateDialog extends SeriesWithFleetsCreateD super(Collections. emptyList(), stringMessages, defaultSeries.getDiscardThresholds(), callback); nameEntryField.setText(defaultSeries.getName()); // Otherwise an errorMessage will pop up isMedalSeriesCheckbox.setValue(defaultSeries.isFirstColumnIsNonDiscardableCarryForward()); + fleetsCanRunInParallelCheckbox.setValue(defaultSeries.isFleetsCanRunInParallel()); startsWithZeroScoreCheckbox.setValue(defaultSeries.isStartsWithZeroScore()); hasSplitFleetContiguousScoringCheckbox.setValue(defaultSeries.hasSplitFleetContiguousScoring()); firstColumnIsNonDiscardableCarryForwardCheckbox.setValue(defaultSeries.isFirstColumnIsNonDiscardableCarryForward()); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java index 923b0224751..9843ffe661f 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java @@ -227,7 +227,7 @@ public interface SailingService extends RemoteService, FileStorageManagementGwtS List> columnNames); void updateSeries(RegattaIdentifier regattaIdentifier, String seriesName, String newSeriesName, boolean isMedal, - int[] resultDiscardingThresholds, boolean startsWithZeroScore, + boolean isFleetsCanRunInParallel, int[] resultDiscardingThresholds, boolean startsWithZeroScore, boolean firstRaceIsNonDiscardableCarryForward, boolean hasSplitFleetScore, List fleets); RaceColumnInSeriesDTO addRaceColumnToSeries(RegattaIdentifier regattaIdentifier, String seriesName, String columnName); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java index 65fb4725221..1b4d9aad5a1 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java @@ -443,7 +443,7 @@ public interface SailingServiceAsync extends ServerInfoRetriever, FileStorageMan List> columnNames, AsyncCallback> callback); void updateSeries(RegattaIdentifier regattaIdentifier, String seriesName, String newSeriesName, boolean isMedal, - int[] resultDiscardingThresholds, boolean startsWithZeroScore, + boolean isFleetsCanRunInParallel, int[] resultDiscardingThresholds, boolean startsWithZeroScore, boolean firstRaceIsNonDiscardableCarryForward, boolean hasSplitFleetScore, List fleets, AsyncCallback callback); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java index 7542b21290d..fc6a57e698a 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java @@ -346,6 +346,7 @@ public interface StringMessages extends com.sap.sse.gwt.client.StringMessages, c String splitFleet(); String color(); String medalSeries(); + String canFleetsRunInParallel(); String noColor(); String pleaseSelectARegatta(); String addFleet(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties index 6d693b221cb..0696d841995 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties @@ -358,6 +358,7 @@ series=Series splitFleet=Split fleet color=Color medalSeries=Medal Series +canFleetsRunInParallel=Fleets can run in parallel noColor=No color pleaseSelectARegatta=Please select a regatta. addFleet=Add fleet diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties index 80977349b2c..e9a77874f5a 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties @@ -359,6 +359,7 @@ fleets=Flotten splitFleet=Aufteilung in Gruppen color=Farbe medalSeries=Medal Series +canFleetsRunInParallel=Flotten können parallel segeln noColor=Keine Farbe pleaseSelectARegatta=Bitte wählen Sie eine Regatta aus. addFleet=Gruppe hinzufügen diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties index 936efd1563b..03c2da16d74 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties @@ -356,6 +356,7 @@ series=Серия splitFleet=Разделить флот color=Цвет medalSeries=Серия на медали +canFleetsRunInParallel=Fleets can run in parallel noColor=Без цвета pleaseSelectARegatta=Выберите регату. addFleet=Добавить флот diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties index 3470236cd97..2eb21b55251 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties @@ -356,6 +356,7 @@ series=系列 splitFleet=船队区分 color=颜色 medalSeries=奖牌轮系列 +canFleetsRunInParallel=Fleets can run in parallel noColor=没有颜色 pleaseSelectARegatta=请选择比赛。 addFleet=添加船队 diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh_CN.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh_CN.properties index 8965d3606a4..3daacf366e9 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh_CN.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh_CN.properties @@ -356,6 +356,7 @@ series=系列 splitFleet=船队区分 color=颜色 medalSeries=奖牌轮系列 +canFleetsRunInParallel=Fleets can run in parallel noColor=没有颜色 pleaseSelectARegatta=请选择比赛。 addFleet=添加船队 diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index 5005c9466e8..6cbd5d97fb3 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -847,7 +847,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S fleets.add(baseDomainFactory.convertToFleetDTO(fleet)); } List raceColumns = convertToRaceColumnDTOs(series.getRaceColumns()); - SeriesDTO result = new SeriesDTO(series.getName(), fleets, raceColumns, series.isMedal(), + SeriesDTO result = new SeriesDTO(series.getName(), fleets, raceColumns, series.isMedal(), series.isFleetsCanRunInParallel(), series.getResultDiscardingRule() == null ? null : series.getResultDiscardingRule().getDiscardIndexResultsStartingWithHowManyRaces(), series.isStartsWithZeroScore(), series.isFirstColumnIsNonDiscardableCarryForward(), series.hasSplitFleetContiguousScoring()); return result; @@ -2804,7 +2804,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S /* startDate*/ null, /*endDate*/ null, RegattaImpl.getDefaultName(replayRaceDTO.rsc, replayRaceDTO.boat_class), Collections.singletonList(new SeriesImpl(LeaderboardNameConstants.DEFAULT_SERIES_NAME, - /* isMedal */false, Collections.singletonList(new FleetImpl( + /* isMedal */false, /* isFleetsCanRunInParallel */ true, Collections.singletonList(new FleetImpl( LeaderboardNameConstants.DEFAULT_FLEET_NAME)), /* race column names */new ArrayList(), getService())), false, baseDomainFactory.createScoringScheme(ScoringSchemeType.LOW_POINT), null, /* useStartTimeInference */ true, @@ -3918,7 +3918,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S } @Override - public void updateSeries(RegattaIdentifier regattaIdentifier, String seriesName, String newSeriesName, boolean isMedal, + public void updateSeries(RegattaIdentifier regattaIdentifier, String seriesName, String newSeriesName, boolean isMedal, boolean isFleetsCanRunInParallel, int[] resultDiscardingThresholds, boolean startsWithZeroScore, boolean firstColumnIsNonDiscardableCarryForward, boolean hasSplitFleetContiguousScoring, List fleets) { @@ -3927,7 +3927,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S SecurityUtils.getSubject().checkPermission(Permission.REGATTA.getStringPermissionForObjects(Mode.UPDATE, regatta.getName())); } getService().apply( - new UpdateSeries(regattaIdentifier, seriesName, newSeriesName, isMedal, resultDiscardingThresholds, + new UpdateSeries(regattaIdentifier, seriesName, newSeriesName, isMedal, isFleetsCanRunInParallel, resultDiscardingThresholds, startsWithZeroScore, firstColumnIsNonDiscardableCarryForward, hasSplitFleetContiguousScoring, fleets)); } @@ -4258,7 +4258,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S for (SeriesDTO series : regattaDTO.series){ SeriesParameters seriesParameters = getSeriesParameters(series); seriesCreationParams.put(series.getName(), new SeriesCreationParametersDTO(series.getFleets(), - false, seriesParameters.isStartswithZeroScore(), seriesParameters.isFirstColumnIsNonDiscardableCarryForward(), + false, true, seriesParameters.isStartswithZeroScore(), seriesParameters.isFirstColumnIsNonDiscardableCarryForward(), seriesParameters.getDiscardingThresholds(), seriesParameters.isHasSplitFleetContiguousScoring())); } return seriesCreationParams; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/SeriesDTO.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/SeriesDTO.java index 39125aa0e75..caf3aa7467b 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/SeriesDTO.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/SeriesDTO.java @@ -12,6 +12,7 @@ public class SeriesDTO extends NamedDTO { private List fleets; private List raceColumns; private boolean isMedal; + private boolean isFleetsCanRunInParallel; private int[] discardThresholds; private Boolean startsWithZeroScore; private boolean firstColumnIsNonDiscardableCarryForward; @@ -19,13 +20,14 @@ public class SeriesDTO extends NamedDTO { public SeriesDTO() {} - public SeriesDTO(String name, List fleets, List raceColumns, boolean isMedal, + public SeriesDTO(String name, List fleets, List raceColumns, boolean isMedal, boolean isFleetsCanRunInParallel, int[] discardThresholds, boolean startsWithZeroScore, boolean firstColumnIsNonDiscardableCarryForward, boolean hasSplitFleetContiguousScoring) { super(name); this.fleets = fleets; this.raceColumns = raceColumns; this.isMedal = isMedal; + this.isFleetsCanRunInParallel = isFleetsCanRunInParallel; this.startsWithZeroScore = startsWithZeroScore; this.hasSplitFleetContiguousScoring = hasSplitFleetContiguousScoring; this.discardThresholds = discardThresholds; @@ -40,7 +42,7 @@ public class SeriesDTO extends NamedDTO { public SeriesDTO(SeriesDTO otherSeries) { this(otherSeries.getName(), otherSeries.getFleets(), otherSeries.getRaceColumns() == null ? null : new ArrayList(otherSeries.getRaceColumns()), - otherSeries.isMedal(), otherSeries.getDiscardThresholds(), otherSeries.isStartsWithZeroScore(), + otherSeries.isMedal(), otherSeries.isFleetsCanRunInParallel(), otherSeries.getDiscardThresholds(), otherSeries.isStartsWithZeroScore(), otherSeries.isFirstColumnIsNonDiscardableCarryForward(), otherSeries.hasSplitFleetContiguousScoring()); } @@ -48,10 +50,6 @@ public class SeriesDTO extends NamedDTO { return hasSplitFleetContiguousScoring; } - public boolean isMedal() { - return isMedal; - } - public List getFleets() { return fleets; } @@ -60,10 +58,22 @@ public class SeriesDTO extends NamedDTO { this.fleets = fleets; } + public boolean isMedal() { + return isMedal; + } + public void setMedal(boolean isMedal) { this.isMedal = isMedal; } + public boolean isFleetsCanRunInParallel() { + return isFleetsCanRunInParallel; + } + + public void setFleetsCanRunInParallel(boolean isFleetsCanRunInParallel) { + this.isFleetsCanRunInParallel = isFleetsCanRunInParallel; + } + public List getRaceColumns() { return raceColumns; } diff --git a/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/AbstractTestStoringAndRetrievingRaceLogInRegatta.java b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/AbstractTestStoringAndRetrievingRaceLogInRegatta.java index ac1099c184c..f018f43c642 100755 --- a/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/AbstractTestStoringAndRetrievingRaceLogInRegatta.java +++ b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/AbstractTestStoringAndRetrievingRaceLogInRegatta.java @@ -1,81 +1,81 @@ -package com.sap.sailing.mongodb.test; - -import java.net.UnknownHostException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import org.junit.Before; - -import com.mongodb.MongoException; -import com.sap.sailing.domain.abstractlog.AbstractLogEventAuthor; -import com.sap.sailing.domain.abstractlog.impl.LogEventAuthorImpl; -import com.sap.sailing.domain.base.BoatClass; -import com.sap.sailing.domain.base.CourseArea; -import com.sap.sailing.domain.base.DomainFactory; -import com.sap.sailing.domain.base.Fleet; -import com.sap.sailing.domain.base.Regatta; -import com.sap.sailing.domain.base.Series; -import com.sap.sailing.domain.base.impl.RegattaImpl; -import com.sap.sailing.domain.base.impl.SeriesImpl; -import com.sap.sailing.domain.common.ScoringSchemeType; -import com.sap.sailing.domain.leaderboard.ScoringScheme; -import com.sap.sailing.domain.persistence.DomainObjectFactory; -import com.sap.sailing.domain.persistence.MongoObjectFactory; -import com.sap.sailing.domain.persistence.PersistenceFactory; -import com.sap.sailing.domain.ranking.OneDesignRankingMetric; -import com.sap.sse.common.impl.MillisecondsTimePoint; - -public abstract class AbstractTestStoringAndRetrievingRaceLogInRegatta extends RaceLogMongoDBTest { - - protected final String raceColumnName = "My.First$Race$1"; - protected final String regattaName = "TestRegatta"; - protected final String yellowFleetName = "Yellow"; - protected final String seriesName = "Qualifying"; - protected MongoObjectFactory mongoObjectFactory = null; - protected DomainObjectFactory domainObjectFactory = null; - protected Regatta regatta = null; - protected AbstractLogEventAuthor author = new LogEventAuthorImpl("Test Author", 1); - - public AbstractTestStoringAndRetrievingRaceLogInRegatta() throws UnknownHostException, MongoException { - super(); - } - - @Before - public void setUp() { - now = MillisecondsTimePoint.now(); - mongoObjectFactory = PersistenceFactory.INSTANCE.getMongoObjectFactory(getMongoService()); - domainObjectFactory = PersistenceFactory.INSTANCE.getDomainObjectFactory(getMongoService(), DomainFactory.INSTANCE); - BoatClass boatClass = DomainFactory.INSTANCE.getOrCreateBoatClass("29erXX", /* typicallyStartsUpwind */ true); - regatta = createRegattaAndAddRaceColumns(1, regattaName, boatClass, true, - DomainFactory.INSTANCE.createScoringScheme(ScoringSchemeType.LOW_POINT)); - } - - private Regatta createRegattaAndAddRaceColumns(final int numberOfQualifyingRaces, final String regattaBaseName, BoatClass boatClass, boolean persistent, ScoringScheme scoringScheme) { - Regatta regatta = createRegattaWithoutRaceColumns(regattaBaseName, boatClass, persistent, scoringScheme, null); - regatta.getSeriesByName(seriesName).addRaceColumn(raceColumnName, /* trackedRegattaRegistry */ null); - return regatta; - } - - private Regatta createRegattaWithoutRaceColumns(final String regattaBaseName, BoatClass boatClass, - boolean persistent, ScoringScheme scoringScheme, CourseArea courseArea) { - List series = createSeriesForTestRegatta(); - Regatta regatta = new RegattaImpl(getRaceLogStore(), getRegattaLogStore(), RegattaImpl.getDefaultName(regattaBaseName, - boatClass == null ? null : boatClass.getName()), boatClass, /*startDate*/ null, /*endDate*/ null, series, persistent, scoringScheme, "123", - courseArea, /* useStartTimeInference */true, OneDesignRankingMetric::new); - return regatta; - } - - private List createSeriesForTestRegatta() { - List series = new ArrayList(); - // -------- qualifying series ------------ - List emptyRaceColumnNames = Collections.emptyList(); - List qualifyingFleets = createQualifyingFleets(); - Series qualifyingSeries = new SeriesImpl(seriesName, /* isMedal */false, qualifyingFleets, - emptyRaceColumnNames, /* trackedRegattaRegistry */ null); - series.add(qualifyingSeries); - return series; - } - - abstract protected List createQualifyingFleets(); -} +package com.sap.sailing.mongodb.test; + +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.junit.Before; + +import com.mongodb.MongoException; +import com.sap.sailing.domain.abstractlog.AbstractLogEventAuthor; +import com.sap.sailing.domain.abstractlog.impl.LogEventAuthorImpl; +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.domain.base.CourseArea; +import com.sap.sailing.domain.base.DomainFactory; +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.Regatta; +import com.sap.sailing.domain.base.Series; +import com.sap.sailing.domain.base.impl.RegattaImpl; +import com.sap.sailing.domain.base.impl.SeriesImpl; +import com.sap.sailing.domain.common.ScoringSchemeType; +import com.sap.sailing.domain.leaderboard.ScoringScheme; +import com.sap.sailing.domain.persistence.DomainObjectFactory; +import com.sap.sailing.domain.persistence.MongoObjectFactory; +import com.sap.sailing.domain.persistence.PersistenceFactory; +import com.sap.sailing.domain.ranking.OneDesignRankingMetric; +import com.sap.sse.common.impl.MillisecondsTimePoint; + +public abstract class AbstractTestStoringAndRetrievingRaceLogInRegatta extends RaceLogMongoDBTest { + + protected final String raceColumnName = "My.First$Race$1"; + protected final String regattaName = "TestRegatta"; + protected final String yellowFleetName = "Yellow"; + protected final String seriesName = "Qualifying"; + protected MongoObjectFactory mongoObjectFactory = null; + protected DomainObjectFactory domainObjectFactory = null; + protected Regatta regatta = null; + protected AbstractLogEventAuthor author = new LogEventAuthorImpl("Test Author", 1); + + public AbstractTestStoringAndRetrievingRaceLogInRegatta() throws UnknownHostException, MongoException { + super(); + } + + @Before + public void setUp() { + now = MillisecondsTimePoint.now(); + mongoObjectFactory = PersistenceFactory.INSTANCE.getMongoObjectFactory(getMongoService()); + domainObjectFactory = PersistenceFactory.INSTANCE.getDomainObjectFactory(getMongoService(), DomainFactory.INSTANCE); + BoatClass boatClass = DomainFactory.INSTANCE.getOrCreateBoatClass("29erXX", /* typicallyStartsUpwind */ true); + regatta = createRegattaAndAddRaceColumns(1, regattaName, boatClass, true, + DomainFactory.INSTANCE.createScoringScheme(ScoringSchemeType.LOW_POINT)); + } + + private Regatta createRegattaAndAddRaceColumns(final int numberOfQualifyingRaces, final String regattaBaseName, BoatClass boatClass, boolean persistent, ScoringScheme scoringScheme) { + Regatta regatta = createRegattaWithoutRaceColumns(regattaBaseName, boatClass, persistent, scoringScheme, null); + regatta.getSeriesByName(seriesName).addRaceColumn(raceColumnName, /* trackedRegattaRegistry */ null); + return regatta; + } + + private Regatta createRegattaWithoutRaceColumns(final String regattaBaseName, BoatClass boatClass, + boolean persistent, ScoringScheme scoringScheme, CourseArea courseArea) { + List series = createSeriesForTestRegatta(); + Regatta regatta = new RegattaImpl(getRaceLogStore(), getRegattaLogStore(), RegattaImpl.getDefaultName(regattaBaseName, + boatClass == null ? null : boatClass.getName()), boatClass, /*startDate*/ null, /*endDate*/ null, series, persistent, scoringScheme, "123", + courseArea, /* useStartTimeInference */true, OneDesignRankingMetric::new); + return regatta; + } + + private List createSeriesForTestRegatta() { + List series = new ArrayList(); + // -------- qualifying series ------------ + List emptyRaceColumnNames = Collections.emptyList(); + List qualifyingFleets = createQualifyingFleets(); + Series qualifyingSeries = new SeriesImpl(seriesName, /* isMedal */false, /* isFleetsCanRunInParallel */ true, qualifyingFleets, + emptyRaceColumnNames, /* trackedRegattaRegistry */ null); + series.add(qualifyingSeries); + return series; + } + + abstract protected List createQualifyingFleets(); +} diff --git a/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/TestStoringAndLoadingEventsAndRegattas.java b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/TestStoringAndLoadingEventsAndRegattas.java index 197c9e784a6..6b7bb6bf1ec 100755 --- a/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/TestStoringAndLoadingEventsAndRegattas.java +++ b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/TestStoringAndLoadingEventsAndRegattas.java @@ -637,6 +637,28 @@ public class TestStoringAndLoadingEventsAndRegattas extends AbstractMongoDBTest assertTrue(loadedRegatta.getSeriesByName("Medal").isStartsWithZeroScore()); } + @Test + public void testLoadStoreRegattaWithSeriesWhereFleetsCanRunInParallel() { + final int numberOfQualifyingRaces = 5; + final int numberOfFinalRaces = 7; + final String regattaBaseName = "Kieler Woche"; + BoatClass boatClass = DomainFactory.INSTANCE.getOrCreateBoatClass("29erXX", /* typicallyStartsUpwind */ true); + Regatta regatta = createRegattaAndAddRaceColumns(numberOfQualifyingRaces, numberOfFinalRaces, RegattaImpl.getDefaultName(regattaBaseName, boatClass.getName()), + boatClass, regattaStartDate, regattaEndDate, /* persistent */false, DomainFactory.INSTANCE.createScoringScheme(ScoringSchemeType.LOW_POINT), OneDesignRankingMetric::new); + regatta.getSeriesByName("Qualifying").setIsFleetsCanRunInParallel(true); + regatta.getSeriesByName("Final").setIsFleetsCanRunInParallel(false); + regatta.getSeriesByName("Medal").setIsFleetsCanRunInParallel(false); + + MongoObjectFactory mof = PersistenceFactory.INSTANCE.getMongoObjectFactory(getMongoService()); + mof.storeRegatta(regatta); + + DomainObjectFactory dof = PersistenceFactory.INSTANCE.getDomainObjectFactory(getMongoService(), DomainFactory.INSTANCE); + Regatta loadedRegatta = dof.loadRegatta(regatta.getName(), /* trackedRegattaRegistry */ null); + assertTrue(loadedRegatta.getSeriesByName("Qualifying").isFleetsCanRunInParallel()); + assertFalse(loadedRegatta.getSeriesByName("Final").isFleetsCanRunInParallel()); + assertFalse(loadedRegatta.getSeriesByName("Medal").isFleetsCanRunInParallel()); + } + @Test public void testLoadStoreSimpleRegattaWithHighPointScoringScheme() { final int numberOfQualifyingRaces = 5; @@ -787,7 +809,7 @@ public class TestStoringAndLoadingEventsAndRegattas extends AbstractMongoDBTest List qualifyingFleets = new ArrayList(); qualifyingFleets.add(new FleetImpl("Yellow")); qualifyingFleets.add(new FleetImpl("Blue")); - Series qualifyingSeries = new SeriesImpl("Qualifying", /* isMedal */false, qualifyingFleets, + Series qualifyingSeries = new SeriesImpl("Qualifying", /* isMedal */false, /* isFleetsCanRunInParallel */ true, qualifyingFleets, emptyRaceColumnNames, /* trackedRegattaRegistry */ null); series.add(qualifyingSeries); @@ -795,13 +817,13 @@ public class TestStoringAndLoadingEventsAndRegattas extends AbstractMongoDBTest List finalFleets = new ArrayList(); finalFleets.add(new FleetImpl("Gold", 1)); finalFleets.add(new FleetImpl("Silver", 2)); - Series finalSeries = new SeriesImpl("Final", /* isMedal */ false, finalFleets, emptyRaceColumnNames, /* trackedRegattaRegistry */ null); + Series finalSeries = new SeriesImpl("Final", /* isMedal */ false, /* isFleetsCanRunInParallel */ true, finalFleets, emptyRaceColumnNames, /* trackedRegattaRegistry */ null); series.add(finalSeries); // ------------ medal -------------- List medalFleets = new ArrayList(); medalFleets.add(new FleetImpl("Medal")); - Series medalSeries = new SeriesImpl("Medal", /* isMedal */ true, medalFleets, emptyRaceColumnNames, /* trackedRegattaRegistry */ null); + Series medalSeries = new SeriesImpl("Medal", /* isMedal */ true, /* isFleetsCanRunInParallel */ true, medalFleets, emptyRaceColumnNames, /* trackedRegattaRegistry */ null); series.add(medalSeries); Regatta regatta = new RegattaImpl(regattaName, boatClass, startDate, endDate, series, persistent, scoringScheme, "123", courseArea, rankingMetricConstructor); return regatta; diff --git a/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/deserialization/racegroup/impl/RaceCellDeserializer.java b/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/deserialization/racegroup/impl/RaceCellDeserializer.java index 018da384eb0..767e41dc1f9 100644 --- a/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/deserialization/racegroup/impl/RaceCellDeserializer.java +++ b/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/deserialization/racegroup/impl/RaceCellDeserializer.java @@ -24,9 +24,10 @@ public class RaceCellDeserializer implements JsonDeserializer { final Object factorJson = object.get(RaceCellJsonSerializer.FIELD_FACTOR); final double factor = factorJson == null ? 1 : ((Number) factorJson).doubleValue(); final Double explicitFactor = (Double) object.get(RaceCellJsonSerializer.FIELD_EXPLICIT_FACTOR); + final Number zeroBasedIndexInFleet = (Number) object.get(RaceCellJsonSerializer.FIELD_ZERO_BASED_INDEX_IN_FLEET); JSONObject logJson = Helpers.getNestedObjectSafe(object, RaceCellJsonSerializer.FIELD_RACE_LOG); RaceLog log = logDeserializer.deserialize(logJson); - return new RaceCellImpl(name, log, factor, explicitFactor); + return new RaceCellImpl(name, log, factor, explicitFactor, zeroBasedIndexInFleet == null ? -1 : zeroBasedIndexInFleet.intValue()); } } diff --git a/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/deserialization/racegroup/impl/RaceRowDeserializer.java b/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/deserialization/racegroup/impl/RaceRowDeserializer.java index 53d75393e6d..e4f22b84f44 100644 --- a/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/deserialization/racegroup/impl/RaceRowDeserializer.java +++ b/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/deserialization/racegroup/impl/RaceRowDeserializer.java @@ -28,6 +28,7 @@ public class RaceRowDeserializer implements JsonDeserializer { public RaceRow deserialize(JSONObject object) throws JsonDeserializationException { JSONObject fleetObject = Helpers.toJSONObjectSafe(object.get(RaceRowJsonSerializer.FIELD_FLEET)); + // the race cells are expected to be written in the order of the race columns in the series / leaderboard JSONArray cellsObject = Helpers.getNestedArraySafe(object, RaceRowJsonSerializer.FIELD_RACE_CELLS); Collection races = new ArrayList(); diff --git a/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/deserialization/racegroup/impl/SeriesWithRowsDeserializer.java b/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/deserialization/racegroup/impl/SeriesWithRowsDeserializer.java index ab8078a91b9..dcb3366cd2d 100644 --- a/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/deserialization/racegroup/impl/SeriesWithRowsDeserializer.java +++ b/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/deserialization/racegroup/impl/SeriesWithRowsDeserializer.java @@ -1,37 +1,42 @@ -package com.sap.sailing.server.gateway.deserialization.racegroup.impl; - -import java.util.ArrayList; -import java.util.Collection; - -import org.json.simple.JSONObject; - -import com.sap.sailing.domain.base.racegroup.RaceRow; -import com.sap.sailing.domain.base.racegroup.SeriesWithRows; -import com.sap.sailing.domain.base.racegroup.impl.SeriesWithRowsImpl; -import com.sap.sailing.server.gateway.deserialization.JsonDeserializationException; -import com.sap.sailing.server.gateway.deserialization.JsonDeserializer; -import com.sap.sailing.server.gateway.deserialization.impl.Helpers; -import com.sap.sailing.server.gateway.serialization.racegroup.impl.RaceRowsOfSeriesWithRowsSerializer; - -public class SeriesWithRowsDeserializer implements JsonDeserializer { - - private JsonDeserializer raceRowDeserializer; - - public SeriesWithRowsDeserializer(JsonDeserializer raceRowDeserializer) { - this.raceRowDeserializer = raceRowDeserializer; - } - - public SeriesWithRows deserialize(JSONObject object) throws JsonDeserializationException { - String name = object.get("name").toString(); - boolean isMedal = (Boolean) object.get("isMedal"); - - Collection rows = new ArrayList(); - for (Object fleetObject : Helpers.getNestedArraySafe(object, RaceRowsOfSeriesWithRowsSerializer.FIELD_FLEETS)) { - JSONObject fleetJson = Helpers.toJSONObjectSafe(fleetObject); - rows.add(raceRowDeserializer.deserialize(fleetJson)); - } - - return new SeriesWithRowsImpl(name, isMedal, rows); - } - +package com.sap.sailing.server.gateway.deserialization.racegroup.impl; + +import java.util.ArrayList; +import java.util.Collection; + +import org.json.simple.JSONObject; + +import com.sap.sailing.domain.base.racegroup.RaceRow; +import com.sap.sailing.domain.base.racegroup.SeriesWithRows; +import com.sap.sailing.domain.base.racegroup.impl.SeriesWithRowsImpl; +import com.sap.sailing.server.gateway.deserialization.JsonDeserializationException; +import com.sap.sailing.server.gateway.deserialization.JsonDeserializer; +import com.sap.sailing.server.gateway.deserialization.impl.Helpers; +import com.sap.sailing.server.gateway.serialization.racegroup.impl.RaceRowsOfSeriesWithRowsSerializer; +import com.sap.sailing.server.gateway.serialization.racegroup.impl.SeriesWithRowsJsonSerializer; + +public class SeriesWithRowsDeserializer implements JsonDeserializer { + + private JsonDeserializer raceRowDeserializer; + + public SeriesWithRowsDeserializer(JsonDeserializer raceRowDeserializer) { + this.raceRowDeserializer = raceRowDeserializer; + } + + public SeriesWithRows deserialize(JSONObject object) throws JsonDeserializationException { + String name = object.get(SeriesWithRowsJsonSerializer.FIELD_NAME).toString(); + boolean isMedal = (Boolean) object.get(SeriesWithRowsJsonSerializer.FIELD_IS_MEDAL); + Boolean isFleetsCanRunInParallel = (Boolean) object.get(SeriesWithRowsJsonSerializer.FIELD_FLEETS_CAN_RUN_IN_PARALLEL); + if (isFleetsCanRunInParallel == null) { + isFleetsCanRunInParallel = true; // that's the default + } + + Collection rows = new ArrayList(); + for (Object fleetObject : Helpers.getNestedArraySafe(object, RaceRowsOfSeriesWithRowsSerializer.FIELD_FLEETS)) { + JSONObject fleetJson = Helpers.toJSONObjectSafe(fleetObject); + rows.add(raceRowDeserializer.deserialize(fleetJson)); + } + + return new SeriesWithRowsImpl(name, isMedal, isFleetsCanRunInParallel, rows); + } + } \ No newline at end of file diff --git a/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/serialization/racegroup/impl/RaceCellJsonSerializer.java b/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/serialization/racegroup/impl/RaceCellJsonSerializer.java index 38369bde1a4..19240a6b646 100644 --- a/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/serialization/racegroup/impl/RaceCellJsonSerializer.java +++ b/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/serialization/racegroup/impl/RaceCellJsonSerializer.java @@ -10,8 +10,9 @@ public class RaceCellJsonSerializer implements JsonSerializer { public static final String FIELD_NAME = "name"; public static final String FIELD_RACE_LOG = "raceLog"; public static final String FIELD_COMPETITORS = "competitors"; - public static final Object FIELD_FACTOR = "factor"; - public static final Object FIELD_EXPLICIT_FACTOR = "explicitfactor"; + public static final String FIELD_FACTOR = "factor"; + public static final String FIELD_EXPLICIT_FACTOR = "explicitfactor"; + public static final String FIELD_ZERO_BASED_INDEX_IN_FLEET = "zerobasedindexinfleet"; private JsonSerializer logSerializer; @@ -25,6 +26,7 @@ public class RaceCellJsonSerializer implements JsonSerializer { result.put(FIELD_NAME, object.getName()); result.put(FIELD_FACTOR, object.getFactor()); result.put(FIELD_EXPLICIT_FACTOR, object.getExplicitFactor()); + result.put(FIELD_ZERO_BASED_INDEX_IN_FLEET, object.getZeroBasedIndexInFleet()); result.put(FIELD_RACE_LOG, logSerializer.serialize(object.getRaceLog())); return result; } diff --git a/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/serialization/racegroup/impl/RaceRowJsonSerializer.java b/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/serialization/racegroup/impl/RaceRowJsonSerializer.java index 3324a5a1c7b..3dc047e08a7 100644 --- a/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/serialization/racegroup/impl/RaceRowJsonSerializer.java +++ b/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/serialization/racegroup/impl/RaceRowJsonSerializer.java @@ -10,6 +10,11 @@ import com.sap.sailing.server.gateway.serialization.JsonSerializer; public class RaceRowJsonSerializer implements JsonSerializer { public static final String FIELD_FLEET = "fleet"; + + /** + * The race cells in the JSONArray keyed by this name are expected to be written + * in the order in which their race columns appear in the series / leaderboard + */ public static final String FIELD_RACE_CELLS = "races"; private JsonSerializer fleetSerializer; @@ -25,14 +30,12 @@ public class RaceRowJsonSerializer implements JsonSerializer { @Override public JSONObject serialize(RaceRow object) { JSONObject result = new JSONObject(); - result.put(FIELD_FLEET, fleetSerializer.serialize(object.getFleet())); JSONArray cells = new JSONArray(); for (RaceCell cell : object.getCells()) { cells.add(cellSerializer.serialize(cell)); } result.put(FIELD_RACE_CELLS, cells); - return result; } diff --git a/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/serialization/racegroup/impl/SeriesWithRowsJsonSerializer.java b/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/serialization/racegroup/impl/SeriesWithRowsJsonSerializer.java index ec114b43381..5dafbc37fe2 100644 --- a/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/serialization/racegroup/impl/SeriesWithRowsJsonSerializer.java +++ b/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/serialization/racegroup/impl/SeriesWithRowsJsonSerializer.java @@ -9,6 +9,7 @@ import com.sap.sailing.server.gateway.serialization.ExtensionJsonSerializer; public class SeriesWithRowsJsonSerializer extends ExtendableJsonSerializer { public static final String FIELD_NAME = "name"; public static final String FIELD_IS_MEDAL = "isMedal"; + public static final String FIELD_FLEETS_CAN_RUN_IN_PARALLEL = "isFleetsCanRunInParallel"; public SeriesWithRowsJsonSerializer(ExtensionJsonSerializer extensionSerializer) { super(extensionSerializer); @@ -17,10 +18,9 @@ public class SeriesWithRowsJsonSerializer extends ExtendableJsonSerializer { - public static final String FIELD_NAME = "name"; - public static final String FIELD_FLEETS = "fleets"; - public static final String FIELD_RACES = "races"; // TODO: 'races' is wrong here... it's actually 'raceColumns' - public static final String FIELD_TRACKED_RACES = "trackedRaces"; - public static final String FIELD_STARTS_WITH_ZERO_SCORE = "startsWithZeroScore"; - public static final String FIELD_HAS_SPLIT_FLEET_CONTIGUOUS_SCORING = "hasSplitFleetContiguousScoring"; - public static final String FIELD_IS_MEDAL_SERIES = "isMedalSeries"; - - private final JsonSerializer fleetSerializer; - - public SeriesJsonSerializer(JsonSerializer fleetSerializer) { - this.fleetSerializer = fleetSerializer; - } - - public JSONObject serialize(Series series) { - JSONObject result = new JSONObject(); - - result.put(FIELD_NAME, series.getName()); - result.put(FIELD_IS_MEDAL_SERIES, series.isMedal()); - result.put(FIELD_STARTS_WITH_ZERO_SCORE, series.isStartsWithZeroScore()); - result.put(FIELD_HAS_SPLIT_FLEET_CONTIGUOUS_SCORING, series.hasSplitFleetContiguousScoring()); - - JSONArray fleetsJson = new JSONArray(); - for (Fleet fleet : series.getFleets()) { - fleetsJson.add(fleetSerializer.serialize(fleet)); - } - result.put(FIELD_FLEETS, fleetsJson); - - JSONArray racesJson = new JSONArray(); - for (RaceColumn raceColumn : series.getRaceColumns()) { - racesJson.add(raceColumn.getName()); - } - result.put(FIELD_RACES, racesJson); - - JSONObject trackedRacesJson = new JSONObject(); - JSONArray trackedFleetsJson = new JSONArray(); - for (Fleet fleet : series.getFleets()) { - JSONObject trackedFleetJson = new JSONObject(); - trackedFleetJson.put(FIELD_NAME, fleet.getName()); - JSONArray racesPerFleetJson = new JSONArray(); - for (RaceColumn raceColumn : series.getRaceColumns()) { - TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); - JSONObject raceColumnJson = new JSONObject(); - raceColumnJson.put(FIELD_NAME, raceColumn.getName()); - raceColumnJson.put("isMedalRace" , raceColumn.isMedalRace()); - if (trackedRace != null) { - raceColumnJson.put("isLive", trackedRace.isLive(MillisecondsTimePoint.now())); - raceColumnJson.put("isTracked", true); - raceColumnJson.put("trackedRaceName", trackedRace.getRace().getName()); - raceColumnJson.put("hasGpsData", trackedRace.hasGPSData()); - raceColumnJson.put("hasWindData", trackedRace.hasWindData()); - } else { - raceColumnJson.put("isLive", false); - raceColumnJson.put("isTracked", false); - raceColumnJson.put("trackedRaceName", null); - raceColumnJson.put("hasGpsData", false); - raceColumnJson.put("hasWindData", false); - } - racesPerFleetJson.add(raceColumnJson); - } - trackedFleetJson.put(FIELD_RACES, racesPerFleetJson); - trackedFleetsJson.add(trackedFleetJson); - } - trackedRacesJson.put(FIELD_FLEETS, trackedFleetsJson); - result.put(FIELD_TRACKED_RACES, trackedRacesJson); - - return result; - } -} +package com.sap.sailing.server.gateway.serialization.impl; + +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; + +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.RaceColumn; +import com.sap.sailing.domain.base.Series; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sailing.server.gateway.serialization.JsonSerializer; +import com.sap.sse.common.impl.MillisecondsTimePoint; + +public class SeriesJsonSerializer implements JsonSerializer { + public static final String FIELD_NAME = "name"; + public static final String FIELD_FLEETS = "fleets"; + public static final String FIELD_RACES = "races"; // TODO: 'races' is wrong here... it's actually 'raceColumns' + public static final String FIELD_TRACKED_RACES = "trackedRaces"; + public static final String FIELD_STARTS_WITH_ZERO_SCORE = "startsWithZeroScore"; + public static final String FIELD_HAS_SPLIT_FLEET_CONTIGUOUS_SCORING = "hasSplitFleetContiguousScoring"; + public static final String FIELD_IS_MEDAL_SERIES = "isMedalSeries"; + public static final String FIELD_FLEETS_CAN_RUN_IN_PARALLEL = "fleetsCanRunInParallel"; + + private final JsonSerializer fleetSerializer; + + public SeriesJsonSerializer(JsonSerializer fleetSerializer) { + this.fleetSerializer = fleetSerializer; + } + + public JSONObject serialize(Series series) { + JSONObject result = new JSONObject(); + + result.put(FIELD_NAME, series.getName()); + result.put(FIELD_IS_MEDAL_SERIES, series.isMedal()); + result.put(FIELD_FLEETS_CAN_RUN_IN_PARALLEL, series.isFleetsCanRunInParallel()); + result.put(FIELD_STARTS_WITH_ZERO_SCORE, series.isStartsWithZeroScore()); + result.put(FIELD_HAS_SPLIT_FLEET_CONTIGUOUS_SCORING, series.hasSplitFleetContiguousScoring()); + + JSONArray fleetsJson = new JSONArray(); + for (Fleet fleet : series.getFleets()) { + fleetsJson.add(fleetSerializer.serialize(fleet)); + } + result.put(FIELD_FLEETS, fleetsJson); + + JSONArray racesJson = new JSONArray(); + for (RaceColumn raceColumn : series.getRaceColumns()) { + racesJson.add(raceColumn.getName()); + } + result.put(FIELD_RACES, racesJson); + + JSONObject trackedRacesJson = new JSONObject(); + JSONArray trackedFleetsJson = new JSONArray(); + for (Fleet fleet : series.getFleets()) { + JSONObject trackedFleetJson = new JSONObject(); + trackedFleetJson.put(FIELD_NAME, fleet.getName()); + JSONArray racesPerFleetJson = new JSONArray(); + for (RaceColumn raceColumn : series.getRaceColumns()) { + TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); + JSONObject raceColumnJson = new JSONObject(); + raceColumnJson.put(FIELD_NAME, raceColumn.getName()); + raceColumnJson.put("isMedalRace" , raceColumn.isMedalRace()); + if (trackedRace != null) { + raceColumnJson.put("isLive", trackedRace.isLive(MillisecondsTimePoint.now())); + raceColumnJson.put("isTracked", true); + raceColumnJson.put("trackedRaceName", trackedRace.getRace().getName()); + raceColumnJson.put("hasGpsData", trackedRace.hasGPSData()); + raceColumnJson.put("hasWindData", trackedRace.hasWindData()); + } else { + raceColumnJson.put("isLive", false); + raceColumnJson.put("isTracked", false); + raceColumnJson.put("trackedRaceName", null); + raceColumnJson.put("hasGpsData", false); + raceColumnJson.put("hasWindData", false); + } + racesPerFleetJson.add(raceColumnJson); + } + trackedFleetJson.put(FIELD_RACES, racesPerFleetJson); + trackedFleetsJson.add(trackedFleetJson); + } + trackedRacesJson.put(FIELD_FLEETS, trackedFleetsJson); + result.put(FIELD_TRACKED_RACES, trackedRacesJson); + + return result; + } +} diff --git a/java/com.sap.sailing.server.gateway.test/src/com/sap/sailing/server/gateway/test/jaxrs/LeaderboardGroupResourceTest.java b/java/com.sap.sailing.server.gateway.test/src/com/sap/sailing/server/gateway/test/jaxrs/LeaderboardGroupResourceTest.java index c2f63c2009c..d35ba2bc03f 100644 --- a/java/com.sap.sailing.server.gateway.test/src/com/sap/sailing/server/gateway/test/jaxrs/LeaderboardGroupResourceTest.java +++ b/java/com.sap.sailing.server.gateway.test/src/com/sap/sailing/server/gateway/test/jaxrs/LeaderboardGroupResourceTest.java @@ -1,92 +1,92 @@ -package com.sap.sailing.server.gateway.test.jaxrs; - -import static org.junit.Assert.assertTrue; - -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -import javax.ws.rs.core.Response; - -import org.json.simple.JSONArray; -import org.json.simple.JSONValue; -import org.junit.Before; -import org.junit.Test; - -import com.sap.sailing.domain.base.Competitor; -import com.sap.sailing.domain.base.DomainFactory; -import com.sap.sailing.domain.base.Fleet; -import com.sap.sailing.domain.base.RaceColumn; -import com.sap.sailing.domain.base.Regatta; -import com.sap.sailing.domain.base.Series; -import com.sap.sailing.domain.base.impl.FleetImpl; -import com.sap.sailing.domain.base.impl.RegattaImpl; -import com.sap.sailing.domain.base.impl.SeriesImpl; -import com.sap.sailing.domain.common.ScoringSchemeType; -import com.sap.sailing.domain.leaderboard.LeaderboardGroup; -import com.sap.sailing.domain.ranking.OneDesignRankingMetric; -import com.sap.sailing.domain.test.mock.MockedTrackedRaceWithStartTimeAndRanks; -import com.sap.sailing.domain.tracking.TrackedRace; -import com.sap.sailing.server.gateway.jaxrs.api.LeaderboardGroupsResource; -import com.sap.sse.common.TimePoint; -import com.sap.sse.common.impl.MillisecondsTimePoint; - -public class LeaderboardGroupResourceTest extends AbstractJaxRsApiTest { - private Regatta regatta; - private LeaderboardGroup leaderboardGroup; - private String boatClassName = "49er"; - private String regattaName = "TestRegatta"; - private String leaderboardGroupName = "TestLeaderboardGroup"; - - @Before - public void setUp() { - super.setUp(); - List series = new ArrayList(); - List fleets = new ArrayList(); - List raceColumnNames = new ArrayList(); - fleets.add(new FleetImpl("Fleet1")); - fleets.add(new FleetImpl("Fleet2")); - Series testSeries = new SeriesImpl("TestSeries", /* isMedal */false, fleets, - raceColumnNames, /* trackedRegattaRegistry */null); - series.add(testSeries); - regatta = racingEventService.createRegatta(RegattaImpl.getDefaultName(regattaName, boatClassName), boatClassName, - /*startDate*/ null, /*endDate*/ null, UUID.randomUUID(), series, /*persistent*/ true, - DomainFactory.INSTANCE.createScoringScheme(ScoringSchemeType.LOW_POINT), null, /* useStartTimeInference */ true, OneDesignRankingMetric::new); - testSeries.addRaceColumn("R1", /* trackedRegattaRegistry */ null); - testSeries.addRaceColumn("R2", /* trackedRegattaRegistry */ null); - List competitors = createCompetitors(4); - List fleet1Competitors = competitors.subList(0, 2); - List fleet2Competitors = competitors.subList(2, 4); - - TimePoint now = MillisecondsTimePoint.now(); - - RaceColumn r1Column = series.get(0).getRaceColumnByName("R1"); - TrackedRace r1Fleet1 = new MockedTrackedRaceWithStartTimeAndRanks(now, fleet1Competitors); - TrackedRace r1Fleet2 = new MockedTrackedRaceWithStartTimeAndRanks(now, fleet2Competitors); - r1Column.setTrackedRace(r1Column.getFleetByName("Fleet1"), r1Fleet1); - r1Column.setTrackedRace(r1Column.getFleetByName("Fleet2"), r1Fleet2); - - racingEventService.addRegattaLeaderboard(regatta.getRegattaIdentifier(), "Testregatta displayName", new int[] { 3, 5 }); - - List leaderboardNames = new ArrayList(); - leaderboardNames.add(regatta.getName()); - leaderboardGroup = racingEventService.addLeaderboardGroup(UUID.randomUUID(), leaderboardGroupName, "description", - "The LG displayName", /* displayGroupsInReverseOrder */ false, - leaderboardNames, /* overallLeaderboardDiscardThresholds */ null, /* overallLeaderboardScoringSchemeType */ null); - } - - @Test - public void testExportLeaderboardGroupsAsJson() throws Exception { - LeaderboardGroupsResource resource = new LeaderboardGroupsResource(); - LeaderboardGroupsResource spyResource = spyResource(resource); - Response leaderboardGroupsResponse = spyResource.getLeaderboardGroups(); - - String jsonString = (String) leaderboardGroupsResponse.getEntity(); - Object obj= JSONValue.parse(jsonString); - JSONArray array= (JSONArray) obj; - - assertTrue(array.size() == 1); - String jsonFirstLeaderboardGroup = (String) array.get(0); - assertTrue(leaderboardGroup.getName().equals(jsonFirstLeaderboardGroup)); - } -} +package com.sap.sailing.server.gateway.test.jaxrs; + +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import javax.ws.rs.core.Response; + +import org.json.simple.JSONArray; +import org.json.simple.JSONValue; +import org.junit.Before; +import org.junit.Test; + +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.DomainFactory; +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.RaceColumn; +import com.sap.sailing.domain.base.Regatta; +import com.sap.sailing.domain.base.Series; +import com.sap.sailing.domain.base.impl.FleetImpl; +import com.sap.sailing.domain.base.impl.RegattaImpl; +import com.sap.sailing.domain.base.impl.SeriesImpl; +import com.sap.sailing.domain.common.ScoringSchemeType; +import com.sap.sailing.domain.leaderboard.LeaderboardGroup; +import com.sap.sailing.domain.ranking.OneDesignRankingMetric; +import com.sap.sailing.domain.test.mock.MockedTrackedRaceWithStartTimeAndRanks; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sailing.server.gateway.jaxrs.api.LeaderboardGroupsResource; +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.impl.MillisecondsTimePoint; + +public class LeaderboardGroupResourceTest extends AbstractJaxRsApiTest { + private Regatta regatta; + private LeaderboardGroup leaderboardGroup; + private String boatClassName = "49er"; + private String regattaName = "TestRegatta"; + private String leaderboardGroupName = "TestLeaderboardGroup"; + + @Before + public void setUp() { + super.setUp(); + List series = new ArrayList(); + List fleets = new ArrayList(); + List raceColumnNames = new ArrayList(); + fleets.add(new FleetImpl("Fleet1")); + fleets.add(new FleetImpl("Fleet2")); + Series testSeries = new SeriesImpl("TestSeries", /* isMedal */false, /* isFleetsCanRunInParallel */ true, fleets, + raceColumnNames, /* trackedRegattaRegistry */null); + series.add(testSeries); + regatta = racingEventService.createRegatta(RegattaImpl.getDefaultName(regattaName, boatClassName), boatClassName, + /*startDate*/ null, /*endDate*/ null, UUID.randomUUID(), series, /*persistent*/ true, + DomainFactory.INSTANCE.createScoringScheme(ScoringSchemeType.LOW_POINT), null, /* useStartTimeInference */ true, OneDesignRankingMetric::new); + testSeries.addRaceColumn("R1", /* trackedRegattaRegistry */ null); + testSeries.addRaceColumn("R2", /* trackedRegattaRegistry */ null); + List competitors = createCompetitors(4); + List fleet1Competitors = competitors.subList(0, 2); + List fleet2Competitors = competitors.subList(2, 4); + + TimePoint now = MillisecondsTimePoint.now(); + + RaceColumn r1Column = series.get(0).getRaceColumnByName("R1"); + TrackedRace r1Fleet1 = new MockedTrackedRaceWithStartTimeAndRanks(now, fleet1Competitors); + TrackedRace r1Fleet2 = new MockedTrackedRaceWithStartTimeAndRanks(now, fleet2Competitors); + r1Column.setTrackedRace(r1Column.getFleetByName("Fleet1"), r1Fleet1); + r1Column.setTrackedRace(r1Column.getFleetByName("Fleet2"), r1Fleet2); + + racingEventService.addRegattaLeaderboard(regatta.getRegattaIdentifier(), "Testregatta displayName", new int[] { 3, 5 }); + + List leaderboardNames = new ArrayList(); + leaderboardNames.add(regatta.getName()); + leaderboardGroup = racingEventService.addLeaderboardGroup(UUID.randomUUID(), leaderboardGroupName, "description", + "The LG displayName", /* displayGroupsInReverseOrder */ false, + leaderboardNames, /* overallLeaderboardDiscardThresholds */ null, /* overallLeaderboardScoringSchemeType */ null); + } + + @Test + public void testExportLeaderboardGroupsAsJson() throws Exception { + LeaderboardGroupsResource resource = new LeaderboardGroupsResource(); + LeaderboardGroupsResource spyResource = spyResource(resource); + Response leaderboardGroupsResponse = spyResource.getLeaderboardGroups(); + + String jsonString = (String) leaderboardGroupsResponse.getEntity(); + Object obj= JSONValue.parse(jsonString); + JSONArray array= (JSONArray) obj; + + assertTrue(array.size() == 1); + String jsonFirstLeaderboardGroup = (String) array.get(0); + assertTrue(leaderboardGroup.getName().equals(jsonFirstLeaderboardGroup)); + } +} diff --git a/java/com.sap.sailing.server.gateway.test/src/com/sap/sailing/server/gateway/test/jaxrs/LeaderboardsResourceCheckinAndOutTest.java b/java/com.sap.sailing.server.gateway.test/src/com/sap/sailing/server/gateway/test/jaxrs/LeaderboardsResourceCheckinAndOutTest.java index 23f3daff110..28178d52f9d 100644 --- a/java/com.sap.sailing.server.gateway.test/src/com/sap/sailing/server/gateway/test/jaxrs/LeaderboardsResourceCheckinAndOutTest.java +++ b/java/com.sap.sailing.server.gateway.test/src/com/sap/sailing/server/gateway/test/jaxrs/LeaderboardsResourceCheckinAndOutTest.java @@ -49,7 +49,7 @@ public class LeaderboardsResourceCheckinAndOutTest extends AbstractJaxRsApiTest c.getColor(), c.getEmail(), c.getFlagImage(), (DynamicTeam) c.getTeam(), (DynamicBoat) c.getBoat(), /* timeOnTimeFactor */ null, /* timeOnDistanceAllowancePerNauticalMile */ null, null); Regatta regatta = new RegattaImpl("regatta", new BoatClassImpl("49er", false), MillisecondsTimePoint.now(), - MillisecondsTimePoint.now(), Collections.singleton(new SeriesImpl("series", false, Collections + MillisecondsTimePoint.now(), Collections.singleton(new SeriesImpl("series", false, /* isFleetsCanRunInParallel */ true, Collections .singleton(new FleetImpl("fleet")), Arrays.asList("column"), racingEventService)), false, new HighPoint(), 0, null, OneDesignRankingMetric::new); racingEventService.addRegattaWithoutReplication(regatta); diff --git a/java/com.sap.sailing.server.gateway.test/src/com/sap/sailing/server/gateway/test/jaxrs/LeaderboardsResourcePingMarkTest.java b/java/com.sap.sailing.server.gateway.test/src/com/sap/sailing/server/gateway/test/jaxrs/LeaderboardsResourcePingMarkTest.java index 6268ea97ab3..dc33adf1a2d 100644 --- a/java/com.sap.sailing.server.gateway.test/src/com/sap/sailing/server/gateway/test/jaxrs/LeaderboardsResourcePingMarkTest.java +++ b/java/com.sap.sailing.server.gateway.test/src/com/sap/sailing/server/gateway/test/jaxrs/LeaderboardsResourcePingMarkTest.java @@ -1,105 +1,105 @@ -package com.sap.sailing.server.gateway.test.jaxrs; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; -import static org.mockito.Mockito.doReturn; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -import javax.ws.rs.core.Response; - -import org.junit.Before; -import org.junit.Test; - -import com.sap.sailing.domain.abstractlog.regatta.RegattaLog; -import com.sap.sailing.domain.abstractlog.regatta.tracking.analyzing.impl.RegattaLogDeviceMarkMappingFinder; -import com.sap.sailing.domain.base.Mark; -import com.sap.sailing.domain.base.Regatta; -import com.sap.sailing.domain.base.impl.BoatClassImpl; -import com.sap.sailing.domain.base.impl.FleetImpl; -import com.sap.sailing.domain.base.impl.RegattaImpl; -import com.sap.sailing.domain.base.impl.SeriesImpl; -import com.sap.sailing.domain.common.racelog.tracking.TransformationException; -import com.sap.sailing.domain.leaderboard.RegattaLeaderboard; -import com.sap.sailing.domain.leaderboard.impl.HighPoint; -import com.sap.sailing.domain.racelogtracking.DeviceIdentifier; -import com.sap.sailing.domain.racelogtracking.DeviceMapping; -import com.sap.sailing.domain.racelogtracking.RaceLogTrackingAdapterFactory; -import com.sap.sailing.domain.ranking.OneDesignRankingMetric; -import com.sap.sailing.server.gateway.jaxrs.api.LeaderboardsResource; -import com.sap.sse.common.NoCorrespondingServiceRegisteredException; -import com.sap.sse.common.impl.MillisecondsTimePoint; - -public class LeaderboardsResourcePingMarkTest extends AbstractJaxRsApiTest { - private Mark mark; - private RegattaLog log; - private RegattaLeaderboard leaderboard; - - public static final String PING_MARK_JSON = - " {\n" + - " \"timestamp\" : 1427142552000,\n" + - " \"latitude\" : 54.325246,\n" + - " \"longitude\" : 10.148556,\n" + - " \"speed\" : 3.61,\n" + - " \"course\" : 258.11,\n" + - " }"; - - public static final String PING2_MARK_JSON = - " {\n" + - " \"timestamp\" : 1427142562000,\n" + - " \"latitude\" : 54.425246,\n" + - " \"longitude\" : 10.248556,\n" + - " \"speed\" : 3.61,\n" + - " \"course\" : 258.11,\n" + - " }"; - - @Before - public void setUp() { - super.setUp(); - - mark = racingEventService.getBaseDomainFactory().getOrCreateMark("id", "name"); - Regatta regatta = new RegattaImpl("regatta", new BoatClassImpl("49er", false), MillisecondsTimePoint.now(), - MillisecondsTimePoint.now(), Collections.singleton(new SeriesImpl("series", false, Collections - .singleton(new FleetImpl("fleet")), Arrays.asList("column"), racingEventService)), false, - new HighPoint(), 0, null, OneDesignRankingMetric::new); - racingEventService.addRegattaWithoutReplication(regatta); - leaderboard = racingEventService.addRegattaLeaderboard(regatta.getRegattaIdentifier(), "regatta", new int[] {}); - log = leaderboard.getRegattaLike().getRegattaLog(); - } - - @Test - public void testCheckinAndCheckout() throws Exception { - LeaderboardsResource resource = spyResource(new LeaderboardsResource()); - doReturn(RaceLogTrackingAdapterFactory.INSTANCE.getAdapter(racingEventService.getBaseDomainFactory())).when(resource).getRaceLogTrackingAdapter(); - { - Response response = resource.pingMark(PING_MARK_JSON, leaderboard.getName(), mark.getId().toString()); - assertThat("response is ok", response.getStatus(), equalTo(Response.Status.OK.getStatusCode())); - Map>> mappings = new RegattaLogDeviceMarkMappingFinder(log).analyze(); - List> mappingsForMark = mappings.get(mark); - assertThat("one mapping was created for the one ping", mappingsForMark.size(), equalTo(1)); - assertOneFixPerMapping(mappingsForMark); - } - { - // now produce a second ping; this should produce also only one fix; no additional artificial fixes are - // created (anymore; they used to be before bug2851) - Response response = resource.pingMark(PING2_MARK_JSON, leaderboard.getName(), mark.getId().toString()); - assertThat("response is ok", response.getStatus(), equalTo(Response.Status.OK.getStatusCode())); - Map>> mappings = new RegattaLogDeviceMarkMappingFinder(log).analyze(); - List> mappingsForMark = mappings.get(mark); - assertThat("Assert no additional mapping was created for the second ping", mappingsForMark.size(), equalTo(2)); - assertOneFixPerMapping(mappingsForMark); - } - } - - private void assertOneFixPerMapping(List> mappingsForMark) throws TransformationException, - NoCorrespondingServiceRegisteredException { - for (DeviceMapping i : mappingsForMark) { - DeviceIdentifier device = i.getDevice(); - // first ping produces exactly one fix: - assertThat("all fixes stored", racingEventService.getGPSFixStore().getNumberOfFixes(device), equalTo(1L)); - } - } -} +package com.sap.sailing.server.gateway.test.jaxrs; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.Mockito.doReturn; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import javax.ws.rs.core.Response; + +import org.junit.Before; +import org.junit.Test; + +import com.sap.sailing.domain.abstractlog.regatta.RegattaLog; +import com.sap.sailing.domain.abstractlog.regatta.tracking.analyzing.impl.RegattaLogDeviceMarkMappingFinder; +import com.sap.sailing.domain.base.Mark; +import com.sap.sailing.domain.base.Regatta; +import com.sap.sailing.domain.base.impl.BoatClassImpl; +import com.sap.sailing.domain.base.impl.FleetImpl; +import com.sap.sailing.domain.base.impl.RegattaImpl; +import com.sap.sailing.domain.base.impl.SeriesImpl; +import com.sap.sailing.domain.common.racelog.tracking.TransformationException; +import com.sap.sailing.domain.leaderboard.RegattaLeaderboard; +import com.sap.sailing.domain.leaderboard.impl.HighPoint; +import com.sap.sailing.domain.racelogtracking.DeviceIdentifier; +import com.sap.sailing.domain.racelogtracking.DeviceMapping; +import com.sap.sailing.domain.racelogtracking.RaceLogTrackingAdapterFactory; +import com.sap.sailing.domain.ranking.OneDesignRankingMetric; +import com.sap.sailing.server.gateway.jaxrs.api.LeaderboardsResource; +import com.sap.sse.common.NoCorrespondingServiceRegisteredException; +import com.sap.sse.common.impl.MillisecondsTimePoint; + +public class LeaderboardsResourcePingMarkTest extends AbstractJaxRsApiTest { + private Mark mark; + private RegattaLog log; + private RegattaLeaderboard leaderboard; + + public static final String PING_MARK_JSON = + " {\n" + + " \"timestamp\" : 1427142552000,\n" + + " \"latitude\" : 54.325246,\n" + + " \"longitude\" : 10.148556,\n" + + " \"speed\" : 3.61,\n" + + " \"course\" : 258.11,\n" + + " }"; + + public static final String PING2_MARK_JSON = + " {\n" + + " \"timestamp\" : 1427142562000,\n" + + " \"latitude\" : 54.425246,\n" + + " \"longitude\" : 10.248556,\n" + + " \"speed\" : 3.61,\n" + + " \"course\" : 258.11,\n" + + " }"; + + @Before + public void setUp() { + super.setUp(); + + mark = racingEventService.getBaseDomainFactory().getOrCreateMark("id", "name"); + Regatta regatta = new RegattaImpl("regatta", new BoatClassImpl("49er", false), MillisecondsTimePoint.now(), + MillisecondsTimePoint.now(), Collections.singleton(new SeriesImpl("series", false, /* isFleetsCanRunInParallel */ true, Collections + .singleton(new FleetImpl("fleet")), Arrays.asList("column"), racingEventService)), false, + new HighPoint(), 0, null, OneDesignRankingMetric::new); + racingEventService.addRegattaWithoutReplication(regatta); + leaderboard = racingEventService.addRegattaLeaderboard(regatta.getRegattaIdentifier(), "regatta", new int[] {}); + log = leaderboard.getRegattaLike().getRegattaLog(); + } + + @Test + public void testCheckinAndCheckout() throws Exception { + LeaderboardsResource resource = spyResource(new LeaderboardsResource()); + doReturn(RaceLogTrackingAdapterFactory.INSTANCE.getAdapter(racingEventService.getBaseDomainFactory())).when(resource).getRaceLogTrackingAdapter(); + { + Response response = resource.pingMark(PING_MARK_JSON, leaderboard.getName(), mark.getId().toString()); + assertThat("response is ok", response.getStatus(), equalTo(Response.Status.OK.getStatusCode())); + Map>> mappings = new RegattaLogDeviceMarkMappingFinder(log).analyze(); + List> mappingsForMark = mappings.get(mark); + assertThat("one mapping was created for the one ping", mappingsForMark.size(), equalTo(1)); + assertOneFixPerMapping(mappingsForMark); + } + { + // now produce a second ping; this should produce also only one fix; no additional artificial fixes are + // created (anymore; they used to be before bug2851) + Response response = resource.pingMark(PING2_MARK_JSON, leaderboard.getName(), mark.getId().toString()); + assertThat("response is ok", response.getStatus(), equalTo(Response.Status.OK.getStatusCode())); + Map>> mappings = new RegattaLogDeviceMarkMappingFinder(log).analyze(); + List> mappingsForMark = mappings.get(mark); + assertThat("Assert no additional mapping was created for the second ping", mappingsForMark.size(), equalTo(2)); + assertOneFixPerMapping(mappingsForMark); + } + } + + private void assertOneFixPerMapping(List> mappingsForMark) throws TransformationException, + NoCorrespondingServiceRegisteredException { + for (DeviceMapping i : mappingsForMark) { + DeviceIdentifier device = i.getDevice(); + // first ping produces exactly one fix: + assertThat("all fixes stored", racingEventService.getGPSFixStore().getNumberOfFixes(device), equalTo(1L)); + } + } +} diff --git a/java/com.sap.sailing.server.gateway.test/src/com/sap/sailing/server/gateway/test/jaxrs/LeaderboardsResourceTest.java b/java/com.sap.sailing.server.gateway.test/src/com/sap/sailing/server/gateway/test/jaxrs/LeaderboardsResourceTest.java index 509dbc22b1f..574c5a5bbfc 100644 --- a/java/com.sap.sailing.server.gateway.test/src/com/sap/sailing/server/gateway/test/jaxrs/LeaderboardsResourceTest.java +++ b/java/com.sap.sailing.server.gateway.test/src/com/sap/sailing/server/gateway/test/jaxrs/LeaderboardsResourceTest.java @@ -1,266 +1,266 @@ -package com.sap.sailing.server.gateway.test.jaxrs; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; - -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -import javax.ws.rs.core.Response; - -import org.json.simple.JSONArray; -import org.json.simple.JSONObject; -import org.json.simple.JSONValue; -import org.junit.Before; -import org.junit.Test; - -import com.sap.sailing.domain.base.Competitor; -import com.sap.sailing.domain.base.DomainFactory; -import com.sap.sailing.domain.base.Fleet; -import com.sap.sailing.domain.base.RaceColumn; -import com.sap.sailing.domain.base.Regatta; -import com.sap.sailing.domain.base.Series; -import com.sap.sailing.domain.base.impl.FleetImpl; -import com.sap.sailing.domain.base.impl.RegattaImpl; -import com.sap.sailing.domain.base.impl.SeriesImpl; -import com.sap.sailing.domain.common.ScoringSchemeType; -import com.sap.sailing.domain.leaderboard.RegattaLeaderboard; -import com.sap.sailing.domain.ranking.OneDesignRankingMetric; -import com.sap.sailing.domain.test.mock.MockedTrackedRaceWithStartTimeAndRanks; -import com.sap.sailing.domain.tracking.TrackedRace; -import com.sap.sailing.server.gateway.jaxrs.api.LeaderboardsResource; -import com.sap.sse.common.TimePoint; -import com.sap.sse.common.impl.MillisecondsTimePoint; - -public class LeaderboardsResourceTest extends AbstractJaxRsApiTest { - private Regatta regatta; - private RegattaLeaderboard regattaLeaderboard; - private String boatClassName = "49er"; - private String regattaBaseName = "TestRegatta"; - - @Before - public void setUp() { - super.setUp(); - } - - private void dropAndCreateRegatta() { - service.getDB().dropDatabase(); - List series = new ArrayList(); - List fleets = new ArrayList(); - List raceColumnNames = new ArrayList(); - fleets.add(new FleetImpl("Fleet1")); - fleets.add(new FleetImpl("Fleet2")); - Series testSeries = new SeriesImpl("TestSeries", /* isMedal */false, fleets, - raceColumnNames, /* trackedRegattaRegistry */null); - series.add(testSeries); - regatta = racingEventService.createRegatta(RegattaImpl.getDefaultName(regattaBaseName, boatClassName), boatClassName, - /*startDate*/ null, /*endDate*/ null, UUID.randomUUID(), series, /*persistent*/ true, - DomainFactory.INSTANCE.createScoringScheme(ScoringSchemeType.LOW_POINT), null, /* useStartTimeInference */ true, OneDesignRankingMetric::new); - testSeries.addRaceColumn("R1", /* trackedRegattaRegistry */ null); - testSeries.addRaceColumn("R2", /* trackedRegattaRegistry */ null); - List competitors = createCompetitors(4); - List fleet1Competitors = competitors.subList(0, 2); - List fleet2Competitors = competitors.subList(2, 4); - - TimePoint now = MillisecondsTimePoint.now(); - - RaceColumn r1Column = series.get(0).getRaceColumnByName("R1"); - TrackedRace r1Fleet1 = new MockedTrackedRaceWithStartTimeAndRanks(now, fleet1Competitors, regatta); - TrackedRace r1Fleet2 = new MockedTrackedRaceWithStartTimeAndRanks(now, fleet2Competitors, regatta); - r1Column.setTrackedRace(r1Column.getFleetByName("Fleet1"), r1Fleet1); - r1Column.setTrackedRace(r1Column.getFleetByName("Fleet2"), r1Fleet2); - - regattaLeaderboard = racingEventService.addRegattaLeaderboard(regatta.getRegattaIdentifier(), "Testregatta displayName", new int[] { 3, 5 }); - - } - - @Test - public void testExportEmptyLeaderboardAsJson() throws Exception { - dropAndCreateRegatta(); - - LeaderboardsResource resource = spyResource(new LeaderboardsResource()); - Response leaderboardReponse = resource.getLeaderboard(regatta.getName(), LeaderboardsResource.ResultStates.Final, null); - - String jsonString = (String) leaderboardReponse.getEntity(); - Object obj= JSONValue.parse(jsonString); - JSONObject jsonObject = (JSONObject) obj; - - String jsonLeaderboardName = (String) jsonObject.get("name"); - String jsonResultState = (String) jsonObject.get("resultState"); - - assertTrue(regattaLeaderboard.getName().equals(jsonLeaderboardName)); - assertTrue(LeaderboardsResource.ResultStates.Final.name().equals(jsonResultState)); - - // resultTimepoint should be null if there are no 'final' results yet - TimePoint resultTimePoint = parseTimepointFromJsonNumber((Long) jsonObject.get("resultTimepoint")); - assertNull(resultTimePoint); - - JSONArray jsonCompetitors = (JSONArray) jsonObject.get("competitors"); - assertTrue(jsonCompetitors.size() == 4); - } - - @Test - public void testExportLeaderboardWithFinalResultStateAsJson() throws Exception { - dropAndCreateRegatta(); - - LeaderboardsResource resource = spyResource(new LeaderboardsResource()); - Response leaderboardReponse = resource.getLeaderboard(regatta.getName(), LeaderboardsResource.ResultStates.Final, null); - - String jsonString = (String) leaderboardReponse.getEntity(); - Object obj= JSONValue.parse(jsonString); - JSONObject jsonObject = (JSONObject) obj; - - String jsonLeaderboardName = (String) jsonObject.get("name"); - String jsonResultState = (String) jsonObject.get("resultState"); - - assertTrue(regattaLeaderboard.getName().equals(jsonLeaderboardName)); - assertTrue(LeaderboardsResource.ResultStates.Final.name().equals(jsonResultState)); - - TimePoint resultTimePoint = parseTimepointFromJsonNumber((Long) jsonObject.get("resultTimepoint")); - assertNull(resultTimePoint); - - regattaLeaderboard.getScoreCorrection().setTimePointOfLastCorrectionsValidity(MillisecondsTimePoint.now()); - - LeaderboardsResource resource2 = spyResource(new LeaderboardsResource()); - Response leaderboardReponse2 = resource2.getLeaderboard(regatta.getName(), LeaderboardsResource.ResultStates.Final, null); - - String jsonString2 = (String) leaderboardReponse2.getEntity(); - obj= JSONValue.parse(jsonString2); - jsonObject = (JSONObject) obj; - - resultTimePoint = parseTimepointFromJsonNumber((Long) jsonObject.get("resultTimepoint")); - assertNotNull(resultTimePoint); - } - - @Test - public void testExportLeaderboardWithLiveResultStateAsJson() throws Exception { - dropAndCreateRegatta(); - - LeaderboardsResource resource = spyResource(new LeaderboardsResource()); - Response leaderboardReponse = resource.getLeaderboard(regatta.getName(), LeaderboardsResource.ResultStates.Live, null); - - String jsonString = (String) leaderboardReponse.getEntity(); - Object obj= JSONValue.parse(jsonString); - JSONObject jsonObject = (JSONObject) obj; - - String jsonLeaderboardName = (String) jsonObject.get("name"); - String jsonResultState = (String) jsonObject.get("resultState"); - - assertTrue(regattaLeaderboard.getName().equals(jsonLeaderboardName)); - assertTrue(LeaderboardsResource.ResultStates.Live.name().equals(jsonResultState)); - - TimePoint resultTimePoint = parseTimepointFromJsonNumber((Long) jsonObject.get("resultTimepoint")); - assertNotNull(resultTimePoint); - - JSONArray jsonCompetitors = (JSONArray) jsonObject.get("competitors"); - assertTrue(jsonCompetitors.size() == 4); - } - - @Test - public void testSmartFutureCacheInExportLeaderboardAsJson() throws Exception { - dropAndCreateRegatta(); - - TimePoint now = MillisecondsTimePoint.now(); - regattaLeaderboard.getScoreCorrection().setTimePointOfLastCorrectionsValidity(now); - - Thread.sleep(100); - - // call the servlet for the first time - LeaderboardsResource resource = spyResource(new LeaderboardsResource()); - Response leaderboardReponse = resource.getLeaderboard(regatta.getName(), LeaderboardsResource.ResultStates.Final, null); - - String jsonString = (String) leaderboardReponse.getEntity(); - Object obj= JSONValue.parse(jsonString); - JSONObject jsonObject = (JSONObject) obj; - - String jsonLeaderboardName = (String) jsonObject.get("name"); - String jsonResultState = (String) jsonObject.get("resultState"); - - assertTrue(regattaLeaderboard.getName().equals(jsonLeaderboardName)); - assertTrue(LeaderboardsResource.ResultStates.Final.name().equals(jsonResultState)); - - TimePoint resultTimePoint = parseTimepointFromJsonNumber((Long) jsonObject.get("resultTimepoint")); - assertNotNull(resultTimePoint); - - // second call - it's expected to get the same result from the cache - Thread.sleep(100); - - LeaderboardsResource resource2 = spyResource(new LeaderboardsResource()); - Response leaderboardReponse2 = resource2.getLeaderboard(regatta.getName(), LeaderboardsResource.ResultStates.Final, null); - - String jsonString2 = (String) leaderboardReponse2.getEntity(); - Object obj2= JSONValue.parse(jsonString2); - JSONObject jsonObject2 = (JSONObject) obj2; - - String jsonLeaderboardName2 = (String) jsonObject2.get("name"); - String jsonResultState2 = (String) jsonObject2.get("resultState"); - - assertTrue(jsonLeaderboardName.equals(jsonLeaderboardName2)); - assertTrue(jsonResultState.equals(jsonResultState2)); - - TimePoint resultTimePoint2 = parseTimepointFromJsonNumber((Long) jsonObject.get("resultTimepoint")); - assertNotNull(resultTimePoint2); - assertTrue(resultTimePoint.equals(resultTimePoint2)); - } - - @Test - public void testExportLeaderboardWithMaxCompetitorsCountAsJson() throws Exception { - dropAndCreateRegatta(); - Integer maxCompetitorsCount = 2; - - LeaderboardsResource resource = spyResource(new LeaderboardsResource()); - Response leaderboardReponse = resource.getLeaderboard(regatta.getName(), LeaderboardsResource.ResultStates.Final, null); - - String jsonString = (String) leaderboardReponse.getEntity(); - Object obj= JSONValue.parse(jsonString); - JSONObject jsonObject = (JSONObject) obj; - - String jsonLeaderboardName = (String) jsonObject.get("name"); - String jsonResultState = (String) jsonObject.get("resultState"); - - assertTrue(regattaLeaderboard.getName().equals(jsonLeaderboardName)); - assertTrue(LeaderboardsResource.ResultStates.Final.name().equals(jsonResultState)); - - TimePoint resultTimePoint = parseTimepointFromJsonNumber((Long) jsonObject.get("resultTimepoint")); - assertNull(resultTimePoint); - - regattaLeaderboard.getScoreCorrection().setTimePointOfLastCorrectionsValidity(MillisecondsTimePoint.now()); - - // first call for 'all' competitors - LeaderboardsResource resource2 = spyResource(new LeaderboardsResource()); - Response leaderboardReponse2 = resource2.getLeaderboard(regatta.getName(), LeaderboardsResource.ResultStates.Final, null); - - jsonString = (String) leaderboardReponse2.getEntity(); - obj= JSONValue.parse(jsonString); - jsonObject = (JSONObject) obj; - JSONArray jsonCompetitors = (JSONArray) jsonObject.get("competitors"); - assertEquals(jsonCompetitors.size(), 4); - - Thread.sleep(100); - - // second call with maxCompetitorsCount set - LeaderboardsResource resource3 = spyResource(new LeaderboardsResource()); - Response leaderboardReponse3 = resource3.getLeaderboard(regatta.getName(), LeaderboardsResource.ResultStates.Final, maxCompetitorsCount); - - jsonString = (String) leaderboardReponse3.getEntity(); - obj= JSONValue.parse(jsonString); - jsonObject = (JSONObject) obj; - jsonCompetitors = (JSONArray) jsonObject.get("competitors"); - assertEquals((Integer) jsonCompetitors.size(), maxCompetitorsCount); - - Thread.sleep(100); - - // third call for 'all' competitors - LeaderboardsResource resource4 = spyResource(new LeaderboardsResource()); - Response leaderboardReponse4 = resource4.getLeaderboard(regatta.getName(), LeaderboardsResource.ResultStates.Final, null); - - jsonString = (String) leaderboardReponse4.getEntity(); - obj= JSONValue.parse(jsonString); - jsonObject = (JSONObject) obj; - jsonCompetitors = (JSONArray) jsonObject.get("competitors"); - assertEquals(jsonCompetitors.size(), 4); - } -} +package com.sap.sailing.server.gateway.test.jaxrs; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import javax.ws.rs.core.Response; + +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.JSONValue; +import org.junit.Before; +import org.junit.Test; + +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.DomainFactory; +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.RaceColumn; +import com.sap.sailing.domain.base.Regatta; +import com.sap.sailing.domain.base.Series; +import com.sap.sailing.domain.base.impl.FleetImpl; +import com.sap.sailing.domain.base.impl.RegattaImpl; +import com.sap.sailing.domain.base.impl.SeriesImpl; +import com.sap.sailing.domain.common.ScoringSchemeType; +import com.sap.sailing.domain.leaderboard.RegattaLeaderboard; +import com.sap.sailing.domain.ranking.OneDesignRankingMetric; +import com.sap.sailing.domain.test.mock.MockedTrackedRaceWithStartTimeAndRanks; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sailing.server.gateway.jaxrs.api.LeaderboardsResource; +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.impl.MillisecondsTimePoint; + +public class LeaderboardsResourceTest extends AbstractJaxRsApiTest { + private Regatta regatta; + private RegattaLeaderboard regattaLeaderboard; + private String boatClassName = "49er"; + private String regattaBaseName = "TestRegatta"; + + @Before + public void setUp() { + super.setUp(); + } + + private void dropAndCreateRegatta() { + service.getDB().dropDatabase(); + List series = new ArrayList(); + List fleets = new ArrayList(); + List raceColumnNames = new ArrayList(); + fleets.add(new FleetImpl("Fleet1")); + fleets.add(new FleetImpl("Fleet2")); + Series testSeries = new SeriesImpl("TestSeries", /* isMedal */false, /* isFleetsCanRunInParallel */ true, fleets, + raceColumnNames, /* trackedRegattaRegistry */null); + series.add(testSeries); + regatta = racingEventService.createRegatta(RegattaImpl.getDefaultName(regattaBaseName, boatClassName), boatClassName, + /*startDate*/ null, /*endDate*/ null, UUID.randomUUID(), series, /*persistent*/ true, + DomainFactory.INSTANCE.createScoringScheme(ScoringSchemeType.LOW_POINT), null, /* useStartTimeInference */ true, OneDesignRankingMetric::new); + testSeries.addRaceColumn("R1", /* trackedRegattaRegistry */ null); + testSeries.addRaceColumn("R2", /* trackedRegattaRegistry */ null); + List competitors = createCompetitors(4); + List fleet1Competitors = competitors.subList(0, 2); + List fleet2Competitors = competitors.subList(2, 4); + + TimePoint now = MillisecondsTimePoint.now(); + + RaceColumn r1Column = series.get(0).getRaceColumnByName("R1"); + TrackedRace r1Fleet1 = new MockedTrackedRaceWithStartTimeAndRanks(now, fleet1Competitors, regatta); + TrackedRace r1Fleet2 = new MockedTrackedRaceWithStartTimeAndRanks(now, fleet2Competitors, regatta); + r1Column.setTrackedRace(r1Column.getFleetByName("Fleet1"), r1Fleet1); + r1Column.setTrackedRace(r1Column.getFleetByName("Fleet2"), r1Fleet2); + + regattaLeaderboard = racingEventService.addRegattaLeaderboard(regatta.getRegattaIdentifier(), "Testregatta displayName", new int[] { 3, 5 }); + + } + + @Test + public void testExportEmptyLeaderboardAsJson() throws Exception { + dropAndCreateRegatta(); + + LeaderboardsResource resource = spyResource(new LeaderboardsResource()); + Response leaderboardReponse = resource.getLeaderboard(regatta.getName(), LeaderboardsResource.ResultStates.Final, null); + + String jsonString = (String) leaderboardReponse.getEntity(); + Object obj= JSONValue.parse(jsonString); + JSONObject jsonObject = (JSONObject) obj; + + String jsonLeaderboardName = (String) jsonObject.get("name"); + String jsonResultState = (String) jsonObject.get("resultState"); + + assertTrue(regattaLeaderboard.getName().equals(jsonLeaderboardName)); + assertTrue(LeaderboardsResource.ResultStates.Final.name().equals(jsonResultState)); + + // resultTimepoint should be null if there are no 'final' results yet + TimePoint resultTimePoint = parseTimepointFromJsonNumber((Long) jsonObject.get("resultTimepoint")); + assertNull(resultTimePoint); + + JSONArray jsonCompetitors = (JSONArray) jsonObject.get("competitors"); + assertTrue(jsonCompetitors.size() == 4); + } + + @Test + public void testExportLeaderboardWithFinalResultStateAsJson() throws Exception { + dropAndCreateRegatta(); + + LeaderboardsResource resource = spyResource(new LeaderboardsResource()); + Response leaderboardReponse = resource.getLeaderboard(regatta.getName(), LeaderboardsResource.ResultStates.Final, null); + + String jsonString = (String) leaderboardReponse.getEntity(); + Object obj= JSONValue.parse(jsonString); + JSONObject jsonObject = (JSONObject) obj; + + String jsonLeaderboardName = (String) jsonObject.get("name"); + String jsonResultState = (String) jsonObject.get("resultState"); + + assertTrue(regattaLeaderboard.getName().equals(jsonLeaderboardName)); + assertTrue(LeaderboardsResource.ResultStates.Final.name().equals(jsonResultState)); + + TimePoint resultTimePoint = parseTimepointFromJsonNumber((Long) jsonObject.get("resultTimepoint")); + assertNull(resultTimePoint); + + regattaLeaderboard.getScoreCorrection().setTimePointOfLastCorrectionsValidity(MillisecondsTimePoint.now()); + + LeaderboardsResource resource2 = spyResource(new LeaderboardsResource()); + Response leaderboardReponse2 = resource2.getLeaderboard(regatta.getName(), LeaderboardsResource.ResultStates.Final, null); + + String jsonString2 = (String) leaderboardReponse2.getEntity(); + obj= JSONValue.parse(jsonString2); + jsonObject = (JSONObject) obj; + + resultTimePoint = parseTimepointFromJsonNumber((Long) jsonObject.get("resultTimepoint")); + assertNotNull(resultTimePoint); + } + + @Test + public void testExportLeaderboardWithLiveResultStateAsJson() throws Exception { + dropAndCreateRegatta(); + + LeaderboardsResource resource = spyResource(new LeaderboardsResource()); + Response leaderboardReponse = resource.getLeaderboard(regatta.getName(), LeaderboardsResource.ResultStates.Live, null); + + String jsonString = (String) leaderboardReponse.getEntity(); + Object obj= JSONValue.parse(jsonString); + JSONObject jsonObject = (JSONObject) obj; + + String jsonLeaderboardName = (String) jsonObject.get("name"); + String jsonResultState = (String) jsonObject.get("resultState"); + + assertTrue(regattaLeaderboard.getName().equals(jsonLeaderboardName)); + assertTrue(LeaderboardsResource.ResultStates.Live.name().equals(jsonResultState)); + + TimePoint resultTimePoint = parseTimepointFromJsonNumber((Long) jsonObject.get("resultTimepoint")); + assertNotNull(resultTimePoint); + + JSONArray jsonCompetitors = (JSONArray) jsonObject.get("competitors"); + assertTrue(jsonCompetitors.size() == 4); + } + + @Test + public void testSmartFutureCacheInExportLeaderboardAsJson() throws Exception { + dropAndCreateRegatta(); + + TimePoint now = MillisecondsTimePoint.now(); + regattaLeaderboard.getScoreCorrection().setTimePointOfLastCorrectionsValidity(now); + + Thread.sleep(100); + + // call the servlet for the first time + LeaderboardsResource resource = spyResource(new LeaderboardsResource()); + Response leaderboardReponse = resource.getLeaderboard(regatta.getName(), LeaderboardsResource.ResultStates.Final, null); + + String jsonString = (String) leaderboardReponse.getEntity(); + Object obj= JSONValue.parse(jsonString); + JSONObject jsonObject = (JSONObject) obj; + + String jsonLeaderboardName = (String) jsonObject.get("name"); + String jsonResultState = (String) jsonObject.get("resultState"); + + assertTrue(regattaLeaderboard.getName().equals(jsonLeaderboardName)); + assertTrue(LeaderboardsResource.ResultStates.Final.name().equals(jsonResultState)); + + TimePoint resultTimePoint = parseTimepointFromJsonNumber((Long) jsonObject.get("resultTimepoint")); + assertNotNull(resultTimePoint); + + // second call - it's expected to get the same result from the cache + Thread.sleep(100); + + LeaderboardsResource resource2 = spyResource(new LeaderboardsResource()); + Response leaderboardReponse2 = resource2.getLeaderboard(regatta.getName(), LeaderboardsResource.ResultStates.Final, null); + + String jsonString2 = (String) leaderboardReponse2.getEntity(); + Object obj2= JSONValue.parse(jsonString2); + JSONObject jsonObject2 = (JSONObject) obj2; + + String jsonLeaderboardName2 = (String) jsonObject2.get("name"); + String jsonResultState2 = (String) jsonObject2.get("resultState"); + + assertTrue(jsonLeaderboardName.equals(jsonLeaderboardName2)); + assertTrue(jsonResultState.equals(jsonResultState2)); + + TimePoint resultTimePoint2 = parseTimepointFromJsonNumber((Long) jsonObject.get("resultTimepoint")); + assertNotNull(resultTimePoint2); + assertTrue(resultTimePoint.equals(resultTimePoint2)); + } + + @Test + public void testExportLeaderboardWithMaxCompetitorsCountAsJson() throws Exception { + dropAndCreateRegatta(); + Integer maxCompetitorsCount = 2; + + LeaderboardsResource resource = spyResource(new LeaderboardsResource()); + Response leaderboardReponse = resource.getLeaderboard(regatta.getName(), LeaderboardsResource.ResultStates.Final, null); + + String jsonString = (String) leaderboardReponse.getEntity(); + Object obj= JSONValue.parse(jsonString); + JSONObject jsonObject = (JSONObject) obj; + + String jsonLeaderboardName = (String) jsonObject.get("name"); + String jsonResultState = (String) jsonObject.get("resultState"); + + assertTrue(regattaLeaderboard.getName().equals(jsonLeaderboardName)); + assertTrue(LeaderboardsResource.ResultStates.Final.name().equals(jsonResultState)); + + TimePoint resultTimePoint = parseTimepointFromJsonNumber((Long) jsonObject.get("resultTimepoint")); + assertNull(resultTimePoint); + + regattaLeaderboard.getScoreCorrection().setTimePointOfLastCorrectionsValidity(MillisecondsTimePoint.now()); + + // first call for 'all' competitors + LeaderboardsResource resource2 = spyResource(new LeaderboardsResource()); + Response leaderboardReponse2 = resource2.getLeaderboard(regatta.getName(), LeaderboardsResource.ResultStates.Final, null); + + jsonString = (String) leaderboardReponse2.getEntity(); + obj= JSONValue.parse(jsonString); + jsonObject = (JSONObject) obj; + JSONArray jsonCompetitors = (JSONArray) jsonObject.get("competitors"); + assertEquals(jsonCompetitors.size(), 4); + + Thread.sleep(100); + + // second call with maxCompetitorsCount set + LeaderboardsResource resource3 = spyResource(new LeaderboardsResource()); + Response leaderboardReponse3 = resource3.getLeaderboard(regatta.getName(), LeaderboardsResource.ResultStates.Final, maxCompetitorsCount); + + jsonString = (String) leaderboardReponse3.getEntity(); + obj= JSONValue.parse(jsonString); + jsonObject = (JSONObject) obj; + jsonCompetitors = (JSONArray) jsonObject.get("competitors"); + assertEquals((Integer) jsonCompetitors.size(), maxCompetitorsCount); + + Thread.sleep(100); + + // third call for 'all' competitors + LeaderboardsResource resource4 = spyResource(new LeaderboardsResource()); + Response leaderboardReponse4 = resource4.getLeaderboard(regatta.getName(), LeaderboardsResource.ResultStates.Final, null); + + jsonString = (String) leaderboardReponse4.getEntity(); + obj= JSONValue.parse(jsonString); + jsonObject = (JSONObject) obj; + jsonCompetitors = (JSONArray) jsonObject.get("competitors"); + assertEquals(jsonCompetitors.size(), 4); + } +} diff --git a/java/com.sap.sailing.server.gateway.test/src/com/sap/sailing/server/gateway/test/jaxrs/RegattasResourceTest.java b/java/com.sap.sailing.server.gateway.test/src/com/sap/sailing/server/gateway/test/jaxrs/RegattasResourceTest.java index aaecab6e0ac..00b3b158769 100644 --- a/java/com.sap.sailing.server.gateway.test/src/com/sap/sailing/server/gateway/test/jaxrs/RegattasResourceTest.java +++ b/java/com.sap.sailing.server.gateway.test/src/com/sap/sailing/server/gateway/test/jaxrs/RegattasResourceTest.java @@ -1,78 +1,78 @@ -package com.sap.sailing.server.gateway.test.jaxrs; - -import static org.junit.Assert.assertTrue; - -import java.util.ArrayList; -import java.util.Calendar; -import java.util.GregorianCalendar; -import java.util.List; -import java.util.UUID; - -import javax.ws.rs.core.Response; - -import org.json.simple.JSONArray; -import org.json.simple.JSONObject; -import org.json.simple.JSONValue; -import org.junit.Before; -import org.junit.Test; - -import com.sap.sailing.domain.base.DomainFactory; -import com.sap.sailing.domain.base.Fleet; -import com.sap.sailing.domain.base.Series; -import com.sap.sailing.domain.base.impl.FleetImpl; -import com.sap.sailing.domain.base.impl.RegattaImpl; -import com.sap.sailing.domain.base.impl.SeriesImpl; -import com.sap.sailing.domain.common.ScoringSchemeType; -import com.sap.sailing.domain.ranking.OneDesignRankingMetric; -import com.sap.sailing.server.gateway.jaxrs.api.RegattasResource; -import com.sap.sse.common.TimePoint; -import com.sap.sse.common.impl.MillisecondsTimePoint; - -public class RegattasResourceTest extends AbstractJaxRsApiTest { - private String boatClassName = "49er"; - private String regattaName = "TestRegatta"; - - @Before - public void setUp() { - super.setUp(); - List series = new ArrayList(); - List fleets = new ArrayList(); - List raceColumnNames = new ArrayList(); - fleets.add(new FleetImpl("Fleet1")); - fleets.add(new FleetImpl("Fleet2")); - final Calendar cal = new GregorianCalendar(); - cal.set(2014, 5, 6, 10, 00); - final TimePoint startDate = new MillisecondsTimePoint(cal.getTime()); - cal.set(2014, 5, 8, 16, 00); - final TimePoint endDate = new MillisecondsTimePoint(cal.getTime()); - Series testSeries = new SeriesImpl("TestSeries", /* isMedal */false, fleets, raceColumnNames, /* trackedRegattaRegistry */null); - series.add(testSeries); - racingEventService.createRegatta(RegattaImpl.getDefaultName(regattaName, boatClassName), boatClassName, - startDate, endDate, UUID.randomUUID(), series, /*persistent*/ true, - DomainFactory.INSTANCE.createScoringScheme(ScoringSchemeType.LOW_POINT), null, /* useStartTimeInference */ true, OneDesignRankingMetric::new); - testSeries.addRaceColumn("R1", /* trackedRegattaRegistry */ null); - testSeries.addRaceColumn("R2", /* trackedRegattaRegistry */ null); - } - - @Test - public void testGetRegattas() throws Exception { - RegattasResource resource = new RegattasResource(); - RegattasResource spyResource = spyResource(resource); - - Response regattasResponse = spyResource.getRegattas(); - - String jsonString = (String) regattasResponse.getEntity(); - Object obj= JSONValue.parse(jsonString); - JSONArray array= (JSONArray) obj; - - assertTrue(array.size() == 1); - - JSONObject firstElement = (JSONObject) array.get(0); - String jsonName = (String) firstElement.get("name"); - String jsonBoatClass = (String) firstElement.get("boatclass"); - - assertTrue(RegattaImpl.getDefaultName(regattaName, boatClassName).equals(jsonName)); - assertTrue(boatClassName.equals(jsonBoatClass)); - } - -} +package com.sap.sailing.server.gateway.test.jaxrs; + +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.UUID; + +import javax.ws.rs.core.Response; + +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.JSONValue; +import org.junit.Before; +import org.junit.Test; + +import com.sap.sailing.domain.base.DomainFactory; +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.Series; +import com.sap.sailing.domain.base.impl.FleetImpl; +import com.sap.sailing.domain.base.impl.RegattaImpl; +import com.sap.sailing.domain.base.impl.SeriesImpl; +import com.sap.sailing.domain.common.ScoringSchemeType; +import com.sap.sailing.domain.ranking.OneDesignRankingMetric; +import com.sap.sailing.server.gateway.jaxrs.api.RegattasResource; +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.impl.MillisecondsTimePoint; + +public class RegattasResourceTest extends AbstractJaxRsApiTest { + private String boatClassName = "49er"; + private String regattaName = "TestRegatta"; + + @Before + public void setUp() { + super.setUp(); + List series = new ArrayList(); + List fleets = new ArrayList(); + List raceColumnNames = new ArrayList(); + fleets.add(new FleetImpl("Fleet1")); + fleets.add(new FleetImpl("Fleet2")); + final Calendar cal = new GregorianCalendar(); + cal.set(2014, 5, 6, 10, 00); + final TimePoint startDate = new MillisecondsTimePoint(cal.getTime()); + cal.set(2014, 5, 8, 16, 00); + final TimePoint endDate = new MillisecondsTimePoint(cal.getTime()); + Series testSeries = new SeriesImpl("TestSeries", /* isMedal */false, /* isFleetsCanRunInParallel */ true, fleets, raceColumnNames, /* trackedRegattaRegistry */null); + series.add(testSeries); + racingEventService.createRegatta(RegattaImpl.getDefaultName(regattaName, boatClassName), boatClassName, + startDate, endDate, UUID.randomUUID(), series, /*persistent*/ true, + DomainFactory.INSTANCE.createScoringScheme(ScoringSchemeType.LOW_POINT), null, /* useStartTimeInference */ true, OneDesignRankingMetric::new); + testSeries.addRaceColumn("R1", /* trackedRegattaRegistry */ null); + testSeries.addRaceColumn("R2", /* trackedRegattaRegistry */ null); + } + + @Test + public void testGetRegattas() throws Exception { + RegattasResource resource = new RegattasResource(); + RegattasResource spyResource = spyResource(resource); + + Response regattasResponse = spyResource.getRegattas(); + + String jsonString = (String) regattasResponse.getEntity(); + Object obj= JSONValue.parse(jsonString); + JSONArray array= (JSONArray) obj; + + assertTrue(array.size() == 1); + + JSONObject firstElement = (JSONObject) array.get(0); + String jsonName = (String) firstElement.get("name"); + String jsonBoatClass = (String) firstElement.get("boatclass"); + + assertTrue(RegattaImpl.getDefaultName(regattaName, boatClassName).equals(jsonName)); + assertTrue(boatClassName.equals(jsonBoatClass)); + } + +} diff --git a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/impl/rc/RaceGroupFactory.java b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/impl/rc/RaceGroupFactory.java index 9141af9f724..602a876e950 100644 --- a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/impl/rc/RaceGroupFactory.java +++ b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/impl/rc/RaceGroupFactory.java @@ -62,11 +62,11 @@ public class RaceGroupFactory { return new RaceGroupImpl(name, leaderboard.getDisplayName(), boatClass, courseArea, series, configuration); } - public Iterable getSeries(Leaderboard leaderboard) { + private Iterable getSeries(Leaderboard leaderboard) { Map> seriesToRaceColumns = getSeriesToRaceColumns(leaderboard); Collection seriesWithRows = new ArrayList<>(); for (Series series : getSeriesIterable(leaderboard, seriesToRaceColumns)) { - seriesWithRows.add(new SeriesWithRowsImpl(series.getName(), series.isMedal(), getRows(series, + seriesWithRows.add(new SeriesWithRowsImpl(series.getName(), series.isMedal(), series.isFleetsCanRunInParallel(), getRows(series, seriesToRaceColumns.get(series)))); } return seriesWithRows; @@ -80,6 +80,9 @@ public class RaceGroupFactory { return seriesToRaceColumns.keySet(); } + /** + * @param raceColumns must be provided in the same order in which they appear in the series / leaderboard + */ private Collection getRows(Series series, List raceColumns) { Collection rows = new ArrayList<>(); for (Fleet fleet : series.getFleets()) { @@ -94,16 +97,21 @@ public class RaceGroupFactory { return series.isFirstColumnIsNonDiscardableCarryForward(); } - private Collection getCells(String fleetName, List raceColumns, boolean isFirstRaceColumnVirtual) { + /** + * Delivers the race cell in the order of the {@link RaceColumn}s provided in {@code raceColumns}. + */ + private List getCells(String fleetName, List raceColumns, boolean isFirstRaceColumnVirtual) { boolean skippedFirst = false; - Collection cells = new ArrayList<>(); + List cells = new ArrayList<>(); if (raceColumns != null) { - for (RaceColumn raceColumn : raceColumns) { + int zeroBasedIndexOfRaceInFleet = 0; + for (final RaceColumn raceColumn : raceColumns) { if (isFirstRaceColumnVirtual && !skippedFirst) { skippedFirst = true; } else { Fleet fleet = raceColumn.getFleetByName(fleetName); - cells.add(new RaceCellImpl(raceColumn.getName(), raceColumn.getRaceLog(fleet), raceColumn.getFactor(), raceColumn.getExplicitFactor())); + cells.add(new RaceCellImpl(raceColumn.getName(), raceColumn.getRaceLog(fleet), + raceColumn.getFactor(), raceColumn.getExplicitFactor(), zeroBasedIndexOfRaceInFleet++)); } } } @@ -112,7 +120,9 @@ public class RaceGroupFactory { /** * Returns a series to race column mapping. If there are no series all race columns will - * be mapped from a default series. + * be mapped from a default series. The race column lists that appear as values in the result + * maintain the order in which the race column objects appear in the original {@link Series} + * or leaderboard. */ private Map> getSeriesToRaceColumns(Leaderboard leaderboard) { Map> seriesToRaceColumns = new HashMap<>(); @@ -135,7 +145,7 @@ public class RaceGroupFactory { private Series createDefaultSeries(Iterable fleets) { Series defaultSeries; - defaultSeries = new SeriesImpl(LeaderboardNameConstants.DEFAULT_SERIES_NAME, false, + defaultSeries = new SeriesImpl(LeaderboardNameConstants.DEFAULT_SERIES_NAME, false, true, fleets, Collections. emptyList(), null); return defaultSeries; } diff --git a/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/AbstractLogReplicationTest.java b/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/AbstractLogReplicationTest.java index f886fb2ad9d..dc5a8edcc94 100644 --- a/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/AbstractLogReplicationTest.java +++ b/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/AbstractLogReplicationTest.java @@ -1,117 +1,117 @@ -package com.sap.sailing.server.replication.test; - -import static org.junit.Assert.assertEquals; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.UUID; - -import com.sap.sailing.domain.abstractlog.AbstractLog; -import com.sap.sailing.domain.abstractlog.AbstractLogEvent; -import com.sap.sailing.domain.abstractlog.race.RaceLog; -import com.sap.sailing.domain.abstractlog.race.RaceLogEvent; -import com.sap.sailing.domain.abstractlog.race.RaceLogRaceStatusEvent; -import com.sap.sailing.domain.base.Regatta; -import com.sap.sailing.domain.base.Series; -import com.sap.sailing.domain.common.RankingMetrics; -import com.sap.sailing.domain.common.RegattaName; -import com.sap.sailing.domain.common.dto.FleetDTO; -import com.sap.sailing.domain.common.dto.RegattaCreationParametersDTO; -import com.sap.sailing.domain.common.dto.SeriesCreationParametersDTO; -import com.sap.sailing.domain.leaderboard.FlexibleLeaderboard; -import com.sap.sailing.domain.leaderboard.impl.LowPoint; -import com.sap.sailing.domain.racelog.RaceLogIdentifier; -import com.sap.sailing.server.impl.RacingEventServiceImpl; -import com.sap.sailing.server.operationaltransformation.AddSpecificRegatta; -import com.sap.sailing.server.operationaltransformation.CreateFlexibleLeaderboard; -import com.sap.sse.common.Color; -import com.sap.sse.common.Util; - -public abstract class AbstractLogReplicationTest, EventT extends AbstractLogEvent, VisitorT> - extends AbstractServerReplicationTest { - protected static final String BOAT_CLASS_NAME_49er = "49er"; - - /** - * Uses a new master that loads the existing regatta and race log to append an event to the race log. This will store it to the DB so that if the original - * master re-loads the race log it should see the new race log event. - * @throws Exception - */ - protected void addEventToDB(RaceLogIdentifier raceLogIdentifier, RaceLogRaceStatusEvent createRaceStatusEvent, String regattaName, String raceColumnName, String fleetName) throws Exception { - final RacingEventServiceImpl temporaryMaster = createNewMaster(); - Regatta regatta = temporaryMaster.getRegatta(new RegattaName(regattaName+" ("+BOAT_CLASS_NAME_49er+")")); - Series series = regatta.getSeries().iterator().next(); - RaceLog masterLog = series.getRaceColumnByName(raceColumnName).getRaceLog(series.getFleetByName(fleetName)); - masterLog.add(createRaceStatusEvent); - } - - /** - * Validation is done based only on the identifier and type of the {@link RaceLogEvent}s. - */ - protected void addAndValidateEventIds(LogT masterLog, LogT replicaLog, @SuppressWarnings("unchecked") EventT... addedEvents) throws InterruptedException { - // 1. Check state of replica after initial load... - assertEqualsOnId(masterLog, replicaLog); - // 2. ... add all incoming events... - for (EventT event : addedEvents) { - masterLog.add(event); - } - // 3. ... and give replication some time to deliver messages. - Thread.sleep(3000); - assertEqualsOnId(masterLog, replicaLog); - } - - /** - * Equality of the events is done based only on the identifier and type. - */ - protected void assertEqualsOnId(LogT masterLog, LogT replicaLog) { - replicaLog.lockForRead(); - try { - masterLog.lockForRead(); - try { - assertEqualsOnId(masterLog.getRawFixes(), replicaLog.getRawFixes()); - } finally { - masterLog.unlockAfterRead(); - } - } finally { - replicaLog.unlockAfterRead(); - } - } - - /** - * Equality of the events is done based only on the {@link RaceLogEvent}'s identifier and type. - */ - private void assertEqualsOnId(Iterable expectedEvents, Iterable actualEvents) { - List expectedCollection = new ArrayList<>(); - Util.addAll(expectedEvents, expectedCollection); - List actualCollection = new ArrayList<>(); - Util.addAll(actualEvents, actualCollection); - assertEquals(Util.size(expectedEvents), Util.size(actualEvents)); - for (int i = 0; i < expectedCollection.size(); i++) { - assertEquals(expectedCollection.get(i).getClass(), actualCollection.get(i).getClass()); - assertEquals(expectedCollection.get(i).getId(), actualCollection.get(i).getId()); - } - } - - protected Regatta setupRegatta(final String regattaName, String seriesName, String fleetName, String boatClassName) { - LinkedHashMap seriesCreationParameters = new LinkedHashMap<>(); - SeriesCreationParametersDTO creationParametersForDefaultSeries = new SeriesCreationParametersDTO( - Arrays.asList(new FleetDTO[] { new FleetDTO(fleetName, 0, Color.BLACK), }), /* medal */false, /* startsWithZero */ - false, /* firstColumnIsNonDiscardableCarryForward */false, /* discardingThresholds */new int[0], /* hasSplitFleetContiguousScoring */ - false); - seriesCreationParameters.put(seriesName, creationParametersForDefaultSeries); - // 1. Install some race column on master... - RegattaCreationParametersDTO regattaCreationParams = new RegattaCreationParametersDTO(seriesCreationParameters); - AddSpecificRegatta addRegattaOperation = new AddSpecificRegatta(regattaName, boatClassName, - /*startDate*/ null, /*endDate*/ null, /* regatta ID */ UUID.randomUUID(), regattaCreationParams, /* persistent */ true, - new LowPoint(), /* default course area ID */ UUID.randomUUID(), /* useStartTimeInference */ true, RankingMetrics.ONE_DESIGN); - return master.apply(addRegattaOperation); - } - - protected FlexibleLeaderboard setupFlexibleLeaderboard(final String leaderboardName) { - CreateFlexibleLeaderboard createTestLeaderboard = new CreateFlexibleLeaderboard(leaderboardName, leaderboardName, new int[] { 19, 44 }, new LowPoint(), null); - FlexibleLeaderboard masterLeaderboard = master.apply(createTestLeaderboard); - return masterLeaderboard; - } - +package com.sap.sailing.server.replication.test; + +import static org.junit.Assert.assertEquals; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.UUID; + +import com.sap.sailing.domain.abstractlog.AbstractLog; +import com.sap.sailing.domain.abstractlog.AbstractLogEvent; +import com.sap.sailing.domain.abstractlog.race.RaceLog; +import com.sap.sailing.domain.abstractlog.race.RaceLogEvent; +import com.sap.sailing.domain.abstractlog.race.RaceLogRaceStatusEvent; +import com.sap.sailing.domain.base.Regatta; +import com.sap.sailing.domain.base.Series; +import com.sap.sailing.domain.common.RankingMetrics; +import com.sap.sailing.domain.common.RegattaName; +import com.sap.sailing.domain.common.dto.FleetDTO; +import com.sap.sailing.domain.common.dto.RegattaCreationParametersDTO; +import com.sap.sailing.domain.common.dto.SeriesCreationParametersDTO; +import com.sap.sailing.domain.leaderboard.FlexibleLeaderboard; +import com.sap.sailing.domain.leaderboard.impl.LowPoint; +import com.sap.sailing.domain.racelog.RaceLogIdentifier; +import com.sap.sailing.server.impl.RacingEventServiceImpl; +import com.sap.sailing.server.operationaltransformation.AddSpecificRegatta; +import com.sap.sailing.server.operationaltransformation.CreateFlexibleLeaderboard; +import com.sap.sse.common.Color; +import com.sap.sse.common.Util; + +public abstract class AbstractLogReplicationTest, EventT extends AbstractLogEvent, VisitorT> + extends AbstractServerReplicationTest { + protected static final String BOAT_CLASS_NAME_49er = "49er"; + + /** + * Uses a new master that loads the existing regatta and race log to append an event to the race log. This will store it to the DB so that if the original + * master re-loads the race log it should see the new race log event. + * @throws Exception + */ + protected void addEventToDB(RaceLogIdentifier raceLogIdentifier, RaceLogRaceStatusEvent createRaceStatusEvent, String regattaName, String raceColumnName, String fleetName) throws Exception { + final RacingEventServiceImpl temporaryMaster = createNewMaster(); + Regatta regatta = temporaryMaster.getRegatta(new RegattaName(regattaName+" ("+BOAT_CLASS_NAME_49er+")")); + Series series = regatta.getSeries().iterator().next(); + RaceLog masterLog = series.getRaceColumnByName(raceColumnName).getRaceLog(series.getFleetByName(fleetName)); + masterLog.add(createRaceStatusEvent); + } + + /** + * Validation is done based only on the identifier and type of the {@link RaceLogEvent}s. + */ + protected void addAndValidateEventIds(LogT masterLog, LogT replicaLog, @SuppressWarnings("unchecked") EventT... addedEvents) throws InterruptedException { + // 1. Check state of replica after initial load... + assertEqualsOnId(masterLog, replicaLog); + // 2. ... add all incoming events... + for (EventT event : addedEvents) { + masterLog.add(event); + } + // 3. ... and give replication some time to deliver messages. + Thread.sleep(3000); + assertEqualsOnId(masterLog, replicaLog); + } + + /** + * Equality of the events is done based only on the identifier and type. + */ + protected void assertEqualsOnId(LogT masterLog, LogT replicaLog) { + replicaLog.lockForRead(); + try { + masterLog.lockForRead(); + try { + assertEqualsOnId(masterLog.getRawFixes(), replicaLog.getRawFixes()); + } finally { + masterLog.unlockAfterRead(); + } + } finally { + replicaLog.unlockAfterRead(); + } + } + + /** + * Equality of the events is done based only on the {@link RaceLogEvent}'s identifier and type. + */ + private void assertEqualsOnId(Iterable expectedEvents, Iterable actualEvents) { + List expectedCollection = new ArrayList<>(); + Util.addAll(expectedEvents, expectedCollection); + List actualCollection = new ArrayList<>(); + Util.addAll(actualEvents, actualCollection); + assertEquals(Util.size(expectedEvents), Util.size(actualEvents)); + for (int i = 0; i < expectedCollection.size(); i++) { + assertEquals(expectedCollection.get(i).getClass(), actualCollection.get(i).getClass()); + assertEquals(expectedCollection.get(i).getId(), actualCollection.get(i).getId()); + } + } + + protected Regatta setupRegatta(final String regattaName, String seriesName, String fleetName, String boatClassName) { + LinkedHashMap seriesCreationParameters = new LinkedHashMap<>(); + SeriesCreationParametersDTO creationParametersForDefaultSeries = new SeriesCreationParametersDTO( + Arrays.asList(new FleetDTO[] { new FleetDTO(fleetName, 0, Color.BLACK), }), /* medal */false, /* fleetsCanRunInParallel */ true, /* startsWithZero */ + false, /* firstColumnIsNonDiscardableCarryForward */false, /* discardingThresholds */new int[0], /* hasSplitFleetContiguousScoring */ + false); + seriesCreationParameters.put(seriesName, creationParametersForDefaultSeries); + // 1. Install some race column on master... + RegattaCreationParametersDTO regattaCreationParams = new RegattaCreationParametersDTO(seriesCreationParameters); + AddSpecificRegatta addRegattaOperation = new AddSpecificRegatta(regattaName, boatClassName, + /*startDate*/ null, /*endDate*/ null, /* regatta ID */ UUID.randomUUID(), regattaCreationParams, /* persistent */ true, + new LowPoint(), /* default course area ID */ UUID.randomUUID(), /* useStartTimeInference */ true, RankingMetrics.ONE_DESIGN); + return master.apply(addRegattaOperation); + } + + protected FlexibleLeaderboard setupFlexibleLeaderboard(final String leaderboardName) { + CreateFlexibleLeaderboard createTestLeaderboard = new CreateFlexibleLeaderboard(leaderboardName, leaderboardName, new int[] { 19, 44 }, new LowPoint(), null); + FlexibleLeaderboard masterLeaderboard = master.apply(createTestLeaderboard); + return masterLeaderboard; + } + } \ No newline at end of file diff --git a/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/RegattaReplicationTest.java b/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/RegattaReplicationTest.java index c9a660ffc08..62ed8b2751e 100755 --- a/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/RegattaReplicationTest.java +++ b/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/RegattaReplicationTest.java @@ -185,11 +185,11 @@ public class RegattaReplicationTest extends AbstractServerReplicationTest { final String baseEventName = "Kiel Week 2012"; final String boatClassName = "49er"; final List emptyRaceColumnNamesList = Collections.emptyList(); - Series qualification = new SeriesImpl("Qualification", /* isMedal */ false, + Series qualification = new SeriesImpl("Qualification", /* isMedal */ false, /* isFleetsCanRunInParallel */ true, Arrays.asList(new Fleet[] { new FleetImpl("Yellow"), new FleetImpl("Blue") }), emptyRaceColumnNamesList, /* trackedRegattaRegistry */ null); - Series finals = new SeriesImpl("Finals", /* isMedal */ false, + Series finals = new SeriesImpl("Finals", /* isMedal */ false, /* isFleetsCanRunInParallel */ true, Arrays.asList(new Fleet[] { new FleetImpl("Gold", 1), new FleetImpl("Silver", 2) }), emptyRaceColumnNamesList, /* trackedRegattaRegistry */ null); - Series medal = new SeriesImpl("Medal", /* isMedal */ true, + Series medal = new SeriesImpl("Medal", /* isMedal */ true, /* isFleetsCanRunInParallel */ true, Arrays.asList(new Fleet[] { new FleetImpl("Medal") }), emptyRaceColumnNamesList, /* trackedRegattaRegistry */ null); Regatta masterRegatta = master.createRegatta(RegattaImpl.getDefaultName(baseEventName, boatClassName), boatClassName, /*startDate*/ null, /*endDate*/ null, UUID.randomUUID(), Arrays.asList(new Series[] { qualification, finals, medal }), /* persistent */ true, DomainFactory.INSTANCE.createScoringScheme(ScoringSchemeType.LOW_POINT), null, /* useStartTimeInference */ true, OneDesignRankingMetric::new); @@ -224,7 +224,7 @@ public class RegattaReplicationTest extends AbstractServerReplicationTest { final String baseEventName = "Extreme Sailing Series 2020"; final String boatClassName = "Extreme40"; final List emptyRaceColumnNamesList = Collections.emptyList(); - Series qualification = new SeriesImpl("Qualification", /* isMedal */ false, + Series qualification = new SeriesImpl("Qualification", /* isMedal */ false, /* isFleetsCanRunInParallel */ true, Arrays.asList(new Fleet[] { new FleetImpl("Yellow"), new FleetImpl("Blue") }), emptyRaceColumnNamesList, /* trackedRegattaRegistry */ null); Regatta masterRegatta = master.createRegatta(RegattaImpl.getDefaultName(baseEventName, boatClassName), boatClassName, /*startDate*/ null, /*endDate*/ null, UUID.randomUUID(), Arrays.asList(new Series[] { qualification }), /* persistent */ true, DomainFactory.INSTANCE.createScoringScheme(ScoringSchemeType.LOW_POINT), null, /* useStartTimeInference */ true, OneDesignRankingMetric::new); @@ -238,10 +238,10 @@ public class RegattaReplicationTest extends AbstractServerReplicationTest { assertEquals("Qualification", replicatedQualification.getName()); assertEquals(2, Util.size(replicatedQualification.getFleets())); assertFalse(seriesIter.hasNext()); - Series finals = new SeriesImpl("Finals", /* isMedal */ false, + Series finals = new SeriesImpl("Finals", /* isMedal */ false, /* isFleetsCanRunInParallel */ true, Arrays.asList(new Fleet[] { new FleetImpl("Gold", 1) }), emptyRaceColumnNamesList, /* trackedRegattaRegistry */ null); FleetDTO finalsGoldFleet = new FleetDTO("Gold", 1, Color.GRAY); - master.apply(new UpdateSeries(masterRegatta.getRegattaIdentifier(), finals.getName(), finals.getName(), finals.isMedal(), + master.apply(new UpdateSeries(masterRegatta.getRegattaIdentifier(), finals.getName(), finals.getName(), finals.isMedal(), finals.isFleetsCanRunInParallel(), new int[] {}, finals.isStartsWithZeroScore(), finals.isFirstColumnIsNonDiscardableCarryForward(), finals.hasSplitFleetContiguousScoring(), Arrays.asList(new FleetDTO[] { finalsGoldFleet }))); @@ -267,7 +267,7 @@ public class RegattaReplicationTest extends AbstractServerReplicationTest { final String baseEventName = "Extreme Sailing Series 2021"; final String boatClassName = "Extreme40"; final List emptyRaceColumnNamesList = Collections.emptyList(); - Series qualification = new SeriesImpl("Qualification", /* isMedal */ false, + Series qualification = new SeriesImpl("Qualification", /* isMedal */ false, /* isFleetsCanRunInParallel */ true, Arrays.asList(new Fleet[] { new FleetImpl("Yellow"), new FleetImpl("Blue") }), emptyRaceColumnNamesList, /* trackedRegattaRegistry */ null); Regatta masterRegatta = master.createRegatta(RegattaImpl.getDefaultName(baseEventName, boatClassName), boatClassName, /*startDate*/ null, /*endDate*/ null, UUID.randomUUID(), Arrays.asList(new Series[] { qualification }), /* persistent */ true, DomainFactory.INSTANCE.createScoringScheme(ScoringSchemeType.LOW_POINT), null, /* useStartTimeInference */ true, OneDesignRankingMetric::new); @@ -279,7 +279,7 @@ public class RegattaReplicationTest extends AbstractServerReplicationTest { Series replicatedQualification = seriesIter.next(); assertEquals("Qualification", replicatedQualification.getName()); master.apply(new UpdateSeries(masterRegatta.getRegattaIdentifier(), qualification.getName(), "Simons Quali", - qualification.isMedal(), + qualification.isMedal(), qualification.isFleetsCanRunInParallel(), new int[] {}, qualification.isStartsWithZeroScore(), qualification.isFirstColumnIsNonDiscardableCarryForward(), qualification.hasSplitFleetContiguousScoring(), Arrays.asList(new FleetDTO[] { }))); diff --git a/java/com.sap.sailing.server.test/src/com/sap/sailing/server/impl/MediaMasterDataExportTest.java b/java/com.sap.sailing.server.test/src/com/sap/sailing/server/impl/MediaMasterDataExportTest.java index 455cd856500..bf6f4c88ca5 100644 --- a/java/com.sap.sailing.server.test/src/com/sap/sailing/server/impl/MediaMasterDataExportTest.java +++ b/java/com.sap.sailing.server.test/src/com/sap/sailing/server/impl/MediaMasterDataExportTest.java @@ -1,190 +1,190 @@ -package com.sap.sailing.server.impl; - -import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.mock; - -import java.io.Serializable; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; - -import org.junit.Test; - -import com.sap.sailing.domain.base.BoatClass; -import com.sap.sailing.domain.base.CourseArea; -import com.sap.sailing.domain.base.Event; -import com.sap.sailing.domain.base.Fleet; -import com.sap.sailing.domain.base.RaceColumn; -import com.sap.sailing.domain.base.Regatta; -import com.sap.sailing.domain.base.Series; -import com.sap.sailing.domain.base.impl.BoatClassImpl; -import com.sap.sailing.domain.base.impl.CourseAreaImpl; -import com.sap.sailing.domain.base.impl.FleetImpl; -import com.sap.sailing.domain.base.impl.RegattaImpl; -import com.sap.sailing.domain.base.impl.SeriesImpl; -import com.sap.sailing.domain.common.BoatClassMasterdata; -import com.sap.sailing.domain.common.RaceIdentifier; -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; -import com.sap.sailing.domain.common.RegattaNameAndRaceName; -import com.sap.sailing.domain.common.media.MediaTrack; -import com.sap.sailing.domain.leaderboard.FlexibleLeaderboard; -import com.sap.sailing.domain.leaderboard.Leaderboard; -import com.sap.sailing.domain.leaderboard.LeaderboardGroup; -import com.sap.sailing.domain.leaderboard.RegattaLeaderboard; -import com.sap.sailing.domain.leaderboard.ScoringScheme; -import com.sap.sailing.domain.leaderboard.ThresholdBasedResultDiscardingRule; -import com.sap.sailing.domain.leaderboard.impl.FlexibleLeaderboardImpl; -import com.sap.sailing.domain.leaderboard.impl.LeaderboardGroupImpl; -import com.sap.sailing.domain.leaderboard.impl.LowPoint; -import com.sap.sailing.domain.leaderboard.impl.RegattaLeaderboardImpl; -import com.sap.sailing.domain.leaderboard.impl.ThresholdBasedResultDiscardingRuleImpl; -import com.sap.sailing.domain.masterdataimport.TopLevelMasterData; -import com.sap.sailing.domain.racelog.tracking.GPSFixStore; -import com.sap.sailing.domain.ranking.OneDesignRankingMetric; -import com.sap.sailing.domain.tracking.TrackedRegattaRegistry; -import com.sap.sse.common.Duration; -import com.sap.sse.common.TimePoint; -import com.sap.sse.common.impl.MillisecondsDurationImpl; -import com.sap.sse.common.impl.MillisecondsTimePoint; -import com.sap.sse.common.media.MimeType; - -public class MediaMasterDataExportTest { - - static final String matchingRegattaName = "Matching Regatta"; - static final String missingRegattaName = "Missing Regatta"; - static final String matchingRace1 = "matching race 1"; - static final String matchingRace2 = "matching race 2"; - static final String matchingRace3 = "matching race 3"; - static final String missingRace1 = "missing race 1"; - static final String missingRace2 = "missing race 2"; - static final String missingRace3 = "missing race 3"; - - static final boolean displayGroupsInReverseOrder = true; - static final ThresholdBasedResultDiscardingRule resultDiscardingRule = new ThresholdBasedResultDiscardingRuleImpl(new int[0]); - static final BoatClass boatClass = new BoatClassImpl("boat class name", BoatClassMasterdata._12M); - static final TimePoint startDate = MillisecondsTimePoint.now(); - static final TimePoint endDate = startDate.plus(MillisecondsDurationImpl.ONE_DAY); - static final boolean isMedal = false; - static final Fleet regattaFleet = new FleetImpl("fleet name"); - static final boolean persistent = false; - static final ScoringScheme scoringScheme = new LowPoint(); - static final CourseArea courseArea = new CourseAreaImpl("Course Area", UUID.randomUUID()); - static final Serializable regatteId = "regatta id"; - - @Test - public void testTrackWithAssignedRaceButEmptyLeaderboardGroup() { - TimePoint startTime = null; - Duration duration = null; - MimeType mimeType = MimeType.mp4; - Collection allMediaTracks = Arrays.asList( - new MediaTrack("dbId 1", "title 1 for existing regatta and race", "url", startTime, duration, mimeType, Collections.singleton(new RegattaNameAndRaceName(matchingRegattaName, matchingRace1))), - new MediaTrack("dbId 2", "title 2 for existing one regatta and race and one missing regatta", "url", startTime, duration, mimeType, new HashSet(Arrays.asList(new RegattaNameAndRaceName(missingRegattaName, matchingRace2), new RegattaNameAndRaceName(matchingRegattaName, matchingRace2)))), - new MediaTrack("dbId 3", "title 1 for missing regatta", "url", startTime, duration, mimeType, Collections.singleton(new RegattaNameAndRaceName(missingRegattaName, matchingRace1))), - new MediaTrack("dbId 4", "title 2 for missing regatta", "url", startTime, duration, mimeType, new HashSet(Arrays.asList(new RegattaNameAndRaceName(missingRegattaName, matchingRace3), new RegattaNameAndRaceName(matchingRegattaName, missingRace1)))) - ); - Collection regattaRaces = Collections.emptyList(); - Collection flexibleRaces = Collections.emptyList(); - TopLevelMasterData topLevelMasterData = createTopLevelMasterData(regattaRaces , flexibleRaces, allMediaTracks); - Collection filteredMediaTracks = topLevelMasterData.getFilteredMediaTracks(); - assertEquals(0, filteredMediaTracks.size()); - } - - @Test - public void testTracksWithAssignedRaceAndMatchingRegattaInLeaderboardGroup() { - TimePoint startTime = null; - Duration duration = null; - MimeType mimeType = MimeType.mp4; - Collection allMediaTracks = Arrays.asList( - new MediaTrack("dbId 1", "title 1 for existing regatta and race", "url", startTime, duration, mimeType, Collections.singleton(new RegattaNameAndRaceName(matchingRegattaName, matchingRace1))), - new MediaTrack("dbId 2", "title 2 for existing one regatta and race and one missing regatta", "url", startTime, duration, mimeType, new HashSet(Arrays.asList(new RegattaNameAndRaceName(missingRegattaName, matchingRace2), new RegattaNameAndRaceName(matchingRegattaName, matchingRace2)))), - new MediaTrack("dbId 3", "title 3 for missing regatta", "url", startTime, duration, mimeType, Collections.singleton(new RegattaNameAndRaceName(missingRegattaName, matchingRace1))), - new MediaTrack("dbId 4", "title 4 for missing regatta", "url", startTime, duration, mimeType, new HashSet(Arrays.asList(new RegattaNameAndRaceName(missingRegattaName, matchingRace3), new RegattaNameAndRaceName(matchingRegattaName, missingRace1)))), - new MediaTrack("dbId 5", "title 5 for existing regatta and race", "url", startTime, duration, mimeType, Collections.singleton(new RegattaNameAndRaceName(matchingRegattaName, matchingRace1))) - ); - Collection regattaRaces = Arrays.asList( - new RegattaNameAndRaceName(matchingRegattaName, matchingRace1), - new RegattaNameAndRaceName(matchingRegattaName, matchingRace2), - new RegattaNameAndRaceName(matchingRegattaName, matchingRace3) - ); - Collection flexibleRaces = Collections.emptyList(); - TopLevelMasterData topLevelMasterData = createTopLevelMasterData(regattaRaces , flexibleRaces, allMediaTracks); - Collection filteredMediaTracks = topLevelMasterData.getFilteredMediaTracks(); - assertEquals(3, filteredMediaTracks.size()); - } - - @Test - public void testTrackWithAssignedRaceAndMatchingFlexibleLeaderboardInLeaderboardGroup() { - TimePoint startTime = null; - Duration duration = null; - MimeType mimeType = MimeType.mp4; - Collection allMediaTracks = Arrays.asList( - new MediaTrack("dbId 1", "title 1 for existing regatta and race", "url", startTime, duration, mimeType, Collections.singleton(new RegattaNameAndRaceName(matchingRegattaName, matchingRace1))), - new MediaTrack("dbId 2", "title 2 for existing one regatta and race and one missing regatta", "url", startTime, duration, mimeType, new HashSet(Arrays.asList(new RegattaNameAndRaceName(missingRegattaName, matchingRace2), new RegattaNameAndRaceName(matchingRegattaName, matchingRace2)))), - new MediaTrack("dbId 3", "title 3 for missing regatta", "url", startTime, duration, mimeType, Collections.singleton(new RegattaNameAndRaceName(missingRegattaName, matchingRace1))), - new MediaTrack("dbId 4", "title 4 for missing regatta", "url", startTime, duration, mimeType, new HashSet(Arrays.asList(new RegattaNameAndRaceName(missingRegattaName, matchingRace3), new RegattaNameAndRaceName(matchingRegattaName, missingRace1)))), - new MediaTrack("dbId 5", "title 5 for existing regatta and race", "url", startTime, duration, mimeType, Collections.singleton(new RegattaNameAndRaceName(matchingRegattaName, matchingRace1))) - ); - Collection regattaRaces = Collections.emptyList(); - Collection flexibleRaces = Arrays.asList( - new RegattaNameAndRaceName(matchingRegattaName, matchingRace1), - new RegattaNameAndRaceName(matchingRegattaName, matchingRace2), - new RegattaNameAndRaceName(matchingRegattaName, matchingRace3) - ); - TopLevelMasterData topLevelMasterData = createTopLevelMasterData(regattaRaces , flexibleRaces, allMediaTracks); - Collection filteredMediaTracks = topLevelMasterData.getFilteredMediaTracks(); - assertEquals(3, filteredMediaTracks.size()); - } - - private TopLevelMasterData createTopLevelMasterData(Collection regattaRaces, Collection flexibleRaces, Collection allMediaTracks) { - Set raceColumnNames = new HashSet<>(); - for (RaceIdentifier regattaAndRaceIdentifier : regattaRaces) { - raceColumnNames.add(regattaAndRaceIdentifier.getRaceName()); - } - Regatta regatta = createTestRegatta("regatta name", raceColumnNames); - RegattaLeaderboard regattaLeaderboard = new RegattaLeaderboardImpl(regatta, resultDiscardingRule); - assignRacesToRegattaLeaderboardColumns(regattaLeaderboard, regattaRaces); - - FlexibleLeaderboard flexibleLeaderboard = new FlexibleLeaderboardImpl("flexible leaderboard", resultDiscardingRule, scoringScheme , courseArea); - for (RaceIdentifier regattaAndRaceIdentifier : flexibleRaces) { - RaceColumn raceColumn = flexibleLeaderboard.addRaceColumn(regattaAndRaceIdentifier.getRaceName(), isMedal); - Fleet defaultFleet = raceColumn.getFleets().iterator().next(); - raceColumn.setRaceIdentifier(defaultFleet, regattaAndRaceIdentifier); - } - - - List leaderboards = Arrays.asList(regattaLeaderboard, flexibleLeaderboard); - LeaderboardGroup leaderboardGroup = new LeaderboardGroupImpl("name", "description", "displayName", displayGroupsInReverseOrder , leaderboards); - Set groupsToExport = Collections.singleton(leaderboardGroup); - Iterable allEvents = Collections.emptyList(); - Map regattaForRaceIdString = Collections.emptyMap(); - GPSFixStore gpsFixStore = mock(GPSFixStore.class); - boolean exportWind = false; - TopLevelMasterData topLevelMasterData = new TopLevelMasterData(groupsToExport , allEvents , regattaForRaceIdString, allMediaTracks , gpsFixStore , exportWind, new HashMap<>()); - return topLevelMasterData; - } - - public static void assignRacesToRegattaLeaderboardColumns(RegattaLeaderboard leaderboard, Collection raceIdentifiers) { - Iterator regattaRacesIterator = raceIdentifiers.iterator(); - for (RaceColumn raceColumn : leaderboard.getRaceColumns()) { - raceColumn.setRaceIdentifier(regattaFleet, regattaRacesIterator.next()); - } - } - - public static RegattaImpl createTestRegatta(String regattaName, Iterable raceColumnNames) { - Iterable regattaFleets = Collections.singleton(regattaFleet); - TrackedRegattaRegistry trackedRegattaRegistry = mock(TrackedRegattaRegistry.class); - Series series = new SeriesImpl("series name", isMedal, regattaFleets, raceColumnNames, trackedRegattaRegistry); - Iterable regattaSeries = Collections.singleton(series); - return new RegattaImpl(regattaName, boatClass, startDate, endDate, regattaSeries, persistent, scoringScheme, regatteId , courseArea, OneDesignRankingMetric::new); - } - - - -} +package com.sap.sailing.server.impl; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import org.junit.Test; + +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.domain.base.CourseArea; +import com.sap.sailing.domain.base.Event; +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.RaceColumn; +import com.sap.sailing.domain.base.Regatta; +import com.sap.sailing.domain.base.Series; +import com.sap.sailing.domain.base.impl.BoatClassImpl; +import com.sap.sailing.domain.base.impl.CourseAreaImpl; +import com.sap.sailing.domain.base.impl.FleetImpl; +import com.sap.sailing.domain.base.impl.RegattaImpl; +import com.sap.sailing.domain.base.impl.SeriesImpl; +import com.sap.sailing.domain.common.BoatClassMasterdata; +import com.sap.sailing.domain.common.RaceIdentifier; +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.domain.common.RegattaNameAndRaceName; +import com.sap.sailing.domain.common.media.MediaTrack; +import com.sap.sailing.domain.leaderboard.FlexibleLeaderboard; +import com.sap.sailing.domain.leaderboard.Leaderboard; +import com.sap.sailing.domain.leaderboard.LeaderboardGroup; +import com.sap.sailing.domain.leaderboard.RegattaLeaderboard; +import com.sap.sailing.domain.leaderboard.ScoringScheme; +import com.sap.sailing.domain.leaderboard.ThresholdBasedResultDiscardingRule; +import com.sap.sailing.domain.leaderboard.impl.FlexibleLeaderboardImpl; +import com.sap.sailing.domain.leaderboard.impl.LeaderboardGroupImpl; +import com.sap.sailing.domain.leaderboard.impl.LowPoint; +import com.sap.sailing.domain.leaderboard.impl.RegattaLeaderboardImpl; +import com.sap.sailing.domain.leaderboard.impl.ThresholdBasedResultDiscardingRuleImpl; +import com.sap.sailing.domain.masterdataimport.TopLevelMasterData; +import com.sap.sailing.domain.racelog.tracking.GPSFixStore; +import com.sap.sailing.domain.ranking.OneDesignRankingMetric; +import com.sap.sailing.domain.tracking.TrackedRegattaRegistry; +import com.sap.sse.common.Duration; +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.impl.MillisecondsDurationImpl; +import com.sap.sse.common.impl.MillisecondsTimePoint; +import com.sap.sse.common.media.MimeType; + +public class MediaMasterDataExportTest { + + static final String matchingRegattaName = "Matching Regatta"; + static final String missingRegattaName = "Missing Regatta"; + static final String matchingRace1 = "matching race 1"; + static final String matchingRace2 = "matching race 2"; + static final String matchingRace3 = "matching race 3"; + static final String missingRace1 = "missing race 1"; + static final String missingRace2 = "missing race 2"; + static final String missingRace3 = "missing race 3"; + + static final boolean displayGroupsInReverseOrder = true; + static final ThresholdBasedResultDiscardingRule resultDiscardingRule = new ThresholdBasedResultDiscardingRuleImpl(new int[0]); + static final BoatClass boatClass = new BoatClassImpl("boat class name", BoatClassMasterdata._12M); + static final TimePoint startDate = MillisecondsTimePoint.now(); + static final TimePoint endDate = startDate.plus(MillisecondsDurationImpl.ONE_DAY); + static final boolean isMedal = false; + static final Fleet regattaFleet = new FleetImpl("fleet name"); + static final boolean persistent = false; + static final ScoringScheme scoringScheme = new LowPoint(); + static final CourseArea courseArea = new CourseAreaImpl("Course Area", UUID.randomUUID()); + static final Serializable regatteId = "regatta id"; + + @Test + public void testTrackWithAssignedRaceButEmptyLeaderboardGroup() { + TimePoint startTime = null; + Duration duration = null; + MimeType mimeType = MimeType.mp4; + Collection allMediaTracks = Arrays.asList( + new MediaTrack("dbId 1", "title 1 for existing regatta and race", "url", startTime, duration, mimeType, Collections.singleton(new RegattaNameAndRaceName(matchingRegattaName, matchingRace1))), + new MediaTrack("dbId 2", "title 2 for existing one regatta and race and one missing regatta", "url", startTime, duration, mimeType, new HashSet(Arrays.asList(new RegattaNameAndRaceName(missingRegattaName, matchingRace2), new RegattaNameAndRaceName(matchingRegattaName, matchingRace2)))), + new MediaTrack("dbId 3", "title 1 for missing regatta", "url", startTime, duration, mimeType, Collections.singleton(new RegattaNameAndRaceName(missingRegattaName, matchingRace1))), + new MediaTrack("dbId 4", "title 2 for missing regatta", "url", startTime, duration, mimeType, new HashSet(Arrays.asList(new RegattaNameAndRaceName(missingRegattaName, matchingRace3), new RegattaNameAndRaceName(matchingRegattaName, missingRace1)))) + ); + Collection regattaRaces = Collections.emptyList(); + Collection flexibleRaces = Collections.emptyList(); + TopLevelMasterData topLevelMasterData = createTopLevelMasterData(regattaRaces , flexibleRaces, allMediaTracks); + Collection filteredMediaTracks = topLevelMasterData.getFilteredMediaTracks(); + assertEquals(0, filteredMediaTracks.size()); + } + + @Test + public void testTracksWithAssignedRaceAndMatchingRegattaInLeaderboardGroup() { + TimePoint startTime = null; + Duration duration = null; + MimeType mimeType = MimeType.mp4; + Collection allMediaTracks = Arrays.asList( + new MediaTrack("dbId 1", "title 1 for existing regatta and race", "url", startTime, duration, mimeType, Collections.singleton(new RegattaNameAndRaceName(matchingRegattaName, matchingRace1))), + new MediaTrack("dbId 2", "title 2 for existing one regatta and race and one missing regatta", "url", startTime, duration, mimeType, new HashSet(Arrays.asList(new RegattaNameAndRaceName(missingRegattaName, matchingRace2), new RegattaNameAndRaceName(matchingRegattaName, matchingRace2)))), + new MediaTrack("dbId 3", "title 3 for missing regatta", "url", startTime, duration, mimeType, Collections.singleton(new RegattaNameAndRaceName(missingRegattaName, matchingRace1))), + new MediaTrack("dbId 4", "title 4 for missing regatta", "url", startTime, duration, mimeType, new HashSet(Arrays.asList(new RegattaNameAndRaceName(missingRegattaName, matchingRace3), new RegattaNameAndRaceName(matchingRegattaName, missingRace1)))), + new MediaTrack("dbId 5", "title 5 for existing regatta and race", "url", startTime, duration, mimeType, Collections.singleton(new RegattaNameAndRaceName(matchingRegattaName, matchingRace1))) + ); + Collection regattaRaces = Arrays.asList( + new RegattaNameAndRaceName(matchingRegattaName, matchingRace1), + new RegattaNameAndRaceName(matchingRegattaName, matchingRace2), + new RegattaNameAndRaceName(matchingRegattaName, matchingRace3) + ); + Collection flexibleRaces = Collections.emptyList(); + TopLevelMasterData topLevelMasterData = createTopLevelMasterData(regattaRaces , flexibleRaces, allMediaTracks); + Collection filteredMediaTracks = topLevelMasterData.getFilteredMediaTracks(); + assertEquals(3, filteredMediaTracks.size()); + } + + @Test + public void testTrackWithAssignedRaceAndMatchingFlexibleLeaderboardInLeaderboardGroup() { + TimePoint startTime = null; + Duration duration = null; + MimeType mimeType = MimeType.mp4; + Collection allMediaTracks = Arrays.asList( + new MediaTrack("dbId 1", "title 1 for existing regatta and race", "url", startTime, duration, mimeType, Collections.singleton(new RegattaNameAndRaceName(matchingRegattaName, matchingRace1))), + new MediaTrack("dbId 2", "title 2 for existing one regatta and race and one missing regatta", "url", startTime, duration, mimeType, new HashSet(Arrays.asList(new RegattaNameAndRaceName(missingRegattaName, matchingRace2), new RegattaNameAndRaceName(matchingRegattaName, matchingRace2)))), + new MediaTrack("dbId 3", "title 3 for missing regatta", "url", startTime, duration, mimeType, Collections.singleton(new RegattaNameAndRaceName(missingRegattaName, matchingRace1))), + new MediaTrack("dbId 4", "title 4 for missing regatta", "url", startTime, duration, mimeType, new HashSet(Arrays.asList(new RegattaNameAndRaceName(missingRegattaName, matchingRace3), new RegattaNameAndRaceName(matchingRegattaName, missingRace1)))), + new MediaTrack("dbId 5", "title 5 for existing regatta and race", "url", startTime, duration, mimeType, Collections.singleton(new RegattaNameAndRaceName(matchingRegattaName, matchingRace1))) + ); + Collection regattaRaces = Collections.emptyList(); + Collection flexibleRaces = Arrays.asList( + new RegattaNameAndRaceName(matchingRegattaName, matchingRace1), + new RegattaNameAndRaceName(matchingRegattaName, matchingRace2), + new RegattaNameAndRaceName(matchingRegattaName, matchingRace3) + ); + TopLevelMasterData topLevelMasterData = createTopLevelMasterData(regattaRaces , flexibleRaces, allMediaTracks); + Collection filteredMediaTracks = topLevelMasterData.getFilteredMediaTracks(); + assertEquals(3, filteredMediaTracks.size()); + } + + private TopLevelMasterData createTopLevelMasterData(Collection regattaRaces, Collection flexibleRaces, Collection allMediaTracks) { + Set raceColumnNames = new HashSet<>(); + for (RaceIdentifier regattaAndRaceIdentifier : regattaRaces) { + raceColumnNames.add(regattaAndRaceIdentifier.getRaceName()); + } + Regatta regatta = createTestRegatta("regatta name", raceColumnNames); + RegattaLeaderboard regattaLeaderboard = new RegattaLeaderboardImpl(regatta, resultDiscardingRule); + assignRacesToRegattaLeaderboardColumns(regattaLeaderboard, regattaRaces); + + FlexibleLeaderboard flexibleLeaderboard = new FlexibleLeaderboardImpl("flexible leaderboard", resultDiscardingRule, scoringScheme , courseArea); + for (RaceIdentifier regattaAndRaceIdentifier : flexibleRaces) { + RaceColumn raceColumn = flexibleLeaderboard.addRaceColumn(regattaAndRaceIdentifier.getRaceName(), isMedal); + Fleet defaultFleet = raceColumn.getFleets().iterator().next(); + raceColumn.setRaceIdentifier(defaultFleet, regattaAndRaceIdentifier); + } + + + List leaderboards = Arrays.asList(regattaLeaderboard, flexibleLeaderboard); + LeaderboardGroup leaderboardGroup = new LeaderboardGroupImpl("name", "description", "displayName", displayGroupsInReverseOrder , leaderboards); + Set groupsToExport = Collections.singleton(leaderboardGroup); + Iterable allEvents = Collections.emptyList(); + Map regattaForRaceIdString = Collections.emptyMap(); + GPSFixStore gpsFixStore = mock(GPSFixStore.class); + boolean exportWind = false; + TopLevelMasterData topLevelMasterData = new TopLevelMasterData(groupsToExport , allEvents , regattaForRaceIdString, allMediaTracks , gpsFixStore , exportWind, new HashMap<>()); + return topLevelMasterData; + } + + public static void assignRacesToRegattaLeaderboardColumns(RegattaLeaderboard leaderboard, Collection raceIdentifiers) { + Iterator regattaRacesIterator = raceIdentifiers.iterator(); + for (RaceColumn raceColumn : leaderboard.getRaceColumns()) { + raceColumn.setRaceIdentifier(regattaFleet, regattaRacesIterator.next()); + } + } + + public static RegattaImpl createTestRegatta(String regattaName, Iterable raceColumnNames) { + Iterable regattaFleets = Collections.singleton(regattaFleet); + TrackedRegattaRegistry trackedRegattaRegistry = mock(TrackedRegattaRegistry.class); + Series series = new SeriesImpl("series name", isMedal, /* isFleetsCanRunInParallel */ true, regattaFleets, raceColumnNames, trackedRegattaRegistry); + Iterable regattaSeries = Collections.singleton(series); + return new RegattaImpl(regattaName, boatClass, startDate, endDate, regattaSeries, persistent, scoringScheme, regatteId , courseArea, OneDesignRankingMetric::new); + } + + + +} diff --git a/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/MasterDataImportTest.java b/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/MasterDataImportTest.java index c6da147d75f..ab6c392a302 100644 --- a/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/MasterDataImportTest.java +++ b/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/MasterDataImportTest.java @@ -204,7 +204,7 @@ public class MasterDataImportTest { FleetImpl testFleet1 = new FleetImpl(testFleet1Name); fleets.add(testFleet1); fleets.add(new FleetImpl("testFleet2")); - series.add(new SeriesImpl("testSeries", false, fleets, emptyRaceColumnNamesList, sourceService)); + series.add(new SeriesImpl("testSeries", false, /* isFleetsCanRunInParallel */ true, fleets, emptyRaceColumnNamesList, sourceService)); UUID regattaUUID = UUID.randomUUID(); Regatta regatta = sourceService.createRegatta( RegattaImpl.getDefaultName(TEST_REGATTA_NAME, TEST_BOAT_CLASS_NAME), TEST_BOAT_CLASS_NAME, @@ -467,7 +467,7 @@ public class MasterDataImportTest { FleetImpl testFleet1 = new FleetImpl("testFleet1"); fleets.add(testFleet1); fleets.add(new FleetImpl("testFleet2")); - series.add(new SeriesImpl("testSeries", false, fleets, emptyRaceColumnNamesList, sourceService)); + series.add(new SeriesImpl("testSeries", false, /* isFleetsCanRunInParallel */ true, fleets, emptyRaceColumnNamesList, sourceService)); UUID regattaUUID = UUID.randomUUID(); Regatta regatta = sourceService.createRegatta( RegattaImpl.getDefaultName(TEST_REGATTA_NAME, TEST_BOAT_CLASS_NAME), TEST_BOAT_CLASS_NAME, @@ -626,7 +626,7 @@ public class MasterDataImportTest { List fleets = new ArrayList(); FleetImpl testFleet1 = new FleetImpl("testFleet1"); fleets.add(testFleet1); - series.add(new SeriesImpl("testSeries", false, fleets, emptyRaceColumnNamesList, sourceService)); + series.add(new SeriesImpl("testSeries", false, /* isFleetsCanRunInParallel */ true, fleets, emptyRaceColumnNamesList, sourceService)); UUID regattaUUID = UUID.randomUUID(); Regatta regatta = sourceService.createRegatta(RegattaImpl.getDefaultName("testRegatta", "29er"), "29er", /* startDate */null, /* endDate */null, regattaUUID, series, true, new LowPoint(), courseAreaUUID, /* useStartTimeInference */ @@ -789,7 +789,7 @@ public class MasterDataImportTest { FleetImpl testFleet1 = new FleetImpl("testFleet1"); fleets.add(testFleet1); fleets.add(new FleetImpl("testFleet2")); - series.add(new SeriesImpl("testSeries", false, fleets, emptyRaceColumnNamesList, sourceService)); + series.add(new SeriesImpl("testSeries", false, /* isFleetsCanRunInParallel */ true, fleets, emptyRaceColumnNamesList, sourceService)); UUID regattaUUID = UUID.randomUUID(); Regatta regatta = sourceService.createRegatta( RegattaImpl.getDefaultName(TEST_REGATTA_NAME, TEST_BOAT_CLASS_NAME), TEST_BOAT_CLASS_NAME, @@ -949,7 +949,7 @@ public class MasterDataImportTest { FleetImpl testFleet1 = new FleetImpl("testFleet1"); fleets.add(testFleet1); fleets.add(new FleetImpl("testFleet2")); - series.add(new SeriesImpl("testSeries", false, fleets, emptyRaceColumnNamesList, sourceService)); + series.add(new SeriesImpl("testSeries", false, /* isFleetsCanRunInParallel */ true, fleets, emptyRaceColumnNamesList, sourceService)); UUID regattaUUID = UUID.randomUUID(); Regatta regatta = sourceService.createRegatta( RegattaImpl.getDefaultName(TEST_REGATTA_NAME, TEST_BOAT_CLASS_NAME), TEST_BOAT_CLASS_NAME, @@ -1063,7 +1063,7 @@ public class MasterDataImportTest { List fleetsNotToOverride = new ArrayList(); FleetImpl testFleet1NotToOverride = new FleetImpl("testFleet1"); fleetsNotToOverride.add(testFleet1NotToOverride); - seriesNotToOverride.add(new SeriesImpl("testSeries", false, fleetsNotToOverride, emptyRaceColumnNamesList, + seriesNotToOverride.add(new SeriesImpl("testSeries", false, /* isFleetsCanRunInParallel */ true, fleetsNotToOverride, emptyRaceColumnNamesList, destService)); Regatta regattaNotToOverride = destService.createRegatta( RegattaImpl.getDefaultName(TEST_REGATTA_NAME, TEST_BOAT_CLASS_NAME), TEST_BOAT_CLASS_NAME, /* startDate */ @@ -1151,7 +1151,7 @@ public class MasterDataImportTest { FleetImpl testFleet1 = new FleetImpl("testFleet1"); fleets.add(testFleet1); fleets.add(new FleetImpl("testFleet2")); - series.add(new SeriesImpl("testSeries", false, fleets, emptyRaceColumnNamesList, sourceService)); + series.add(new SeriesImpl("testSeries", false, /* isFleetsCanRunInParallel */ true, fleets, emptyRaceColumnNamesList, sourceService)); UUID regattaUUID = UUID.randomUUID(); Regatta regatta = sourceService.createRegatta( RegattaImpl.getDefaultName(TEST_REGATTA_NAME, TEST_BOAT_CLASS_NAME), TEST_BOAT_CLASS_NAME, @@ -1261,7 +1261,7 @@ public class MasterDataImportTest { List fleetsToOverride = new ArrayList(); FleetImpl testFleet1ToOverride = new FleetImpl("testFleet1"); fleetsToOverride.add(testFleet1ToOverride); - seriesToOverride.add(new SeriesImpl("testSeries", false, fleetsToOverride, emptyRaceColumnNamesList, + seriesToOverride.add(new SeriesImpl("testSeries", false, /* isFleetsCanRunInParallel */ true, fleetsToOverride, emptyRaceColumnNamesList, destService)); Regatta regattaToOverride = destService.createRegatta( RegattaImpl.getDefaultName(TEST_REGATTA_NAME, TEST_BOAT_CLASS_NAME), TEST_BOAT_CLASS_NAME, /* startDate */ @@ -1452,7 +1452,7 @@ public class MasterDataImportTest { FleetImpl testFleet1 = new FleetImpl("testFleet1"); fleets.add(testFleet1); fleets.add(new FleetImpl("testFleet2")); - series.add(new SeriesImpl("testSeries", false, fleets, emptyRaceColumnNamesList, sourceService)); + series.add(new SeriesImpl("testSeries", false, /* isFleetsCanRunInParallel */ true, fleets, emptyRaceColumnNamesList, sourceService)); UUID regattaUUID = UUID.randomUUID(); Regatta regatta = sourceService.createRegatta( @@ -1587,7 +1587,7 @@ public class MasterDataImportTest { FleetImpl testFleet1 = new FleetImpl("testFleet1"); fleets.add(testFleet1); fleets.add(new FleetImpl("testFleet2")); - series.add(new SeriesImpl("testSeries", false, fleets, emptyRaceColumnNamesList, sourceService)); + series.add(new SeriesImpl("testSeries", false, /* isFleetsCanRunInParallel */ true, fleets, emptyRaceColumnNamesList, sourceService)); UUID regattaUUID = UUID.randomUUID(); Regatta regatta = sourceService.createRegatta( RegattaImpl.getDefaultName(TEST_REGATTA_NAME, TEST_BOAT_CLASS_NAME), TEST_BOAT_CLASS_NAME, @@ -1833,7 +1833,7 @@ public class MasterDataImportTest { FleetImpl testFleet1 = new FleetImpl("testFleet1"); fleets.add(testFleet1); fleets.add(new FleetImpl("testFleet2")); - series.add(new SeriesImpl("testSeries", false, fleets, emptyRaceColumnNamesList, sourceService)); + series.add(new SeriesImpl("testSeries", false, /* isFleetsCanRunInParallel */ true, fleets, emptyRaceColumnNamesList, sourceService)); UUID regattaUUID = UUID.randomUUID(); Regatta regatta = sourceService.createRegatta( RegattaImpl.getDefaultName(TEST_REGATTA_NAME, TEST_BOAT_CLASS_NAME), TEST_BOAT_CLASS_NAME, @@ -2063,8 +2063,8 @@ public class MasterDataImportTest { FleetImpl testFleet1 = new FleetImpl(testFleet1Name); fleets.add(testFleet1); fleets.add(new FleetImpl("testFleet2")); - seriesOnSource.add(new SeriesImpl("testSeries", false, fleets, emptyRaceColumnNamesList, sourceService)); - seriesOnTarget.add(new SeriesImpl("testSeries", false, fleets, emptyRaceColumnNamesList, sourceService)); + seriesOnSource.add(new SeriesImpl("testSeries", false, /* isFleetsCanRunInParallel */ true, fleets, emptyRaceColumnNamesList, sourceService)); + seriesOnTarget.add(new SeriesImpl("testSeries", false, /* isFleetsCanRunInParallel */ true, fleets, emptyRaceColumnNamesList, sourceService)); UUID regattaUUID = UUID.randomUUID(); Regatta regatta = sourceService.createRegatta( RegattaImpl.getDefaultName(TEST_REGATTA_NAME, TEST_BOAT_CLASS_NAME), TEST_BOAT_CLASS_NAME, /* startDate */ diff --git a/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/SearchServiceTest.java b/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/SearchServiceTest.java index 94b5884d201..fa6a3ed3f87 100755 --- a/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/SearchServiceTest.java +++ b/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/SearchServiceTest.java @@ -1,322 +1,322 @@ -package com.sap.sailing.server.test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Calendar; -import java.util.Collections; -import java.util.GregorianCalendar; -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -import org.junit.Before; -import org.junit.Test; - -import com.sap.sailing.domain.base.Competitor; -import com.sap.sailing.domain.base.DomainFactory; -import com.sap.sailing.domain.base.Event; -import com.sap.sailing.domain.base.LeaderboardGroupBase; -import com.sap.sailing.domain.base.LeaderboardSearchResult; -import com.sap.sailing.domain.base.LeaderboardSearchResultBase; -import com.sap.sailing.domain.base.Regatta; -import com.sap.sailing.domain.base.Venue; -import com.sap.sailing.domain.base.Waypoint; -import com.sap.sailing.domain.base.impl.CourseAreaImpl; -import com.sap.sailing.domain.base.impl.CourseImpl; -import com.sap.sailing.domain.base.impl.RaceDefinitionImpl; -import com.sap.sailing.domain.base.impl.RegattaImpl; -import com.sap.sailing.domain.common.RankingMetrics; -import com.sap.sailing.domain.common.RegattaName; -import com.sap.sailing.domain.common.dto.FleetDTO; -import com.sap.sailing.domain.common.dto.RegattaCreationParametersDTO; -import com.sap.sailing.domain.common.dto.SeriesCreationParametersDTO; -import com.sap.sailing.domain.leaderboard.Leaderboard; -import com.sap.sailing.domain.leaderboard.LeaderboardGroup; -import com.sap.sailing.domain.leaderboard.RegattaLeaderboard; -import com.sap.sailing.domain.leaderboard.impl.LowPoint; -import com.sap.sailing.domain.racelog.tracking.EmptyGPSFixStore; -import com.sap.sailing.domain.test.AbstractTracTracLiveTest; -import com.sap.sailing.domain.tracking.DynamicTrackedRace; -import com.sap.sailing.domain.tracking.impl.EmptyWindStore; -import com.sap.sailing.server.RacingEventService; -import com.sap.sailing.server.gateway.deserialization.JsonDeserializationException; -import com.sap.sailing.server.gateway.deserialization.impl.CourseAreaJsonDeserializer; -import com.sap.sailing.server.gateway.deserialization.impl.EventBaseJsonDeserializer; -import com.sap.sailing.server.gateway.deserialization.impl.LeaderboardGroupBaseJsonDeserializer; -import com.sap.sailing.server.gateway.deserialization.impl.LeaderboardSearchResultBaseJsonDeserializer; -import com.sap.sailing.server.gateway.deserialization.impl.VenueJsonDeserializer; -import com.sap.sailing.server.gateway.serialization.impl.CourseAreaJsonSerializer; -import com.sap.sailing.server.gateway.serialization.impl.EventBaseJsonSerializer; -import com.sap.sailing.server.gateway.serialization.impl.LeaderboardGroupBaseJsonSerializer; -import com.sap.sailing.server.gateway.serialization.impl.LeaderboardSearchResultJsonSerializer; -import com.sap.sailing.server.gateway.serialization.impl.VenueJsonSerializer; -import com.sap.sailing.server.impl.RacingEventServiceImpl; -import com.sap.sailing.server.operationaltransformation.AddColumnToSeries; -import com.sap.sailing.server.operationaltransformation.AddRaceDefinition; -import com.sap.sailing.server.operationaltransformation.AddSpecificRegatta; -import com.sap.sailing.server.operationaltransformation.CreateEvent; -import com.sap.sailing.server.operationaltransformation.CreateRegattaLeaderboard; -import com.sap.sailing.server.operationaltransformation.CreateTrackedRace; -import com.sap.sailing.server.operationaltransformation.RemoveEvent; -import com.sap.sailing.server.operationaltransformation.RemoveLeaderboard; -import com.sap.sailing.server.operationaltransformation.RemoveRegatta; -import com.sap.sse.common.Color; -import com.sap.sse.common.TimePoint; -import com.sap.sse.common.Util; -import com.sap.sse.common.impl.MillisecondsTimePoint; -import com.sap.sse.common.search.KeywordQuery; -import com.sap.sse.common.search.Result; -import com.sap.sse.shared.media.ImageDescriptor; -import com.sap.sse.shared.media.VideoDescriptor; - -public class SearchServiceTest { - private RacingEventService server; - private Venue kiel; - private Venue flensburg; - private Event pfingstbusch; - private Regatta pfingstbusch29er; - private Competitor hassoPlattner; - private Competitor alexanderRies; - private Competitor antonKoch; - private Competitor tobiasSchadewaldt; - private Competitor philippBuhl; - private Competitor dennisGehrlein; - private Regatta pfingstbusch470; - private Regatta aalRegatta; - private DynamicTrackedRace pfingstbusch29erTrackedR1; - private DynamicTrackedRace pfingstbusch29erTrackedR3; - private Event aalEvent; - private DynamicTrackedRace pfingstbusch470TrackedR1; - private DynamicTrackedRace pfingstbusch470TrackedR2; - private DynamicTrackedRace aalOrcTrackedR1; - private DynamicTrackedRace aalOrcTrackedR2; - - @Before - public void setUp() { - server = new RacingEventServiceImpl(); - List allEvents = new ArrayList<>(); - Util.addAll(server.getAllEvents(), allEvents); - for (Event e : allEvents) { - server.apply(new RemoveEvent(e.getId())); - } - Map allLeaderboards = new HashMap<>(server.getLeaderboards()); - for (String leaderboardName : allLeaderboards.keySet()) { - server.apply(new RemoveLeaderboard(leaderboardName)); - } - server.apply(new RemoveRegatta(new RegattaName("Pfingstbusch (29er)"))); - server.apply(new RemoveRegatta(new RegattaName("Pfingstbusch (470)"))); - server.apply(new RemoveRegatta(new RegattaName("Aalregatta (ORC)"))); - final Calendar cal = new GregorianCalendar(); - cal.set(2014, 5, 6, 10, 00); - final TimePoint pfingstbuschStartDate = new MillisecondsTimePoint(cal.getTime()); - cal.set(2014, 5, 8, 16, 00); - final TimePoint pfingstbuschEndDate = new MillisecondsTimePoint(cal.getTime()); - pfingstbusch = server.apply(new CreateEvent("Pfingsbusch", /* eventDescription */ null, pfingstbuschStartDate, pfingstbuschEndDate, /* isPublic */ - "Kiel", true, UUID.randomUUID(), /* officialWebsiteURLAsString */ null, /* sailorsInfoWebsiteURLAsString */ null, - /* images */Collections. emptyList(), /* videos */Collections. emptyList())); - kiel = pfingstbusch.getVenue(); - final CourseAreaImpl kielAlpha = new CourseAreaImpl("Alpha", UUID.randomUUID()); - kiel.addCourseArea(kielAlpha); - final CourseAreaImpl kielBravo = new CourseAreaImpl("Bravo", UUID.randomUUID()); - kiel.addCourseArea(kielBravo); - final LinkedHashMap seriesCreationParams = new LinkedHashMap(); - seriesCreationParams.put("Default", - new SeriesCreationParametersDTO(Collections.singletonList(new FleetDTO("Default", /* order */-1, Color.RED)), - /* medal */false, /* startsWithZero */false, /* firstColumnIsNonDiscardableCarryForward */false, - /* discardingThresholds */null, /* hasSplitFleetContiguousScoring */false)); - pfingstbusch29er = server.apply(new AddSpecificRegatta(RegattaImpl.getDefaultName("Pfingstbusch", "29er"), "29er", - /*startDate*/ null, /*endDate*/ null, UUID.randomUUID(), - new RegattaCreationParametersDTO(seriesCreationParams), /* persistent */ - true, new LowPoint(), kielAlpha.getId(), /* useStartTimeInference */ true, RankingMetrics.ONE_DESIGN)); - server.apply(new AddColumnToSeries(pfingstbusch29er.getRegattaIdentifier(), "Default", "R1")); - server.apply(new AddColumnToSeries(pfingstbusch29er.getRegattaIdentifier(), "Default", "R2")); - server.apply(new AddColumnToSeries(pfingstbusch29er.getRegattaIdentifier(), "Default", "R3")); - RegattaLeaderboard pfingstbusch29erLeaderboard = server.apply(new CreateRegattaLeaderboard(pfingstbusch29er.getRegattaIdentifier(), - /* leaderboardDisplayName */ null, /* discardThresholds */ new int[0])); - pfingstbusch470 = server.apply(new AddSpecificRegatta(RegattaImpl.getDefaultName("Pfingstbusch", "470"), "470", - /*startDate*/ null, /*endDate*/ null, UUID.randomUUID(), new RegattaCreationParametersDTO(seriesCreationParams), /* persistent */ - true, new LowPoint(), kielBravo.getId(), /* useStartTimeInference */ true, RankingMetrics.ONE_DESIGN)); - server.apply(new AddColumnToSeries(pfingstbusch470.getRegattaIdentifier(), "Default", "R1")); - server.apply(new AddColumnToSeries(pfingstbusch470.getRegattaIdentifier(), "Default", "R2")); - server.apply(new AddColumnToSeries(pfingstbusch470.getRegattaIdentifier(), "Default", "R3")); - RegattaLeaderboard pfingstbusch470Leaderboard = server.apply(new CreateRegattaLeaderboard(pfingstbusch470.getRegattaIdentifier(), - /* leaderboardDisplayName */ null, /* discardThresholds */ new int[0])); - cal.set(2014, 5, 7, 10, 00); - final TimePoint aalStartDate = new MillisecondsTimePoint(cal.getTime()); - cal.set(2014, 5, 8, 18, 00); - final TimePoint aalEndDate = new MillisecondsTimePoint(cal.getTime()); - aalEvent = server.apply(new CreateEvent("Aalregatta", /* eventDescription */ null, aalStartDate, aalEndDate, /* isPublic */ - "Flensburg", true, UUID.randomUUID(), /* officialWebsiteURLAsString */ null, /*sailorsInfoWebsiteURLAsString */ null, - /* images */Collections. emptyList(), /* videos */Collections. emptyList())); - flensburg = aalEvent.getVenue(); - final CourseAreaImpl flensburgStandard = new CourseAreaImpl("Standard", UUID.randomUUID()); - flensburg.addCourseArea(flensburgStandard); - aalRegatta = server.apply(new AddSpecificRegatta(RegattaImpl.getDefaultName("Aalregatta", "ORC"), "ORC", - /*startDate*/ null, /*endDate*/ null, UUID.randomUUID(), - new RegattaCreationParametersDTO(seriesCreationParams), /* persistent */ - true, new LowPoint(), flensburgStandard.getId(), /* useStartTimeInference */ true, RankingMetrics.ONE_DESIGN)); - server.apply(new AddColumnToSeries(aalRegatta.getRegattaIdentifier(), "Default", "R1")); - server.apply(new AddColumnToSeries(aalRegatta.getRegattaIdentifier(), "Default", "R2")); - RegattaLeaderboard aalRegattaLeaderboard = server.apply(new CreateRegattaLeaderboard(aalRegatta.getRegattaIdentifier(), - /* leaderboardDisplayName */ null, /* discardThresholds */ new int[0])); - hassoPlattner = AbstractTracTracLiveTest.createCompetitor("Hasso Plattner"); - alexanderRies = AbstractTracTracLiveTest.createCompetitor("Alexander Ries"); - antonKoch = AbstractTracTracLiveTest.createCompetitor("Anton Koch"); - tobiasSchadewaldt = AbstractTracTracLiveTest.createCompetitor("Tobias Schadewaldt"); - philippBuhl = AbstractTracTracLiveTest.createCompetitor("Philipp Buhl"); - dennisGehrlein = AbstractTracTracLiveTest.createCompetitor("Dennis Gehrlein"); - final RaceDefinitionImpl pfingstbusch29erR1 = new RaceDefinitionImpl("R1", new CourseImpl("up/down", Collections.emptyList()), pfingstbusch29er.getBoatClass(), - Arrays.asList(new Competitor[] { alexanderRies, tobiasSchadewaldt })); - final RaceDefinitionImpl pfingstbusch29erR2 = new RaceDefinitionImpl("R2", new CourseImpl("up/down", Collections.emptyList()), pfingstbusch29er.getBoatClass(), - Arrays.asList(new Competitor[] { alexanderRies, tobiasSchadewaldt })); - final RaceDefinitionImpl pfingstbusch29erR3 = new RaceDefinitionImpl("R3", new CourseImpl("up/down", Collections.emptyList()), pfingstbusch29er.getBoatClass(), - Arrays.asList(new Competitor[] { alexanderRies, tobiasSchadewaldt })); - server.apply(new AddRaceDefinition(pfingstbusch29er.getRegattaIdentifier(), pfingstbusch29erR1)); - server.apply(new AddRaceDefinition(pfingstbusch29er.getRegattaIdentifier(), pfingstbusch29erR2)); - server.apply(new AddRaceDefinition(pfingstbusch29er.getRegattaIdentifier(), pfingstbusch29erR3)); - // track only R1 and R3 - pfingstbusch29erTrackedR1 = server.apply(new CreateTrackedRace(pfingstbusch29er.getRaceIdentifier(pfingstbusch29erR1), EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, /* delayToLiveInMillis */ 3000, - /* millisecondsOverWhichToAverageWind */ 15000, /* millisecondsOverWhichToAverageSpeed */ 15000)); - pfingstbusch29erTrackedR1.setStartOfTrackingReceived(pfingstbuschStartDate.plus(1)); - pfingstbusch29erLeaderboard.getRaceColumnByName("R1").setTrackedRace(pfingstbusch29erLeaderboard.getRaceColumnByName("R1").getFleetByName("Default"), pfingstbusch29erTrackedR1); - pfingstbusch29erTrackedR3 = server.apply(new CreateTrackedRace(pfingstbusch29er.getRaceIdentifier(pfingstbusch29erR3), EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, /* delayToLiveInMillis */ 3000, - /* millisecondsOverWhichToAverageWind */ 15000, /* millisecondsOverWhichToAverageSpeed */ 15000)); - pfingstbusch29erLeaderboard.getRaceColumnByName("R3").setTrackedRace(pfingstbusch29erLeaderboard.getRaceColumnByName("R3").getFleetByName("Default"), pfingstbusch29erTrackedR3); - - final RaceDefinitionImpl pfingstbush470R1 = new RaceDefinitionImpl("R1", new CourseImpl("up/down", Collections.emptyList()), pfingstbusch470.getBoatClass(), - Arrays.asList(new Competitor[] { philippBuhl, antonKoch })); - final RaceDefinitionImpl pfingstbush470R2 = new RaceDefinitionImpl("R2", new CourseImpl("up/down", Collections.emptyList()), pfingstbusch470.getBoatClass(), - Arrays.asList(new Competitor[] { philippBuhl, antonKoch })); - server.apply(new AddRaceDefinition(pfingstbusch470.getRegattaIdentifier(), pfingstbush470R1)); - server.apply(new AddRaceDefinition(pfingstbusch470.getRegattaIdentifier(), pfingstbush470R2)); - // track only R1 and R2 - pfingstbusch470TrackedR1 = server.apply(new CreateTrackedRace(pfingstbusch470.getRaceIdentifier(pfingstbush470R1), EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, /* delayToLiveInMillis */ 3000, - /* millisecondsOverWhichToAverageWind */ 15000, /* millisecondsOverWhichToAverageSpeed */ 15000)); - pfingstbusch470TrackedR1.setStartOfTrackingReceived(pfingstbuschStartDate.plus(2)); // starts later than 29er - pfingstbusch470Leaderboard.getRaceColumnByName("R1").setTrackedRace(pfingstbusch470Leaderboard.getRaceColumnByName("R1").getFleetByName("Default"), pfingstbusch470TrackedR1); - pfingstbusch470TrackedR2 = server.apply(new CreateTrackedRace(pfingstbusch470.getRaceIdentifier(pfingstbush470R2), EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, /* delayToLiveInMillis */ 3000, - /* millisecondsOverWhichToAverageWind */ 15000, /* millisecondsOverWhichToAverageSpeed */ 15000)); - pfingstbusch470Leaderboard.getRaceColumnByName("R2").setTrackedRace(pfingstbusch470Leaderboard.getRaceColumnByName("R2").getFleetByName("Default"), pfingstbusch470TrackedR2); - - final RaceDefinitionImpl aalOrcR1 = new RaceDefinitionImpl("R1", new CourseImpl("up/down", Collections.emptyList()), aalRegatta.getBoatClass(), - Arrays.asList(new Competitor[] { hassoPlattner, dennisGehrlein, philippBuhl })); - final RaceDefinitionImpl aalOrcR2 = new RaceDefinitionImpl("R2", new CourseImpl("up/down", Collections.emptyList()), aalRegatta.getBoatClass(), - Arrays.asList(new Competitor[] { hassoPlattner, dennisGehrlein, philippBuhl })); - server.apply(new AddRaceDefinition(aalRegatta.getRegattaIdentifier(), aalOrcR1)); - server.apply(new AddRaceDefinition(aalRegatta.getRegattaIdentifier(), aalOrcR2)); - // track only R1 and R2 - aalOrcTrackedR1 = server.apply(new CreateTrackedRace(aalRegatta.getRaceIdentifier(aalOrcR1), EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, /* delayToLiveInMillis */ 3000, - /* millisecondsOverWhichToAverageWind */ 15000, /* millisecondsOverWhichToAverageSpeed */ 15000)); - aalOrcTrackedR1.setStartOfTrackingReceived(aalStartDate); - aalRegattaLeaderboard.getRaceColumnByName("R1").setTrackedRace(aalRegattaLeaderboard.getRaceColumnByName("R1").getFleetByName("Default"), aalOrcTrackedR1); - aalOrcTrackedR2 = server.apply(new CreateTrackedRace(aalRegatta.getRaceIdentifier(aalOrcR2), EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, /* delayToLiveInMillis */ 3000, - /* millisecondsOverWhichToAverageWind */ 15000, /* millisecondsOverWhichToAverageSpeed */ 15000)); - aalRegattaLeaderboard.getRaceColumnByName("R2").setTrackedRace(aalRegattaLeaderboard.getRaceColumnByName("R2").getFleetByName("Default"), aalOrcTrackedR2); - } - - @Test - public void testSetup() { - assertNotNull(pfingstbusch29er); - assertNotNull(pfingstbusch29erTrackedR1); - assertNotNull(pfingstbusch29erTrackedR3); - assertNotNull(pfingstbusch470TrackedR1); - assertNotNull(pfingstbusch470TrackedR2); - assertNotNull(aalEvent); - assertNotNull(aalRegatta); - assertNotNull(aalOrcTrackedR1); - assertNotNull(aalOrcTrackedR2); - } - - @Test - public void testSimpleSearchByCompetitorName() { - Result searchResults = server.search(new KeywordQuery(Arrays.asList(new String[] { "Tobi" }))); - assertEquals(1, Util.size(searchResults.getHits())); - Regatta foundRegatta = searchResults.getHits().iterator().next().getRegatta(); - assertSame(pfingstbusch29er, foundRegatta); - } - - @Test - public void testSimpleSearchByCompetitorName2() { - Result searchResults = server.search(new KeywordQuery(Arrays.asList(new String[] { "Hasso" }))); - assertEquals(1, Util.size(searchResults.getHits())); - Regatta foundRegatta = searchResults.getHits().iterator().next().getRegatta(); - assertSame(aalRegatta, foundRegatta); - } - - @Test - public void testSimpleSearchByVenueName() { - Result searchResults = server.search(new KeywordQuery(Arrays.asList(new String[] { "Flensburg" }))); - assertEquals(1, Util.size(searchResults.getHits())); - Regatta foundRegatta = searchResults.getHits().iterator().next().getRegatta(); - assertSame(aalRegatta, foundRegatta); - } - - @Test - public void testSimpleSearchByVenueName2() { - Result searchResults = server.search(new KeywordQuery(Arrays.asList(new String[] { "Kiel" }))); - assertEquals(2, Util.size(searchResults.getHits())); - final Iterator iterator = searchResults.getHits().iterator(); - final LeaderboardSearchResult firstMatch = iterator.next(); - Regatta firstFoundRegatta = firstMatch.getRegatta(); - assertSame(pfingstbusch29er, firstFoundRegatta); - assertSame(pfingstbusch, firstMatch.getEvents().iterator().next()); - final LeaderboardSearchResult secondMatch = iterator.next(); - Regatta secondFoundRegatta = secondMatch.getRegatta(); - assertSame(pfingstbusch470, secondFoundRegatta); - assertSame(pfingstbusch, secondMatch.getEvents().iterator().next()); - } - - @Test - public void testMultipleMatchesSortedCorrectly() { - Result searchResults = server.search(new KeywordQuery(Arrays.asList(new String[] { "Buhl" }))); - assertEquals(2, Util.size(searchResults.getHits())); - final Iterator iter = searchResults.getHits().iterator(); - final LeaderboardSearchResult firstMatch = iter.next(); - Regatta earlierStartRegatta = firstMatch.getRegatta(); - assertSame(pfingstbusch470, earlierStartRegatta); - assertSame(pfingstbusch, firstMatch.getEvents().iterator().next()); - final LeaderboardSearchResult secondMatch = iter.next(); - Regatta laterStartRegatta = secondMatch.getRegatta(); - assertSame(aalRegatta, laterStartRegatta); - assertSame(aalEvent, secondMatch.getEvents().iterator().next()); - } - - @Test - public void testSerializeAndDeserializeSearchResult() throws JsonDeserializationException { - Result searchResults = server.search(new KeywordQuery(Arrays.asList(new String[] { "Buhl" }))); - LeaderboardGroupBaseJsonSerializer leaderboardGroupBaseJsonSerializer = new LeaderboardGroupBaseJsonSerializer(); - LeaderboardSearchResultJsonSerializer serializer = new LeaderboardSearchResultJsonSerializer(new EventBaseJsonSerializer(new VenueJsonSerializer(new CourseAreaJsonSerializer()), - leaderboardGroupBaseJsonSerializer), - leaderboardGroupBaseJsonSerializer); - LeaderboardGroupBaseJsonDeserializer leaderboardGroupBaseJsonDeserializer = new LeaderboardGroupBaseJsonDeserializer(); - LeaderboardSearchResultBaseJsonDeserializer deserializer = new LeaderboardSearchResultBaseJsonDeserializer( - new EventBaseJsonDeserializer(new VenueJsonDeserializer(new CourseAreaJsonDeserializer(DomainFactory.INSTANCE)), - leaderboardGroupBaseJsonDeserializer), leaderboardGroupBaseJsonDeserializer); - final LeaderboardSearchResult expected = searchResults.getHits().iterator().next(); - LeaderboardSearchResultBase deserialized = deserializer.deserialize(serializer.serialize(expected)); - assertEquals("Pfingstbusch (470)", deserialized.getRegattaName()); - assertEquals(expected.getEvents().iterator().next().getName(), deserialized.getEvents().iterator().next().getName()); - assertEquals(expected.getEvents().iterator().next().getId(), deserialized.getEvents().iterator().next().getId()); - assertEquals(expected.getEvents().iterator().next().getStartDate(), deserialized.getEvents().iterator().next().getStartDate()); - assertEquals(expected.getEvents().iterator().next().getEndDate(), deserialized.getEvents().iterator().next().getEndDate()); - assertEquals(expected.getEvents().iterator().next().getVenue().getName(), deserialized.getEvents().iterator().next().getVenue().getName()); - Iterator expectedLGs = expected.getLeaderboardGroups().iterator(); - Iterator deserializedLGs = deserialized.getLeaderboardGroups().iterator(); - while (expectedLGs.hasNext()) { - assertTrue(deserializedLGs.hasNext()); - LeaderboardGroup expectedLG = expectedLGs.next(); - LeaderboardGroupBase deserializedLG = deserializedLGs.next(); - assertEquals(expectedLG.getId(), deserializedLG.getId()); - assertEquals(expectedLG.getName(), deserializedLG.getName()); - assertEquals(expectedLG.getDescription(), deserializedLG.getDescription()); - } - } -} +package com.sap.sailing.server.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collections; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.junit.Before; +import org.junit.Test; + +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.DomainFactory; +import com.sap.sailing.domain.base.Event; +import com.sap.sailing.domain.base.LeaderboardGroupBase; +import com.sap.sailing.domain.base.LeaderboardSearchResult; +import com.sap.sailing.domain.base.LeaderboardSearchResultBase; +import com.sap.sailing.domain.base.Regatta; +import com.sap.sailing.domain.base.Venue; +import com.sap.sailing.domain.base.Waypoint; +import com.sap.sailing.domain.base.impl.CourseAreaImpl; +import com.sap.sailing.domain.base.impl.CourseImpl; +import com.sap.sailing.domain.base.impl.RaceDefinitionImpl; +import com.sap.sailing.domain.base.impl.RegattaImpl; +import com.sap.sailing.domain.common.RankingMetrics; +import com.sap.sailing.domain.common.RegattaName; +import com.sap.sailing.domain.common.dto.FleetDTO; +import com.sap.sailing.domain.common.dto.RegattaCreationParametersDTO; +import com.sap.sailing.domain.common.dto.SeriesCreationParametersDTO; +import com.sap.sailing.domain.leaderboard.Leaderboard; +import com.sap.sailing.domain.leaderboard.LeaderboardGroup; +import com.sap.sailing.domain.leaderboard.RegattaLeaderboard; +import com.sap.sailing.domain.leaderboard.impl.LowPoint; +import com.sap.sailing.domain.racelog.tracking.EmptyGPSFixStore; +import com.sap.sailing.domain.test.AbstractTracTracLiveTest; +import com.sap.sailing.domain.tracking.DynamicTrackedRace; +import com.sap.sailing.domain.tracking.impl.EmptyWindStore; +import com.sap.sailing.server.RacingEventService; +import com.sap.sailing.server.gateway.deserialization.JsonDeserializationException; +import com.sap.sailing.server.gateway.deserialization.impl.CourseAreaJsonDeserializer; +import com.sap.sailing.server.gateway.deserialization.impl.EventBaseJsonDeserializer; +import com.sap.sailing.server.gateway.deserialization.impl.LeaderboardGroupBaseJsonDeserializer; +import com.sap.sailing.server.gateway.deserialization.impl.LeaderboardSearchResultBaseJsonDeserializer; +import com.sap.sailing.server.gateway.deserialization.impl.VenueJsonDeserializer; +import com.sap.sailing.server.gateway.serialization.impl.CourseAreaJsonSerializer; +import com.sap.sailing.server.gateway.serialization.impl.EventBaseJsonSerializer; +import com.sap.sailing.server.gateway.serialization.impl.LeaderboardGroupBaseJsonSerializer; +import com.sap.sailing.server.gateway.serialization.impl.LeaderboardSearchResultJsonSerializer; +import com.sap.sailing.server.gateway.serialization.impl.VenueJsonSerializer; +import com.sap.sailing.server.impl.RacingEventServiceImpl; +import com.sap.sailing.server.operationaltransformation.AddColumnToSeries; +import com.sap.sailing.server.operationaltransformation.AddRaceDefinition; +import com.sap.sailing.server.operationaltransformation.AddSpecificRegatta; +import com.sap.sailing.server.operationaltransformation.CreateEvent; +import com.sap.sailing.server.operationaltransformation.CreateRegattaLeaderboard; +import com.sap.sailing.server.operationaltransformation.CreateTrackedRace; +import com.sap.sailing.server.operationaltransformation.RemoveEvent; +import com.sap.sailing.server.operationaltransformation.RemoveLeaderboard; +import com.sap.sailing.server.operationaltransformation.RemoveRegatta; +import com.sap.sse.common.Color; +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.Util; +import com.sap.sse.common.impl.MillisecondsTimePoint; +import com.sap.sse.common.search.KeywordQuery; +import com.sap.sse.common.search.Result; +import com.sap.sse.shared.media.ImageDescriptor; +import com.sap.sse.shared.media.VideoDescriptor; + +public class SearchServiceTest { + private RacingEventService server; + private Venue kiel; + private Venue flensburg; + private Event pfingstbusch; + private Regatta pfingstbusch29er; + private Competitor hassoPlattner; + private Competitor alexanderRies; + private Competitor antonKoch; + private Competitor tobiasSchadewaldt; + private Competitor philippBuhl; + private Competitor dennisGehrlein; + private Regatta pfingstbusch470; + private Regatta aalRegatta; + private DynamicTrackedRace pfingstbusch29erTrackedR1; + private DynamicTrackedRace pfingstbusch29erTrackedR3; + private Event aalEvent; + private DynamicTrackedRace pfingstbusch470TrackedR1; + private DynamicTrackedRace pfingstbusch470TrackedR2; + private DynamicTrackedRace aalOrcTrackedR1; + private DynamicTrackedRace aalOrcTrackedR2; + + @Before + public void setUp() { + server = new RacingEventServiceImpl(); + List allEvents = new ArrayList<>(); + Util.addAll(server.getAllEvents(), allEvents); + for (Event e : allEvents) { + server.apply(new RemoveEvent(e.getId())); + } + Map allLeaderboards = new HashMap<>(server.getLeaderboards()); + for (String leaderboardName : allLeaderboards.keySet()) { + server.apply(new RemoveLeaderboard(leaderboardName)); + } + server.apply(new RemoveRegatta(new RegattaName("Pfingstbusch (29er)"))); + server.apply(new RemoveRegatta(new RegattaName("Pfingstbusch (470)"))); + server.apply(new RemoveRegatta(new RegattaName("Aalregatta (ORC)"))); + final Calendar cal = new GregorianCalendar(); + cal.set(2014, 5, 6, 10, 00); + final TimePoint pfingstbuschStartDate = new MillisecondsTimePoint(cal.getTime()); + cal.set(2014, 5, 8, 16, 00); + final TimePoint pfingstbuschEndDate = new MillisecondsTimePoint(cal.getTime()); + pfingstbusch = server.apply(new CreateEvent("Pfingsbusch", /* eventDescription */ null, pfingstbuschStartDate, pfingstbuschEndDate, /* isPublic */ + "Kiel", true, UUID.randomUUID(), /* officialWebsiteURLAsString */ null, /* sailorsInfoWebsiteURLAsString */ null, + /* images */Collections. emptyList(), /* videos */Collections. emptyList())); + kiel = pfingstbusch.getVenue(); + final CourseAreaImpl kielAlpha = new CourseAreaImpl("Alpha", UUID.randomUUID()); + kiel.addCourseArea(kielAlpha); + final CourseAreaImpl kielBravo = new CourseAreaImpl("Bravo", UUID.randomUUID()); + kiel.addCourseArea(kielBravo); + final LinkedHashMap seriesCreationParams = new LinkedHashMap(); + seriesCreationParams.put("Default", + new SeriesCreationParametersDTO(Collections.singletonList(new FleetDTO("Default", /* order */-1, Color.RED)), + /* medal */false, /* fleetsCanRunInParallel */ true, /* startsWithZero */false, /* firstColumnIsNonDiscardableCarryForward */false, + /* discardingThresholds */null, /* hasSplitFleetContiguousScoring */false)); + pfingstbusch29er = server.apply(new AddSpecificRegatta(RegattaImpl.getDefaultName("Pfingstbusch", "29er"), "29er", + /*startDate*/ null, /*endDate*/ null, UUID.randomUUID(), + new RegattaCreationParametersDTO(seriesCreationParams), /* persistent */ + true, new LowPoint(), kielAlpha.getId(), /* useStartTimeInference */ true, RankingMetrics.ONE_DESIGN)); + server.apply(new AddColumnToSeries(pfingstbusch29er.getRegattaIdentifier(), "Default", "R1")); + server.apply(new AddColumnToSeries(pfingstbusch29er.getRegattaIdentifier(), "Default", "R2")); + server.apply(new AddColumnToSeries(pfingstbusch29er.getRegattaIdentifier(), "Default", "R3")); + RegattaLeaderboard pfingstbusch29erLeaderboard = server.apply(new CreateRegattaLeaderboard(pfingstbusch29er.getRegattaIdentifier(), + /* leaderboardDisplayName */ null, /* discardThresholds */ new int[0])); + pfingstbusch470 = server.apply(new AddSpecificRegatta(RegattaImpl.getDefaultName("Pfingstbusch", "470"), "470", + /*startDate*/ null, /*endDate*/ null, UUID.randomUUID(), new RegattaCreationParametersDTO(seriesCreationParams), /* persistent */ + true, new LowPoint(), kielBravo.getId(), /* useStartTimeInference */ true, RankingMetrics.ONE_DESIGN)); + server.apply(new AddColumnToSeries(pfingstbusch470.getRegattaIdentifier(), "Default", "R1")); + server.apply(new AddColumnToSeries(pfingstbusch470.getRegattaIdentifier(), "Default", "R2")); + server.apply(new AddColumnToSeries(pfingstbusch470.getRegattaIdentifier(), "Default", "R3")); + RegattaLeaderboard pfingstbusch470Leaderboard = server.apply(new CreateRegattaLeaderboard(pfingstbusch470.getRegattaIdentifier(), + /* leaderboardDisplayName */ null, /* discardThresholds */ new int[0])); + cal.set(2014, 5, 7, 10, 00); + final TimePoint aalStartDate = new MillisecondsTimePoint(cal.getTime()); + cal.set(2014, 5, 8, 18, 00); + final TimePoint aalEndDate = new MillisecondsTimePoint(cal.getTime()); + aalEvent = server.apply(new CreateEvent("Aalregatta", /* eventDescription */ null, aalStartDate, aalEndDate, /* isPublic */ + "Flensburg", true, UUID.randomUUID(), /* officialWebsiteURLAsString */ null, /*sailorsInfoWebsiteURLAsString */ null, + /* images */Collections. emptyList(), /* videos */Collections. emptyList())); + flensburg = aalEvent.getVenue(); + final CourseAreaImpl flensburgStandard = new CourseAreaImpl("Standard", UUID.randomUUID()); + flensburg.addCourseArea(flensburgStandard); + aalRegatta = server.apply(new AddSpecificRegatta(RegattaImpl.getDefaultName("Aalregatta", "ORC"), "ORC", + /*startDate*/ null, /*endDate*/ null, UUID.randomUUID(), + new RegattaCreationParametersDTO(seriesCreationParams), /* persistent */ + true, new LowPoint(), flensburgStandard.getId(), /* useStartTimeInference */ true, RankingMetrics.ONE_DESIGN)); + server.apply(new AddColumnToSeries(aalRegatta.getRegattaIdentifier(), "Default", "R1")); + server.apply(new AddColumnToSeries(aalRegatta.getRegattaIdentifier(), "Default", "R2")); + RegattaLeaderboard aalRegattaLeaderboard = server.apply(new CreateRegattaLeaderboard(aalRegatta.getRegattaIdentifier(), + /* leaderboardDisplayName */ null, /* discardThresholds */ new int[0])); + hassoPlattner = AbstractTracTracLiveTest.createCompetitor("Hasso Plattner"); + alexanderRies = AbstractTracTracLiveTest.createCompetitor("Alexander Ries"); + antonKoch = AbstractTracTracLiveTest.createCompetitor("Anton Koch"); + tobiasSchadewaldt = AbstractTracTracLiveTest.createCompetitor("Tobias Schadewaldt"); + philippBuhl = AbstractTracTracLiveTest.createCompetitor("Philipp Buhl"); + dennisGehrlein = AbstractTracTracLiveTest.createCompetitor("Dennis Gehrlein"); + final RaceDefinitionImpl pfingstbusch29erR1 = new RaceDefinitionImpl("R1", new CourseImpl("up/down", Collections.emptyList()), pfingstbusch29er.getBoatClass(), + Arrays.asList(new Competitor[] { alexanderRies, tobiasSchadewaldt })); + final RaceDefinitionImpl pfingstbusch29erR2 = new RaceDefinitionImpl("R2", new CourseImpl("up/down", Collections.emptyList()), pfingstbusch29er.getBoatClass(), + Arrays.asList(new Competitor[] { alexanderRies, tobiasSchadewaldt })); + final RaceDefinitionImpl pfingstbusch29erR3 = new RaceDefinitionImpl("R3", new CourseImpl("up/down", Collections.emptyList()), pfingstbusch29er.getBoatClass(), + Arrays.asList(new Competitor[] { alexanderRies, tobiasSchadewaldt })); + server.apply(new AddRaceDefinition(pfingstbusch29er.getRegattaIdentifier(), pfingstbusch29erR1)); + server.apply(new AddRaceDefinition(pfingstbusch29er.getRegattaIdentifier(), pfingstbusch29erR2)); + server.apply(new AddRaceDefinition(pfingstbusch29er.getRegattaIdentifier(), pfingstbusch29erR3)); + // track only R1 and R3 + pfingstbusch29erTrackedR1 = server.apply(new CreateTrackedRace(pfingstbusch29er.getRaceIdentifier(pfingstbusch29erR1), EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, /* delayToLiveInMillis */ 3000, + /* millisecondsOverWhichToAverageWind */ 15000, /* millisecondsOverWhichToAverageSpeed */ 15000)); + pfingstbusch29erTrackedR1.setStartOfTrackingReceived(pfingstbuschStartDate.plus(1)); + pfingstbusch29erLeaderboard.getRaceColumnByName("R1").setTrackedRace(pfingstbusch29erLeaderboard.getRaceColumnByName("R1").getFleetByName("Default"), pfingstbusch29erTrackedR1); + pfingstbusch29erTrackedR3 = server.apply(new CreateTrackedRace(pfingstbusch29er.getRaceIdentifier(pfingstbusch29erR3), EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, /* delayToLiveInMillis */ 3000, + /* millisecondsOverWhichToAverageWind */ 15000, /* millisecondsOverWhichToAverageSpeed */ 15000)); + pfingstbusch29erLeaderboard.getRaceColumnByName("R3").setTrackedRace(pfingstbusch29erLeaderboard.getRaceColumnByName("R3").getFleetByName("Default"), pfingstbusch29erTrackedR3); + + final RaceDefinitionImpl pfingstbush470R1 = new RaceDefinitionImpl("R1", new CourseImpl("up/down", Collections.emptyList()), pfingstbusch470.getBoatClass(), + Arrays.asList(new Competitor[] { philippBuhl, antonKoch })); + final RaceDefinitionImpl pfingstbush470R2 = new RaceDefinitionImpl("R2", new CourseImpl("up/down", Collections.emptyList()), pfingstbusch470.getBoatClass(), + Arrays.asList(new Competitor[] { philippBuhl, antonKoch })); + server.apply(new AddRaceDefinition(pfingstbusch470.getRegattaIdentifier(), pfingstbush470R1)); + server.apply(new AddRaceDefinition(pfingstbusch470.getRegattaIdentifier(), pfingstbush470R2)); + // track only R1 and R2 + pfingstbusch470TrackedR1 = server.apply(new CreateTrackedRace(pfingstbusch470.getRaceIdentifier(pfingstbush470R1), EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, /* delayToLiveInMillis */ 3000, + /* millisecondsOverWhichToAverageWind */ 15000, /* millisecondsOverWhichToAverageSpeed */ 15000)); + pfingstbusch470TrackedR1.setStartOfTrackingReceived(pfingstbuschStartDate.plus(2)); // starts later than 29er + pfingstbusch470Leaderboard.getRaceColumnByName("R1").setTrackedRace(pfingstbusch470Leaderboard.getRaceColumnByName("R1").getFleetByName("Default"), pfingstbusch470TrackedR1); + pfingstbusch470TrackedR2 = server.apply(new CreateTrackedRace(pfingstbusch470.getRaceIdentifier(pfingstbush470R2), EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, /* delayToLiveInMillis */ 3000, + /* millisecondsOverWhichToAverageWind */ 15000, /* millisecondsOverWhichToAverageSpeed */ 15000)); + pfingstbusch470Leaderboard.getRaceColumnByName("R2").setTrackedRace(pfingstbusch470Leaderboard.getRaceColumnByName("R2").getFleetByName("Default"), pfingstbusch470TrackedR2); + + final RaceDefinitionImpl aalOrcR1 = new RaceDefinitionImpl("R1", new CourseImpl("up/down", Collections.emptyList()), aalRegatta.getBoatClass(), + Arrays.asList(new Competitor[] { hassoPlattner, dennisGehrlein, philippBuhl })); + final RaceDefinitionImpl aalOrcR2 = new RaceDefinitionImpl("R2", new CourseImpl("up/down", Collections.emptyList()), aalRegatta.getBoatClass(), + Arrays.asList(new Competitor[] { hassoPlattner, dennisGehrlein, philippBuhl })); + server.apply(new AddRaceDefinition(aalRegatta.getRegattaIdentifier(), aalOrcR1)); + server.apply(new AddRaceDefinition(aalRegatta.getRegattaIdentifier(), aalOrcR2)); + // track only R1 and R2 + aalOrcTrackedR1 = server.apply(new CreateTrackedRace(aalRegatta.getRaceIdentifier(aalOrcR1), EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, /* delayToLiveInMillis */ 3000, + /* millisecondsOverWhichToAverageWind */ 15000, /* millisecondsOverWhichToAverageSpeed */ 15000)); + aalOrcTrackedR1.setStartOfTrackingReceived(aalStartDate); + aalRegattaLeaderboard.getRaceColumnByName("R1").setTrackedRace(aalRegattaLeaderboard.getRaceColumnByName("R1").getFleetByName("Default"), aalOrcTrackedR1); + aalOrcTrackedR2 = server.apply(new CreateTrackedRace(aalRegatta.getRaceIdentifier(aalOrcR2), EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, /* delayToLiveInMillis */ 3000, + /* millisecondsOverWhichToAverageWind */ 15000, /* millisecondsOverWhichToAverageSpeed */ 15000)); + aalRegattaLeaderboard.getRaceColumnByName("R2").setTrackedRace(aalRegattaLeaderboard.getRaceColumnByName("R2").getFleetByName("Default"), aalOrcTrackedR2); + } + + @Test + public void testSetup() { + assertNotNull(pfingstbusch29er); + assertNotNull(pfingstbusch29erTrackedR1); + assertNotNull(pfingstbusch29erTrackedR3); + assertNotNull(pfingstbusch470TrackedR1); + assertNotNull(pfingstbusch470TrackedR2); + assertNotNull(aalEvent); + assertNotNull(aalRegatta); + assertNotNull(aalOrcTrackedR1); + assertNotNull(aalOrcTrackedR2); + } + + @Test + public void testSimpleSearchByCompetitorName() { + Result searchResults = server.search(new KeywordQuery(Arrays.asList(new String[] { "Tobi" }))); + assertEquals(1, Util.size(searchResults.getHits())); + Regatta foundRegatta = searchResults.getHits().iterator().next().getRegatta(); + assertSame(pfingstbusch29er, foundRegatta); + } + + @Test + public void testSimpleSearchByCompetitorName2() { + Result searchResults = server.search(new KeywordQuery(Arrays.asList(new String[] { "Hasso" }))); + assertEquals(1, Util.size(searchResults.getHits())); + Regatta foundRegatta = searchResults.getHits().iterator().next().getRegatta(); + assertSame(aalRegatta, foundRegatta); + } + + @Test + public void testSimpleSearchByVenueName() { + Result searchResults = server.search(new KeywordQuery(Arrays.asList(new String[] { "Flensburg" }))); + assertEquals(1, Util.size(searchResults.getHits())); + Regatta foundRegatta = searchResults.getHits().iterator().next().getRegatta(); + assertSame(aalRegatta, foundRegatta); + } + + @Test + public void testSimpleSearchByVenueName2() { + Result searchResults = server.search(new KeywordQuery(Arrays.asList(new String[] { "Kiel" }))); + assertEquals(2, Util.size(searchResults.getHits())); + final Iterator iterator = searchResults.getHits().iterator(); + final LeaderboardSearchResult firstMatch = iterator.next(); + Regatta firstFoundRegatta = firstMatch.getRegatta(); + assertSame(pfingstbusch29er, firstFoundRegatta); + assertSame(pfingstbusch, firstMatch.getEvents().iterator().next()); + final LeaderboardSearchResult secondMatch = iterator.next(); + Regatta secondFoundRegatta = secondMatch.getRegatta(); + assertSame(pfingstbusch470, secondFoundRegatta); + assertSame(pfingstbusch, secondMatch.getEvents().iterator().next()); + } + + @Test + public void testMultipleMatchesSortedCorrectly() { + Result searchResults = server.search(new KeywordQuery(Arrays.asList(new String[] { "Buhl" }))); + assertEquals(2, Util.size(searchResults.getHits())); + final Iterator iter = searchResults.getHits().iterator(); + final LeaderboardSearchResult firstMatch = iter.next(); + Regatta earlierStartRegatta = firstMatch.getRegatta(); + assertSame(pfingstbusch470, earlierStartRegatta); + assertSame(pfingstbusch, firstMatch.getEvents().iterator().next()); + final LeaderboardSearchResult secondMatch = iter.next(); + Regatta laterStartRegatta = secondMatch.getRegatta(); + assertSame(aalRegatta, laterStartRegatta); + assertSame(aalEvent, secondMatch.getEvents().iterator().next()); + } + + @Test + public void testSerializeAndDeserializeSearchResult() throws JsonDeserializationException { + Result searchResults = server.search(new KeywordQuery(Arrays.asList(new String[] { "Buhl" }))); + LeaderboardGroupBaseJsonSerializer leaderboardGroupBaseJsonSerializer = new LeaderboardGroupBaseJsonSerializer(); + LeaderboardSearchResultJsonSerializer serializer = new LeaderboardSearchResultJsonSerializer(new EventBaseJsonSerializer(new VenueJsonSerializer(new CourseAreaJsonSerializer()), + leaderboardGroupBaseJsonSerializer), + leaderboardGroupBaseJsonSerializer); + LeaderboardGroupBaseJsonDeserializer leaderboardGroupBaseJsonDeserializer = new LeaderboardGroupBaseJsonDeserializer(); + LeaderboardSearchResultBaseJsonDeserializer deserializer = new LeaderboardSearchResultBaseJsonDeserializer( + new EventBaseJsonDeserializer(new VenueJsonDeserializer(new CourseAreaJsonDeserializer(DomainFactory.INSTANCE)), + leaderboardGroupBaseJsonDeserializer), leaderboardGroupBaseJsonDeserializer); + final LeaderboardSearchResult expected = searchResults.getHits().iterator().next(); + LeaderboardSearchResultBase deserialized = deserializer.deserialize(serializer.serialize(expected)); + assertEquals("Pfingstbusch (470)", deserialized.getRegattaName()); + assertEquals(expected.getEvents().iterator().next().getName(), deserialized.getEvents().iterator().next().getName()); + assertEquals(expected.getEvents().iterator().next().getId(), deserialized.getEvents().iterator().next().getId()); + assertEquals(expected.getEvents().iterator().next().getStartDate(), deserialized.getEvents().iterator().next().getStartDate()); + assertEquals(expected.getEvents().iterator().next().getEndDate(), deserialized.getEvents().iterator().next().getEndDate()); + assertEquals(expected.getEvents().iterator().next().getVenue().getName(), deserialized.getEvents().iterator().next().getVenue().getName()); + Iterator expectedLGs = expected.getLeaderboardGroups().iterator(); + Iterator deserializedLGs = deserialized.getLeaderboardGroups().iterator(); + while (expectedLGs.hasNext()) { + assertTrue(deserializedLGs.hasNext()); + LeaderboardGroup expectedLG = expectedLGs.next(); + LeaderboardGroupBase deserializedLG = deserializedLGs.next(); + assertEquals(expectedLG.getId(), deserializedLG.getId()); + assertEquals(expectedLG.getName(), deserializedLG.getName()); + assertEquals(expectedLG.getDescription(), deserializedLG.getDescription()); + } + } +} diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RaceLogReplicator.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RaceLogReplicator.java index 9aa44d096da..df2e1f6b302 100644 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RaceLogReplicator.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RaceLogReplicator.java @@ -78,6 +78,10 @@ public class RaceLogReplicator implements RaceColumnListener { public void isMedalRaceChanged(RaceColumn raceColumn, boolean newIsMedalRace) { } + @Override + public void isFleetsCanRunInParallelChanged(RaceColumn raceColumn, boolean newIsFleetsCanRunInParallel) { + } + @Override public void isStartsWithZeroScoreChanged(RaceColumn raceColumn, boolean newIsStartsWithZeroScore) { } diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RaceLogScoringReplicator.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RaceLogScoringReplicator.java index a2b3ad0ec64..1efadc59320 100644 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RaceLogScoringReplicator.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RaceLogScoringReplicator.java @@ -51,6 +51,10 @@ public class RaceLogScoringReplicator implements RaceColumnListener { public void isMedalRaceChanged(RaceColumn raceColumn, boolean newIsMedalRace) { } + @Override + public void isFleetsCanRunInParallelChanged(RaceColumn raceColumn, boolean newIsFleetsCanRunInParallel) { + } + @Override public void isStartsWithZeroScoreChanged(RaceColumn raceColumn, boolean newIsStartsWithZeroScore) { } diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java index 6541b9a4d8c..1de83e5335d 100644 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java @@ -1444,7 +1444,7 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes } result.put( s.getName(), - new SeriesCreationParametersDTO(fleetNamesAndOrdering, s.isMedal(), s.isStartsWithZeroScore(), s + new SeriesCreationParametersDTO(fleetNamesAndOrdering, s.isMedal(), s.isFleetsCanRunInParallel(), s.isStartsWithZeroScore(), s .isFirstColumnIsNonDiscardableCarryForward(), s.getResultDiscardingRule() == null ? null : s.getResultDiscardingRule().getDiscardIndexResultsStartingWithHowManyRaces(), s .hasSplitFleetContiguousScoring())); diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/operationaltransformation/AddSpecificRegatta.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/operationaltransformation/AddSpecificRegatta.java index a996da27431..b58bf4d24c5 100755 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/operationaltransformation/AddSpecificRegatta.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/operationaltransformation/AddSpecificRegatta.java @@ -56,7 +56,7 @@ public class AddSpecificRegatta extends AbstractAddRegattaOperation { List result = new ArrayList(); for (Map.Entry e : seriesNamesWithFleetNamesAndFleetOrderingAndMedalAndStartsWithZeroScoreAndDiscardingThresholds.getSeriesCreationParameters().entrySet()) { final List emptyRaceColumnNamesList = Collections.emptyList(); - Series s = new SeriesImpl(e.getKey(), e.getValue().isMedal(), createFleets(e.getValue().getFleets()), + Series s = new SeriesImpl(e.getKey(), e.getValue().isMedal(), e.getValue().isFleetsCanRunInParallel(), createFleets(e.getValue().getFleets()), emptyRaceColumnNamesList, trackedRegattaRegistry); if (e.getValue().getDiscardingThresholds() != null) { s.setResultDiscardingRule(new ThresholdBasedResultDiscardingRuleImpl(e.getValue().getDiscardingThresholds())); diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/operationaltransformation/UpdateSeries.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/operationaltransformation/UpdateSeries.java index b3e5f88ef34..88d9208e9dd 100755 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/operationaltransformation/UpdateSeries.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/operationaltransformation/UpdateSeries.java @@ -1,90 +1,93 @@ -package com.sap.sailing.server.operationaltransformation; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import com.sap.sailing.domain.base.Fleet; -import com.sap.sailing.domain.base.Regatta; -import com.sap.sailing.domain.base.Series; -import com.sap.sailing.domain.base.impl.FleetImpl; -import com.sap.sailing.domain.base.impl.SeriesImpl; -import com.sap.sailing.domain.common.RegattaIdentifier; -import com.sap.sailing.domain.common.dto.FleetDTO; -import com.sap.sailing.domain.leaderboard.impl.ThresholdBasedResultDiscardingRuleImpl; -import com.sap.sailing.domain.tracking.TrackedRegattaRegistry; -import com.sap.sailing.server.RacingEventService; -import com.sap.sailing.server.RacingEventServiceOperation; - -public class UpdateSeries extends AbstractSeriesOperation { - private static final long serialVersionUID = 6356089749049112710L; - - private final List fleets; - private final boolean isMedal; - private final int[] resultDiscardingThresholds; - private final boolean startsWithZeroScore; - private final boolean firstColumnIsNonDiscardableCarryForward; - private final boolean hasSplitFleetContiguousScoring; - private final boolean seriesNameChanged; - private final String newSeriesName; - - public UpdateSeries(RegattaIdentifier regattaIdentifier, String seriesName, String newSeriesName, boolean isMedal, - int[] resultDiscardingThresholds, boolean startsWithZeroScore, - boolean firstColumnIsNonDiscardableCarryForward, boolean hasSplitFleetContiguousScoring, - List fleets) { - super(regattaIdentifier, seriesName); - this.seriesNameChanged = !seriesName.equals(newSeriesName); - this.newSeriesName = newSeriesName; - this.isMedal = isMedal; - this.resultDiscardingThresholds = resultDiscardingThresholds; - this.startsWithZeroScore = startsWithZeroScore; - this.firstColumnIsNonDiscardableCarryForward = firstColumnIsNonDiscardableCarryForward; - this.hasSplitFleetContiguousScoring = hasSplitFleetContiguousScoring; - this.fleets = fleets; - } - - @Override - public Void internalApplyTo(RacingEventService toState) throws Exception { - Series series = getSeries(toState); - if (series == null) { - series = createSeries(toState); - } - if (seriesNameChanged) { - series.setName(newSeriesName); - } - series.setIsMedal(isMedal); - series.setResultDiscardingRule(resultDiscardingThresholds == null ? - null : new ThresholdBasedResultDiscardingRuleImpl(resultDiscardingThresholds)); - series.setStartsWithZeroScore(startsWithZeroScore); - series.setFirstColumnIsNonDiscardableCarryForward(firstColumnIsNonDiscardableCarryForward); - series.setSplitFleetContiguousScoring(hasSplitFleetContiguousScoring); - if (series.getRegatta().isPersistent()) { - toState.updateStoredRegatta(series.getRegatta()); - } - return null; - } - - private Series createSeries(RacingEventService toState) { - Regatta regatta = toState.getRegatta(getRegattaIdentifier()); - final List emptyRaceColumnNames = Collections.emptyList(); - List result = new ArrayList(); - for (FleetDTO fleetNameAndOrderingAndColor : fleets) { - Fleet fleet = new FleetImpl(fleetNameAndOrderingAndColor.getName(), fleetNameAndOrderingAndColor.getOrderNo(), fleetNameAndOrderingAndColor.getColor()); - result.add(fleet); - } - regatta.addSeries(new SeriesImpl(getSeriesName(), isMedal, result, emptyRaceColumnNames, (TrackedRegattaRegistry)toState)); - return regatta.getSeriesByName(getSeriesName()); - } - - @Override - public RacingEventServiceOperation transformClientOp(RacingEventServiceOperation serverOp) { - // TODO Auto-generated method stub - return null; - } - - @Override - public RacingEventServiceOperation transformServerOp(RacingEventServiceOperation clientOp) { - // TODO Auto-generated method stub - return null; - } -} +package com.sap.sailing.server.operationaltransformation; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.Regatta; +import com.sap.sailing.domain.base.Series; +import com.sap.sailing.domain.base.impl.FleetImpl; +import com.sap.sailing.domain.base.impl.SeriesImpl; +import com.sap.sailing.domain.common.RegattaIdentifier; +import com.sap.sailing.domain.common.dto.FleetDTO; +import com.sap.sailing.domain.leaderboard.impl.ThresholdBasedResultDiscardingRuleImpl; +import com.sap.sailing.domain.tracking.TrackedRegattaRegistry; +import com.sap.sailing.server.RacingEventService; +import com.sap.sailing.server.RacingEventServiceOperation; + +public class UpdateSeries extends AbstractSeriesOperation { + private static final long serialVersionUID = 6356089749049112710L; + + private final List fleets; + private final boolean isMedal; + private final boolean isFleetsCanRunInParallel; + private final int[] resultDiscardingThresholds; + private final boolean startsWithZeroScore; + private final boolean firstColumnIsNonDiscardableCarryForward; + private final boolean hasSplitFleetContiguousScoring; + private final boolean seriesNameChanged; + private final String newSeriesName; + + public UpdateSeries(RegattaIdentifier regattaIdentifier, String seriesName, String newSeriesName, boolean isMedal, boolean isFleetsCanRunInParallel, + int[] resultDiscardingThresholds, boolean startsWithZeroScore, + boolean firstColumnIsNonDiscardableCarryForward, boolean hasSplitFleetContiguousScoring, + List fleets) { + super(regattaIdentifier, seriesName); + this.seriesNameChanged = !seriesName.equals(newSeriesName); + this.newSeriesName = newSeriesName; + this.isMedal = isMedal; + this.isFleetsCanRunInParallel = isFleetsCanRunInParallel; + this.resultDiscardingThresholds = resultDiscardingThresholds; + this.startsWithZeroScore = startsWithZeroScore; + this.firstColumnIsNonDiscardableCarryForward = firstColumnIsNonDiscardableCarryForward; + this.hasSplitFleetContiguousScoring = hasSplitFleetContiguousScoring; + this.fleets = fleets; + } + + @Override + public Void internalApplyTo(RacingEventService toState) throws Exception { + Series series = getSeries(toState); + if (series == null) { + series = createSeries(toState); + } + if (seriesNameChanged) { + series.setName(newSeriesName); + } + series.setIsMedal(isMedal); + series.setIsFleetsCanRunInParallel(isFleetsCanRunInParallel); + series.setResultDiscardingRule(resultDiscardingThresholds == null ? + null : new ThresholdBasedResultDiscardingRuleImpl(resultDiscardingThresholds)); + series.setStartsWithZeroScore(startsWithZeroScore); + series.setFirstColumnIsNonDiscardableCarryForward(firstColumnIsNonDiscardableCarryForward); + series.setSplitFleetContiguousScoring(hasSplitFleetContiguousScoring); + if (series.getRegatta().isPersistent()) { + toState.updateStoredRegatta(series.getRegatta()); + } + return null; + } + + private Series createSeries(RacingEventService toState) { + Regatta regatta = toState.getRegatta(getRegattaIdentifier()); + final List emptyRaceColumnNames = Collections.emptyList(); + List result = new ArrayList(); + for (FleetDTO fleetNameAndOrderingAndColor : fleets) { + Fleet fleet = new FleetImpl(fleetNameAndOrderingAndColor.getName(), fleetNameAndOrderingAndColor.getOrderNo(), fleetNameAndOrderingAndColor.getColor()); + result.add(fleet); + } + regatta.addSeries(new SeriesImpl(getSeriesName(), isMedal, isFleetsCanRunInParallel, result, emptyRaceColumnNames, (TrackedRegattaRegistry)toState)); + return regatta.getSeriesByName(getSeriesName()); + } + + @Override + public RacingEventServiceOperation transformClientOp(RacingEventServiceOperation serverOp) { + // TODO Auto-generated method stub + return null; + } + + @Override + public RacingEventServiceOperation transformServerOp(RacingEventServiceOperation clientOp) { + // TODO Auto-generated method stub + return null; + } +} diff --git a/java/com.sap.sailing.xrr.structureimport/src/com/sap/sailing/xrr/structureimport/StructureImporter.java b/java/com.sap.sailing.xrr.structureimport/src/com/sap/sailing/xrr/structureimport/StructureImporter.java index 1e46db1ecf2..f132381a169 100755 --- a/java/com.sap.sailing.xrr.structureimport/src/com/sap/sailing/xrr/structureimport/StructureImporter.java +++ b/java/com.sap.sailing.xrr.structureimport/src/com/sap/sailing/xrr/structureimport/StructureImporter.java @@ -143,7 +143,7 @@ public class StructureImporter { for (Series raceType : regattaStructure.getSeries()) { List fleets = getFleets(raceType.getFleets()); setRaceNames(index, raceType, raceType.getFleets()); - series.add(new SeriesImpl(raceType.getSeries(), raceType.isMedal(), fleets, raceType.getRaceNames(), null)); + series.add(new SeriesImpl(raceType.getSeries(), raceType.isMedal(), true, fleets, raceType.getRaceNames(), null)); } } return series; diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/Util.java b/java/com.sap.sse.common/src/com/sap/sse/common/Util.java index 624d09fbc0b..1756ebf710d 100644 --- a/java/com.sap.sse.common/src/com/sap/sse/common/Util.java +++ b/java/com.sap.sse.common/src/com/sap/sse/common/Util.java @@ -253,7 +253,7 @@ public class Util { List l = (List) iterable; return l.get(i); } else { - Iterator iter = iterable.iterator(); + final Iterator iter = iterable.iterator(); T result = iter.next(); for (int j=0; j T first(Iterable iterable) { + final Iterator iter = iterable.iterator(); + final T result; + if (iter.hasNext()) { + result = iter.next(); + } else { + result = null; + } + return result; + } + + public static T last(Iterable iterable) { + final T result; + if (isEmpty(iterable)) { + result = null; + } else { + result = get(iterable, size(iterable)-1); + } + return result; + } /** * A null-safe check whether t is contained in ts. For ts==null the method diff --git a/mobile/com.sap.sailing.android.tracking.app/res/values/strings_uss_sailing.xml b/mobile/com.sap.sailing.android.tracking.app/res/values/strings_uss_sailing.xml index 819858d6e45..e011b303434 100644 --- a/mobile/com.sap.sailing.android.tracking.app/res/values/strings_uss_sailing.xml +++ b/mobile/com.sap.sailing.android.tracking.app/res/values/strings_uss_sailing.xml @@ -1,7 +1,7 @@ - The SAP Sail InSight app supports live tracking of race boats by leveraging mobile phone technology, allowing the ability for friends and fans to follow racing whether onsite or online through live leaderboards and 2D visualization. Following racing sailors can replay their races allowing the opportunity to review performance and optimize strategies all available at: https://www.sapsailing.com\n\nSAP supports US Sailing through the implementation of a range of innovative technology solutions, delivering analytics to help teams and sailors improve performance whilst also providing applications to help sailing operations run more efficiently.\n\nAs the national governing body for sailing, US Sailing provides leadership, integrity and growth for the sport including training and education programs for instructors and race officials alongside administration and oversight of competitive sailing across the country. - https://www.sapsailing.com + The SAP Sail InSight app supports live tracking of race boats by leveraging mobile phone technology, allowing the ability for friends and fans to follow racing whether onsite or online through live leaderboards and 2D visualization. Following racing sailors can replay their races allowing the opportunity to review performance and optimize strategies all available at: https://www.sapsailing.com\n\nSAP supports US Sailing through the implementation of a range of innovative technology solutions, delivering analytics to help teams and sailors improve performance whilst also providing applications to help sailing operations run more efficiently.\n\nAs the national governing body for sailing, US Sailing provides leadership, integrity and growth for the sport including training and education programs for instructors and race officials alongside administration and oversight of competitive sailing across the country. + https://www.sapsailing.com \ No newline at end of file diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/AppConstants.java b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/AppConstants.java index 2362c8bd529..3d915925d2b 100755 --- a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/AppConstants.java +++ b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/AppConstants.java @@ -80,6 +80,10 @@ public class AppConstants { public final static String INTENT_ACTION_SHOW_MAIN_CONTENT = PACKAGE_NAME + ".action.show.main"; public final static String INTENT_ACTION_SHOW_SUMMARY_CONTENT = PACKAGE_NAME + ".action.show.summary"; + /** + * As extra this intent expects the String obtained for a FilterableRace / ManagedRace using + * {@code new RaceGroupSeries(race.getRaceGroup(), race.getSeries()).getDisplayName()}. + */ public final static String INTENT_ACTION_SHOW_PROTEST = PACKAGE_NAME + ".action.show.protest"; public final static String INTENT_ACTION_REMOVE_PROTEST = PACKAGE_NAME + ".action.remove.protest"; diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/data/InMemoryDataStore.java b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/data/InMemoryDataStore.java index 8f6c9244c5b..f213b524b75 100644 --- a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/data/InMemoryDataStore.java +++ b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/data/InMemoryDataStore.java @@ -189,7 +189,7 @@ public enum InMemoryDataStore implements DataStore { } private SimpleRaceLogIdentifier convertManagedRaceIdentifierToSimpleRaceLogIdentifier(ManagedRaceIdentifier id) { - return new SimpleRaceLogIdentifierImpl(id.getRaceGroup().getName(), id.getRaceName(), id.getFleet().getName()); + return new SimpleRaceLogIdentifierImpl(id.getRaceGroup().getName(), id.getRaceColumnName(), id.getFleet().getName()); } @Override diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/data/OfflineDataManager.java b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/data/OfflineDataManager.java index 71683a1d6cc..ec72a1187e1 100644 --- a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/data/OfflineDataManager.java +++ b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/data/OfflineDataManager.java @@ -87,8 +87,8 @@ public class OfflineDataManager extends DataManager { newEvent.getVenue().addCourseArea(new CourseAreaImpl("Stadium", UUID.randomUUID())); dataStore.addEvent(newEvent); - SeriesWithRows qualifying = new SeriesWithRowsImpl("Qualifying", false, null); - SeriesWithRows medal = new SeriesWithRowsImpl("Medal", true, null); + SeriesWithRows qualifying = new SeriesWithRowsImpl("Qualifying", false, /* isFleetsCanRunInParallel */ true, null); + SeriesWithRows medal = new SeriesWithRowsImpl("Medal", true, /* isFleetsCanRunInParallel */ true, null); RaceGroup raceGroup = new RaceGroupImpl("ESS", /* displayName */ null, new BoatClassImpl("X40", false), null, Arrays.asList(qualifying, medal), new EmptyRegattaConfiguration()); @@ -111,7 +111,8 @@ public class OfflineDataManager extends DataManager { AppPreferences.on(context).getAuthor(), 1, RaceLogRaceStatus.FINISHING)); ManagedRace q1 = new ManagedRaceImpl(new ManagedRaceIdentifierImpl("A.B", new FleetImpl("A"), qualifying, raceGroup), - RaceStateImpl.create(new AndroidRaceLogResolver(), log, AppPreferences.on(context).getAuthor(), configuration)); + RaceStateImpl.create(new AndroidRaceLogResolver(), log, AppPreferences.on(context).getAuthor(), configuration), + /* zeroBasedIndexInFleet */ 0); log = new RaceLogImpl(UUID.randomUUID()); /* @@ -120,7 +121,8 @@ public class OfflineDataManager extends DataManager { */ ManagedRace q2 = new ManagedRaceImpl(new ManagedRaceIdentifierImpl("B", new FleetImpl("A.A"), qualifying, raceGroup), - RaceStateImpl.create(new AndroidRaceLogResolver(), log, AppPreferences.on(context).getAuthor(), configuration)); + RaceStateImpl.create(new AndroidRaceLogResolver(), log, AppPreferences.on(context).getAuthor(), configuration), + /* zeroBasedIndexInFleet */ 1); log = new RaceLogImpl(UUID.randomUUID()); /* @@ -128,7 +130,8 @@ public class OfflineDataManager extends DataManager { * RaceLogRaceStatus.FINISHED)); */ ManagedRace q3 = new ManagedRaceImpl(new ManagedRaceIdentifierImpl("Q3", new FleetImpl("Default"), qualifying, raceGroup), - RaceStateImpl.create(new AndroidRaceLogResolver(), log, AppPreferences.on(context).getAuthor(), configuration)); + RaceStateImpl.create(new AndroidRaceLogResolver(), log, AppPreferences.on(context).getAuthor(), configuration), + /* zeroBasedIndexInFleet */ 2); /* * ManagedRace m1 = new ManagedRaceImpl( new ManagedRaceIdentifierImpl( "M1", new FleetImpl("Default"), medal, * raceGroup), null); diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/data/OnlineDataManager.java b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/data/OnlineDataManager.java index 98e9a8a43c8..1c4c8b36d46 100644 --- a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/data/OnlineDataManager.java +++ b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/data/OnlineDataManager.java @@ -202,7 +202,7 @@ public class OnlineDataManager extends DataManager { ManagedRaceIdentifier identifier = managedRace.getIdentifier(); // no parameter encoding required here; the UrlHelper.generateUrl call uses Url.Builder which handles encoding String raceGroupName = identifier.getRaceGroup().getName(); - String raceColumnName = identifier.getRaceName(); + String raceColumnName = identifier.getRaceColumnName(); String fleetName = identifier.getFleet().getName(); List> params = new ArrayList<>(); @@ -229,7 +229,7 @@ public class OnlineDataManager extends DataManager { ManagedRaceIdentifier identifier = managedRace.getIdentifier(); String raceGroupName = identifier.getRaceGroup().getName(); - String raceColumnName = identifier.getRaceName(); + String raceColumnName = identifier.getRaceColumnName(); String fleetName = identifier.getFleet().getName(); List> params = new ArrayList<>(); @@ -257,7 +257,7 @@ public class OnlineDataManager extends DataManager { ManagedRaceIdentifier identifier = managedRace.getIdentifier(); String raceGroupName = identifier.getRaceGroup().getName(); - String raceColumnName = identifier.getRaceName(); + String raceColumnName = identifier.getRaceColumnName(); String fleetName = identifier.getFleet().getName(); List> params = new ArrayList<>(); diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/data/parsers/ManagedRacesDataParser.java b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/data/parsers/ManagedRacesDataParser.java index dd12e0090eb..c6cb55eba88 100755 --- a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/data/parsers/ManagedRacesDataParser.java +++ b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/data/parsers/ManagedRacesDataParser.java @@ -11,7 +11,6 @@ import org.json.simple.JSONValue; import com.sap.sailing.domain.abstractlog.AbstractLogEventAuthor; import com.sap.sailing.domain.abstractlog.race.RaceLog; import com.sap.sailing.domain.base.Fleet; -import com.sap.sailing.domain.base.SeriesBase; import com.sap.sailing.domain.base.configuration.ConfigurationLoader; import com.sap.sailing.domain.base.configuration.RegattaConfiguration; import com.sap.sailing.domain.base.racegroup.RaceCell; @@ -29,6 +28,11 @@ import com.sap.sailing.racecommittee.app.utils.ManagedRaceCalculator; import com.sap.sailing.server.gateway.deserialization.JsonDeserializer; import com.sap.sailing.server.gateway.deserialization.impl.Helpers; +/** + * Produces an collection of {@link ManagedRace} objects whose order is determined by the order of the {@link RaceGroup}s + * as provided by the JSON object, and within each {@link RaceGroup} by the order of the series in the group, and within + * the series by the order of the "race rows" as provided by the server. + */ public class ManagedRacesDataParser implements DataParser> { private final JsonDeserializer deserializer; @@ -45,11 +49,9 @@ public class ManagedRacesDataParser implements DataParser parse(Reader reader) throws Exception { Object parsedResult = JSONValue.parse(reader); JSONArray jsonArray = Helpers.toJSONArraySafe(parsedResult); - Collection managedRaces = new ArrayList(); for (Object element : jsonArray) { JSONObject json = Helpers.toJSONObjectSafe(element); - RaceGroup group = deserializer.deserialize(json); addManagedRaces(managedRaces, group); } @@ -61,23 +63,25 @@ public class ManagedRacesDataParser implements DataParser configurationLoader = globalConfigurationLoader; RegattaConfiguration localConfiguration = raceGroup.getRegattaConfiguration(); if (localConfiguration != null) { configurationLoader = new MergingRegattaConfigurationLoader(localConfiguration, globalConfigurationLoader); } FleetIdentifier fleetIdentifier = new FleetIdentifierImpl(fleet, series, raceGroup); - ManagedRaceIdentifier identifier = new ManagedRaceIdentifierImpl(name, fleetIdentifier); - return new ManagedRaceImpl(identifier, new ManagedRaceCalculator(raceLog, author, configurationLoader), factor, explicitFactor); + ManagedRaceIdentifier identifier = new ManagedRaceIdentifierImpl(raceColumnName, fleetIdentifier); + return new ManagedRaceImpl(identifier, new ManagedRaceCalculator(raceLog, author, configurationLoader), + factor, explicitFactor, zeroBasedIndexInFleet); } } diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/domain/FleetIdentifier.java b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/domain/FleetIdentifier.java index 597b257aa5b..b1f87e47112 100644 --- a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/domain/FleetIdentifier.java +++ b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/domain/FleetIdentifier.java @@ -1,14 +1,14 @@ package com.sap.sailing.racecommittee.app.domain; import com.sap.sailing.domain.base.Fleet; -import com.sap.sailing.domain.base.SeriesBase; import com.sap.sailing.domain.base.racegroup.RaceGroup; +import com.sap.sailing.domain.base.racegroup.SeriesWithRows; public interface FleetIdentifier { public Fleet getFleet(); - public SeriesBase getSeries(); + public SeriesWithRows getSeries(); public RaceGroup getRaceGroup(); diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/domain/ManagedRace.java b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/domain/ManagedRace.java index 7743e00baac..3e30571d206 100644 --- a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/domain/ManagedRace.java +++ b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/domain/ManagedRace.java @@ -7,13 +7,20 @@ import com.sap.sailing.domain.abstractlog.race.RaceLog; import com.sap.sailing.domain.abstractlog.race.state.RaceState; import com.sap.sailing.domain.base.Competitor; import com.sap.sailing.domain.base.CourseBase; +import com.sap.sailing.domain.base.racegroup.FilterableRace; import com.sap.sailing.domain.common.racelog.RaceLogRaceStatus; import com.sap.sailing.racecommittee.app.domain.impl.Result; import com.sap.sse.common.Named; import com.sap.sse.common.TimePoint; import com.sap.sse.common.WithID; -public interface ManagedRace extends ManagedRaceIdentifier, Named, WithID { +/** + * A managed race's {@link #getName()} is the race column's name. + * + * @author Axel Uhl (d043530) + * + */ +public interface ManagedRace extends FilterableRace, ManagedRaceIdentifier, Named, WithID { /** * @return the identifier of the race. diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/domain/ManagedRaceIdentifier.java b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/domain/ManagedRaceIdentifier.java index d35587af6ce..91dfd4482b1 100644 --- a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/domain/ManagedRaceIdentifier.java +++ b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/domain/ManagedRaceIdentifier.java @@ -1,5 +1,5 @@ package com.sap.sailing.racecommittee.app.domain; public interface ManagedRaceIdentifier extends FleetIdentifier { - public String getRaceName(); + public String getRaceColumnName(); } diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/domain/impl/FleetIdentifierImpl.java b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/domain/impl/FleetIdentifierImpl.java index 0ab155c64b0..25ecf802f76 100644 --- a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/domain/impl/FleetIdentifierImpl.java +++ b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/domain/impl/FleetIdentifierImpl.java @@ -1,18 +1,18 @@ package com.sap.sailing.racecommittee.app.domain.impl; import com.sap.sailing.domain.base.Fleet; -import com.sap.sailing.domain.base.SeriesBase; import com.sap.sailing.domain.base.racegroup.RaceGroup; +import com.sap.sailing.domain.base.racegroup.SeriesWithRows; import com.sap.sailing.racecommittee.app.domain.FleetIdentifier; import com.sap.sse.common.Util.Triple; public class FleetIdentifierImpl implements FleetIdentifier { private Fleet fleet; - private SeriesBase series; + private SeriesWithRows series; private RaceGroup raceGroup; - public FleetIdentifierImpl(Fleet fleet, SeriesBase series, RaceGroup raceGroup) { + public FleetIdentifierImpl(Fleet fleet, SeriesWithRows series, RaceGroup raceGroup) { this.fleet = fleet; this.series = series; this.raceGroup = raceGroup; @@ -22,7 +22,7 @@ public class FleetIdentifierImpl implements FleetIdentifier { return fleet; } - public SeriesBase getSeries() { + public SeriesWithRows getSeries() { return series; } diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/domain/impl/ManagedRaceIdentifierImpl.java b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/domain/impl/ManagedRaceIdentifierImpl.java index 04ba5293317..4390fe5f574 100644 --- a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/domain/impl/ManagedRaceIdentifierImpl.java +++ b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/domain/impl/ManagedRaceIdentifierImpl.java @@ -1,35 +1,35 @@ package com.sap.sailing.racecommittee.app.domain.impl; import com.sap.sailing.domain.base.Fleet; -import com.sap.sailing.domain.base.SeriesBase; import com.sap.sailing.domain.base.racegroup.RaceGroup; +import com.sap.sailing.domain.base.racegroup.SeriesWithRows; import com.sap.sailing.racecommittee.app.domain.FleetIdentifier; import com.sap.sailing.racecommittee.app.domain.ManagedRaceIdentifier; public class ManagedRaceIdentifierImpl extends FleetIdentifierImpl implements ManagedRaceIdentifier { - private String raceName; + private String raceColumnName; - public ManagedRaceIdentifierImpl(String raceName, Fleet fleetWithRaceNames, SeriesBase series, RaceGroup raceGroup) { + public ManagedRaceIdentifierImpl(String raceColumnName, Fleet fleetWithRaceNames, SeriesWithRows series, RaceGroup raceGroup) { super(fleetWithRaceNames, series, raceGroup); - this.raceName = raceName; + this.raceColumnName = raceColumnName; } - public ManagedRaceIdentifierImpl(String raceName, FleetIdentifier identifier) { - this(raceName, identifier.getFleet(), identifier.getSeries(), identifier.getRaceGroup()); + public ManagedRaceIdentifierImpl(String raceColumnName, FleetIdentifier identifier) { + this(raceColumnName, identifier.getFleet(), identifier.getSeries(), identifier.getRaceGroup()); } - public String getRaceName() { - return raceName; + public String getRaceColumnName() { + return raceColumnName; } @Override public String getId() { - return String.format("%s.%s", super.getId(), escapeIdentifierFragment(getRaceName())); + return String.format("%s.%s", super.getId(), escapeIdentifierFragment(getRaceColumnName())); } @Override public String toString() { - return getRaceName() + " - " + getFleet().getName(); + return getRaceColumnName() + " - " + getFleet().getName(); } } diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/domain/impl/ManagedRaceImpl.java b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/domain/impl/ManagedRaceImpl.java index 009be0cf918..c79c4f17b9d 100755 --- a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/domain/impl/ManagedRaceImpl.java +++ b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/domain/impl/ManagedRaceImpl.java @@ -11,8 +11,9 @@ import com.sap.sailing.domain.abstractlog.race.state.RaceState; import com.sap.sailing.domain.base.Competitor; import com.sap.sailing.domain.base.CourseBase; import com.sap.sailing.domain.base.Fleet; -import com.sap.sailing.domain.base.SeriesBase; +import com.sap.sailing.domain.base.racegroup.RaceCell; import com.sap.sailing.domain.base.racegroup.RaceGroup; +import com.sap.sailing.domain.base.racegroup.SeriesWithRows; import com.sap.sailing.domain.common.racelog.RaceLogRaceStatus; import com.sap.sailing.racecommittee.app.R; import com.sap.sailing.racecommittee.app.data.AndroidRaceLogResolver; @@ -21,6 +22,7 @@ import com.sap.sailing.racecommittee.app.domain.ManagedRaceIdentifier; import com.sap.sailing.racecommittee.app.domain.MapMarker; import com.sap.sailing.racecommittee.app.utils.ManagedRaceCalculator; import com.sap.sse.common.TimePoint; +import com.sap.sse.common.Util; import com.sap.sse.common.impl.MillisecondsTimePoint; public class ManagedRaceImpl implements ManagedRace { @@ -33,22 +35,39 @@ public class ManagedRaceImpl implements ManagedRace { private ManagedRaceCalculator calculator; private double factor; private Double explicitFactor; + private final int zeroBasedIndexInFleet; - private ManagedRaceImpl(ManagedRaceIdentifier identifier, double factor, Double explicitFactor) { + /** + * @param zeroBasedIndexInFleet + * A Series offers a sequence of RaceColumns, each of them split according to the Fleets modeled for the + * Series. A {@link RaceCell} describes a "slot" in this grid, defined by the series, the fleet and the + * race column. While this object's {@link #getName() name} represents the race column's name, this + * doesn't tell anything about the "horizontal" position in the "grid" or in other words what the index + * is of the race column in which this cell lies. + *

+ * + * Indices returned by this method start with zero, meaning the first race column in the series. This + * corresponds to what one would get by asking {@link Util#indexOf(Iterable, Object) + * Util.indexOf(series.getRaceColumns(), thisCellsRaceColumn)}, except in case the first race column is a + * "virtual" one that holds a non-discardable carry-forward result. In this case, the second Race Column, + * which is the first "non-virtual" one, receives index 0. + */ + private ManagedRaceImpl(ManagedRaceIdentifier identifier, double factor, Double explicitFactor, int zeroBasedIndexInFleet) { this.identifier = identifier; this.competitors = new ArrayList<>(); this.courseOnServer = null; this.factor = factor; this.explicitFactor = explicitFactor; + this.zeroBasedIndexInFleet = zeroBasedIndexInFleet; } - public ManagedRaceImpl(ManagedRaceIdentifier identifier, RaceState state) { - this(identifier, 0, null); + public ManagedRaceImpl(ManagedRaceIdentifier identifier, RaceState state, int zeroBasedIndexInFleet) { + this(identifier, 0, null, zeroBasedIndexInFleet); this.state = state; } - public ManagedRaceImpl(ManagedRaceIdentifier identifier, ManagedRaceCalculator calculator, double factor, Double explicitFactor) { - this(identifier, factor, explicitFactor); + public ManagedRaceImpl(ManagedRaceIdentifier identifier, ManagedRaceCalculator calculator, double factor, Double explicitFactor, int zeroBasedIndexInFleet) { + this(identifier, factor, explicitFactor, zeroBasedIndexInFleet); this.calculator = calculator; } @@ -64,11 +83,11 @@ public class ManagedRaceImpl implements ManagedRace { @Override public String getName() { - return identifier.getRaceName(); + return identifier.getRaceColumnName(); } @Override - public String getRaceName() { + public String getRaceColumnName() { return getName(); } @@ -78,7 +97,7 @@ public class ManagedRaceImpl implements ManagedRace { } @Override - public SeriesBase getSeries() { + public SeriesWithRows getSeries() { return identifier.getSeries(); } @@ -201,4 +220,31 @@ public class ManagedRaceImpl implements ManagedRace { public String toString() { return "ManagedRaceImpl [identifier=" + identifier + "]"; } + + @Override + public int getZeroBasedIndexInFleet() { + int result = -1; + if (zeroBasedIndexInFleet != -1) { + // it was properly delivered by a compatible server; that's our result + result = zeroBasedIndexInFleet; + } else { + // we deal with an incompatible server that doesn't know about this field yet; + // try to compute from the surrounding race group: + int i=0; + for (final RaceCell cell : getSeries().getRaceRow(getFleet()).getCells()) { + if (cell.getName().equals(getRaceColumnName())) { + result = i; + break; + } + i++; + } + // if a cell with this race's race column name is not found, leave the index at -1 + } + return result; + } + + @Override + public int getZeroBasedSeriesIndex() { + return Util.indexOf(getRaceGroup().getSeries(), getSeries()); + } } diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/activities/RacingActivity.java b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/activities/RacingActivity.java index 46205dc3116..703e25f7e92 100644 --- a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/activities/RacingActivity.java +++ b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/activities/RacingActivity.java @@ -16,6 +16,7 @@ import com.sap.sailing.domain.abstractlog.race.SimpleRaceLogIdentifier; import com.sap.sailing.domain.abstractlog.race.analyzing.impl.StartTimeFinderResult; import com.sap.sailing.domain.base.CourseArea; import com.sap.sailing.domain.base.EventBase; +import com.sap.sailing.domain.base.racegroup.RaceGroupSeriesFleet; import com.sap.sailing.domain.common.Wind; import com.sap.sailing.domain.common.racelog.RaceLogRaceStatus; import com.sap.sailing.racecommittee.app.AppConstants; @@ -26,7 +27,6 @@ import com.sap.sailing.racecommittee.app.data.ReadonlyDataManager; import com.sap.sailing.racecommittee.app.data.clients.LoadClient; import com.sap.sailing.racecommittee.app.data.loaders.DataLoaderResult; import com.sap.sailing.racecommittee.app.domain.ManagedRace; -import com.sap.sailing.racecommittee.app.domain.impl.RaceGroupSeriesFleet; import com.sap.sailing.racecommittee.app.logging.LogEvent; import com.sap.sailing.racecommittee.app.services.RaceStateService; import com.sap.sailing.racecommittee.app.ui.adapters.racelist.RaceListDataType; @@ -689,13 +689,10 @@ public class RacingActivity extends SessionActivity implements RaceListCallbacks ExLog.i(RacingActivity.this, TAG, "Same races are already loaded..."); } else { lastSeenRaces = data; - registerOnService(data); mRaceList.setupOn(data); - Toast.makeText(RacingActivity.this, String.format(getString(R.string.racing_load_success), data.size()), Toast.LENGTH_SHORT).show(); } - setProgressSpinnerVisibility(false); } } diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/adapters/racelist/ManagedRaceListAdapter.java b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/adapters/racelist/ManagedRaceListAdapter.java index 55fbd28f6d9..631eff7fe50 100644 --- a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/adapters/racelist/ManagedRaceListAdapter.java +++ b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/adapters/racelist/ManagedRaceListAdapter.java @@ -2,22 +2,14 @@ package com.sap.sailing.racecommittee.app.ui.adapters.racelist; import java.text.DecimalFormat; import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; - -import android.content.Context; -import android.content.Intent; -import android.content.res.Resources; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.LayerDrawable; -import android.text.TextUtils; -import android.util.TypedValue; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; +import java.util.Map; +import java.util.Set; import com.sap.sailing.android.shared.logging.ExLog; import com.sap.sailing.android.shared.util.BitmapHelper; @@ -28,6 +20,10 @@ import com.sap.sailing.domain.abstractlog.race.analyzing.impl.StartTimeFinderRes import com.sap.sailing.domain.abstractlog.race.state.RaceState; import com.sap.sailing.domain.abstractlog.race.state.racingprocedure.FlagPoleState; import com.sap.sailing.domain.abstractlog.race.state.racingprocedure.RacingProcedure; +import com.sap.sailing.domain.base.SeriesBase; +import com.sap.sailing.domain.base.racegroup.CurrentRaceComparator; +import com.sap.sailing.domain.base.racegroup.RaceGroupSeries; +import com.sap.sailing.domain.base.racegroup.RaceGroupSeriesComparator; import com.sap.sailing.domain.common.racelog.FlagPole; import com.sap.sailing.domain.common.racelog.Flags; import com.sap.sailing.racecommittee.app.AppConstants; @@ -43,15 +39,32 @@ import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util; import com.sap.sse.common.impl.MillisecondsTimePoint; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; +import android.text.TextUtils; +import android.util.Log; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + public class ManagedRaceListAdapter extends ArrayAdapter implements FilterSubscriber { private final static String TAG = ManagedRaceListAdapter.class.getName(); private final Object mLockObject = new Object(); - private List mAllViewItems; - private RaceFilter mFilter; + private final RaceFilter mFilter; private LayoutInflater mInflater; private Resources mResources; - private List mShownViewItems; + private final List mShownViewItems; + private final Map viewItemsRaces; + private final Map viewItemsSeriesHeaders; private ImageView marker; private ImageView update_badge; private LinearLayout race_flag; @@ -67,6 +80,7 @@ public class ManagedRaceListAdapter extends ArrayAdapter imple private TextView boat_class; private TextView fleet_series; private ImageView protest_image; + private final Set protestFlagsWithClickListenerSet; private ImageView has_dependent_races; private SimpleDateFormat dateFormat; private RaceListDataType mSelectedRace; @@ -75,12 +89,17 @@ public class ManagedRaceListAdapter extends ArrayAdapter imple private TextView explicit_factor; private int flag_size; private DecimalFormat factor_format; + private final Set mAllRaces; - public ManagedRaceListAdapter(Context context, List viewItems) { + public ManagedRaceListAdapter(Context context, Set allRaces) { super(context, 0); - - mAllViewItems = viewItems; - mShownViewItems = viewItems; + protestFlagsWithClickListenerSet = new HashSet<>(); + mAllRaces = allRaces; + mShownViewItems = new ArrayList<>(); + this.viewItemsRaces = new HashMap<>(); + this.viewItemsSeriesHeaders = new HashMap<>(); + createViewItemsForRacesAndSeries(); + mFilter = new RaceFilter(allRaces, this); mInflater = LayoutInflater.from(getContext()); mResources = getContext().getResources(); dateFormat = new SimpleDateFormat("kk:mm", getContext().getResources().getConfiguration().locale); @@ -88,6 +107,18 @@ public class ManagedRaceListAdapter extends ArrayAdapter imple factor_format = new DecimalFormat(context.getString(R.string.race_factor_format)); } + private void createViewItemsForRacesAndSeries() { + viewItemsRaces.clear(); + viewItemsSeriesHeaders.clear(); + for (final ManagedRace race : mAllRaces) { + viewItemsRaces.put(race, new RaceListDataTypeRace(race, mInflater)); + final SeriesBase series = race.getSeries(); + if (!viewItemsSeriesHeaders.containsKey(series)) { + viewItemsSeriesHeaders.put(series, new RaceListDataTypeHeader(new RaceGroupSeries(race), mInflater)); + } + } + } + @Override public int getCount() { synchronized (mLockObject) { @@ -97,9 +128,6 @@ public class ManagedRaceListAdapter extends ArrayAdapter imple @Override public RaceFilter getFilter() { - if (mFilter == null) { - mFilter = new RaceFilter(mAllViewItems, this); - } return mFilter; } @@ -122,18 +150,13 @@ public class ManagedRaceListAdapter extends ArrayAdapter imple @Override public View getView(int position, View convertView, ViewGroup parent) { - RaceListDataType raceListElement; - raceListElement = getItem(position); + final RaceListDataType raceListElement = getItem(position); int type = getItemViewType(position); TimePoint now = MillisecondsTimePoint.now(); if (convertView == null) { - if (type == ViewType.HEADER.index) { - convertView = mInflater.inflate(R.layout.race_list_area_header, parent, false); - } else if (type == ViewType.RACE.index) { - convertView = mInflater.inflate(R.layout.race_list_area_item, parent, false); - } + convertView = raceListElement.getView(parent); } findViews(convertView); resetValues(convertView); @@ -145,25 +168,25 @@ public class ManagedRaceListAdapter extends ArrayAdapter imple regatta = header.getRaceGroup().getName(); } boat_class.setText(regatta); - if (header.isFleetVisible()) { - fleet_series.setText(RaceHelper.getFleetSeries(header.getFleet(), header.getSeries())); - } else { - fleet_series.setText(RaceHelper.getSeriesName(header.getSeries(), "")); - } + fleet_series.setText(RaceHelper.getSeriesName(header.getSeries(), "")); if (fleet_series.getText().length() == 0) { fleet_series.setVisibility(View.GONE); } else { fleet_series.setVisibility(View.VISIBLE); } protest_image.setImageDrawable(FlagsResources.getFlagDrawable(getContext(), Flags.BRAVO.name(), flag_size)); - protest_image.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Intent intent = new Intent(AppConstants.INTENT_ACTION_SHOW_PROTEST); - intent.putExtra(AppConstants.INTENT_ACTION_EXTRA, header.toString()); - BroadcastManager.getInstance(getContext()).addIntent(intent); - } - }); + if (!protestFlagsWithClickListenerSet.contains(protest_image)) { + protestFlagsWithClickListenerSet.add(protest_image); + protest_image.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent intent = new Intent(AppConstants.INTENT_ACTION_SHOW_PROTEST); + // TODO don't use toString() to convey semantics and perform check; bug 3617 + intent.putExtra(AppConstants.INTENT_ACTION_EXTRA, new RaceGroupSeries(header.getRaceGroup(), header.getSeries()).getDisplayName()); + BroadcastManager.getInstance(getContext()).addIntent(intent); + } + }); + } } else if (type == ViewType.RACE.index) { final RaceListDataTypeRace race = (RaceListDataTypeRace) raceListElement; @@ -259,13 +282,82 @@ public class ManagedRaceListAdapter extends ArrayAdapter imple } @Override - public void onResult(List filtered) { + public void onResult(List filteredRaces) { synchronized (mLockObject) { - mShownViewItems = filtered; + mShownViewItems.clear(); + if (filteredRaces != null) { + mShownViewItems.addAll(getShownViewItems(filteredRaces)); + } notifyDataSetChanged(); } } + /** + * For the {@code filteredRaces} ensure that exactly the required view items are in {@link #mShownViewItems}. This + * encompasses the header items for all non-empty series and the race items for all races in their series. + */ + private List getShownViewItems(List races) { + final Map> raceItemsByHeader = getRaceItemsGroupedBySeriesHeader(races); + final List result = serializeItems(raceItemsByHeader); + return result; + } + + private List serializeItems(final Map> raceItemsByHeader) { + final List result = new ArrayList<>(); + final List headers = new ArrayList<>(raceItemsByHeader.keySet()); + Collections.sort(headers, new Comparator() { + private final RaceGroupSeriesComparator c = new RaceGroupSeriesComparator(); + + @Override + public int compare(RaceListDataTypeHeader lhs, RaceListDataTypeHeader rhs) { + return c.compare(lhs.getRegattaSeries(), rhs.getRegattaSeries()); + } + }); + for (final RaceListDataTypeHeader header : headers) { + result.add(header); + final List raceItems = new ArrayList<>(raceItemsByHeader.get(header)); + Collections.sort(raceItems, new Comparator() { + final CurrentRaceComparator c = new CurrentRaceComparator(); + + @Override + public int compare(RaceListDataTypeRace lhs, RaceListDataTypeRace rhs) { + final int result; + if (lhs != null && rhs != null) { + result = c.compare(lhs.getRace(), rhs.getRace()); + } else { + Log.e(TAG, "Internal error; found null for NavDrawer item while sorting"); + result = 0; + } + return result; + } + }); + for (final RaceListDataTypeRace raceItem : raceItems) { + result.add(raceItem); + } + } + return result; + } + + private Map> getRaceItemsGroupedBySeriesHeader(final List filteredRaces) { + final Map> raceItemsByHeader = new HashMap<>(); + for (final ManagedRace race : filteredRaces) { + final SeriesBase series = race.getSeries(); + final RaceListDataTypeHeader seriesHeader = viewItemsSeriesHeaders.get(series); + List raceItemsInSeries = raceItemsByHeader.get(seriesHeader); + if (raceItemsInSeries == null) { + raceItemsInSeries = new ArrayList<>(); + raceItemsByHeader.put(seriesHeader, raceItemsInSeries); + } + final RaceListDataTypeRace viewItemForRace = viewItemsRaces.get(race); + if (viewItemForRace != null) { + raceItemsInSeries.add(viewItemForRace); + } else { + Log.w(TAG, "A view item for race "+race+" provided by the filter could not be found", new RuntimeException("Here is where it happened")); + } + } + return raceItemsByHeader; + } + public void setSelectedRace(RaceListDataType id) { mSelectedRace = id; } @@ -450,4 +542,11 @@ public class ManagedRaceListAdapter extends ArrayAdapter imple this.index = index; } } + + public void onRacesChanged() { + synchronized (mLockObject) { + createViewItemsForRacesAndSeries(); + getFilter().refreshRegattaStructures(); + } + } } diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/adapters/racelist/RaceFilter.java b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/adapters/racelist/RaceFilter.java index dea47f84b2e..e32288b65fe 100644 --- a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/adapters/racelist/RaceFilter.java +++ b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/adapters/racelist/RaceFilter.java @@ -1,33 +1,46 @@ package com.sap.sailing.racecommittee.app.ui.adapters.racelist; import java.util.ArrayList; -import java.util.Collection; import java.util.List; +import java.util.Set; + +import com.sap.sailing.domain.base.racegroup.CurrentRaceFilter; +import com.sap.sailing.domain.base.racegroup.impl.CurrentRaceFilterImpl; +import com.sap.sailing.racecommittee.app.domain.ManagedRace; +import com.sap.sailing.racecommittee.app.ui.fragments.RaceListFragment.FilterMode; import android.widget.Filter; -import com.sap.sailing.domain.common.racelog.RaceLogRaceStatus; -import com.sap.sailing.racecommittee.app.ui.fragments.RaceListFragment.FilterMode; - /** - * Filters races by status. - * - * For each "run" of unscheduled races only the first will be taken. - * For each "run" of finished races only the last will be taken. + * Filters {@link ManagedRace} objects by status and place in the regatta structure. See {@link CurrentRaceFilter}. If + * the {@link #filterMode} is {@link FilterMode#ALL}, no filtering takes place at all, and all races will be displayed. + * If the {@link #filterMode} is {@link FilterMode#ACTIVE}, the set of races to be displayed is computed using a + * {@link CurrentRaceFilter}. */ public class RaceFilter extends Filter { + /** + * A filter that is updated each time an update for {@link #allRaces} is announced by {@link #refreshRegattaStructures()}. + */ + private CurrentRaceFilter currentRaceFilter; public interface FilterSubscriber { - void onResult(List filtered); + void onResult(List filtered); } - protected final FilterSubscriber subscriber; - protected Collection items; - protected FilterMode filterMode; + private final FilterSubscriber subscriber; + private final Set allRaces; + private FilterMode filterMode; - public RaceFilter(Collection items, FilterSubscriber resultSubscriber) { - this.items = items; + /** + * @param allRaces + * callers must call {@link #refreshRegattaStructures()} whenever the contents of this set have changed + * @param resultSubscriber + * triggered when the filter results have changed + */ + public RaceFilter(Set allRaces, FilterSubscriber resultSubscriber) { + this.allRaces = allRaces; this.subscriber = resultSubscriber; + refreshRegattaStructures(); } public void filterByMode(FilterMode filterMode) { @@ -35,7 +48,7 @@ public class RaceFilter extends Filter { filter(""); } - protected static FilterResults createResults(Collection result) { + protected static FilterResults createResults(Set result) { FilterResults results = new FilterResults(); results.values = new ArrayList<>(result); results.count = result.size(); @@ -44,87 +57,26 @@ public class RaceFilter extends Filter { @Override protected FilterResults performFiltering(CharSequence constraint) { + final FilterResults result; if (filterMode == null || filterMode.equals(FilterMode.ALL)) { - return createResults(items); + result = createResults(allRaces); + } else { + // filter using the CurrentRaceFilter + result = createResults(currentRaceFilter.getCurrentRaces()); } - - List filteredItems = new ArrayList(); - RaceListDataTypeRace currentUnscheduledItem = null; - RaceListDataTypeRace currentFinishedItem = null; - int finishedItems = 0; - int subItems = 0; - List headersToRemove= new ArrayList<>(); - RaceListDataTypeHeader currentHeader = null; - - List allItems = new ArrayList(items); - for (RaceListDataType item : allItems) { - if (item instanceof RaceListDataTypeHeader) { - if (currentHeader != null && subItems > 0 && subItems == finishedItems) { - headersToRemove.add(currentHeader); - } - RaceListDataTypeHeader headerItem = (RaceListDataTypeHeader) item; - filteredItems.add(headerItem); - - // new run for all types! - currentHeader = (RaceListDataTypeHeader) item; - currentUnscheduledItem = null; - currentFinishedItem = null; - finishedItems = 0; - subItems = 0; - } else if (item instanceof RaceListDataTypeRace) { - subItems ++; - RaceListDataTypeRace raceItem = (RaceListDataTypeRace) item; - RaceLogRaceStatus status = raceItem.getCurrentStatus(); - if (currentUnscheduledItem == null && status.equals(RaceLogRaceStatus.UNSCHEDULED)) { - filteredItems.add(raceItem); - currentUnscheduledItem = raceItem; - } else if (status.equals(RaceLogRaceStatus.FINISHED)) { - finishedItems ++; - if (filteredItems.contains(currentFinishedItem)) { - filteredItems.remove(currentFinishedItem); - } - filteredItems.add(raceItem); - currentFinishedItem = raceItem; - currentUnscheduledItem = null; - } else if (RaceLogRaceStatus.isActive(status)) { - filteredItems.add(raceItem); - // new run for all types! - currentUnscheduledItem = null; - currentFinishedItem = null; - } - } - } - if (currentHeader != null && subItems > 0 && subItems == finishedItems) { - headersToRemove.add(currentHeader); - } - removeFinishedSeries(filteredItems, headersToRemove); - return createResults(filteredItems); + return result; } - private void removeFinishedSeries(List filteredItems, List headersToRemove) { - List itemsToKeep = new ArrayList<>(); - boolean keepItems = false; - for (RaceListDataType item : filteredItems) { - if (item instanceof RaceListDataTypeHeader) { - keepItems = !headersToRemove.contains(item); - if (keepItems) { - itemsToKeep.add(item); - } - - } else if (item instanceof RaceListDataTypeRace) { - if (keepItems) { - itemsToKeep.add(item); - } - } - } - filteredItems.clear(); - filteredItems.addAll(itemsToKeep); - } - - @SuppressWarnings("unchecked") @Override protected void publishResults(CharSequence constraint, FilterResults results) { - subscriber.onResult((List) results.values); + @SuppressWarnings("unchecked") + final List filterResults = (List) results.values; + subscriber.onResult(filterResults); + } + + public void refreshRegattaStructures() { + this.currentRaceFilter = new CurrentRaceFilterImpl(allRaces); + filter(""); } } diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/adapters/racelist/RaceListDataType.java b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/adapters/racelist/RaceListDataType.java index 363f49d4e60..1e74c471acf 100644 --- a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/adapters/racelist/RaceListDataType.java +++ b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/adapters/racelist/RaceListDataType.java @@ -1,5 +1,7 @@ package com.sap.sailing.racecommittee.app.ui.adapters.racelist; +import android.view.View; +import android.view.ViewGroup; /* * author: I065873 @@ -7,6 +9,5 @@ package com.sap.sailing.racecommittee.app.ui.adapters.racelist; * Data Type for displaying the Race list in the left part of the second activity */ public interface RaceListDataType { - - + View getView(ViewGroup parent); } diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/adapters/racelist/RaceListDataTypeHeader.java b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/adapters/racelist/RaceListDataTypeHeader.java index bb29a5636d0..570da733ef7 100644 --- a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/adapters/racelist/RaceListDataTypeHeader.java +++ b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/adapters/racelist/RaceListDataTypeHeader.java @@ -1,25 +1,29 @@ package com.sap.sailing.racecommittee.app.ui.adapters.racelist; -import com.sap.sailing.domain.base.Fleet; import com.sap.sailing.domain.base.SeriesBase; +import com.sap.sailing.domain.base.racegroup.IsFleetFragment; import com.sap.sailing.domain.base.racegroup.RaceGroup; -import com.sap.sailing.racecommittee.app.domain.impl.RaceGroupSeriesFleet; +import com.sap.sailing.domain.base.racegroup.RaceGroupSeries; +import com.sap.sailing.racecommittee.app.R; -public class RaceListDataTypeHeader implements RaceListDataType { +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; - private RaceGroupSeriesFleet data; - private boolean isFleetVisible; +public class RaceListDataTypeHeader implements RaceListDataType, IsFleetFragment { + private RaceGroupSeries data; + private final LayoutInflater mInflater; - public RaceListDataTypeHeader(RaceGroupSeriesFleet data) { - this(data, true); + public RaceListDataTypeHeader(RaceGroupSeries data, LayoutInflater layoutInflater) { + this(data, true, layoutInflater); } - public RaceListDataTypeHeader(RaceGroupSeriesFleet data, boolean isFleetVisible) { + public RaceListDataTypeHeader(RaceGroupSeries data, boolean isFleetVisible, LayoutInflater layoutInflater) { this.data = data; - this.isFleetVisible = isFleetVisible; + this.mInflater = layoutInflater; } - public RaceGroupSeriesFleet getRegattaSeriesFleet() { + public RaceGroupSeries getRegattaSeries() { return data; } @@ -31,14 +35,14 @@ public class RaceListDataTypeHeader implements RaceListDataType { return data.getSeries(); } - public Fleet getFleet() { - return data.getFleet(); - } - - public boolean isFleetVisible() { return isFleetVisible; } - @Override public String toString() { return data.getDisplayName(); } + + @Override + public View getView(ViewGroup parent) { + return mInflater.inflate(R.layout.race_list_area_header, parent, false); + } + } diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/adapters/racelist/RaceListDataTypeRace.java b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/adapters/racelist/RaceListDataTypeRace.java index 9dd5f655638..fd298433d8e 100644 --- a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/adapters/racelist/RaceListDataTypeRace.java +++ b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/adapters/racelist/RaceListDataTypeRace.java @@ -5,11 +5,14 @@ import java.text.SimpleDateFormat; import java.util.Locale; import android.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import com.sap.sailing.domain.base.racegroup.RaceGroupSeriesFleet; import com.sap.sailing.domain.common.racelog.RaceLogRaceStatus; import com.sap.sailing.racecommittee.app.R; import com.sap.sailing.racecommittee.app.domain.ManagedRace; -import com.sap.sailing.racecommittee.app.domain.impl.RaceGroupSeriesFleet; import com.sap.sse.common.TimePoint; public class RaceListDataTypeRace implements RaceListDataType { @@ -23,10 +26,11 @@ public class RaceListDataTypeRace implements RaceListDataType { private static String finishedTemplate; private static String unknownTemplate; + private final ManagedRace race; + private final RaceGroupSeriesFleet fleet; + private final LayoutInflater mInflater; private boolean updateIndicatorVisible = false; private RaceLogRaceStatus currentStatus = RaceLogRaceStatus.UNKNOWN; - private ManagedRace race; - private RaceGroupSeriesFleet fleet; private Format scheduleFormatter = new SimpleDateFormat("HH:mm", Locale.US); @@ -41,14 +45,15 @@ public class RaceListDataTypeRace implements RaceListDataType { unknownTemplate = fragment.getString(R.string.racelist_unknown); } - public RaceListDataTypeRace(ManagedRace race) { - this(race, null); + public RaceListDataTypeRace(ManagedRace race, LayoutInflater layoutInflater) { + this(race, null, layoutInflater); } - public RaceListDataTypeRace(ManagedRace race, RaceGroupSeriesFleet fleet) { + public RaceListDataTypeRace(ManagedRace race, RaceGroupSeriesFleet fleet, LayoutInflater layoutInflater) { this.race = race; this.fleet = fleet; this.currentStatus = race.getStatus(); + this.mInflater = layoutInflater; } public void onStatusChanged(RaceLogRaceStatus status, boolean allowUpdateIndicator) { @@ -114,4 +119,9 @@ public class RaceListDataTypeRace implements RaceListDataType { return scheduleFormatter.format(startTime.asDate()); } + @Override + public View getView(ViewGroup parent) { + return mInflater.inflate(R.layout.race_list_area_item, parent, false); + } + } diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/fragments/RaceListFragment.java b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/fragments/RaceListFragment.java index 203f571fbd9..c83d61505b9 100644 --- a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/fragments/RaceListFragment.java +++ b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/fragments/RaceListFragment.java @@ -2,13 +2,38 @@ package com.sap.sailing.racecommittee.app.ui.fragments; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; +import java.util.HashSet; import java.util.LinkedHashMap; -import java.util.LinkedList; import java.util.List; -import java.util.Map; import java.util.Set; +import com.sap.sailing.android.shared.logging.ExLog; +import com.sap.sailing.android.shared.util.AppUtils; +import com.sap.sailing.android.shared.util.BitmapHelper; +import com.sap.sailing.android.shared.util.BroadcastManager; +import com.sap.sailing.domain.abstractlog.race.state.ReadonlyRaceState; +import com.sap.sailing.domain.abstractlog.race.state.impl.BaseRaceStateChangedListener; +import com.sap.sailing.domain.base.racegroup.RaceGroupSeries; +import com.sap.sailing.domain.base.racegroup.RaceGroupSeriesFleet; +import com.sap.sailing.racecommittee.app.AppConstants; +import com.sap.sailing.racecommittee.app.AppPreferences; +import com.sap.sailing.racecommittee.app.R; +import com.sap.sailing.racecommittee.app.RaceApplication; +import com.sap.sailing.racecommittee.app.data.DataManager; +import com.sap.sailing.racecommittee.app.data.ReadonlyDataManager; +import com.sap.sailing.racecommittee.app.domain.ManagedRace; +import com.sap.sailing.racecommittee.app.ui.activities.SessionActivity; +import com.sap.sailing.racecommittee.app.ui.adapters.racelist.ManagedRaceListAdapter; +import com.sap.sailing.racecommittee.app.ui.adapters.racelist.RaceListDataType; +import com.sap.sailing.racecommittee.app.ui.adapters.racelist.RaceListDataTypeRace; +import com.sap.sailing.racecommittee.app.ui.fragments.dialogs.ProtestTimeDialogFragment; +import com.sap.sailing.racecommittee.app.utils.StringHelper; +import com.sap.sailing.racecommittee.app.utils.ThemeHelper; +import com.sap.sailing.racecommittee.app.utils.TickListener; +import com.sap.sailing.racecommittee.app.utils.TickSingleton; +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.Util; + import android.app.Activity; import android.app.FragmentTransaction; import android.content.BroadcastReceiver; @@ -41,34 +66,6 @@ import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; -import com.sap.sailing.android.shared.logging.ExLog; -import com.sap.sailing.android.shared.util.AppUtils; -import com.sap.sailing.android.shared.util.BitmapHelper; -import com.sap.sailing.android.shared.util.BroadcastManager; -import com.sap.sailing.domain.abstractlog.race.state.ReadonlyRaceState; -import com.sap.sailing.domain.abstractlog.race.state.impl.BaseRaceStateChangedListener; -import com.sap.sailing.domain.base.SeriesBase; -import com.sap.sailing.racecommittee.app.AppConstants; -import com.sap.sailing.racecommittee.app.AppPreferences; -import com.sap.sailing.racecommittee.app.R; -import com.sap.sailing.racecommittee.app.RaceApplication; -import com.sap.sailing.racecommittee.app.data.DataManager; -import com.sap.sailing.racecommittee.app.data.ReadonlyDataManager; -import com.sap.sailing.racecommittee.app.domain.ManagedRace; -import com.sap.sailing.racecommittee.app.domain.impl.RaceGroupSeriesFleet; -import com.sap.sailing.racecommittee.app.ui.activities.SessionActivity; -import com.sap.sailing.racecommittee.app.ui.adapters.racelist.ManagedRaceListAdapter; -import com.sap.sailing.racecommittee.app.ui.adapters.racelist.RaceListDataType; -import com.sap.sailing.racecommittee.app.ui.adapters.racelist.RaceListDataTypeHeader; -import com.sap.sailing.racecommittee.app.ui.adapters.racelist.RaceListDataTypeRace; -import com.sap.sailing.racecommittee.app.ui.fragments.dialogs.ProtestTimeDialogFragment; -import com.sap.sailing.racecommittee.app.utils.RaceHelper; -import com.sap.sailing.racecommittee.app.utils.StringHelper; -import com.sap.sailing.racecommittee.app.utils.ThemeHelper; -import com.sap.sailing.racecommittee.app.utils.TickListener; -import com.sap.sailing.racecommittee.app.utils.TickSingleton; -import com.sap.sse.common.TimePoint; - public class RaceListFragment extends LoggableFragment implements OnItemClickListener, OnItemSelectedListener, TickListener, OnScrollListener { private final static String TAG = RaceListFragment.class.getName(); @@ -88,8 +85,9 @@ public class RaceListFragment extends LoggableFragment implements OnItemClickLis private ManagedRace mSelectedRace; private IntentReceiver mReceiver; private boolean mUpdateList = true; - private ArrayList mViewItems; private View mProgress; + private final Set mAllRaces; + private BaseRaceStateChangedListener stateListener = new BaseRaceStateChangedListener() { @Override public void onStartTimeChanged(ReadonlyRaceState state) { @@ -106,13 +104,12 @@ public class RaceListFragment extends LoggableFragment implements OnItemClickLis filterChanged(); } }; - public RaceListFragment() { mFilterMode = FilterMode.ACTIVE; mSelectedRace = null; mManagedRacesById = new LinkedHashMap<>(); mRacesByGroup = new LinkedHashMap<>(); - mViewItems = new ArrayList<>(); + mAllRaces = new HashSet<>(); } public static RaceListFragment newInstance(int layout) { @@ -125,9 +122,7 @@ public class RaceListFragment extends LoggableFragment implements OnItemClickLis public static void showProtest(Context context, ManagedRace race) { Intent intent = new Intent(AppConstants.INTENT_ACTION_SHOW_PROTEST); - String extra = race.getRaceGroup().getName(); - extra += RaceHelper.getSeriesName(race.getSeries()); - extra += RaceHelper.getFleetName(race.getFleet()); + String extra = new RaceGroupSeries(race.getRaceGroup(), race.getSeries()).getDisplayName(); intent.putExtra(AppConstants.INTENT_ACTION_EXTRA, extra); BroadcastManager.getInstance(context).addIntent(intent); } @@ -199,11 +194,9 @@ public class RaceListFragment extends LoggableFragment implements OnItemClickLis @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - RaceListDataTypeRace.initializeTemplates(this); - if (mListView != null) { - mAdapter = new ManagedRaceListAdapter(getActivity(), mViewItems); + mAdapter = new ManagedRaceListAdapter(getActivity(), mAllRaces); mListView.setAdapter(mAdapter); mListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); mListView.setOnItemClickListener(this); @@ -316,9 +309,7 @@ public class RaceListFragment extends LoggableFragment implements OnItemClickLis @Override public void onResume() { super.onResume(); - TickSingleton.INSTANCE.registerListener(this); - IntentFilter filter = new IntentFilter(); filter.addAction(AppConstants.INTENT_ACTION_SHOW_PROTEST); LocalBroadcastManager.getInstance(getActivity()).registerReceiver(mReceiver, filter); @@ -346,7 +337,6 @@ public class RaceListFragment extends LoggableFragment implements OnItemClickLis @Override public void onStart() { super.onStart(); - unregisterOnAllRaces(); registerOnAllRaces(); for (ManagedRace race : mManagedRacesById.values()) { @@ -378,154 +368,57 @@ public class RaceListFragment extends LoggableFragment implements OnItemClickLis @Override public void onDrawerClosed(View drawerView) { super.onDrawerClosed(drawerView); - mUpdateList = false; } @Override public void onDrawerOpened(View drawerView) { super.onDrawerOpened(drawerView); - mUpdateList = true; } }; mDrawerLayout.post(new Runnable() { - @Override public void run() { mDrawerToggle.syncState(); } }); mDrawerLayout.setDrawerListener(mDrawerToggle); - if (mCourse != null) { SpannableString text = new SpannableString(course); StyleSpan spanBold = new StyleSpan(Typeface.BOLD); text.setSpan(spanBold, 0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); mCourse.setText(text); } - if (mData != null) { mData.setText(StringHelper.on(getActivity()).getAuthor(author)); } } - private void initializeRacesByGroup() { - mRacesByGroup.clear(); - for (ManagedRace race : mManagedRacesById.values()) { - RaceGroupSeriesFleet container = new RaceGroupSeriesFleet(race); - - if (!mRacesByGroup.containsKey(container)) { - mRacesByGroup.put(container, new LinkedList()); - } - mRacesByGroup.get(container).add(race); - } - } - - private void initializeViewElements() { - // 1. Group races by - initializeRacesByGroup(); - - // 2. Remove previous view items - mViewItems.clear(); - - // 3. Group active fleets from tree by series - Map> seriesWithFleets = getFleetsGroupedBySeries(mRacesByGroup.keySet()); - - // 4. Create view elements from series with fleets - for (SeriesBase series : seriesWithFleets.keySet()) { - List fleets = seriesWithFleets.get(series); - if (fleets.size() == 1) { // Default fleet - // ... add the header view for default fleet... - RaceGroupSeriesFleet fleet = fleets.get(0); - mViewItems.add(new RaceListDataTypeHeader(fleet)); - List races = mRacesByGroup.get(fleet); - for (ManagedRace race : races) { - // ... and add the race view! - mViewItems.add(new RaceListDataTypeRace(race)); - } - } else if (fleets.size() > 1) { - // ... add the header view for all following fleets... - mViewItems.add(new RaceListDataTypeHeader(fleets.get(0), false)); - // ... and add all the sorted race views! - List raceListDataTypeRaces = getRaceListDataTypeRaces(mRacesByGroup, fleets); - if (fleetsEqual(fleets)) { - raceListDataTypeRaces = sortRaceList(raceListDataTypeRaces, fleets.size()); - } - for (RaceListDataTypeRace raceListDataTypeRace : raceListDataTypeRaces) { - mViewItems.add(raceListDataTypeRace); - } - } - } - } - - private boolean fleetsEqual(List fleets) { - if (!fleets.isEmpty()) { - int lastFleetOrder = fleets.get(0).getFleet().getOrdering(); - for (RaceGroupSeriesFleet fleet : fleets) { - int currentFleetOrder = fleet.getFleet().getOrdering(); - if (currentFleetOrder != lastFleetOrder) { - return false; - } - } - } - - return true; - } - - private List sortRaceList(List raceList, int fleetSize) { - int racePerFleet = raceList.size() / fleetSize; - List sortedList = new ArrayList<>(); - for (int race = 0; race < racePerFleet; race++) { - for (int fleet = 0; fleet < fleetSize; fleet++) { - sortedList.add(raceList.get(race + (fleet * racePerFleet))); - } - } - return sortedList; - } - - private static LinkedHashMap> getFleetsGroupedBySeries(Set raceGroupSeriesFleets) { - LinkedHashMap> seriesWithFleets = new LinkedHashMap<>(); - for (RaceGroupSeriesFleet key : raceGroupSeriesFleets) { - SeriesBase series = key.getSeries(); - List fleets = seriesWithFleets.get(series); - if (fleets == null) { - fleets = new ArrayList<>(); - } - fleets.add(key); - seriesWithFleets.put(series, fleets); - } - return seriesWithFleets; - } - - private static List getRaceListDataTypeRaces(LinkedHashMap> racesByGroup, - List fleets) { - List races = new ArrayList<>(); - for (RaceGroupSeriesFleet fleet : fleets) { - for (ManagedRace race : racesByGroup.get(fleet)) { - races.add(new RaceListDataTypeRace(race, fleet)); - } - } - return races; - } - - public void setupOn(Collection races) { - ExLog.i(getActivity(), TAG, String.format("Setting up %s with %d races.", this.getClass().getSimpleName(), races.size())); - + public void setupOn(Iterable races) { + ExLog.i(getActivity(), TAG, String.format("Setting up %s with %d races.", this.getClass().getSimpleName(), Util.size(races))); unregisterOnAllRaces(); - mManagedRacesById.clear(); - + mRacesByGroup.clear(); for (ManagedRace managedRace : races) { mManagedRacesById.put(managedRace.getId(), managedRace); + final RaceGroupSeriesFleet key = new RaceGroupSeriesFleet(managedRace); + List raceList = mRacesByGroup.get(key); + if (raceList == null) { + raceList = new ArrayList<>(); + mRacesByGroup.put(key, raceList); + } + raceList.add(managedRace); } + mAllRaces.clear(); + Util.addAll(races, mAllRaces); registerOnAllRaces(); - - initializeViewElements(); - // prepare views and do initial filtering - filterChanged(); + // prepare views and do initial filtering; update the adapter first, so it can create the new + // view items for the new races; then trigger the filter + mAdapter.onRacesChanged(); mAdapter.notifyDataSetChanged(); + filterChanged(); } public void openDrawer() { @@ -550,10 +443,11 @@ public class RaceListFragment extends LoggableFragment implements OnItemClickLis } } - private void showProtestTimeDialog(String raceGroupName) { + private void showProtestTimeDialog(String raceGroupSeriesDisplayName) { // Find the race group for which the for (RaceGroupSeriesFleet raceGroupSeriesFleet : mRacesByGroup.keySet()) { - Boolean matchingRaceGroup = raceGroupName.equals(raceGroupSeriesFleet.getDisplayName()); + Boolean matchingRaceGroup = raceGroupSeriesDisplayName.equals( + new RaceGroupSeries(raceGroupSeriesFleet.getRaceGroup(), raceGroupSeriesFleet.getSeries()).getDisplayName()); if (matchingRaceGroup) { List races = mRacesByGroup.get(raceGroupSeriesFleet); if (!isRaceListDirty(races)) { @@ -620,9 +514,9 @@ public class RaceListFragment extends LoggableFragment implements OnItemClickLis @Override public void onReceive(Context context, Intent intent) { if (AppConstants.INTENT_ACTION_SHOW_PROTEST.equals(intent.getAction())) { - String raceGroupName = intent.getExtras().getString(AppConstants.INTENT_ACTION_EXTRA); - if (raceGroupName != null) { - showProtestTimeDialog(raceGroupName); + String raceGroupSeriesDisplayName = intent.getExtras().getString(AppConstants.INTENT_ACTION_EXTRA); + if (raceGroupSeriesDisplayName != null) { + showProtestTimeDialog(raceGroupSeriesDisplayName); mDrawerLayout.closeDrawers(); } else { ExLog.e(getActivity(), TAG, "INTENT_ACTION_SHOW_PROTEST does not carry an INTENT_ACTION_EXTRA with the race group name!"); diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/fragments/dialogs/ProtestTimeDialogFragment.java b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/fragments/dialogs/ProtestTimeDialogFragment.java index 321bc293daa..55900bd2398 100644 --- a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/fragments/dialogs/ProtestTimeDialogFragment.java +++ b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/fragments/dialogs/ProtestTimeDialogFragment.java @@ -27,13 +27,13 @@ import com.sap.sailing.android.shared.util.BroadcastManager; import com.sap.sailing.android.shared.util.ScreenHelper; import com.sap.sailing.android.shared.util.ViewHelper; import com.sap.sailing.domain.abstractlog.race.analyzing.impl.FinishedTimeFinder; +import com.sap.sailing.domain.base.racegroup.RaceGroupSeriesFleet; import com.sap.sailing.domain.common.racelog.RaceLogRaceStatus; import com.sap.sailing.racecommittee.app.AppConstants; import com.sap.sailing.racecommittee.app.R; import com.sap.sailing.racecommittee.app.data.DataManager; import com.sap.sailing.racecommittee.app.data.ReadonlyDataManager; import com.sap.sailing.racecommittee.app.domain.ManagedRace; -import com.sap.sailing.racecommittee.app.domain.impl.RaceGroupSeriesFleet; import com.sap.sailing.racecommittee.app.utils.ThemeHelper; import com.sap.sailing.racecommittee.app.utils.TimeUtils; import com.sap.sse.common.TimePoint; @@ -381,7 +381,7 @@ public class ProtestTimeDialogFragment extends AttachedDialogFragment implements @Override public String toString() { - return String.format("%s - %s", group.getDisplayName(true), race.getRaceName()); + return String.format("%s - %s", group.getDisplayName(true), race.getRaceColumnName()); } } } \ No newline at end of file diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/fragments/raceinfo/StartTimeFragment.java b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/fragments/raceinfo/StartTimeFragment.java index 04bae87c114..897b5191a65 100755 --- a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/fragments/raceinfo/StartTimeFragment.java +++ b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/fragments/raceinfo/StartTimeFragment.java @@ -33,6 +33,7 @@ import com.sap.sailing.domain.abstractlog.race.impl.SimpleRaceLogIdentifierImpl; import com.sap.sailing.domain.abstractlog.race.state.RaceStateChangedListener; import com.sap.sailing.domain.abstractlog.race.state.ReadonlyRaceState; import com.sap.sailing.domain.abstractlog.race.state.impl.BaseRaceStateChangedListener; +import com.sap.sailing.domain.base.racegroup.RaceGroupSeriesFleet; import com.sap.sailing.domain.common.LeaderboardNameConstants; import com.sap.sailing.domain.common.racelog.RaceLogRaceStatus; import com.sap.sailing.domain.common.racelog.RacingProcedureType; @@ -42,7 +43,6 @@ import com.sap.sailing.racecommittee.app.R; import com.sap.sailing.racecommittee.app.data.DataStore; import com.sap.sailing.racecommittee.app.data.OnlineDataManager; import com.sap.sailing.racecommittee.app.domain.ManagedRace; -import com.sap.sailing.racecommittee.app.domain.impl.RaceGroupSeriesFleet; import com.sap.sailing.racecommittee.app.ui.adapters.DependentRaceSpinnerAdapter; import com.sap.sailing.racecommittee.app.ui.fragments.RaceFragment; import com.sap.sailing.racecommittee.app.utils.ThemeHelper; @@ -428,7 +428,7 @@ public class StartTimeFragment extends BaseFragment if (races.getFleet().getName().equals(fleet.getA())) { for (ManagedRace race : mGroupHeaders.get(races)) { if (!getRace().equals(race) && race.getStatus() != RaceLogRaceStatus.FINISHED) { - Util.Pair data = new Util.Pair<>(race.getRaceName(), null); + Util.Pair data = new Util.Pair<>(race.getRaceColumnName(), null); int position = mRaceAdapter.add(data); if (position >= 0) { if (mRaceId != null) { diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/fragments/raceinfo/WindFragment.java b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/fragments/raceinfo/WindFragment.java index d6a01771273..75b62f836df 100644 --- a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/fragments/raceinfo/WindFragment.java +++ b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/fragments/raceinfo/WindFragment.java @@ -39,6 +39,7 @@ import com.google.android.gms.location.LocationServices; import com.sap.sailing.android.shared.logging.ExLog; import com.sap.sailing.android.shared.util.AppUtils; import com.sap.sailing.android.shared.util.ViewHelper; +import com.sap.sailing.domain.base.racegroup.RaceGroupSeriesFleet; import com.sap.sailing.domain.common.Bearing; import com.sap.sailing.domain.common.Position; import com.sap.sailing.domain.common.SpeedWithBearing; @@ -50,7 +51,6 @@ import com.sap.sailing.domain.common.impl.WindImpl; import com.sap.sailing.racecommittee.app.AppConstants; import com.sap.sailing.racecommittee.app.R; import com.sap.sailing.racecommittee.app.domain.ManagedRace; -import com.sap.sailing.racecommittee.app.domain.impl.RaceGroupSeriesFleet; import com.sap.sailing.racecommittee.app.ui.activities.RacingActivity; import com.sap.sailing.racecommittee.app.ui.utils.OnRaceUpdatedListener; import com.sap.sailing.racecommittee.app.ui.views.AccuracyView; diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/utils/RaceHelper.java b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/utils/RaceHelper.java old mode 100644 new mode 100755 index dd9090dabf6..2b0129ccf28 --- a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/utils/RaceHelper.java +++ b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/utils/RaceHelper.java @@ -15,12 +15,12 @@ import com.sap.sailing.domain.abstractlog.race.state.racingprocedure.gate.GateSt import com.sap.sailing.domain.base.Fleet; import com.sap.sailing.domain.base.SeriesBase; import com.sap.sailing.domain.base.racegroup.RaceGroup; +import com.sap.sailing.domain.base.racegroup.RaceGroupSeriesFleet; import com.sap.sailing.domain.common.LeaderboardNameConstants; import com.sap.sailing.domain.common.racelog.RaceLogRaceStatus; import com.sap.sailing.racecommittee.app.AppPreferences; import com.sap.sailing.racecommittee.app.R; import com.sap.sailing.racecommittee.app.domain.ManagedRace; -import com.sap.sailing.racecommittee.app.domain.impl.RaceGroupSeriesFleet; import com.sap.sailing.racecommittee.app.ui.fragments.raceinfo.GateStartTimingFragment; public class RaceHelper { @@ -36,7 +36,7 @@ public class RaceHelper { raceName = getRaceGroupName(race); raceName += getSeriesName(race.getSeries(), delimiter); raceName += getFleetName(race.getFleet(), delimiter); - raceName += delimiter + race.getRaceName(); + raceName += delimiter + race.getRaceColumnName(); } return raceName; @@ -51,7 +51,7 @@ public class RaceHelper { String raceName = ""; if (race != null) { - raceName += race.getRaceName(); + raceName += race.getRaceColumnName(); raceName += getFleetName(race.getFleet(), delimiter); raceName += getSeriesName(race.getSeries(), delimiter); raceName += delimiter + getRaceGroupName(race); @@ -69,7 +69,7 @@ public class RaceHelper { String raceName = ""; if (race != null) { - raceName += race.getRaceName(); + raceName += race.getRaceColumnName(); raceName += getFleetName(race.getFleet(), delimiter); } @@ -81,7 +81,7 @@ public class RaceHelper { String raceName = ""; if (race != null) { int maxElements = 3; - raceName += race.getRaceName(); + raceName += race.getRaceColumnName(); String groupName = getRaceGroupName(race); String seriesName = getSeriesName(race.getSeries(), delimiter); String fleetName = getFleetName(race.getFleet(), delimiter); @@ -246,7 +246,7 @@ public class RaceHelper { public static SimpleRaceLogIdentifier getSimpleRaceLogIdentifier(@Nullable ManagedRace race) { SimpleRaceLogIdentifier identifier = null; if (race != null) { - identifier = new SimpleRaceLogIdentifierImpl(race.getRaceGroup().getName(), race.getRaceName(), race.getFleet().getName()); + identifier = new SimpleRaceLogIdentifierImpl(race.getRaceGroup().getName(), race.getRaceColumnName(), race.getFleet().getName()); } return identifier; } diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/utils/WindHelper.java b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/utils/WindHelper.java index 268121450cd..b31f8bdbf77 100644 --- a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/utils/WindHelper.java +++ b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/utils/WindHelper.java @@ -85,7 +85,7 @@ public class WindHelper { String raceName = raceState.getString("raceName"); boolean matchingLeaderboardName = leaderboardName != null && leaderboardName.equals(triple.getA()); boolean matchingFleetName = fleetName != null && fleetName.equals(triple.getC()); - boolean matchingRaceName = raceName != null && raceName.equals(race.getRaceName()); + boolean matchingRaceName = raceName != null && raceName.equals(race.getRaceColumnName()); if (matchingLeaderboardName && matchingFleetName && matchingRaceName){ if (raceState.getBoolean("trackedRaceLinked")){ isTracked = true; diff --git a/mobile/com.sap.sailing.racecommittee.app/tests-jvm/src/com/sap/sailing/racecommittee/app/domain/impl/SimpleRaceIdentifierEscapingTest.java b/mobile/com.sap.sailing.racecommittee.app/tests-jvm/src/com/sap/sailing/racecommittee/app/domain/impl/SimpleRaceIdentifierEscapingTest.java index d74114461d8..be50ab207a4 100755 --- a/mobile/com.sap.sailing.racecommittee.app/tests-jvm/src/com/sap/sailing/racecommittee/app/domain/impl/SimpleRaceIdentifierEscapingTest.java +++ b/mobile/com.sap.sailing.racecommittee.app/tests-jvm/src/com/sap/sailing/racecommittee/app/domain/impl/SimpleRaceIdentifierEscapingTest.java @@ -7,8 +7,8 @@ import static org.mockito.Mockito.when; import org.junit.Test; import com.sap.sailing.domain.base.Fleet; -import com.sap.sailing.domain.base.SeriesBase; import com.sap.sailing.domain.base.racegroup.RaceGroup; +import com.sap.sailing.domain.base.racegroup.SeriesWithRows; import com.sap.sailing.domain.base.racegroup.impl.RaceGroupImpl; import com.sap.sailing.racecommittee.app.domain.ManagedRaceIdentifier; import com.sap.sse.common.Util.Triple; @@ -43,7 +43,7 @@ public class SimpleRaceIdentifierEscapingTest { private String build(final String leaderboardName, final String seriesName, final String fleetName, final String raceColumnName) { Fleet fleet = mock(Fleet.class); - SeriesBase series = mock(SeriesBase.class); + SeriesWithRows series = mock(SeriesWithRows.class); RaceGroup raceGroup = mock(RaceGroupImpl.class); when(fleet.getName()).thenReturn(fleetName); diff --git a/wiki/kieler-woche-2015.md b/wiki/kieler-woche-2015.md index dbd26340daa..466d27c74ec 100644 --- a/wiki/kieler-woche-2015.md +++ b/wiki/kieler-woche-2015.md @@ -46,7 +46,7 @@ Whichever master is current, we map the main Apache URL http://kielerwoche2015-m - http://club.tractrac.com/events/event_20150625_KielerWoch/jsonservice.php for the Silbernes Band ## Replication -- RabbitMQ server for replication: 52.16.112.6 (Web: http://52.16.112.6:15672/) or internal host name rabbit.internal.sapsailing.com) +- RabbitMQ server for replication: rabbit.sapsailing.com (Web: http://rabbit.sapsailing.com:15672/) or internal host name rabbit.internal.sapsailing.com) - Replication Exchange Name: KW2015 ## Switching from one Master to Another @@ -64,7 +64,7 @@ Whichever master is current, we map the main Apache URL http://kielerwoche2015-m - upgrade the replicas (including the Cube Replica) to the new master if the new master runs a different release than the old master, using `refreshInstance.sh` - adjust the replicas' (including the Cube Replica) `env.sh` to point to the new master with their `REPLICATE_MASTER_SERVLET_HOST` setting - restart the Java server instance on the replicas (including the Cube Replica), using the usual `./stop; ./start` sequence. This will automatically re-start the replication - - when the replicas have finished their initial load (can be verified by looking at the RabbitMQ queues at http://ec2-52-16-112-6.eu-west-1.compute.amazonaws.com:15672/#/queues which should no longer have any "initialLoad" queues), add them again to the load balancer + - when the replicas have finished their initial load (can be verified by looking at the RabbitMQ queues at http://rabbit.sapsailing.com:15672/#/queues which should no longer have any "initialLoad" queues), add them again to the load balancer - in the Cube server's Apache configuration let http://kielerwoche2015.sapsailing.com point again to the local replica by editing `/etc/apache2/sites-enabled/001-events.conf`, then as user `root` run `service httpd reload` - shut down the other master server to avoid confusion with database writes @@ -75,4 +75,4 @@ Whichever master is current, we map the main Apache URL http://kielerwoche2015-m ## Local network setup basically see [[Set up local network with replication server|wiki/event-network-with-replica]] operating with a Meraki and a local replica server using 1 physical WAN interface on the Meraki. Meraki network config for this event can be found under [[Meraki Dashboard|https://n142.meraki.com/KiWo-TraWo/n/Rzu9tdoc]] -- Accessing replica server from `sailing-demo.sapsailing.com` via ssh or http from `ADMIN` network \ No newline at end of file +- Accessing replica server from `sailing-demo.sapsailing.com` via ssh or http from `ADMIN` network