mirror of
https://github.com/eclipse-sailing-analytics/sailing-analytics.git
synced 2026-09-17 11:19:15 +00:00
Merge remote-tracking branch 'github/main' into main
This commit is contained in:
@@ -1,400 +0,0 @@
|
||||
# LeaderboardPanel Selection State Refactoring Analysis
|
||||
|
||||
## Current Architecture
|
||||
|
||||
### Three Selection Models in Play
|
||||
|
||||
1. **`leaderboardSelectionModel`** (LeaderboardPanel:239)
|
||||
- Type: `MultiSelectionModel<LeaderboardRowDTO>`
|
||||
- Created at line 527
|
||||
- Attached to the CellTable at line 547
|
||||
- Has a `selectionChangeHandler` that syncs TO CompetitorSelectionProvider (lines 530-544)
|
||||
|
||||
2. **`SelectionCheckboxColumn.selectionModel`** (SelectionCheckboxColumn:51)
|
||||
- Type: `RefreshableMultiSelectionModel<T>`
|
||||
- Created by SelectionCheckboxColumn constructor
|
||||
- **Currently NOT used by the LeaderboardPanel's CellTable**
|
||||
- Has display.flush() callbacks for UI updates
|
||||
|
||||
3. **`CompetitorSelectionProvider`**
|
||||
- Application-level selection state
|
||||
- Both LeaderboardPanel and SelectionCheckboxColumn listen to it
|
||||
- Notifies listeners via `addedToSelection()` / `removedFromSelection()`
|
||||
|
||||
### Current Selection Flow Problems
|
||||
|
||||
#### Flow 1: User clicks checkbox in UI
|
||||
```
|
||||
User clicks checkbox
|
||||
↓
|
||||
SelectionCheckboxColumn handles click event
|
||||
↓
|
||||
leaderboardSelectionModel.setSelected() called ← Uses WRONG selection model!
|
||||
↓
|
||||
selectionChangeHandler fires (lines 530-544)
|
||||
↓
|
||||
competitorSelectionProvider.setSelection() called
|
||||
↓
|
||||
CompetitorSelectionProvider fires addedToSelection/removedFromSelection
|
||||
↓
|
||||
BOTH LeaderboardPanel AND SelectionCheckboxColumn receive callback:
|
||||
- LeaderboardPanel.addedToSelection() (line 3393)
|
||||
→ calls leaderboardSelectionModel.setSelected()
|
||||
- SelectionCheckboxColumn.addedToSelection() (line 2160)
|
||||
→ calls getSelectionModel().setSelected() ← Different model!
|
||||
```
|
||||
|
||||
**Problem**: Two selection models exist but only one (leaderboardSelectionModel) is attached to the table. The SelectionCheckboxColumn's RefreshableMultiSelectionModel is created but orphaned.
|
||||
|
||||
#### Flow 2: CompetitorSelectionProvider changes externally
|
||||
```
|
||||
External change to CompetitorSelectionProvider
|
||||
↓
|
||||
Fires addedToSelection/removedFromSelection
|
||||
↓
|
||||
BOTH listeners receive callback:
|
||||
- LeaderboardPanel.addedToSelection() (line 3393)
|
||||
→ leaderboardSelectionModel.setSelected(row, true)
|
||||
→ Could trigger selectionChangeHandler → infinite loop potential!
|
||||
- SelectionCheckboxColumn.addedToSelection() (line 2160)
|
||||
→ getSelectionModel().setSelected(row, true) ← Orphaned model!
|
||||
```
|
||||
|
||||
**Problem**: LeaderboardPanel guards against recursion by temporarily removing the handler (line 2712), but this is complex. SelectionCheckboxColumn updates an unused selection model.
|
||||
|
||||
### Inconsistencies Found
|
||||
|
||||
1. **Line 527**: LeaderboardPanel creates its own `MultiSelectionModel`
|
||||
2. **Line 547**: Attaches that model to the table, ignoring SelectionCheckboxColumn's model
|
||||
3. **Line 2163/2174**: SelectionCheckboxColumn callbacks update its own orphaned model
|
||||
4. **Line 3396/3404**: LeaderboardPanel callbacks update the table's model
|
||||
5. **SelectionCheckboxColumn.getValue()** (line 2140): Checks CompetitorSelectionProvider directly, NOT its own selection model!
|
||||
|
||||
## Proposed Refactoring
|
||||
|
||||
### Goal
|
||||
Use **ONLY** the `RefreshableMultiSelectionModel` from SelectionCheckboxColumn as the single source of truth, synchronized bidirectionally with CompetitorSelectionProvider.
|
||||
|
||||
### Step-by-Step Plan
|
||||
|
||||
#### Step 1: Use SelectionCheckboxColumn's Model as Table's Model
|
||||
|
||||
**File**: `LeaderboardPanel.java`
|
||||
|
||||
**Current code** (lines 525-547):
|
||||
```java
|
||||
selectionCheckboxColumn = new LeaderboardSelectionCheckboxColumn(competitorSelectionProvider);
|
||||
leaderboardTable.setWidth("100%");
|
||||
leaderboardSelectionModel = new MultiSelectionModel<LeaderboardRowDTO>(); // ← REMOVE THIS
|
||||
selectionChangeHandler = new Handler() {
|
||||
@Override
|
||||
public void onSelectionChange(SelectionChangeEvent event) {
|
||||
List<CompetitorDTO> selection = new ArrayList<>();
|
||||
for (LeaderboardRowDTO row : getSelectedRows()) {
|
||||
selection.add(row.competitor);
|
||||
}
|
||||
LeaderboardPanel.this.competitorSelectionProvider.setSelection(selection,
|
||||
/* listenersNotToNotify */LeaderboardPanel.this);
|
||||
if (blurInOnSelectionChanged > 0) {
|
||||
blurInOnSelectionChanged--;
|
||||
blurFocusedElementAfterSelectionChange();
|
||||
}
|
||||
}
|
||||
};
|
||||
leaderboardAsTableSelectionModelRegistration = leaderboardSelectionModel
|
||||
.addSelectionChangeHandler(selectionChangeHandler);
|
||||
leaderboardTable.setSelectionModel(leaderboardSelectionModel, selectionCheckboxColumn.getSelectionManager());
|
||||
```
|
||||
|
||||
**Refactored**:
|
||||
```java
|
||||
selectionCheckboxColumn = new LeaderboardSelectionCheckboxColumn(competitorSelectionProvider);
|
||||
leaderboardTable.setWidth("100%");
|
||||
|
||||
// Use the SelectionCheckboxColumn's model as THE selection model
|
||||
leaderboardSelectionModel = selectionCheckboxColumn.getSelectionModel();
|
||||
|
||||
selectionChangeHandler = new Handler() {
|
||||
@Override
|
||||
public void onSelectionChange(SelectionChangeEvent event) {
|
||||
if (updatingSelectionFromProvider) {
|
||||
// Guard: don't sync back to provider if we're currently syncing FROM provider
|
||||
return;
|
||||
}
|
||||
List<CompetitorDTO> selection = new ArrayList<>();
|
||||
for (LeaderboardRowDTO row : getSelectedRows()) {
|
||||
selection.add(row.competitor);
|
||||
}
|
||||
LeaderboardPanel.this.competitorSelectionProvider.setSelection(selection,
|
||||
/* listenersNotToNotify */LeaderboardPanel.this);
|
||||
if (blurInOnSelectionChanged > 0) {
|
||||
blurInOnSelectionChanged--;
|
||||
blurFocusedElementAfterSelectionChange();
|
||||
}
|
||||
}
|
||||
};
|
||||
leaderboardAsTableSelectionModelRegistration = leaderboardSelectionModel
|
||||
.addSelectionChangeHandler(selectionChangeHandler);
|
||||
leaderboardTable.setSelectionModel(leaderboardSelectionModel, selectionCheckboxColumn.getSelectionManager());
|
||||
```
|
||||
|
||||
**Changes**:
|
||||
- Line 527: Change from `new MultiSelectionModel<>()` to `selectionCheckboxColumn.getSelectionModel()`
|
||||
- Add guard flag `updatingSelectionFromProvider` to prevent recursion
|
||||
- Declare field: `private boolean updatingSelectionFromProvider = false;`
|
||||
|
||||
#### Step 2: Update Field Declaration
|
||||
|
||||
**Current** (line 239):
|
||||
```java
|
||||
private final MultiSelectionModel<LeaderboardRowDTO> leaderboardSelectionModel;
|
||||
```
|
||||
|
||||
**Refactored**:
|
||||
```java
|
||||
private final RefreshableMultiSelectionModel<LeaderboardRowDTO> leaderboardSelectionModel;
|
||||
```
|
||||
|
||||
Add import:
|
||||
```java
|
||||
import com.sap.sse.gwt.client.celltable.RefreshableMultiSelectionModel;
|
||||
```
|
||||
|
||||
#### Step 3: Fix LeaderboardPanel's Selection Callbacks
|
||||
|
||||
**Current** (lines 3393-3406):
|
||||
```java
|
||||
@Override
|
||||
public void addedToSelection(CompetitorDTO competitor) {
|
||||
LeaderboardRowDTO row = getRow(competitor.getIdAsString());
|
||||
if (row != null) {
|
||||
leaderboardSelectionModel.setSelected(row, true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removedFromSelection(CompetitorDTO competitor) {
|
||||
LeaderboardRowDTO row = getRow(competitor.getIdAsString());
|
||||
if (row != null) {
|
||||
leaderboardSelectionModel.setSelected(row, false);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Refactored**:
|
||||
```java
|
||||
@Override
|
||||
public void addedToSelection(CompetitorDTO competitor) {
|
||||
LeaderboardRowDTO row = getRow(competitor.getIdAsString());
|
||||
if (row != null) {
|
||||
updatingSelectionFromProvider = true;
|
||||
try {
|
||||
leaderboardSelectionModel.setSelected(row, true);
|
||||
} finally {
|
||||
updatingSelectionFromProvider = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removedFromSelection(CompetitorDTO competitor) {
|
||||
LeaderboardRowDTO row = getRow(competitor.getIdAsString());
|
||||
if (row != null) {
|
||||
updatingSelectionFromProvider = true;
|
||||
try {
|
||||
leaderboardSelectionModel.setSelected(row, false);
|
||||
} finally {
|
||||
updatingSelectionFromProvider = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Changes**:
|
||||
- Set guard flag before updating selection model
|
||||
- Ensures selectionChangeHandler won't sync back to CompetitorSelectionProvider
|
||||
|
||||
#### Step 4: Remove SelectionCheckboxColumn's Redundant Listener
|
||||
|
||||
**File**: `LeaderboardPanel.java`, inner class `LeaderboardSelectionCheckboxColumn`
|
||||
|
||||
**Current** (lines 2120-2136):
|
||||
```java
|
||||
protected LeaderboardSelectionCheckboxColumn(final CompetitorSelectionProvider competitorSelectionProvider) {
|
||||
super(style.getTableresources().cellTableStyle().cellTableCheckboxSelected(),
|
||||
style.getTableresources().cellTableStyle().cellTableCheckboxDeselected(),
|
||||
style.getTableresources().cellTableStyle().cellTableCheckboxColumnCell(),
|
||||
new EntityIdentityComparator<LeaderboardRowDTO>() {
|
||||
@Override
|
||||
public boolean representSameEntity(LeaderboardRowDTO dto1, LeaderboardRowDTO dto2) {
|
||||
return dto1.competitor.getIdAsString().equals(dto2.competitor.getIdAsString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode(LeaderboardRowDTO t) {
|
||||
return t.competitor.getIdAsString().hashCode();
|
||||
}
|
||||
}, getData(), leaderboardTable);
|
||||
competitorSelectionProvider.addCompetitorSelectionChangeListener(this); // ← REMOVE THIS
|
||||
}
|
||||
```
|
||||
|
||||
**Refactored**:
|
||||
```java
|
||||
protected LeaderboardSelectionCheckboxColumn(final CompetitorSelectionProvider competitorSelectionProvider) {
|
||||
super(style.getTableresources().cellTableStyle().cellTableCheckboxSelected(),
|
||||
style.getTableresources().cellTableStyle().cellTableCheckboxDeselected(),
|
||||
style.getTableresources().cellTableStyle().cellTableCheckboxColumnCell(),
|
||||
new EntityIdentityComparator<LeaderboardRowDTO>() {
|
||||
@Override
|
||||
public boolean representSameEntity(LeaderboardRowDTO dto1, LeaderboardRowDTO dto2) {
|
||||
return dto1.competitor.getIdAsString().equals(dto2.competitor.getIdAsString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode(LeaderboardRowDTO t) {
|
||||
return t.competitor.getIdAsString().hashCode();
|
||||
}
|
||||
}, getData(), leaderboardTable);
|
||||
// REMOVED: competitorSelectionProvider.addCompetitorSelectionChangeListener(this);
|
||||
// LeaderboardPanel is now the ONLY listener that syncs selection state
|
||||
}
|
||||
```
|
||||
|
||||
**Current** (lines 2160-2176, now unnecessary):
|
||||
```java
|
||||
@Override
|
||||
public void addedToSelection(CompetitorDTO competitor) {
|
||||
final LeaderboardRowDTO row = getRow(competitor.getIdAsString());
|
||||
if (row != null) {
|
||||
getSelectionModel().setSelected(row, true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removedFromSelection(CompetitorDTO competitor) {
|
||||
final LeaderboardRowDTO row = getRow(competitor.getIdAsString());
|
||||
if (row != null) {
|
||||
getSelectionModel().setSelected(row, false);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Refactored**: Remove these methods entirely. The SelectionCheckboxColumn no longer needs to implement `CompetitorSelectionChangeListener`.
|
||||
|
||||
**Also remove** (line 2119):
|
||||
```java
|
||||
// FROM:
|
||||
private class LeaderboardSelectionCheckboxColumn
|
||||
extends com.sap.sse.gwt.client.celltable.SelectionCheckboxColumn<LeaderboardRowDTO>
|
||||
implements CompetitorSelectionChangeListener {
|
||||
|
||||
// TO:
|
||||
private class LeaderboardSelectionCheckboxColumn
|
||||
extends com.sap.sse.gwt.client.celltable.SelectionCheckboxColumn<LeaderboardRowDTO> {
|
||||
```
|
||||
|
||||
Remove the empty listener method implementations at lines 2144-2154.
|
||||
|
||||
#### Step 5: Simplify updateSelection()
|
||||
|
||||
**Current** (lines 2707-2721):
|
||||
```java
|
||||
private void updateSelection(LeaderboardRowDTO row) {
|
||||
final boolean shallBeSelected = competitorSelectionProvider.isSelected(row.competitor);
|
||||
if (leaderboardAsTableSelectionModelRegistration != null) {
|
||||
// suspend selection events while actively adjusting the leaderboardSelectionModel to match the
|
||||
// competitorSelectionProvider
|
||||
leaderboardAsTableSelectionModelRegistration.removeHandler();
|
||||
leaderboardAsTableSelectionModelRegistration = null;
|
||||
}
|
||||
if (leaderboardSelectionModel.isSelected(row) != shallBeSelected) {
|
||||
leaderboardSelectionModel.setSelected(row, shallBeSelected);
|
||||
}
|
||||
// register the selection change handler again
|
||||
leaderboardAsTableSelectionModelRegistration = leaderboardTable.getSelectionModel()
|
||||
.addSelectionChangeHandler(selectionChangeHandler);
|
||||
}
|
||||
```
|
||||
|
||||
**Refactored**:
|
||||
```java
|
||||
private void updateSelection(LeaderboardRowDTO row) {
|
||||
final boolean shallBeSelected = competitorSelectionProvider.isSelected(row.competitor);
|
||||
updatingSelectionFromProvider = true;
|
||||
try {
|
||||
if (leaderboardSelectionModel.isSelected(row) != shallBeSelected) {
|
||||
leaderboardSelectionModel.setSelected(row, shallBeSelected);
|
||||
}
|
||||
} finally {
|
||||
updatingSelectionFromProvider = false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Changes**:
|
||||
- Removed handler removal/re-registration complexity
|
||||
- Use guard flag instead
|
||||
- Much simpler and clearer
|
||||
|
||||
#### Step 6: Fix getValue() in LeaderboardSelectionCheckboxColumn
|
||||
|
||||
**Current** (lines 2139-2141):
|
||||
```java
|
||||
@Override
|
||||
public Boolean getValue(LeaderboardRowDTO row) {
|
||||
return competitorSelectionProvider.isSelected(row.competitor);
|
||||
}
|
||||
```
|
||||
|
||||
**Refactored**:
|
||||
```java
|
||||
@Override
|
||||
public Boolean getValue(LeaderboardRowDTO row) {
|
||||
// Use the selection model as the source of truth, not CompetitorSelectionProvider
|
||||
return getSelectionModel().isSelected(row);
|
||||
}
|
||||
```
|
||||
|
||||
**Rationale**: The selection model should be the single source of truth for rendering. It stays synchronized with CompetitorSelectionProvider via the LeaderboardPanel's listeners.
|
||||
|
||||
### Summary of Changes
|
||||
|
||||
1. **LeaderboardPanel.java line 527**: Change to use `selectionCheckboxColumn.getSelectionModel()`
|
||||
2. **LeaderboardPanel.java line 239**: Change type to `RefreshableMultiSelectionModel`
|
||||
3. **LeaderboardPanel.java**: Add field `private boolean updatingSelectionFromProvider = false;`
|
||||
4. **LeaderboardPanel.java lines 530-544**: Add guard check in selectionChangeHandler
|
||||
5. **LeaderboardPanel.java lines 3393-3406**: Add guard flag around setSelected calls
|
||||
6. **LeaderboardPanel.java lines 2707-2721**: Simplify updateSelection() using guard flag
|
||||
7. **LeaderboardPanel.LeaderboardSelectionCheckboxColumn line 2135**: Remove listener registration
|
||||
8. **LeaderboardPanel.LeaderboardSelectionCheckboxColumn line 2119**: Remove `implements CompetitorSelectionChangeListener`
|
||||
9. **LeaderboardPanel.LeaderboardSelectionCheckboxColumn lines 2144-2176**: Remove listener methods
|
||||
10. **LeaderboardPanel.LeaderboardSelectionCheckboxColumn line 2140**: Use selection model instead of provider
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Single Selection Model**: RefreshableMultiSelectionModel is the only selection model
|
||||
2. **No Orphaned State**: Eliminates the unused selection model in SelectionCheckboxColumn
|
||||
3. **Simpler Synchronization**: Single bidirectional sync point between model and provider
|
||||
4. **No Handler Removal**: Guard flag is simpler than removing/re-adding handlers
|
||||
5. **Clearer Ownership**: LeaderboardPanel owns synchronization, SelectionCheckboxColumn just renders
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
1. Test user clicking checkboxes - selection should sync to CompetitorSelectionProvider
|
||||
2. Test external CompetitorSelectionProvider changes - should update checkboxes
|
||||
3. Test rapid clicking - no infinite loops or race conditions
|
||||
4. Test with filtering - filtered rows should maintain correct selection state
|
||||
5. Test leaderboard updates - selection should persist across data refreshes
|
||||
|
||||
## Potential Risks
|
||||
|
||||
1. **GWT Selection Model Flush Timing**: GWT selection models flush changes at end of event loop. The guard flag approach should handle this correctly since the flag is set synchronously.
|
||||
|
||||
2. **Multiple LeaderboardPanels**: If multiple LeaderboardPanels share the same CompetitorSelectionProvider, each will receive callbacks. This should work fine as each has its own guard flag.
|
||||
|
||||
3. **RefreshableMultiSelectionModel.refreshSelectionModel()**: This method has its own `dontCheckSelectionState` guard (RefreshableMultiSelectionModel.java line 131). Verify this doesn't interfere with our guard flag.
|
||||
|
||||
## Future Improvements
|
||||
|
||||
Consider enhancing RefreshableMultiSelectionModel to accept a callback for selection changes, eliminating the need for the external SelectionChangeHandler and making the synchronization more encapsulated within the model itself.
|
||||
+1
-1
@@ -2107,7 +2107,7 @@ public class MongoObjectFactoryImpl implements MongoObjectFactory {
|
||||
final Document fingerprintDoc = Document.parse(fingerprintjson.toString());
|
||||
result.put(FieldNames.MANEUVER_FINGERPRINT.name(), fingerprintDoc);
|
||||
storeRaceIdentifier(result, raceIdentifier);
|
||||
final List<Document> maneuverDoc = storeManeuvers( maneuvers , raceIdentifier, course);
|
||||
final List<Document> maneuverDoc = storeManeuvers(maneuvers , raceIdentifier, course);
|
||||
result.put(FieldNames.MANEUVERS.name(), maneuverDoc);
|
||||
maneuverCollection.replaceOne(query, result, new ReplaceOptions().upsert(true));
|
||||
}
|
||||
|
||||
+4
-4
@@ -39,10 +39,10 @@ public class GPSFixStoreListenerTest extends AbstractGPSFixStoreTest {
|
||||
Thread thread = new Thread() {
|
||||
public void run() {
|
||||
try {
|
||||
barrier.await(100, TimeUnit.MILLISECONDS);
|
||||
barrier.await(1000, TimeUnit.MILLISECONDS);
|
||||
// During iteration in the main thread this causes a modification that makes the iterator throw a
|
||||
// ConcurrentModificationException on next()
|
||||
store.addListener((DeviceIdentifier device, GPSFixMoving fix, boolean returnManeuverChanges, boolean returnLiveDelay) -> {
|
||||
store.addListener((DeviceIdentifier device, Iterable<GPSFixMoving> fixes, boolean returnManeuverChanges, boolean returnLiveDelay) -> {
|
||||
return null;
|
||||
}, device);
|
||||
barrier.await(100, TimeUnit.MILLISECONDS);
|
||||
@@ -71,9 +71,9 @@ public class GPSFixStoreListenerTest extends AbstractGPSFixStoreTest {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<Triple<RegattaAndRaceIdentifier, Boolean, Duration>> fixReceived(DeviceIdentifier device, GPSFixMoving fix, boolean returnManeuverChanges, boolean returnLiveDelay) {
|
||||
public Iterable<Triple<RegattaAndRaceIdentifier, Boolean, Duration>> fixesReceived(DeviceIdentifier device, Iterable<GPSFixMoving> fixes, boolean returnManeuverChanges, boolean returnLiveDelay) {
|
||||
try {
|
||||
barrier.await(100, TimeUnit.MILLISECONDS);
|
||||
barrier.await(1000, TimeUnit.MILLISECONDS);
|
||||
} catch (TimeoutException e) {
|
||||
throw new TimoutRuntimeException(e);
|
||||
} catch (Exception e) {
|
||||
|
||||
-1
@@ -139,7 +139,6 @@ public class SensorFixStoreTest {
|
||||
for (int i = 0; i < fixes; i++) {
|
||||
addBravoFix(device, FIX_TIMESTAMP + 1, FIX_RIDE_HEIGHT);
|
||||
}
|
||||
|
||||
List<Double> progressData = new ArrayList<>();
|
||||
store.loadFixes((fix) -> {
|
||||
}, device, new MillisecondsTimePoint(FIX_TIMESTAMP - 1), new MillisecondsTimePoint(FIX_TIMESTAMP + fixes + 1),
|
||||
|
||||
+153
-146
@@ -206,161 +206,163 @@ public class FixLoaderAndTracker implements TrackingDataLoader {
|
||||
|
||||
private final FixReceivedListener<Timed> listener = new FixReceivedListener<Timed>() {
|
||||
@Override
|
||||
public Iterable<Triple<RegattaAndRaceIdentifier, Boolean, Duration>> fixReceived(DeviceIdentifier device,
|
||||
Timed fix, boolean returnManeuverChanges, boolean returnLiveDelay) {
|
||||
public Iterable<Triple<RegattaAndRaceIdentifier, Boolean, Duration>> fixesReceived(DeviceIdentifier device,
|
||||
Iterable<Timed> fixes, boolean returnManeuverChanges, boolean returnLiveDelay) {
|
||||
final Set<RegattaAndRaceIdentifier> maneuverChanged = new HashSet<>();
|
||||
final Map<RegattaAndRaceIdentifier, Duration> delayToLive = new HashMap<>();
|
||||
if (!preemptiveStopRequested.get() && trackedRace.getStartOfTracking() != null) {
|
||||
final TimePoint timePoint = fix.getTimePoint();
|
||||
deviceMappings.forEachMappingOfDeviceIncludingTimePoint(device, fix.getTimePoint(),
|
||||
new Consumer<DeviceMappingWithRegattaLogEvent<WithID>>() {
|
||||
@Override
|
||||
public void accept(DeviceMappingWithRegattaLogEvent<WithID> mapping) {
|
||||
mapping.getRegattaLogEvent().accept(new MappingEventVisitor() {
|
||||
@Override
|
||||
public void visit(RegattaLogDeviceCompetitorSensorDataMappingEvent event) {
|
||||
recordSensorFixForCompetitor(event.getMappedTo(), event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(RegattaLogDeviceBoatSensorDataMappingEvent event) {
|
||||
final Boat boat = event.getMappedTo();
|
||||
final Competitor competitor = trackedRace.getCompetitorOfBoat(boat);
|
||||
if (competitor != null) {
|
||||
recordSensorFixForCompetitor(competitor, event);
|
||||
} else {
|
||||
logger.log(Level.FINE, ()->"Could not record fix for boat because no competitor could be determined. Boat: " + boat);
|
||||
for (final Timed fix : fixes) {
|
||||
final TimePoint timePoint = fix.getTimePoint();
|
||||
deviceMappings.forEachMappingOfDeviceIncludingTimePoint(device, fix.getTimePoint(),
|
||||
new Consumer<DeviceMappingWithRegattaLogEvent<WithID>>() {
|
||||
@Override
|
||||
public void accept(DeviceMappingWithRegattaLogEvent<WithID> mapping) {
|
||||
mapping.getRegattaLogEvent().accept(new MappingEventVisitor() {
|
||||
@Override
|
||||
public void visit(RegattaLogDeviceCompetitorSensorDataMappingEvent event) {
|
||||
recordSensorFixForCompetitor(event.getMappedTo(), event);
|
||||
}
|
||||
}
|
||||
|
||||
private void recordSensorFixForCompetitor(Competitor competitor, RegattaLogDeviceMappingEvent<?> event) {
|
||||
if (!preemptiveStopRequested.get()) {
|
||||
@SuppressWarnings("unchecked")
|
||||
SensorFixMapper<SensorFix, DynamicSensorFixTrack<Competitor, SensorFix>, Competitor> mapper = sensorFixMapperFactory
|
||||
.createCompetitorMapper((Class<? extends RegattaLogDeviceMappingEvent<?>>) event.getClass());
|
||||
DynamicSensorFixTrack<Competitor, SensorFix> track = mapper.getTrack(trackedRace, competitor);
|
||||
if (track != null && trackedRace.isWithinStartAndEndOfTracking(fix.getTimePoint())) {
|
||||
mapper.addFix(track, (DoubleVectorFix) fix);
|
||||
|
||||
@Override
|
||||
public void visit(RegattaLogDeviceBoatSensorDataMappingEvent event) {
|
||||
final Boat boat = event.getMappedTo();
|
||||
final Competitor competitor = trackedRace.getCompetitorOfBoat(boat);
|
||||
if (competitor != null) {
|
||||
recordSensorFixForCompetitor(competitor, event);
|
||||
} else {
|
||||
logger.log(Level.FINE, ()->"Could not record fix for boat because no competitor could be determined. Boat: " + boat);
|
||||
}
|
||||
}
|
||||
|
||||
private void recordSensorFixForCompetitor(Competitor competitor, RegattaLogDeviceMappingEvent<?> event) {
|
||||
if (!preemptiveStopRequested.get()) {
|
||||
@SuppressWarnings("unchecked")
|
||||
SensorFixMapper<SensorFix, DynamicSensorFixTrack<Competitor, SensorFix>, Competitor> mapper = sensorFixMapperFactory
|
||||
.createCompetitorMapper((Class<? extends RegattaLogDeviceMappingEvent<?>>) event.getClass());
|
||||
DynamicSensorFixTrack<Competitor, SensorFix> track = mapper.getTrack(trackedRace, competitor);
|
||||
if (track != null && trackedRace.isWithinStartAndEndOfTracking(fix.getTimePoint())) {
|
||||
mapper.addFix(track, (DoubleVectorFix) fix);
|
||||
if (returnLiveDelay) {
|
||||
delayToLive.put(trackedRace.getRaceIdentifier(), new MillisecondsDurationImpl(trackedRace.getDelayToLiveInMillis()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(RegattaLogDeviceCompetitorMappingEvent event) {
|
||||
recordForCompetitor(event.getMappedTo());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(RegattaLogDeviceBoatMappingEvent event) {
|
||||
final Boat boat = event.getMappedTo();
|
||||
final Competitor comp = trackedRace.getCompetitorOfBoat(boat);
|
||||
if (comp != null) {
|
||||
recordForCompetitor(comp);
|
||||
} else {
|
||||
// this is not necessarily something to warn of; while a boat tracker may continuously track
|
||||
logger.log(Level.FINE,
|
||||
()->"Could not record fix for boat because no competitor could be determined. Boat: " + boat);
|
||||
}
|
||||
}
|
||||
|
||||
private void recordForCompetitor(Competitor comp) {
|
||||
if (!preemptiveStopRequested.get()) {
|
||||
if (fix instanceof GPSFixMoving) {
|
||||
// try to record the fix, and only if it was really to the track,
|
||||
// check for maneuvers; otherwise, the fix may not have been accepted
|
||||
// by the race or the track, e.g., because the race's end-of-tracking
|
||||
// comes before the fix's time point
|
||||
if (trackedRace.recordFix(comp, (GPSFixMoving) fix)) { // TOOD bug6229: this checks the TrackedRace's tracking interval, but for an MDI we'd also want to intersect with Event/Regatta end date if set
|
||||
if (returnManeuverChanges) {
|
||||
RegattaAndRaceIdentifier maneuverChangedAnswer = detectIfManeuverChanged(comp);
|
||||
if (maneuverChangedAnswer != null) {
|
||||
maneuverChanged.add(maneuverChangedAnswer);
|
||||
}
|
||||
}
|
||||
if (returnLiveDelay) {
|
||||
delayToLive.put(trackedRace.getRaceIdentifier(), new MillisecondsDurationImpl(trackedRace.getDelayToLiveInMillis()));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.log(Level.WARNING,
|
||||
String.format(
|
||||
"Could not add fix for competitor (%s) in race (%s), as it"
|
||||
+ " is no GPSFixMoving, meaning it is missing COG/SOG values",
|
||||
comp, trackedRace.getRace().getName()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(RegattaLogDeviceMarkMappingEvent event) {
|
||||
if (!preemptiveStopRequested.get()) {
|
||||
Mark mark = event.getMappedTo();
|
||||
final DynamicGPSFixTrack<Mark, GPSFix> markTrack = trackedRace.getOrCreateTrack(mark);
|
||||
final GPSFix firstFixAtOrAfter;
|
||||
final boolean forceFix;
|
||||
if (trackedRace.isWithinStartAndEndOfTracking(fix.getTimePoint())) {
|
||||
forceFix = false;
|
||||
} else {
|
||||
markTrack.lockForRead();
|
||||
try {
|
||||
if (Util.isEmpty(markTrack.getRawFixes())
|
||||
|| (firstFixAtOrAfter = markTrack.getFirstFixAtOrAfter(timePoint)) != null
|
||||
&& firstFixAtOrAfter.getTimePoint().equals(timePoint)) {
|
||||
// either the first fix or overwriting an existing one
|
||||
forceFix = true;
|
||||
} else {
|
||||
// checking if the given fix is "better" than an existing one
|
||||
TimePoint startOfTracking = trackedRace.getStartOfTracking();
|
||||
TimePoint endOfTracking = trackedRace.getEndOfTracking();
|
||||
if (startOfTracking != null) {
|
||||
GPSFix fixAfterStartOfTracking = markTrack
|
||||
.getFirstFixAtOrAfter(startOfTracking);
|
||||
if (fixAfterStartOfTracking == null
|
||||
|| !trackedRace.isWithinStartAndEndOfTracking(
|
||||
fixAfterStartOfTracking.getTimePoint())) {
|
||||
// There is no fix in the tracking interval, so this fix could be "better"
|
||||
// than ones already available in the track
|
||||
// Better means closer before/after the beginning/end of the tracking
|
||||
// interval
|
||||
if (timePoint.before(startOfTracking)) {
|
||||
// check if it is closer to the beginning of the tracking interval
|
||||
GPSFix fixBeforeStartOfTracking = markTrack
|
||||
.getLastFixAtOrBefore(startOfTracking);
|
||||
forceFix = (fixBeforeStartOfTracking == null
|
||||
|| fixBeforeStartOfTracking.getTimePoint().before(timePoint));
|
||||
} else if (endOfTracking != null && timePoint.after(endOfTracking)) {
|
||||
// check if it is closer to the end of the tracking interval
|
||||
GPSFix fixAfterEndOfTracking = markTrack
|
||||
.getFirstFixAtOrAfter(endOfTracking);
|
||||
forceFix = (fixAfterEndOfTracking == null
|
||||
|| fixAfterEndOfTracking.getTimePoint().after(timePoint));
|
||||
} else {
|
||||
forceFix = false;
|
||||
}
|
||||
} else {
|
||||
// there is already a fix in the tracking interval
|
||||
forceFix = false;
|
||||
}
|
||||
} else {
|
||||
forceFix = false;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
markTrack.unlockAfterRead();
|
||||
}
|
||||
}
|
||||
trackedRace.recordFix(mark, (GPSFix) fix, /* only when in tracking interval */ !forceFix);
|
||||
if (returnLiveDelay) {
|
||||
delayToLive.put(trackedRace.getRaceIdentifier(), new MillisecondsDurationImpl(trackedRace.getDelayToLiveInMillis()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(RegattaLogDeviceCompetitorMappingEvent event) {
|
||||
recordForCompetitor(event.getMappedTo());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(RegattaLogDeviceBoatMappingEvent event) {
|
||||
final Boat boat = event.getMappedTo();
|
||||
final Competitor comp = trackedRace.getCompetitorOfBoat(boat);
|
||||
if (comp != null) {
|
||||
recordForCompetitor(comp);
|
||||
} else {
|
||||
// this is not necessarily something to warn of; while a boat tracker may continuously track
|
||||
logger.log(Level.FINE,
|
||||
()->"Could not record fix for boat because no competitor could be determined. Boat: " + boat);
|
||||
}
|
||||
}
|
||||
|
||||
private void recordForCompetitor(Competitor comp) {
|
||||
if (!preemptiveStopRequested.get()) {
|
||||
if (fix instanceof GPSFixMoving) {
|
||||
// try to record the fix, and only if it was really to the track,
|
||||
// check for maneuvers; otherwise, the fix may not have been accepted
|
||||
// by the race or the track, e.g., because the race's end-of-tracking
|
||||
// comes before the fix's time point
|
||||
if (trackedRace.recordFix(comp, (GPSFixMoving) fix)) { // TOOD bug6229: this checks the TrackedRace's tracking interval, but for an MDI we'd also want to intersect with Event/Regatta end date if set
|
||||
if (returnManeuverChanges) {
|
||||
RegattaAndRaceIdentifier maneuverChangedAnswer = detectIfManeuverChanged(comp);
|
||||
if (maneuverChangedAnswer != null) {
|
||||
maneuverChanged.add(maneuverChangedAnswer);
|
||||
}
|
||||
}
|
||||
if (returnLiveDelay) {
|
||||
delayToLive.put(trackedRace.getRaceIdentifier(), new MillisecondsDurationImpl(trackedRace.getDelayToLiveInMillis()));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.log(Level.WARNING,
|
||||
String.format(
|
||||
"Could not add fix for competitor (%s) in race (%s), as it"
|
||||
+ " is no GPSFixMoving, meaning it is missing COG/SOG values",
|
||||
comp, trackedRace.getRace().getName()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(RegattaLogDeviceMarkMappingEvent event) {
|
||||
if (!preemptiveStopRequested.get()) {
|
||||
Mark mark = event.getMappedTo();
|
||||
final DynamicGPSFixTrack<Mark, GPSFix> markTrack = trackedRace.getOrCreateTrack(mark);
|
||||
final GPSFix firstFixAtOrAfter;
|
||||
final boolean forceFix;
|
||||
if (trackedRace.isWithinStartAndEndOfTracking(fix.getTimePoint())) {
|
||||
forceFix = false;
|
||||
} else {
|
||||
markTrack.lockForRead();
|
||||
try {
|
||||
if (Util.isEmpty(markTrack.getRawFixes())
|
||||
|| (firstFixAtOrAfter = markTrack.getFirstFixAtOrAfter(timePoint)) != null
|
||||
&& firstFixAtOrAfter.getTimePoint().equals(timePoint)) {
|
||||
// either the first fix or overwriting an existing one
|
||||
forceFix = true;
|
||||
} else {
|
||||
// checking if the given fix is "better" than an existing one
|
||||
TimePoint startOfTracking = trackedRace.getStartOfTracking();
|
||||
TimePoint endOfTracking = trackedRace.getEndOfTracking();
|
||||
if (startOfTracking != null) {
|
||||
GPSFix fixAfterStartOfTracking = markTrack
|
||||
.getFirstFixAtOrAfter(startOfTracking);
|
||||
if (fixAfterStartOfTracking == null
|
||||
|| !trackedRace.isWithinStartAndEndOfTracking(
|
||||
fixAfterStartOfTracking.getTimePoint())) {
|
||||
// There is no fix in the tracking interval, so this fix could be "better"
|
||||
// than ones already available in the track
|
||||
// Better means closer before/after the beginning/end of the tracking
|
||||
// interval
|
||||
if (timePoint.before(startOfTracking)) {
|
||||
// check if it is closer to the beginning of the tracking interval
|
||||
GPSFix fixBeforeStartOfTracking = markTrack
|
||||
.getLastFixAtOrBefore(startOfTracking);
|
||||
forceFix = (fixBeforeStartOfTracking == null
|
||||
|| fixBeforeStartOfTracking.getTimePoint().before(timePoint));
|
||||
} else if (endOfTracking != null && timePoint.after(endOfTracking)) {
|
||||
// check if it is closer to the end of the tracking interval
|
||||
GPSFix fixAfterEndOfTracking = markTrack
|
||||
.getFirstFixAtOrAfter(endOfTracking);
|
||||
forceFix = (fixAfterEndOfTracking == null
|
||||
|| fixAfterEndOfTracking.getTimePoint().after(timePoint));
|
||||
} else {
|
||||
forceFix = false;
|
||||
}
|
||||
} else {
|
||||
// there is already a fix in the tracking interval
|
||||
forceFix = false;
|
||||
}
|
||||
} else {
|
||||
forceFix = false;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
markTrack.unlockAfterRead();
|
||||
}
|
||||
}
|
||||
trackedRace.recordFix(mark, (GPSFix) fix, /* only when in tracking interval */ !forceFix);
|
||||
if (returnLiveDelay) {
|
||||
delayToLive.put(trackedRace.getRaceIdentifier(), new MillisecondsDurationImpl(trackedRace.getDelayToLiveInMillis()));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return mergeManeuverChangedAndLiveDelayResult(maneuverChanged, delayToLive);
|
||||
}
|
||||
@@ -878,6 +880,11 @@ public class FixLoaderAndTracker implements TrackingDataLoader {
|
||||
addLoadingJob(new LoadFixesForNewlyCoveredTimeRangesJob(item, newlyCoveredTimeRanges));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "FixLoaderDeviceMappings for race "+trackedRace.getRaceIdentifier();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+84
-6
@@ -4,6 +4,7 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
@@ -69,6 +70,39 @@ public abstract class RegattaLogDeviceMappings<ItemT extends WithID> {
|
||||
private final Map<ItemT, List<DeviceMappingWithRegattaLogEvent<ItemT>>> mappings = new HashMap<>();
|
||||
private final Map<DeviceIdentifier, List<DeviceMappingWithRegattaLogEvent<ItemT>>> mappingsByDevice = new HashMap<>();
|
||||
|
||||
/**
|
||||
* A cache that holds the device mappings as the {@link Pair#getB() second} component of the values in this map,
|
||||
* such that exactly these device mappings apply for any time point {@link TimeRange#includes(TimePoint) included}
|
||||
* by the {@link TimeRange} that is the {@link Pair#getA() first} component of a value in this map. This map's keys
|
||||
* match with the {@link DeviceMapping#getDevice() device identifiers} of the {@link Pair#getB() second} components
|
||||
* of their corresponding values.
|
||||
* <p>
|
||||
*
|
||||
* This cache is designed to work well for cases where mappings change at a frequency orders of magnitude less than
|
||||
* the frequency with which fixes arrive and are to be mapped to items. Furthermore, the cache hit rates benefit
|
||||
* from mappings covering large time ranges.
|
||||
* <p>
|
||||
*
|
||||
* Any change to the mappings for a device will remove the mapping for the device's {@link DeviceIdentifier
|
||||
* identifier} from this map.
|
||||
* <p>
|
||||
*
|
||||
* Access to this map has to undergo the same locking drill as any access to {@link #mappings}, using the
|
||||
* {@link #mappingsLock}.
|
||||
*/
|
||||
private final Map<DeviceIdentifier, Pair<TimeRange, List<DeviceMappingWithRegattaLogEvent<ItemT>>>> cachedMappings = new HashMap<>();
|
||||
|
||||
/**
|
||||
* When the {@link #cachedMappings} are to be updated, the corresponding update job is stored in this field. It must be executed
|
||||
* under the {@link #mappingsLock}'s write lock. However, when other updates to {@link #mappings} or {@link #mappingsByDevice}
|
||||
* are performed (usually by the {@link #updateMappingsInternal()} method), this field will be cleared because the cache update
|
||||
* will most likely be obsolete.
|
||||
*/
|
||||
private Runnable cacheUpdateJob;
|
||||
|
||||
private int cacheHits;
|
||||
private int cacheMisses;
|
||||
|
||||
private final RegattaLogEventVisitor regattaLogEventVisitor = new BaseRegattaLogEventVisitor() {
|
||||
@Override
|
||||
public void visit(RegattaLogDeviceCompetitorSensorDataMappingEvent event) {
|
||||
@@ -156,7 +190,13 @@ public abstract class RegattaLogDeviceMappings<ItemT extends WithID> {
|
||||
|
||||
/**
|
||||
* Calls the given callback for every DeviceMapping that is known for the given {@link DeviceIdentifier} that
|
||||
* includes the given {@link TimePoint}.
|
||||
* includes the given {@link TimePoint}.<p>
|
||||
*
|
||||
* Searches the {@link #cachedMappings} for a match for the {@code device}; if found, checks whether {@code timePoint}
|
||||
* is within the time range for which device mappings were cached, and if so, uses those device mappings. Otherwise,
|
||||
* the device mappings are calculated and a cache update is carried out after releasing the read-lock of
|
||||
* {@link #mappingsLock} and after obtaining its write-lock. Should an update have been squeezed in between releasing
|
||||
* the read-lock and obtaining the write-lock, the cache update is not carried out.
|
||||
*
|
||||
* @param device
|
||||
* the device to get the mappings for
|
||||
@@ -169,15 +209,51 @@ public abstract class RegattaLogDeviceMappings<ItemT extends WithID> {
|
||||
public void forEachMappingOfDeviceIncludingTimePoint(DeviceIdentifier device, TimePoint timePoint,
|
||||
Consumer<DeviceMappingWithRegattaLogEvent<ItemT>> callback) {
|
||||
LockUtil.executeWithReadLock(mappingsLock, () -> {
|
||||
List<DeviceMappingWithRegattaLogEvent<ItemT>> mappingsForDevice = mappingsByDevice.get(device);
|
||||
if (mappingsForDevice != null) {
|
||||
for (DeviceMappingWithRegattaLogEvent<ItemT> mapping : mappingsForDevice) {
|
||||
if (mapping.getTimeRange().includes(timePoint)) {
|
||||
callback.accept(mapping);
|
||||
final Pair<TimeRange, List<DeviceMappingWithRegattaLogEvent<ItemT>>> cachedTimeRangeForDevice = cachedMappings.get(device);
|
||||
if (cachedTimeRangeForDevice != null && cachedTimeRangeForDevice.getA().includes(timePoint)) {
|
||||
cacheHits++;
|
||||
logger.fine(() -> "Device mapping cache hit for mapper " + this + " for device " + device
|
||||
+ " and time point " + timePoint + ", included in cached range "
|
||||
+ cachedTimeRangeForDevice.getA() + "; " + cacheHits + " hits, " + cacheMisses + " misses");
|
||||
cachedTimeRangeForDevice.getB().forEach(mapping->callback.accept(mapping));
|
||||
} else {
|
||||
final List<DeviceMappingWithRegattaLogEvent<ItemT>> mappingsForDevice = mappingsByDevice.get(device);
|
||||
TimeRange timeRangeForCache = null;
|
||||
final List<DeviceMappingWithRegattaLogEvent<ItemT>> deviceMappingsForCache = new LinkedList<>();
|
||||
cacheMisses++;
|
||||
if (mappingsForDevice != null) {
|
||||
for (final DeviceMappingWithRegattaLogEvent<ItemT> mapping : mappingsForDevice) {
|
||||
if (mapping.getTimeRange().includes(timePoint)) {
|
||||
if (timeRangeForCache == null) {
|
||||
timeRangeForCache = mapping.getTimeRange();
|
||||
} else {
|
||||
timeRangeForCache = timeRangeForCache.intersection(mapping.getTimeRange());
|
||||
}
|
||||
deviceMappingsForCache.add(mapping);
|
||||
callback.accept(mapping);
|
||||
}
|
||||
}
|
||||
}
|
||||
final TimeRange finalTimeRangeForCache = timeRangeForCache;
|
||||
logger.fine(() -> "Device mapping cache miss for mapper " + this + " for device " + device
|
||||
+ " and time point " + timePoint + ", determined cachable range "
|
||||
+ finalTimeRangeForCache + "; " + cacheHits + " hits, " + cacheMisses + " misses");
|
||||
if (timeRangeForCache != null) {
|
||||
cacheUpdateJob = ()->cachedMappings.put(device, new Pair<>(finalTimeRangeForCache, deviceMappingsForCache));
|
||||
}
|
||||
}
|
||||
});
|
||||
if (cacheUpdateJob != null) {
|
||||
LockUtil.executeWithWriteLock(mappingsLock, ()->{
|
||||
if (cacheUpdateJob != null) {
|
||||
logger.fine(()-> "Device mapping cache miss for mapper " + this + " performs cache update.");
|
||||
cacheUpdateJob.run();
|
||||
} else {
|
||||
logger.fine(() -> "Device mapping cache miss for mapper " + this
|
||||
+ " does not update the cache because the mappings were updated in between");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void forEachItemAndCoveredTimeRanges(final BiConsumer<ItemT, Map<RegattaLogDeviceMappingEvent<ItemT>, MultiTimeRange>> consumer) {
|
||||
@@ -245,8 +321,10 @@ public abstract class RegattaLogDeviceMappings<ItemT extends WithID> {
|
||||
oldMappings.putAll(mappings);
|
||||
oldDeviceIds.addAll(mappingsByDevice.keySet());
|
||||
mappings.clear();
|
||||
cachedMappings.clear();
|
||||
mappings.putAll(newMappings);
|
||||
mappingsByDevice.clear();
|
||||
cacheUpdateJob = null;
|
||||
for (ItemT item : newMappings.keySet()) {
|
||||
for (DeviceMappingWithRegattaLogEvent<ItemT> mapping : newMappings.get(item)) {
|
||||
List<DeviceMappingWithRegattaLogEvent<ItemT>> list = mappingsByDevice.get(mapping.getDevice());
|
||||
|
||||
+30
-1
@@ -1,5 +1,7 @@
|
||||
package com.sap.sailing.domain.racelog.tracking;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import com.sap.sailing.domain.common.DeviceIdentifier;
|
||||
import com.sap.sailing.domain.common.RegattaAndRaceIdentifier;
|
||||
import com.sap.sse.common.Duration;
|
||||
@@ -12,6 +14,7 @@ import com.sap.sse.common.Util.Triple;
|
||||
* @param <FixT>
|
||||
* the type of fixes this listener can consume.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface FixReceivedListener<FixT extends Timed> {
|
||||
/**
|
||||
*
|
||||
@@ -34,6 +37,32 @@ public interface FixReceivedListener<FixT extends Timed> {
|
||||
* returned can be empty but is never {@code null}. It can also contain multiple identifiers if the device
|
||||
* mapping is currently ambiguous.
|
||||
*/
|
||||
Iterable<Triple<RegattaAndRaceIdentifier, Boolean, Duration>> fixReceived(DeviceIdentifier device, FixT fix,
|
||||
default Iterable<Triple<RegattaAndRaceIdentifier, Boolean, Duration>> fixReceived(DeviceIdentifier device, FixT fix,
|
||||
boolean returnManeuverChanges, boolean returnLiveDelay) {
|
||||
return fixesReceived(device, Collections.singleton(fix), returnManeuverChanges, returnLiveDelay);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param device
|
||||
* the device that recorded the fix. Cannot be <code>null</code>.
|
||||
* @param fixes
|
||||
* The fixes that were stored. Must not be <code>null</code> but may be empty
|
||||
* @param returnLiveDelay
|
||||
* if {@code true} then all listeners to which the fixes are forwarded shall check to which races the fix
|
||||
* maps and report the live delay for the newest of those {@code fixes} for all those races as the third
|
||||
* component of the resulting {@link Triple}s.
|
||||
* @param returnManeuverUpdate
|
||||
* if {@code true}, all listeners to which this fixes are forwarded shall check whether the fixes feed
|
||||
* into a competitor's track in the scope of a race where for that competitor the maneuver list has
|
||||
* changed since the last call of this type; if so, the race identifier will be part of the result, with
|
||||
* the {@link Boolean} component being {@code true} for that race. Otherwise, the {@link Boolean}
|
||||
* component is {@code false} or the race is not listed in the result.
|
||||
* @return An {@link Iterable} with {@link RegattaAndRaceIdentifier}s is returned that will contain races with new
|
||||
* maneuvers which were not available at the last time the given device stored a fix. The {@link Iterable}
|
||||
* returned can be empty but is never {@code null}. It can also contain multiple identifiers if the device
|
||||
* mapping is currently ambiguous.
|
||||
*/
|
||||
Iterable<Triple<RegattaAndRaceIdentifier, Boolean, Duration>> fixesReceived(DeviceIdentifier device, Iterable<FixT> fixes,
|
||||
boolean returnManeuverChanges, boolean returnLiveDelay);
|
||||
}
|
||||
|
||||
+7
-7
@@ -580,7 +580,7 @@ factor=Factor
|
||||
errorUpdatingIsMedalRace=Error updating the medal race setting: {0}
|
||||
maneuverLoss=Maneuver loss
|
||||
averageManeuverLossInMeters=\u2205 Maneuver Loss
|
||||
averageManeuverLossInMetersTooltip=The average distance loss during any mneuver.
|
||||
averageManeuverLossInMetersTooltip=The average distance loss during any maneuver.
|
||||
averageTackLossInMeters=\u2205 Tack Loss
|
||||
averageTackLossInMetersTooltip=The average distance loss during tacks.
|
||||
averageJibeLossInMeters=\u2205 Jibe Loss
|
||||
@@ -637,7 +637,7 @@ polarSheetRemoveOutliersTooltip=Outliers are removed by a distance based approac
|
||||
polarSheetOutlierDetectionRadius=Outlier detection neighborhood radius
|
||||
polarSheetOutlierDetectionRadiusTooltip=The radius used in the distance based outlier\ndetection algorithm.
|
||||
polarSheetOutlierDetectionMinimumPerc=Minimum outlier detection neighborhood percentage
|
||||
polarSheetOutlierDetectionMinimumPercTooltip=The percentage of neightborhood datapoints / overall datapoints\nneeds to be at less than the given value for the point to be considered an outlier.
|
||||
polarSheetOutlierDetectionMinimumPercTooltip=The percentage of neighborhood datapoints / overall datapoints\nneeds to be at less than the given value for the point to be considered an outlier.
|
||||
polarSheetNumberOfHistogramColumns=Number of histogram columns per wind range
|
||||
polarSheetWindSteppingInKnots=Wind stepping in knots
|
||||
polarSheetWindSteppingMaxDistance=Max. distance to wind level
|
||||
@@ -1021,7 +1021,7 @@ addIgtimiUser=Add Igtimi User
|
||||
errorTryingToRemoveIgtimiDevice=Error trying to remove Igtimi device {0}: {1}
|
||||
successfullyRemoveIgtimiDevice=Igtimi device {0} removed successfully
|
||||
errorCreatingDataAccessWindow=Error creating data access window for Igtimi device {0}: {1}
|
||||
successfullyCreatedIgtimiDataAccessWindow=Data access window for Igtimi devic {0} created successfully
|
||||
successfullyCreatedIgtimiDataAccessWindow=Data access window for Igtimi device {0} created successfully
|
||||
errorFetchingIgtimiDataAccessWindows=Error fetching Igtimi data access windows: {0}
|
||||
doYouReallyWantToRemoveTheSelectedIgtimiDataAccessWindows=Do you really want to remove the selected Igtimi data access windows?
|
||||
igtimiDataAccessWindows=Igtimi Data Access Windows
|
||||
@@ -1525,7 +1525,7 @@ showOnlyCompetitorsOfLog=Show only competitors registered in Log
|
||||
showOnlyBoatsOfLog=Show only boats registered in Log
|
||||
confirmLosingCompetitorEditsWhenTogglingLogBasedView=This will reset your current, unsaved changes to the competitors.\nDo you really want to continue?
|
||||
confirmLosingBoatEditsWhenTogglingLogBasedView=This will reset your current, unsaved changes to the boats.\nDo you really want to continue?
|
||||
removalOfMarkDisabledMayBeUsedInRaces=Removal deavticated, marks are used in races {0}
|
||||
removalOfMarkDisabledMayBeUsedInRaces=Removal deactivated, marks are used in races {0}
|
||||
createDefaultLeaderboardGroup=Would you like to create a default LeaderboardGroup for your event?
|
||||
pleaseCreateAtLeastOneMappingBy=Please create at least one mapping by first selecting a track on the left hand side and then selecting a mark, boat or competitor on the right hand side
|
||||
matcherType=Matcher type
|
||||
@@ -2464,8 +2464,8 @@ errorLoadingPolarDataForBoatClass=Error loading polar data for boat class {0}: {
|
||||
videoGuide=Video guide
|
||||
orcExplanation=ORC explanation
|
||||
confirmNonRenewingSubscriptionTitle=Stop auto-renewal
|
||||
confirmNonRenewingSubscriptionText=Do you really want to stop the auto-renewal of your subscription?\n You will loose your aquired premium privileges at the end of the current term.
|
||||
subscriptionOneTimePlanLockedText=This plan may only be aquired once.
|
||||
confirmNonRenewingSubscriptionText=Do you really want to stop the auto-renewal of your subscription?\n You will loose your acquired premium privileges at the end of the current term.
|
||||
subscriptionOneTimePlanLockedText=This plan may only be acquired once.
|
||||
errorSettingRaceToDefineItsOwnCompetitors=Error setting race for fleet {2} in column {1} of leaderboard {0} to define its own competitors: {3}
|
||||
twdInDegrees=TWD (from) in degrees
|
||||
legDirectionInDegrees=Leg direction in degrees
|
||||
@@ -2578,7 +2578,7 @@ selectRaceColumnsWhosePairingsToCopy=Race columns whose pairings to copy
|
||||
errorCopyingPairings=Error copying pairings: {0}
|
||||
successfullyCopiedPairings=Successfully copied pairings
|
||||
selectFromRaceColumn=Select race column from where to start copying pairings
|
||||
selectToRaceColumn=Select race colunm to where to copy pairings
|
||||
selectToRaceColumn=Select race column to where to copy pairings
|
||||
exportTWAHistogramToCsv=Export True Wind Angle histogram to CSV
|
||||
exportWindSpeedHistogramToCsv=Export Wind Speed histogram to CSV
|
||||
optionalBearerTokenForWindImport=Optional bearer token for wind import
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ dataMiningSettings=Data Mining Settings
|
||||
runPredefinedQuery=Run Predefined Query
|
||||
viewQueryDefinition=View Query Definition
|
||||
selectPredefinedQuery=Select Predefined Query
|
||||
errorRunningDataMiningQuery=An error occured running the query
|
||||
errorRunningDataMiningQuery=An error occurred running the query
|
||||
predefinedQueryRunner=Predefined Query Runner
|
||||
useClassGetNameTooltip=More robust against changes in the code base, but the code snippet can only be used in the scope where the classes are available.
|
||||
useClassGetName=Use Class.getName() for type names
|
||||
|
||||
+1
-1
@@ -2,6 +2,6 @@ s3Desc=Store files on Amazon S3. The resulting file name is a random UUID plus t
|
||||
s3AccessIdDesc=Access ID (leave blank to use ~/.aws/credentials instead)
|
||||
s3AccessKeyDesc=Secret Access Key (leave blank to use ~/.aws/credentials instead)
|
||||
s3BucketNameDesc=Name of Bucket to use (has to already exist, and user needs sufficient permissions)
|
||||
localDesc=Service for storing files in the local file system. Files get stored in localPath+fileName and can be accessed at baseUrl+fileName Note that the content lying in baseUrl must therefore be accessible remotely. This can for examplebe achieved by mounting a remote file system for example from static.sapsailing.com on the replicas to localPath.
|
||||
localDesc=Service for storing files in the local file system. Files get stored in localPath+fileName and can be accessed at baseUrl+fileName Note that the content lying in baseUrl must therefore be accessible remotely. This can for example be achieved by mounting a remote file system for example from static.sapsailing.com on the replicas to localPath.
|
||||
localBaseUrlDesc=Base URL + fileName = URL where uploaded file will be accessible
|
||||
localLocalPathDesc=Local Path to use for file storage
|
||||
|
||||
+2
-2
@@ -44,9 +44,9 @@ explainReplicationServletPort=Servlet port number that is used to connect to reg
|
||||
setUpStorageService=Please set up a file storage service first
|
||||
usernameAndPasswordMustBothBeSet=Username and password must both be filled or empty
|
||||
username=User name
|
||||
explainUserName=Username to use for authentification on the master
|
||||
explainUserName=Username to use for authentication on the master
|
||||
password=Password
|
||||
explainPassword=Password to use for authentification on the master
|
||||
explainPassword=Password to use for authentication on the master
|
||||
additionalInformation=Info
|
||||
serverInformation=Server Information
|
||||
serverName=Server Name: {0}
|
||||
|
||||
@@ -68,4 +68,7 @@ com.sap.sse.landscape.impl.GithubReleasesRepository.level = FINE
|
||||
|
||||
# Produce wind-from-maneuver estimation graph:
|
||||
#com.sap.sailing.windestimation.integration.IncrementalMstHmmWindEstimationForTrackedRaceTest.level = FINE
|
||||
#com.sap.sailing.windestimation.integration.IncrementalMstHmmWindEstimationForTrackedRace.level = FINE
|
||||
#com.sap.sailing.windestimation.integration.IncrementalMstHmmWindEstimationForTrackedRace.level = FINE
|
||||
|
||||
# Log device mapping caching
|
||||
com.sap.sailing.domain.racelogtracking.impl.fixtracker.RegattaLogDeviceMappings.level = FINE
|
||||
|
||||
Reference in New Issue
Block a user