mirror of
https://github.com/eclipse-sailing-analytics/sailing-analytics.git
synced 2026-10-11 23:00:59 +00:00
Merge remote-tracking branch 'origin/master'
This commit is contained in:
commit
8be675705b
36 files changed
+1414
-139
No files matched your search
+2
-2
@@ -1,6 +1,6 @@
|
||||
package com.sap.sailing.domain.common;
|
||||
|
||||
public enum ScoringSchemeType {
|
||||
LOW_POINT, HIGH_POINT, HIGH_POINT_ESS_OVERALL, HIGH_POINT_LAST_BREAKS_TIE,
|
||||
HIGH_POINT_FIRST_GETS_TEN, HIGH_POINT_FIRST_GETS_ONE, LOW_POINT_WINNER_GETS_ZERO
|
||||
LOW_POINT, HIGH_POINT, HIGH_POINT_ESS_OVERALL, HIGH_POINT_LAST_BREAKS_TIE, HIGH_POINT_FIRST_GETS_TEN,
|
||||
HIGH_POINT_FIRST_GETS_ONE, LOW_POINT_WINNER_GETS_ZERO, HIGH_POINT_WINNER_GETS_SIX
|
||||
}
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
package com.sap.sailing.domain.base;
|
||||
|
||||
public interface BoatChangeListener {
|
||||
void sailIdChanged(String oldSailId, String newSailId);
|
||||
}
|
||||
+13
@@ -10,4 +10,17 @@ public interface Competitor extends Named, WithID, IsManagedBySharedDomainFactor
|
||||
Boat getBoat();
|
||||
|
||||
Color getColor();
|
||||
|
||||
/**
|
||||
* Adds a listener to this competitor. The listener is also added to the boat and the team for changes.
|
||||
* Adding a listener that is already part of this competitor's listeners set remains without effect.
|
||||
*/
|
||||
void addCompetitorChangeListener(CompetitorChangeListener listener);
|
||||
|
||||
/**
|
||||
* Removes a listener from this competitor. The listener is also removed from the boat and the team.
|
||||
* Trying to remove a listener that is not currently in the set of this competitor's listener remains
|
||||
* without effect.
|
||||
*/
|
||||
void removeCompetitorChangeListener(CompetitorChangeListener listener);
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package com.sap.sailing.domain.base;
|
||||
|
||||
import com.sap.sailing.domain.common.Color;
|
||||
|
||||
public interface CompetitorChangeListener extends BoatChangeListener, NationalityChangeListener {
|
||||
void colorChanged(Color oldColor, Color newColor);
|
||||
|
||||
void nameChanged(String oldName, String newName);
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package com.sap.sailing.domain.base;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public interface NationalityChangeListener extends Serializable {
|
||||
void nationalityChanged(WithNationality what, Nationality oldNationality, Nationality newNationality);
|
||||
}
|
||||
+41
-1
@@ -1,17 +1,31 @@
|
||||
package com.sap.sailing.domain.base.impl;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import com.sap.sailing.domain.base.BoatChangeListener;
|
||||
import com.sap.sailing.domain.base.BoatClass;
|
||||
import com.sap.sailing.domain.common.impl.NamedImpl;
|
||||
import com.sap.sailing.domain.common.impl.Util;
|
||||
|
||||
public class BoatImpl extends NamedImpl implements DynamicBoat {
|
||||
private static final long serialVersionUID = 3489730487528955788L;
|
||||
private final BoatClass boatClass;
|
||||
private String sailID;
|
||||
private transient Set<BoatChangeListener> listeners;
|
||||
|
||||
public BoatImpl(String name, BoatClass boatClass, String sailID) {
|
||||
super(name);
|
||||
this.boatClass = boatClass;
|
||||
this.sailID = sailID;
|
||||
this.listeners = new HashSet<BoatChangeListener>();
|
||||
}
|
||||
|
||||
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
|
||||
ois.defaultReadObject();
|
||||
listeners = new HashSet<BoatChangeListener>();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -26,6 +40,32 @@ public class BoatImpl extends NamedImpl implements DynamicBoat {
|
||||
|
||||
@Override
|
||||
public void setSailId(String newSailId) {
|
||||
this.sailID = newSailId;
|
||||
final String oldSailId = this.sailID;
|
||||
if (!Util.equalsWithNull(oldSailId, newSailId)) {
|
||||
this.sailID = newSailId;
|
||||
for (BoatChangeListener listener : getListeners()) {
|
||||
listener.sailIdChanged(oldSailId, newSailId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addBoatChangeListener(BoatChangeListener listener) {
|
||||
synchronized (listeners) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeCompetitorChangeListener(BoatChangeListener listener) {
|
||||
synchronized (listeners) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
}
|
||||
|
||||
private Iterable<BoatChangeListener> getListeners() {
|
||||
synchronized (listeners) {
|
||||
return new HashSet<BoatChangeListener>(listeners);
|
||||
}
|
||||
}
|
||||
}
|
||||
+51
-3
@@ -1,10 +1,16 @@
|
||||
package com.sap.sailing.domain.base.impl;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.Serializable;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import com.sap.sailing.domain.base.Competitor;
|
||||
import com.sap.sailing.domain.base.CompetitorChangeListener;
|
||||
import com.sap.sailing.domain.base.SharedDomainFactory;
|
||||
import com.sap.sailing.domain.common.Color;
|
||||
import com.sap.sailing.domain.common.impl.Util;
|
||||
|
||||
public class CompetitorImpl implements DynamicCompetitor {
|
||||
private static final long serialVersionUID = 294603681016643157L;
|
||||
@@ -12,8 +18,8 @@ public class CompetitorImpl implements DynamicCompetitor {
|
||||
private final DynamicBoat boat;
|
||||
private final Serializable id;
|
||||
private String name;
|
||||
|
||||
private Color color;
|
||||
private transient Set<CompetitorChangeListener> listeners;
|
||||
|
||||
public CompetitorImpl(Serializable id, String name, Color color, DynamicTeam team, DynamicBoat boat) {
|
||||
this.id = id;
|
||||
@@ -21,6 +27,12 @@ public class CompetitorImpl implements DynamicCompetitor {
|
||||
this.team = team;
|
||||
this.boat = boat;
|
||||
this.color = color;
|
||||
this.listeners = new HashSet<CompetitorChangeListener>();
|
||||
}
|
||||
|
||||
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
|
||||
ois.defaultReadObject();
|
||||
listeners = new HashSet<CompetitorChangeListener>();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -35,7 +47,13 @@ public class CompetitorImpl implements DynamicCompetitor {
|
||||
|
||||
@Override
|
||||
public void setName(String newName) {
|
||||
this.name = newName;
|
||||
final String oldName = this.name;
|
||||
if (!Util.equalsWithNull(oldName, newName)) {
|
||||
this.name = newName;
|
||||
for (CompetitorChangeListener listener : getListeners()) {
|
||||
listener.nameChanged(oldName, newName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -64,6 +82,36 @@ public class CompetitorImpl implements DynamicCompetitor {
|
||||
}
|
||||
|
||||
public void setColor(Color color) {
|
||||
this.color = color;
|
||||
final Color oldColor = this.color;
|
||||
if (!Util.equalsWithNull(oldColor, color)) {
|
||||
this.color = color;
|
||||
for (CompetitorChangeListener listener : getListeners()) {
|
||||
listener.colorChanged(oldColor, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addCompetitorChangeListener(CompetitorChangeListener listener) {
|
||||
synchronized (listeners) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
getBoat().addBoatChangeListener(listener);
|
||||
getTeam().addNationalityChangeListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeCompetitorChangeListener(CompetitorChangeListener listener) {
|
||||
synchronized (listeners) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
getBoat().removeCompetitorChangeListener(listener);
|
||||
getTeam().removeNationalityChangeListener(listener);
|
||||
}
|
||||
|
||||
private Iterable<CompetitorChangeListener> getListeners() {
|
||||
synchronized (listeners) {
|
||||
return new HashSet<CompetitorChangeListener>(listeners);
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
@@ -1,7 +1,13 @@
|
||||
package com.sap.sailing.domain.base.impl;
|
||||
|
||||
import com.sap.sailing.domain.base.Boat;
|
||||
import com.sap.sailing.domain.base.BoatChangeListener;
|
||||
|
||||
public interface DynamicBoat extends Boat {
|
||||
void setSailId(String newSailId);
|
||||
|
||||
void addBoatChangeListener(BoatChangeListener listener);
|
||||
|
||||
void removeCompetitorChangeListener(BoatChangeListener listener);
|
||||
|
||||
}
|
||||
+5
@@ -1,8 +1,13 @@
|
||||
package com.sap.sailing.domain.base.impl;
|
||||
|
||||
import com.sap.sailing.domain.base.Nationality;
|
||||
import com.sap.sailing.domain.base.NationalityChangeListener;
|
||||
import com.sap.sailing.domain.base.Person;
|
||||
|
||||
public interface DynamicPerson extends Person {
|
||||
void setNationality(Nationality newNationality);
|
||||
|
||||
void addNationalityChangeListener(NationalityChangeListener listener);
|
||||
|
||||
void removeNationalityChangeListener(NationalityChangeListener listener);
|
||||
}
|
||||
+5
@@ -1,8 +1,13 @@
|
||||
package com.sap.sailing.domain.base.impl;
|
||||
|
||||
import com.sap.sailing.domain.base.Nationality;
|
||||
import com.sap.sailing.domain.base.NationalityChangeListener;
|
||||
import com.sap.sailing.domain.base.Team;
|
||||
|
||||
public interface DynamicTeam extends Team {
|
||||
void setNationality(Nationality newNationality);
|
||||
|
||||
void addNationalityChangeListener(NationalityChangeListener listener);
|
||||
|
||||
void removeNationalityChangeListener(NationalityChangeListener listener);
|
||||
}
|
||||
+39
-1
@@ -1,24 +1,37 @@
|
||||
package com.sap.sailing.domain.base.impl;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import com.sap.sailing.domain.base.Nationality;
|
||||
import com.sap.sailing.domain.base.NationalityChangeListener;
|
||||
import com.sap.sailing.domain.common.impl.NamedImpl;
|
||||
import com.sap.sailing.domain.common.impl.Util;
|
||||
|
||||
public class PersonImpl extends NamedImpl implements DynamicPerson {
|
||||
private static final long serialVersionUID = -2104903799224233508L;
|
||||
private Nationality nationality;
|
||||
private final Date dateOfBirth;
|
||||
private final String description;
|
||||
private transient Set<NationalityChangeListener> listeners;
|
||||
|
||||
public PersonImpl(String name, Nationality nationality, Date dateOfBirth, String description) {
|
||||
super(name);
|
||||
this.nationality = nationality;
|
||||
this.dateOfBirth = dateOfBirth;
|
||||
this.description = description;
|
||||
this.listeners = new HashSet<NationalityChangeListener>();
|
||||
}
|
||||
|
||||
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
|
||||
ois.defaultReadObject();
|
||||
listeners = new HashSet<NationalityChangeListener>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getImage() {
|
||||
throw new UnsupportedOperationException("Image for person not yet implemented");
|
||||
@@ -31,7 +44,13 @@ public class PersonImpl extends NamedImpl implements DynamicPerson {
|
||||
|
||||
@Override
|
||||
public void setNationality(Nationality newNationality) {
|
||||
this.nationality = newNationality;
|
||||
final Nationality oldNationality = this.nationality;
|
||||
if (!Util.equalsWithNull(oldNationality, newNationality)) {
|
||||
this.nationality = newNationality;
|
||||
for (NationalityChangeListener listener : getListeners()) {
|
||||
listener.nationalityChanged(this, oldNationality, newNationality);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -44,4 +63,23 @@ public class PersonImpl extends NamedImpl implements DynamicPerson {
|
||||
return description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addNationalityChangeListener(NationalityChangeListener listener) {
|
||||
synchronized (listeners) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeNationalityChangeListener(NationalityChangeListener listener) {
|
||||
synchronized (listeners) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
}
|
||||
|
||||
private Iterable<NationalityChangeListener> getListeners() {
|
||||
synchronized (listeners) {
|
||||
return new HashSet<NationalityChangeListener>(listeners);
|
||||
}
|
||||
}
|
||||
}
|
||||
+80
-8
@@ -1,18 +1,80 @@
|
||||
package com.sap.sailing.domain.base.impl;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import com.sap.sailing.domain.base.Nationality;
|
||||
import com.sap.sailing.domain.base.NationalityChangeListener;
|
||||
import com.sap.sailing.domain.base.Person;
|
||||
import com.sap.sailing.domain.base.WithNationality;
|
||||
import com.sap.sailing.domain.common.impl.NamedImpl;
|
||||
import com.sap.sailing.domain.common.impl.Util;
|
||||
|
||||
public class TeamImpl extends NamedImpl implements DynamicTeam {
|
||||
private static final long serialVersionUID = 4646922280429210183L;
|
||||
private final Iterable<? extends DynamicPerson> sailors;
|
||||
private final Person coach;
|
||||
private final DynamicPerson coach;
|
||||
private final NationalityChangeListener personNationalityChangeForwarder;
|
||||
private transient Set<NationalityChangeListener> listeners;
|
||||
|
||||
public TeamImpl(String name, Iterable<? extends DynamicPerson> sailors, Person coach) {
|
||||
public TeamImpl(String name, Iterable<? extends DynamicPerson> sailors, DynamicPerson coach) {
|
||||
super(name);
|
||||
listeners = new HashSet<NationalityChangeListener>();
|
||||
this.sailors = sailors;
|
||||
this.coach = coach;
|
||||
this.personNationalityChangeForwarder = new NationalityChangeListener() {
|
||||
private static final long serialVersionUID = -6024941913105410444L;
|
||||
@Override
|
||||
public void nationalityChanged(WithNationality what, Nationality oldNationality, Nationality newNationality) {
|
||||
if (what == getNationalityDonor()) {
|
||||
for (NationalityChangeListener listener : getNationalityChangeListeners()) {
|
||||
listener.nationalityChanged(TeamImpl.this, oldNationality, newNationality);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
setTransitiveListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* The listeners on {@link DynamicPerson} are transient. Our {@link #personNationalityChangeForwarder} needs to
|
||||
* be restored after deserialization.
|
||||
*/
|
||||
private void setTransitiveListeners() {
|
||||
if (this.sailors != null) {
|
||||
for (DynamicPerson sailor : this.sailors) {
|
||||
sailor.addNationalityChangeListener(personNationalityChangeForwarder);
|
||||
}
|
||||
}
|
||||
if (this.coach != null) {
|
||||
this.coach.addNationalityChangeListener(personNationalityChangeForwarder);
|
||||
}
|
||||
}
|
||||
|
||||
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
|
||||
ois.defaultReadObject();
|
||||
listeners = new HashSet<NationalityChangeListener>();
|
||||
setTransitiveListeners();
|
||||
}
|
||||
|
||||
public void addNationalityChangeListener(NationalityChangeListener listener) {
|
||||
synchronized (listeners) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeNationalityChangeListener(NationalityChangeListener listener) {
|
||||
synchronized (listeners) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
}
|
||||
|
||||
private Iterable<NationalityChangeListener> getNationalityChangeListeners() {
|
||||
synchronized (listeners) {
|
||||
return new HashSet<NationalityChangeListener>(listeners);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -21,28 +83,38 @@ public class TeamImpl extends NamedImpl implements DynamicTeam {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Person getCoach() {
|
||||
public DynamicPerson getCoach() {
|
||||
return coach;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Nationality getNationality() {
|
||||
return getNationalityDonor().getNationality();
|
||||
}
|
||||
|
||||
private WithNationality getNationalityDonor() {
|
||||
for (Person sailor : getSailors()) {
|
||||
if (sailor.getNationality() != null) {
|
||||
return sailor.getNationality();
|
||||
return sailor;
|
||||
}
|
||||
}
|
||||
if (getCoach() != null) {
|
||||
return getCoach().getNationality();
|
||||
return getCoach();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNationality(Nationality newNationality) {
|
||||
for (Person sailor : getSailors()) {
|
||||
((DynamicPerson) sailor).setNationality(newNationality);
|
||||
Nationality oldNationality = getNationality();
|
||||
if (!Util.equalsWithNull(oldNationality, newNationality)) {
|
||||
for (Person sailor : getSailors()) {
|
||||
((DynamicPerson) sailor).setNationality(newNationality);
|
||||
}
|
||||
for (NationalityChangeListener listener : getNationalityChangeListeners()) {
|
||||
listener.nationalityChanged(this, oldNationality, newNationality);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+8
-1
@@ -299,12 +299,19 @@ public class TrackImpl<FixType extends Timed> implements Track<FixType> {
|
||||
protected boolean add(FixType fix) {
|
||||
lockForWrite();
|
||||
try {
|
||||
return getInternalRawFixes().add(fix);
|
||||
return addWithoutLocking(fix);
|
||||
} finally {
|
||||
unlockAfterWrite();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The caller must ensure to hold the write lock for this track when calling this methos
|
||||
*/
|
||||
protected boolean addWithoutLocking(FixType fix) {
|
||||
return getInternalRawFixes().add(fix);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Duration getAverageIntervalBetweenFixes() {
|
||||
lockForRead();
|
||||
|
||||
Executable
+243
@@ -0,0 +1,243 @@
|
||||
package com.sap.sailing.util.impl;
|
||||
|
||||
import java.lang.ref.Reference;
|
||||
import java.lang.ref.ReferenceQueue;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.AbstractSet;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import com.sap.sailing.domain.common.impl.Util;
|
||||
|
||||
/**
|
||||
* A map that is concurrency-safe as provided by the {@link ConcurrentHashMap} implementation, and referencing
|
||||
* keys only weakly. Like {@link ConcurrentHashMap}, this map does not support <code>null</code> keys.
|
||||
*
|
||||
* @author Axel Uhl (D043530)
|
||||
*/
|
||||
public class ConcurrentWeakHashMap<K, V> implements Map<K, V> {
|
||||
private final ConcurrentHashMap<WeakReferenceThatIsEqualToItselfAndItsReferent, V> map;
|
||||
|
||||
private final ReferenceQueue<K> queue;
|
||||
|
||||
/**
|
||||
* A reference whose hash code is based on the hash code of the original referent, also after the
|
||||
* reference has been enqueued. Equality is defined such that this reference is equal to itself which is
|
||||
* helpful to remove the entries from the {@link ConcurrentHashMap}, as well as equal to any other reference
|
||||
* of this type if their referents are equal.
|
||||
*
|
||||
* @author Axel Uhl (D043530)
|
||||
*
|
||||
*/
|
||||
private class WeakReferenceThatIsEqualToItselfAndItsReferent extends WeakReference<K> {
|
||||
private final int hashCode;
|
||||
|
||||
public WeakReferenceThatIsEqualToItselfAndItsReferent(K referent, ReferenceQueue<? super K> q) {
|
||||
super(referent, q);
|
||||
hashCode = referent.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return hashCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* If <code>other</code> is this reference, it it considered equal. Otherwise, <code>other</code> is considered
|
||||
* equal to this reference if the referent is still set and the referent equals <code>other</code>.
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
final boolean result;
|
||||
if (other == this) {
|
||||
result = true;
|
||||
} else {
|
||||
@SuppressWarnings("unchecked") // don't care about the generics here; important is that we can get at the referent
|
||||
WeakReferenceThatIsEqualToItselfAndItsReferent otherAsRef = (WeakReferenceThatIsEqualToItselfAndItsReferent) other;
|
||||
K referent = get();
|
||||
K otherReferent = otherAsRef.get();
|
||||
result = Util.equalsWithNull(referent, otherReferent);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private class KeySet extends AbstractSet<K> {
|
||||
@Override
|
||||
public int size() {
|
||||
return map.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return map.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(Object o) {
|
||||
return map.containsKey(o);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean remove(Object o) {
|
||||
return map.remove(o) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<K> iterator() {
|
||||
purgeStaleEntries();
|
||||
final Iterator<Map.Entry<WeakReferenceThatIsEqualToItselfAndItsReferent, V>> iterator = map.entrySet().iterator();
|
||||
return new Iterator<K>() {
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return iterator.hasNext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public K next() {
|
||||
return iterator.next().getKey().get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
iterator.remove();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private class EntrySet extends AbstractSet<Map.Entry<K, V>> {
|
||||
@Override
|
||||
public int size() {
|
||||
return map.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return map.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<Map.Entry<K, V>> iterator() {
|
||||
purgeStaleEntries();
|
||||
final Iterator<Map.Entry<WeakReferenceThatIsEqualToItselfAndItsReferent, V>> iterator = map.entrySet().iterator();
|
||||
return new Iterator<Map.Entry<K, V>>() {
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return iterator.hasNext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map.Entry<K, V> next() {
|
||||
final Map.Entry<WeakReferenceThatIsEqualToItselfAndItsReferent, V> e = iterator.next();
|
||||
return new Map.Entry<K, V>() {
|
||||
@Override
|
||||
public K getKey() {
|
||||
return e.getKey().get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public V getValue() {
|
||||
return e.getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public V setValue(V value) {
|
||||
return e.setValue(value);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
iterator.remove();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public ConcurrentWeakHashMap() {
|
||||
map = new ConcurrentHashMap<WeakReferenceThatIsEqualToItselfAndItsReferent, V>();
|
||||
queue = new ReferenceQueue<K>();
|
||||
}
|
||||
|
||||
private void purgeStaleEntries() {
|
||||
Reference<? extends K> ref;
|
||||
while ((ref=queue.poll()) != null) {
|
||||
map.remove(ref);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
purgeStaleEntries();
|
||||
return map.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
purgeStaleEntries();
|
||||
return map.isEmpty();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked") // if the cast doesn't work then neither will the equals call, so false will result which is ok
|
||||
@Override
|
||||
public boolean containsKey(Object key) {
|
||||
purgeStaleEntries();
|
||||
return map.containsKey(new WeakReferenceThatIsEqualToItselfAndItsReferent((K) key, /* queue */ null));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsValue(Object value) {
|
||||
return map.containsValue(value);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked") // if the cast doesn't work then neither will the equals call, so null will result which is ok
|
||||
@Override
|
||||
public V get(Object key) {
|
||||
purgeStaleEntries();
|
||||
return map.get(new WeakReferenceThatIsEqualToItselfAndItsReferent((K) key, /* queue */ null));
|
||||
}
|
||||
|
||||
@Override
|
||||
public V put(K key, V value) {
|
||||
return map.put(new WeakReferenceThatIsEqualToItselfAndItsReferent(key, queue), value);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked") // if the cast doesn't work then neither will the equals call, so null will result which is ok
|
||||
@Override
|
||||
public V remove(Object key) {
|
||||
return map.remove(new WeakReferenceThatIsEqualToItselfAndItsReferent((K) key, /* queue */ null));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putAll(Map<? extends K, ? extends V> m) {
|
||||
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
|
||||
put(e.getKey(), e.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
map.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<K> keySet() {
|
||||
return new KeySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<V> values() {
|
||||
return map.values();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<java.util.Map.Entry<K, V>> entrySet() {
|
||||
return new EntrySet();
|
||||
}
|
||||
|
||||
}
|
||||
+27
-21
@@ -1,9 +1,10 @@
|
||||
package com.sap.sailing.util.impl;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.WeakHashMap;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
@@ -51,7 +52,7 @@ public class LockUtil {
|
||||
|
||||
private static final int NUMBER_OF_SECONDS_TO_WAIT_FOR_LOCK = 5;
|
||||
private static final Logger logger = Logger.getLogger(Util.class.getName());
|
||||
private static final Map<NamedReentrantReadWriteLock, TimePoint> lastTimeWriteLockWasObtained = new WeakHashMap<NamedReentrantReadWriteLock, TimePoint>();
|
||||
private static final Map<NamedReentrantReadWriteLock, TimePoint> lastTimeWriteLockWasObtained = new ConcurrentWeakHashMap<NamedReentrantReadWriteLock, TimePoint>();
|
||||
|
||||
/**
|
||||
* Tells how many other threads propagated which held lock to the key thread. During propagation, a lock is
|
||||
@@ -63,20 +64,20 @@ public class LockUtil {
|
||||
* The thread-specific value maps are used as monitor objects whenever decisions about thread-specific lock counts
|
||||
* need to be made.
|
||||
*/
|
||||
private static final Map<Thread, Map<Lock, Integer>> propagationCounts = new ConcurrentHashMap<Thread, Map<Lock,Integer>>();
|
||||
private static final Map<Thread, Map<Lock, Integer>> propagationCounts = new ConcurrentWeakHashMap<Thread, Map<Lock,Integer>>();
|
||||
|
||||
/**
|
||||
* Counts the "virtual" locks. A "virtual" lock is obtained if and only if at the time of calling
|
||||
* {@link #lockForRead(NamedReentrantReadWriteLock)} or {@link #lockForWrite(NamedReentrantReadWriteLock)} the
|
||||
* respective lock has a positive {@link #propagationCounts propagation count} for the current thread.
|
||||
*/
|
||||
private static final Map<Thread, Map<Lock, Integer>> virtualLockCounts = new ConcurrentHashMap<Thread, Map<Lock, Integer>>();
|
||||
private static final Map<Thread, Map<Lock, Integer>> virtualLockCounts = new ConcurrentWeakHashMap<Thread, Map<Lock, Integer>>();
|
||||
|
||||
/**
|
||||
* Redundant but easily accessible hold count per thread and lock. These are the actual lock hold counts as they are
|
||||
* recorded in the actual {@link NamedReentrantReadWriteLock} locks.
|
||||
*/
|
||||
private static final Map<Thread, Map<Lock, Integer>> lockCounts = new ConcurrentHashMap<Thread, Map<Lock, Integer>>();
|
||||
private static final Map<Thread, Map<Lock, Integer>> lockCounts = new ConcurrentWeakHashMap<Thread, Map<Lock, Integer>>();
|
||||
|
||||
public static void lockForRead(NamedReentrantReadWriteLock lock) {
|
||||
acquireLockVirtuallyOrActually(lock, lock.readLock(), ReadOrWrite.READ);
|
||||
@@ -84,9 +85,7 @@ public class LockUtil {
|
||||
|
||||
public static void lockForWrite(NamedReentrantReadWriteLock lock) {
|
||||
acquireLockVirtuallyOrActually(lock, lock.writeLock(), ReadOrWrite.WRITE);
|
||||
synchronized (lastTimeWriteLockWasObtained) {
|
||||
lastTimeWriteLockWasObtained.put(lock, MillisecondsTimePoint.now());
|
||||
}
|
||||
lastTimeWriteLockWasObtained.put(lock, MillisecondsTimePoint.now());
|
||||
}
|
||||
|
||||
private static void acquireLockVirtuallyOrActually(NamedReentrantReadWriteLock lock, final Lock readOrWriteLock, final ReadOrWrite readOrWrite) {
|
||||
@@ -147,9 +146,7 @@ public class LockUtil {
|
||||
unlockVirtuallyOrActually(lock, lock.writeLock());
|
||||
}
|
||||
final TimePoint timePointWriteLockWasObtained;
|
||||
synchronized (lastTimeWriteLockWasObtained) {
|
||||
timePointWriteLockWasObtained = lastTimeWriteLockWasObtained.get(lock);
|
||||
}
|
||||
timePointWriteLockWasObtained = lastTimeWriteLockWasObtained.get(lock);
|
||||
if (timePointWriteLockWasObtained == null) {
|
||||
logger.info("Internal error: write lock " + lock.getName()
|
||||
+ " to be unlocked but no time recorded for when it was last obtained.\n"
|
||||
@@ -355,9 +352,9 @@ public class LockUtil {
|
||||
}
|
||||
|
||||
|
||||
private static String getStackTrace(Thread thread) {
|
||||
private static String formatStackTrace(StackTraceElement[] stackTrace) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (StackTraceElement sf : thread.getStackTrace()) {
|
||||
for (StackTraceElement sf : stackTrace) {
|
||||
sb.append(sf.toString());
|
||||
sb.append('\n');
|
||||
}
|
||||
@@ -365,7 +362,7 @@ public class LockUtil {
|
||||
}
|
||||
|
||||
private static String getCurrentStackTrace() {
|
||||
return getStackTrace(Thread.currentThread());
|
||||
return formatStackTrace(Thread.currentThread().getStackTrace());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -381,6 +378,14 @@ public class LockUtil {
|
||||
try {
|
||||
locked = lock.tryLock(NUMBER_OF_SECONDS_TO_WAIT_FOR_LOCK, TimeUnit.SECONDS);
|
||||
if (!locked) {
|
||||
Thread writer = lockParent.getWriter();
|
||||
// capture the stack traces as quickly as possible to try to reflect the situation as it was when the lock couuldn't be obtained
|
||||
StackTraceElement[] writerStackTrace = writer != null ? writer.getStackTrace() : null;
|
||||
Map<Thread, StackTraceElement[]> readerStackTraces = new HashMap<Thread, StackTraceElement[]>();
|
||||
final List<Thread> readers = lockParent.getReaders();
|
||||
for (Thread reader : readers) {
|
||||
readerStackTraces.put(reader, reader.getStackTrace());
|
||||
}
|
||||
StringBuilder message = new StringBuilder();
|
||||
message.append("Couldn't acquire lock ");
|
||||
message.append(lockDescriptionForTimeoutLogMessage);
|
||||
@@ -388,14 +393,15 @@ public class LockUtil {
|
||||
message.append(NUMBER_OF_SECONDS_TO_WAIT_FOR_LOCK);
|
||||
message.append("s in thread " + Thread.currentThread().getName() + " at ");
|
||||
message.append(getCurrentStackTrace());
|
||||
Thread writer = lockParent.getWriter();
|
||||
if (writer != null) {
|
||||
message.append("\nThe current writer is:\n");
|
||||
appendThreadData(message, writer);
|
||||
appendThreadData(message, writer, writerStackTrace);
|
||||
}
|
||||
message.append("\nThe current readers are:\n");
|
||||
for (Thread reader : lockParent.getReaders()) {
|
||||
appendThreadData(message, reader);
|
||||
if (readers != null && !readers.isEmpty()) {
|
||||
message.append("\nThe current readers are:\n");
|
||||
for (Thread reader : readers) {
|
||||
appendThreadData(message, reader, readerStackTraces.get(reader));
|
||||
}
|
||||
}
|
||||
message.append("Trying again...");
|
||||
logger.info(message.toString());
|
||||
@@ -406,10 +412,10 @@ public class LockUtil {
|
||||
return locked;
|
||||
}
|
||||
|
||||
private static void appendThreadData(StringBuilder message, Thread writer) {
|
||||
private static void appendThreadData(StringBuilder message, Thread writer, StackTraceElement[] stackTrace) {
|
||||
message.append(writer);
|
||||
message.append('\n');
|
||||
message.append(getStackTrace(writer));
|
||||
message.append(formatStackTrace(stackTrace));
|
||||
message.append('\n');
|
||||
}
|
||||
|
||||
|
||||
Executable
+197
@@ -0,0 +1,197 @@
|
||||
package com.sap.sailing.domain.test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import com.sap.sailing.domain.base.CompetitorChangeListener;
|
||||
import com.sap.sailing.domain.base.DomainFactory;
|
||||
import com.sap.sailing.domain.base.Nationality;
|
||||
import com.sap.sailing.domain.base.Team;
|
||||
import com.sap.sailing.domain.base.WithNationality;
|
||||
import com.sap.sailing.domain.base.impl.DomainFactoryImpl;
|
||||
import com.sap.sailing.domain.base.impl.DynamicCompetitor;
|
||||
import com.sap.sailing.domain.common.Color;
|
||||
import com.sap.sailing.domain.common.impl.RGBColor;
|
||||
|
||||
/**
|
||||
* Tests the {@link CompetitorChangeListener} stuff including the serialization of the transitive listener
|
||||
* pattern on the {@link Team} structure.
|
||||
*
|
||||
* @author Axel Uhl (D043530)
|
||||
*
|
||||
*/
|
||||
public class CompetitorListenerTest extends AbstractSerializationTest {
|
||||
private CompetitorChangeListener listener;
|
||||
private boolean nationalityChanged;
|
||||
private Object whatChangedNationality;
|
||||
private Object oldNationality;
|
||||
private Object newNationality;
|
||||
private boolean sailIdChanged;
|
||||
private String oldSailId;
|
||||
private String newSailId;
|
||||
private boolean nameChanged;
|
||||
private String oldName;
|
||||
private String newName;
|
||||
private boolean colorChanged;
|
||||
private Color oldColor;
|
||||
private Color newColor;
|
||||
private DynamicCompetitor competitor;
|
||||
private DomainFactoryImpl baseDomainFactory;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
baseDomainFactory = new DomainFactoryImpl();
|
||||
competitor = TrackBasedTest.createCompetitor("Hasso");
|
||||
nationalityChanged = false;
|
||||
whatChangedNationality = null;
|
||||
oldNationality = null;
|
||||
newNationality = null;
|
||||
sailIdChanged = false;
|
||||
oldSailId = null;
|
||||
newSailId = null;
|
||||
nameChanged = false;
|
||||
oldName = null;
|
||||
newName = null;
|
||||
colorChanged = false;
|
||||
oldColor = null;
|
||||
newColor = null;
|
||||
listener = new CompetitorChangeListener() {
|
||||
private static final long serialVersionUID = 4581029778988240209L;
|
||||
|
||||
@Override
|
||||
public void nationalityChanged(WithNationality what, Nationality oldNationality, Nationality newNationality) {
|
||||
nationalityChanged = true;
|
||||
whatChangedNationality = what;
|
||||
CompetitorListenerTest.this.oldNationality = oldNationality;
|
||||
CompetitorListenerTest.this.newNationality = newNationality;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sailIdChanged(String oldSailId, String newSailId) {
|
||||
sailIdChanged = true;
|
||||
CompetitorListenerTest.this.oldSailId = oldSailId;
|
||||
CompetitorListenerTest.this.newSailId = newSailId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void nameChanged(String oldName, String newName) {
|
||||
nameChanged = true;
|
||||
CompetitorListenerTest.this.oldName = oldName;
|
||||
CompetitorListenerTest.this.newName = newName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void colorChanged(Color oldColor, Color newColor) {
|
||||
colorChanged = true;
|
||||
CompetitorListenerTest.this.oldColor = oldColor;
|
||||
CompetitorListenerTest.this.newColor = newColor;
|
||||
}
|
||||
};
|
||||
competitor.addCompetitorChangeListener(listener);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSimpleCompetitorListenerPatternForName() {
|
||||
competitor.setName("Dr. Hasso Plattner");
|
||||
assertTrue(nameChanged);
|
||||
assertEquals("Hasso", oldName);
|
||||
assertEquals("Dr. Hasso Plattner", newName);
|
||||
assertFalse(sailIdChanged);
|
||||
assertFalse(colorChanged);
|
||||
assertFalse(nationalityChanged);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSimpleCompetitorListenerPatternForSailId() {
|
||||
final String myOldSailId = competitor.getBoat().getSailID();
|
||||
competitor.getBoat().setSailId("POR 40");
|
||||
assertFalse(nameChanged);
|
||||
assertTrue(sailIdChanged);
|
||||
assertEquals(myOldSailId, oldSailId);
|
||||
assertEquals("POR 40", newSailId);
|
||||
assertFalse(colorChanged);
|
||||
assertFalse(nationalityChanged);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSimpleCompetitorListenerPatternForColor() {
|
||||
final Color myOldColor = competitor.getColor();
|
||||
final RGBColor myNewColor = new RGBColor(123, 12, 234);
|
||||
competitor.setColor(myNewColor);
|
||||
assertFalse(nameChanged);
|
||||
assertFalse(sailIdChanged);
|
||||
assertTrue(colorChanged);
|
||||
assertEquals(myOldColor, oldColor);
|
||||
assertEquals(myNewColor, newColor);
|
||||
assertFalse(nationalityChanged);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSimpleCompetitorListenerPatternForNationality() {
|
||||
final Nationality myOldNationality = competitor.getTeam().getNationality();
|
||||
final Nationality myNewNationality = DomainFactory.INSTANCE.getOrCreateNationality("POR");
|
||||
competitor.getTeam().setNationality(myNewNationality);
|
||||
assertFalse(nameChanged);
|
||||
assertFalse(sailIdChanged);
|
||||
assertFalse(colorChanged);
|
||||
assertTrue(nationalityChanged);
|
||||
assertEquals(myOldNationality, oldNationality);
|
||||
assertEquals(myNewNationality, newNationality);
|
||||
assertSame(competitor.getTeam(), whatChangedNationality);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCompetitorSerializationLosesAllExternalListeners() throws ClassNotFoundException, IOException {
|
||||
DomainFactory baseDomainFactory = new DomainFactoryImpl();
|
||||
DynamicCompetitor clonedCompetitor = cloneBySerialization(competitor, baseDomainFactory);
|
||||
clonedCompetitor.setName("Dr. Hasso Plattner");
|
||||
clonedCompetitor.getBoat().setSailId("POR 40");
|
||||
final RGBColor myNewColor = new RGBColor(123, 12, 234);
|
||||
clonedCompetitor.setColor(myNewColor);
|
||||
final Nationality myNewNationality = baseDomainFactory.getOrCreateNationality("POR");
|
||||
clonedCompetitor.getTeam().setNationality(myNewNationality);
|
||||
assertFalse(nameChanged);
|
||||
assertFalse(sailIdChanged);
|
||||
assertFalse(colorChanged);
|
||||
assertFalse(nationalityChanged);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCompetitorSerializationListenerPatternForNameOnSerializedCompetitor() throws ClassNotFoundException, IOException {
|
||||
DynamicCompetitor clonedCompetitor = cloneBySerialization(competitor, baseDomainFactory);
|
||||
competitor = clonedCompetitor;
|
||||
competitor.addCompetitorChangeListener(listener);
|
||||
testSimpleCompetitorListenerPatternForName();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCompetitorSerializationListenerPatternForColorOnSerializedCompetitor() throws ClassNotFoundException, IOException {
|
||||
DynamicCompetitor clonedCompetitor = cloneBySerialization(competitor, baseDomainFactory);
|
||||
competitor = clonedCompetitor;
|
||||
competitor.addCompetitorChangeListener(listener);
|
||||
testSimpleCompetitorListenerPatternForColor();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCompetitorSerializationListenerPatternForSailIdOnSerializedCompetitor() throws ClassNotFoundException, IOException {
|
||||
DynamicCompetitor clonedCompetitor = cloneBySerialization(competitor, baseDomainFactory);
|
||||
competitor = clonedCompetitor;
|
||||
competitor.addCompetitorChangeListener(listener);
|
||||
testSimpleCompetitorListenerPatternForSailId();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCompetitorSerializationListenerPatternForNationalityOnSerializedCompetitor() throws ClassNotFoundException, IOException {
|
||||
DynamicCompetitor clonedCompetitor = cloneBySerialization(competitor, baseDomainFactory);
|
||||
competitor = clonedCompetitor;
|
||||
competitor.addCompetitorChangeListener(listener);
|
||||
testSimpleCompetitorListenerPatternForNationality();
|
||||
}
|
||||
}
|
||||
Executable
+133
@@ -0,0 +1,133 @@
|
||||
package com.sap.sailing.domain.test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import com.sap.sailing.util.impl.ConcurrentWeakHashMap;
|
||||
|
||||
/**
|
||||
* Tests the class {@link ConcurrentWeakHashMap}.
|
||||
*
|
||||
* @author Axel Uhl (D043530)
|
||||
*
|
||||
*/
|
||||
public class ConcurrentWeakHashMapTest {
|
||||
@Test
|
||||
public void testPutAndGet() {
|
||||
ConcurrentWeakHashMap<String, String> m = new ConcurrentWeakHashMap<>();
|
||||
m.put("a", "b");
|
||||
assertEquals("b", m.get("a"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPutAndIterate() {
|
||||
ConcurrentWeakHashMap<String, String> m = new ConcurrentWeakHashMap<>();
|
||||
m.put("a", "b");
|
||||
assertEquals("b", m.values().iterator().next());
|
||||
assertEquals("a", m.keySet().iterator().next());
|
||||
assertEquals("a", m.entrySet().iterator().next().getKey());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPutAndPutAgain() {
|
||||
ConcurrentWeakHashMap<Integer, String> m = new ConcurrentWeakHashMap<>();
|
||||
Integer one1 = new Integer(1);
|
||||
Integer one2 = new Integer(1);
|
||||
assertNotSame(one1, one2);
|
||||
m.put(one1, "a");
|
||||
String replaced = m.put(one2, "b");
|
||||
assertEquals("a", replaced);
|
||||
assertEquals(1, m.size());
|
||||
assertEquals(m.keySet().iterator().next(), one2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemoveWithSame() {
|
||||
ConcurrentWeakHashMap<Integer, String> m = new ConcurrentWeakHashMap<>();
|
||||
Integer one = new Integer(1);
|
||||
m.put(one, "a");
|
||||
m.remove(one);
|
||||
assertTrue(m.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemoveImpactOnKeySetAndEntrySet() {
|
||||
ConcurrentWeakHashMap<Integer, String> m = new ConcurrentWeakHashMap<>();
|
||||
Integer one = new Integer(1);
|
||||
m.put(one, "a");
|
||||
Set<Integer> keySet = m.keySet();
|
||||
Set<Entry<Integer, String>> entrySet = m.entrySet();
|
||||
assertEquals(1, keySet.size());
|
||||
assertEquals(1, entrySet.size());
|
||||
m.remove(one);
|
||||
assertTrue(m.isEmpty());
|
||||
assertTrue(keySet.isEmpty());
|
||||
assertTrue(entrySet.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemoveWithEqual() {
|
||||
ConcurrentWeakHashMap<Integer, String> m = new ConcurrentWeakHashMap<>();
|
||||
Integer one1 = new Integer(1);
|
||||
Integer one2 = new Integer(1);
|
||||
assertNotSame(one1, one2);
|
||||
m.put(one1, "a");
|
||||
m.remove(one2);
|
||||
assertTrue(m.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void removeThrougKeySet() {
|
||||
ConcurrentWeakHashMap<Integer, String> m = new ConcurrentWeakHashMap<>();
|
||||
Integer one1 = new Integer(1);
|
||||
m.put(one1, "a");
|
||||
final Set<Integer> keySet = m.keySet();
|
||||
Iterator<Integer> keySetIter = keySet.iterator();
|
||||
keySetIter.next();
|
||||
keySetIter.remove();
|
||||
assertTrue(m.isEmpty());
|
||||
assertTrue(keySet.isEmpty());
|
||||
assertTrue(m.entrySet().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPutAndGetWithSubsequentGC() {
|
||||
Object key = createKeyObject();
|
||||
ConcurrentWeakHashMap<Object, String> m = new ConcurrentWeakHashMap<>();
|
||||
m.put(key, "b");
|
||||
assertEquals("b", m.get(key));
|
||||
key = null;
|
||||
forceEnqueuableWeakReferencesToBeEnqueued();
|
||||
assertTrue(m.isEmpty());
|
||||
}
|
||||
|
||||
private void forceEnqueuableWeakReferencesToBeEnqueued() {
|
||||
System.gc();
|
||||
try {
|
||||
long[] hugeArrayThatWontGetAllocated = new long[Integer.MAX_VALUE];
|
||||
assertTrue(hugeArrayThatWontGetAllocated.length > 0);
|
||||
} catch (OutOfMemoryError e) {
|
||||
// Ignore OME
|
||||
}
|
||||
try {
|
||||
Thread.sleep(50); // give GC some time to finish enqueuing and finalizing all weakly-referenced objects
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use a separate method for this, assuming that the compiler won't put the object on the using method's stack
|
||||
* @return
|
||||
*/
|
||||
private Object createKeyObject() {
|
||||
return new Object();
|
||||
}
|
||||
}
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
package com.sap.sailing.domain.test;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.stubbing.Answer;
|
||||
|
||||
import com.sap.sailing.domain.base.RaceDefinition;
|
||||
import com.sap.sailing.domain.common.Position;
|
||||
import com.sap.sailing.domain.common.TimePoint;
|
||||
import com.sap.sailing.domain.common.TrackedRaceStatusEnum;
|
||||
import com.sap.sailing.domain.common.WindSource;
|
||||
import com.sap.sailing.domain.common.WindSourceType;
|
||||
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.MillisecondsTimePoint;
|
||||
import com.sap.sailing.domain.common.impl.WindSourceWithAdditionalID;
|
||||
import com.sap.sailing.domain.tracking.TrackedRace;
|
||||
import com.sap.sailing.domain.tracking.Wind;
|
||||
import com.sap.sailing.domain.tracking.WindTrack;
|
||||
import com.sap.sailing.domain.tracking.WindWithConfidence;
|
||||
import com.sap.sailing.domain.tracking.impl.TrackBasedEstimationWindTrackImpl;
|
||||
import com.sap.sailing.domain.tracking.impl.TrackedRaceStatusImpl;
|
||||
import com.sap.sailing.domain.tracking.impl.WindImpl;
|
||||
import com.sap.sailing.domain.tracking.impl.WindTrackImpl;
|
||||
import com.sap.sailing.domain.tracking.impl.WindWithConfidenceImpl;
|
||||
|
||||
/**
|
||||
* See the issues documented in bug 1923 (http://bugzilla.sapsailing.com/bugzilla/show_bug.cgi?id=1923).
|
||||
* This test mocks {@link TrackedRace} to the degree necessary and fires more or less random "measured"
|
||||
* wind fixes against the {@link TrackBasedEstimationWindTrackImpl} while concurrently reading massively
|
||||
* from it. With this, the test is supposed to reproduce conditions as observed during events, leading
|
||||
* up to bug 1923. This can then be the basis for a fix.
|
||||
*
|
||||
* @author Axel Uhl (D043530)
|
||||
*
|
||||
*/
|
||||
public class WindEstimationLockingUnderLoadTest {
|
||||
private static final Logger logger = Logger.getLogger(WindEstimationLockingUnderLoadTest.class.getName());
|
||||
|
||||
private static final int MIN_WIND_SPEED_IN_KNOTS = 5;
|
||||
|
||||
private static final int MAX_WIND_SPEED_IN_KNOTS = 20;
|
||||
|
||||
private TrackedRace mockedTrackedRace;
|
||||
|
||||
private WindSource realWindSource;
|
||||
|
||||
private TrackBasedEstimationWindTrackImpl estimationTrack;
|
||||
|
||||
private WindTrackImpl measuredTrack;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
realWindSource = new WindSourceWithAdditionalID(WindSourceType.EXPEDITION, "1");
|
||||
measuredTrack = new WindTrackImpl(WindTrack.DEFAULT_MILLISECONDS_OVER_WHICH_TO_AVERAGE_WIND, /* useSpeed */ true, /* nameForReadWriteLock */ "Test wind track in "+getClass().getName());
|
||||
mockedTrackedRace = mockTrackedRace();
|
||||
estimationTrack = new TrackBasedEstimationWindTrackImpl(mockedTrackedRace, WindTrack.DEFAULT_MILLISECONDS_OVER_WHICH_TO_AVERAGE_WIND, 0.5);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
// clean up all Mockito stubbing leaks, particularly the InvocationImpl objects attached to any ThreadLocal;
|
||||
// see also bug 1923, comment #9.
|
||||
Mockito.reset();
|
||||
}
|
||||
|
||||
private TrackedRace mockTrackedRace() {
|
||||
TrackedRace result = mock(TrackedRace.class);
|
||||
RaceDefinition mockedRaceDefinition = mock(RaceDefinition.class);
|
||||
when(result.getRace()).thenReturn(mockedRaceDefinition);
|
||||
when(mockedRaceDefinition.getName()).thenReturn("Test Race");
|
||||
when(result.getEstimatedWindDirectionWithConfidence((Position) any(), (TimePoint) any())).thenAnswer(new Answer<WindWithConfidence<TimePoint>>() {
|
||||
public WindWithConfidence<TimePoint> answer(InvocationOnMock invocation) {
|
||||
return randomWindOrNull();
|
||||
}
|
||||
});
|
||||
when(result.getOrCreateWindTrack(realWindSource)).thenReturn(measuredTrack);
|
||||
when(result.getStatus()).thenReturn(new TrackedRaceStatusImpl(TrackedRaceStatusEnum.TRACKING, /* loadingProgress */ 1.0));
|
||||
when(result.getMillisecondsOverWhichToAverageWind()).thenReturn(30000l);
|
||||
return result;
|
||||
}
|
||||
|
||||
private WindWithConfidence<TimePoint> randomWindOrNull() {
|
||||
final WindWithConfidence<TimePoint> result;
|
||||
double speedInKnots = MAX_WIND_SPEED_IN_KNOTS*Math.random();
|
||||
if (speedInKnots < MIN_WIND_SPEED_IN_KNOTS) {
|
||||
result = null;
|
||||
} else {
|
||||
double directionInDegrees = 360*Math.random();
|
||||
double confidence = Math.random();
|
||||
final MillisecondsTimePoint now = MillisecondsTimePoint.now();
|
||||
result = new WindWithConfidenceImpl<TimePoint>(new WindImpl(new DegreePosition(49, 8), now, new KnotSpeedWithBearingImpl(speedInKnots, new DegreeBearingImpl(directionInDegrees))),
|
||||
confidence, now, /* useSpeed */ true);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSimpleWindEstimation() {
|
||||
// We can expect MIN_WIND_SPEED_IN_KNOTS/MAX_WIND_SPEED_IN_KNOTS results to be null, but as there is a significant
|
||||
// difference between the two, with a sufficient amount of random samples there has to be at least one non-null
|
||||
// result
|
||||
int notNullCount = 0;
|
||||
for (int i=0; i<1000; i++) {
|
||||
Wind estimatedWind = estimationTrack.getAveragedWind(/* position */ null, MillisecondsTimePoint.now().minus(10000*i));
|
||||
if (estimatedWind != null) {
|
||||
notNullCount++;
|
||||
}
|
||||
}
|
||||
assertTrue(notNullCount > 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendingNewWindFixToEstimationTrack() {
|
||||
final WindImpl wind = new WindImpl(new DegreePosition(49, 8), MillisecondsTimePoint.now(),
|
||||
new KnotSpeedWithBearingImpl(/* speedInKnots */12., new DegreeBearingImpl(47)));
|
||||
addMeasuredWindFix(wind);
|
||||
// no verdict, really; just expecting that there is no exception raised
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddingManyWindFixesWhileReadingLikeCrazy() throws InterruptedException {
|
||||
Thread inserter = new Thread("Inserter thread in "+getClass().getName()+".testAddingManyWindFixesWhileReadingLikeCrazy") {
|
||||
@Override
|
||||
public void run() {
|
||||
TimePoint now = MillisecondsTimePoint.now();
|
||||
for (int i = 0; i < 100000; i++) {
|
||||
final WindImpl wind = new WindImpl(new DegreePosition(49, 8), now, new KnotSpeedWithBearingImpl(
|
||||
/* speedInKnots */12., new DegreeBearingImpl(47)));
|
||||
addMeasuredWindFix(wind);
|
||||
now = now.plus(10);
|
||||
}
|
||||
logger.info("Inserter thread done");
|
||||
}
|
||||
};
|
||||
Runnable readerRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
TimePoint now = MillisecondsTimePoint.now();
|
||||
for (int i=0; i<100000; i++) {
|
||||
estimationTrack.getAveragedWind(/* position */ null, now);
|
||||
now = now.plus(10); // the cache quantizes to a full second; so not all time points will automatically fetch uncached values
|
||||
}
|
||||
logger.info(Thread.currentThread().getName()+" done");
|
||||
}
|
||||
};
|
||||
inserter.start();
|
||||
List<Thread> readers = new ArrayList<Thread>();
|
||||
for (int i = 0; i < 4; i++) {
|
||||
Thread reader = new Thread(readerRunnable, "Reader thread "+i+" in " + getClass().getName()
|
||||
+ ".testAddingManyWindFixesWhileReadingLikeCrazy");
|
||||
reader.start();
|
||||
readers.add(reader);
|
||||
}
|
||||
inserter.join();
|
||||
for (Thread reader : readers) {
|
||||
reader.join();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the wind fix to the {@link #measuredTrack} and explicitly notifies {@link #estimationTrack} which is
|
||||
* not a listener on the mocked tracked race or the wind track and therefore otherwise wouldn't receive the fix
|
||||
*/
|
||||
private void addMeasuredWindFix(final WindImpl wind) {
|
||||
measuredTrack.add(wind);
|
||||
estimationTrack.windDataReceived(wind, realWindSource);
|
||||
}
|
||||
}
|
||||
+3
@@ -35,6 +35,7 @@ import com.sap.sailing.domain.leaderboard.impl.HighPointExtremeSailingSeriesOver
|
||||
import com.sap.sailing.domain.leaderboard.impl.HighPointFirstGets10LastBreaksTie;
|
||||
import com.sap.sailing.domain.leaderboard.impl.HighPointFirstGets1LastBreaksTie;
|
||||
import com.sap.sailing.domain.leaderboard.impl.HighPointLastBreaksTie;
|
||||
import com.sap.sailing.domain.leaderboard.impl.HighPointWinnerGetsSix;
|
||||
import com.sap.sailing.domain.leaderboard.impl.LowPoint;
|
||||
import com.sap.sailing.domain.leaderboard.impl.LowPointWinnerGetsZero;
|
||||
import com.sap.sailing.domain.tracking.GPSFix;
|
||||
@@ -86,6 +87,8 @@ public class DomainFactoryImpl extends SharedDomainFactoryImpl implements Domain
|
||||
return new HighPointFirstGets10LastBreaksTie();
|
||||
case LOW_POINT_WINNER_GETS_ZERO:
|
||||
return new LowPointWinnerGetsZero();
|
||||
case HIGH_POINT_WINNER_GETS_SIX:
|
||||
return new HighPointWinnerGetsSix();
|
||||
}
|
||||
throw new RuntimeException("Unknown scoring scheme type "+scoringSchemeType.name());
|
||||
}
|
||||
|
||||
+86
-11
@@ -7,11 +7,15 @@ import java.util.Set;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
import com.sap.sailing.domain.base.Competitor;
|
||||
import com.sap.sailing.domain.base.CompetitorChangeListener;
|
||||
import com.sap.sailing.domain.base.Fleet;
|
||||
import com.sap.sailing.domain.base.Mark;
|
||||
import com.sap.sailing.domain.base.Nationality;
|
||||
import com.sap.sailing.domain.base.RaceColumn;
|
||||
import com.sap.sailing.domain.base.RaceColumnListener;
|
||||
import com.sap.sailing.domain.base.Waypoint;
|
||||
import com.sap.sailing.domain.base.WithNationality;
|
||||
import com.sap.sailing.domain.common.Color;
|
||||
import com.sap.sailing.domain.common.MaxPointsReason;
|
||||
import com.sap.sailing.domain.common.TimePoint;
|
||||
import com.sap.sailing.domain.common.WindSource;
|
||||
@@ -39,7 +43,8 @@ import com.sap.sailing.util.impl.NamedReentrantReadWriteLock;
|
||||
*/
|
||||
public class LeaderboardCacheManager {
|
||||
private final WeakHashMap<Leaderboard, CacheInvalidationUponScoreCorrectionListener> scoreCorrectionListeners;
|
||||
private final NamedReentrantReadWriteLock scoreCorrectionListenersLock;
|
||||
private final NamedReentrantReadWriteLock scoreCorrectionAndCompetitorChangeListenersLock;
|
||||
private final WeakHashMap<Leaderboard, CacheInvalidationUponCompetitorChangeListener> competitorChangeListeners;
|
||||
private final WeakHashMap<Leaderboard, Map<TrackedRace, Set<CacheInvalidationListener>>> invalidationListenersPerLeaderboard;
|
||||
private final WeakHashMap<Leaderboard, RaceColumnListener> raceColumnListeners;
|
||||
|
||||
@@ -113,6 +118,64 @@ public class LeaderboardCacheManager {
|
||||
}
|
||||
}
|
||||
|
||||
private class CacheInvalidationUponCompetitorChangeListener implements CompetitorChangeListener {
|
||||
private static final long serialVersionUID = 8308312509904366143L;
|
||||
private final Leaderboard leaderboard;
|
||||
private final Set<Competitor> observedCompetitors;
|
||||
|
||||
/**
|
||||
* Registers this listener as listener for all of the <code>leaderboard</code>'s current
|
||||
* {@link Leaderboard#getCompetitors() competitors}
|
||||
*/
|
||||
public CacheInvalidationUponCompetitorChangeListener(Leaderboard leaderboard) {
|
||||
this.leaderboard = leaderboard;
|
||||
observedCompetitors = new HashSet<>();
|
||||
updateCompetitorListeners();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sailIdChanged(String oldSailId, String newSailId) {
|
||||
removeFromCache(leaderboard);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void nationalityChanged(WithNationality what, Nationality oldNationality, Nationality newNationality) {
|
||||
removeFromCache(leaderboard);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void colorChanged(Color oldColor, Color newColor) {
|
||||
removeFromCache(leaderboard);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void nameChanged(String oldName, String newName) {
|
||||
removeFromCache(leaderboard);
|
||||
}
|
||||
|
||||
public synchronized void updateCompetitorListeners() {
|
||||
Set<Competitor> competitorsToStopObserving = new HashSet<>(observedCompetitors);
|
||||
for (Competitor competitor : leaderboard.getCompetitors()) {
|
||||
if (!observedCompetitors.contains(competitor)) {
|
||||
competitor.addCompetitorChangeListener(this); // start observing competitor
|
||||
} else {
|
||||
competitorsToStopObserving.remove(competitor); // keep observing competitor
|
||||
}
|
||||
}
|
||||
for (Competitor competitorToStopObserving : competitorsToStopObserving) {
|
||||
competitorToStopObserving.removeCompetitorChangeListener(this);
|
||||
observedCompetitors.remove(competitorToStopObserving);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void removeFromAllCompetitors() {
|
||||
for (Competitor competitor : observedCompetitors) {
|
||||
competitor.removeCompetitorChangeListener(this);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class CacheInvalidationUponScoreCorrectionListener implements ScoreCorrectionListener {
|
||||
private final Leaderboard leaderboard;
|
||||
|
||||
@@ -146,10 +209,12 @@ public class LeaderboardCacheManager {
|
||||
|
||||
public LeaderboardCacheManager(LeaderboardCache leaderboardCache) {
|
||||
this.leaderboardCache = leaderboardCache;
|
||||
this.invalidationListenersPerLeaderboard = new WeakHashMap<Leaderboard, Map<TrackedRace, Set<CacheInvalidationListener>>>();
|
||||
this.invalidationListenersPerLeaderboard = new WeakHashMap<>();
|
||||
this.raceColumnListeners = new WeakHashMap<Leaderboard, RaceColumnListener>();
|
||||
this.scoreCorrectionListeners = new WeakHashMap<Leaderboard, CacheInvalidationUponScoreCorrectionListener>();
|
||||
this.scoreCorrectionListenersLock = new NamedReentrantReadWriteLock("Score correction listeners", /* fair */ false);
|
||||
this.scoreCorrectionListeners = new WeakHashMap<>();
|
||||
this.scoreCorrectionAndCompetitorChangeListenersLock = new NamedReentrantReadWriteLock(
|
||||
"Score correction and competitor change listeners", /* fair */false);
|
||||
this.competitorChangeListeners = new WeakHashMap<>();
|
||||
}
|
||||
|
||||
private void removeFromCache(Leaderboard leaderboard) {
|
||||
@@ -167,14 +232,21 @@ public class LeaderboardCacheManager {
|
||||
synchronized (raceColumnListeners) {
|
||||
leaderboard.removeRaceColumnListener(raceColumnListeners.remove(leaderboard));
|
||||
}
|
||||
LockUtil.lockForWrite(scoreCorrectionListenersLock);
|
||||
LockUtil.lockForWrite(scoreCorrectionAndCompetitorChangeListenersLock);
|
||||
final CacheInvalidationUponScoreCorrectionListener removedScoreCorrectionListener;
|
||||
final CacheInvalidationUponCompetitorChangeListener removedCompetitorChangeListener;
|
||||
try {
|
||||
removedScoreCorrectionListener = scoreCorrectionListeners.remove(leaderboard);
|
||||
removedCompetitorChangeListener = competitorChangeListeners.remove(leaderboard);
|
||||
} finally {
|
||||
LockUtil.unlockAfterWrite(scoreCorrectionListenersLock);
|
||||
LockUtil.unlockAfterWrite(scoreCorrectionAndCompetitorChangeListenersLock);
|
||||
}
|
||||
if (removedScoreCorrectionListener != null) {
|
||||
leaderboard.getScoreCorrection().removeScoreCorrectionListener(removedScoreCorrectionListener);
|
||||
}
|
||||
if (removedCompetitorChangeListener != null) {
|
||||
removedCompetitorChangeListener.removeFromAllCompetitors();
|
||||
}
|
||||
leaderboard.getScoreCorrection().removeScoreCorrectionListener(removedScoreCorrectionListener);
|
||||
// invalidate after the listeners have been removed; this is important because invalidation may trigger a
|
||||
// re-calculation which then in turn may call add(leaderboard) asynchronously again which may be executed
|
||||
// before the listener removal happens here. This could lead to a race condition where listeners are
|
||||
@@ -195,22 +267,25 @@ public class LeaderboardCacheManager {
|
||||
private void registerAsListener(final Leaderboard leaderboard) {
|
||||
// only add as listener again if not yet added
|
||||
final boolean containsKey;
|
||||
LockUtil.lockForRead(scoreCorrectionListenersLock);
|
||||
LockUtil.lockForRead(scoreCorrectionAndCompetitorChangeListenersLock);
|
||||
try {
|
||||
containsKey = scoreCorrectionListeners.containsKey(leaderboard);
|
||||
} finally {
|
||||
LockUtil.unlockAfterRead(scoreCorrectionListenersLock);
|
||||
LockUtil.unlockAfterRead(scoreCorrectionAndCompetitorChangeListenersLock);
|
||||
}
|
||||
if (!containsKey) {
|
||||
LockUtil.lockForWrite(scoreCorrectionListenersLock);
|
||||
LockUtil.lockForWrite(scoreCorrectionAndCompetitorChangeListenersLock);
|
||||
try {
|
||||
for (TrackedRace trackedRace : leaderboard.getTrackedRaces()) {
|
||||
registerListener(leaderboard, trackedRace);
|
||||
}
|
||||
final CacheInvalidationUponScoreCorrectionListener scoreCorrectionListener = new CacheInvalidationUponScoreCorrectionListener(
|
||||
leaderboard);
|
||||
final CacheInvalidationUponCompetitorChangeListener competitorChangeListener = new CacheInvalidationUponCompetitorChangeListener(
|
||||
leaderboard);
|
||||
leaderboard.getScoreCorrection().addScoreCorrectionListener(scoreCorrectionListener);
|
||||
scoreCorrectionListeners.put(leaderboard, scoreCorrectionListener);
|
||||
competitorChangeListeners.put(leaderboard, competitorChangeListener);
|
||||
final RaceColumnListener raceColumnListener = new RaceColumnListener() {
|
||||
private static final long serialVersionUID = 8165124797028386317L;
|
||||
|
||||
@@ -312,7 +387,7 @@ public class LeaderboardCacheManager {
|
||||
raceColumnListeners.put(leaderboard, raceColumnListener);
|
||||
}
|
||||
} finally {
|
||||
LockUtil.unlockAfterWrite(scoreCorrectionListenersLock);
|
||||
LockUtil.unlockAfterWrite(scoreCorrectionAndCompetitorChangeListenersLock);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+16
-7
@@ -24,11 +24,25 @@ public abstract class HighPointFirstGetsFixedScore extends HighPoint {
|
||||
public HighPointFirstGetsFixedScore(double scoreForRaceWinner) {
|
||||
this.scoreForRaceWinner = scoreForRaceWinner;
|
||||
}
|
||||
|
||||
protected double getScoreForRaceWinner() {
|
||||
return scoreForRaceWinner;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Double getScoreForRank(RaceColumn raceColumn, Competitor competitor, int rank, Callable<Integer> numberOfCompetitorsInRaceFetcher, NumberOfCompetitorsInLeaderboardFetcher numberOfCompetitorsInLeaderboardFetcher) {
|
||||
final int effectiveRank;
|
||||
final int effectiveRank = getEffectiveRank(raceColumn, competitor, rank);
|
||||
final Double result;
|
||||
if (effectiveRank == 0) {
|
||||
result = null;
|
||||
} else {
|
||||
result = Math.max(1.0, (double) (getScoreForRaceWinner() - effectiveRank + 1));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
protected int getEffectiveRank(RaceColumn raceColumn, Competitor competitor, int rank) {
|
||||
final int effectiveRank;
|
||||
int competitorFleetOrdering;
|
||||
if (rank == 0) {
|
||||
effectiveRank = 0;
|
||||
@@ -41,11 +55,6 @@ public abstract class HighPointFirstGetsFixedScore extends HighPoint {
|
||||
effectiveRank = rank;
|
||||
}
|
||||
}
|
||||
if (effectiveRank == 0) {
|
||||
result = null;
|
||||
} else {
|
||||
result = Math.max(1.0, (double) (scoreForRaceWinner - effectiveRank + 1));
|
||||
}
|
||||
return result;
|
||||
return effectiveRank;
|
||||
}
|
||||
}
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
package com.sap.sailing.domain.leaderboard.impl;
|
||||
|
||||
import com.sap.sailing.domain.common.ScoringSchemeType;
|
||||
|
||||
public class HighPointWinnerGetsSix extends HighPointFirstGetsFixedScore {
|
||||
private static final long serialVersionUID = -4116295702464748719L;
|
||||
|
||||
public HighPointWinnerGetsSix() {
|
||||
super(/* scoreForRaceWinner */ 6);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScoringSchemeType getType() {
|
||||
return ScoringSchemeType.HIGH_POINT_WINNER_GETS_SIX;
|
||||
}
|
||||
}
|
||||
+9
@@ -2,6 +2,7 @@ package com.sap.sailing.domain.tracking.impl;
|
||||
|
||||
import com.sap.sailing.domain.base.Boat;
|
||||
import com.sap.sailing.domain.base.Competitor;
|
||||
import com.sap.sailing.domain.base.CompetitorChangeListener;
|
||||
import com.sap.sailing.domain.base.IsManagedBySharedDomainFactory;
|
||||
import com.sap.sailing.domain.base.SharedDomainFactory;
|
||||
import com.sap.sailing.domain.base.Team;
|
||||
@@ -63,6 +64,14 @@ public class DummyMarkPassingWithTimePointOnly implements MarkPassing {
|
||||
public Color getColor() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addCompetitorChangeListener(CompetitorChangeListener listener) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeCompetitorChangeListener(CompetitorChangeListener listener) {
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1009,7 +1009,7 @@ public class GPSFixTrackImpl<ItemType, FixType extends GPSFix> extends TrackImpl
|
||||
final boolean result;
|
||||
lockForWrite();
|
||||
try {
|
||||
result = super.add(fix);
|
||||
result = addWithoutLocking(fix);
|
||||
invalidateValidityAndDistanceCaches(fix);
|
||||
} finally {
|
||||
unlockAfterWrite();
|
||||
|
||||
+161
-52
@@ -82,13 +82,21 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl
|
||||
|
||||
private final NavigableSet<TimePoint> timePointsWithCachedNullResult;
|
||||
|
||||
/**
|
||||
* Access to this cache is guarded by {@link #cacheLock}.
|
||||
*/
|
||||
private final NavigableSet<WindWithConfidence<TimePoint>> cache;
|
||||
|
||||
/**
|
||||
* Access to {@link #cache} and {@link #timePointsWithCachedNullResult} and
|
||||
* {@link #timePointsWithCachedNullResultFastContains} is guarded by this lock.
|
||||
*/
|
||||
private final NamedReentrantReadWriteLock cacheLock;
|
||||
|
||||
/**
|
||||
* Access to {@link #scheduledRefreshInterval} is guarded by this lock. In particular, when a client
|
||||
* holds this lock the client can be sure that {@link InvalidationInterval#isSet()} implies that a
|
||||
* cache update is scheduled or already running.
|
||||
*/
|
||||
private final NamedReentrantReadWriteLock scheduledRefreshIntervalLock;
|
||||
|
||||
private final Weigher<TimePoint> weigher;
|
||||
|
||||
/**
|
||||
@@ -106,7 +114,13 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl
|
||||
|
||||
private static class InvalidationInterval implements Serializable {
|
||||
private static final long serialVersionUID = -6406690520919193690L;
|
||||
/**
|
||||
* Either both {@link #start} and {@link #end} are <code>null</code>, or both are non-<code>null</code>
|
||||
*/
|
||||
private WindWithConfidence<TimePoint> start;
|
||||
/**
|
||||
* Either both {@link #start} and {@link #end} are <code>null</code>, or both are non-<code>null</code>
|
||||
*/
|
||||
private TimePoint end;
|
||||
public InvalidationInterval() {
|
||||
super();
|
||||
@@ -117,18 +131,38 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl
|
||||
public TimePoint getEnd() {
|
||||
return end;
|
||||
}
|
||||
public synchronized void clear() {
|
||||
public void clear() {
|
||||
start = null;
|
||||
end = null;
|
||||
}
|
||||
public synchronized boolean isSet() {
|
||||
public boolean isSet() {
|
||||
return start != null && end != null;
|
||||
}
|
||||
public synchronized void set(WindWithConfidence<TimePoint> startOfInvalidation, TimePoint endOfInvalidation) {
|
||||
|
||||
/**
|
||||
* Checks whether the interval starting at {@link #start} and ending at {@link #end} is fully contained in this
|
||||
* interval. Neither of the two arguments is allowed to be <code>null</code>. The interval is contained in this
|
||||
* interval if this interval {@link #isSet() is set} and this interval's {@link #getStart() start} is at or
|
||||
* before <code>start</code> and this interval's {@link #getEnd() end} is at or after <code>end</code>.
|
||||
*/
|
||||
public boolean contains(TimePoint start, TimePoint end) {
|
||||
return isSet() && !getStart().getObject().getTimePoint().after(start) && !end.after(getEnd());
|
||||
}
|
||||
/**
|
||||
* Neither of the two arguments is allowed to be <code>null</code>.
|
||||
*/
|
||||
public void set(WindWithConfidence<TimePoint> startOfInvalidation, TimePoint endOfInvalidation) {
|
||||
assert startOfInvalidation != null;
|
||||
assert endOfInvalidation != null;
|
||||
this.start = startOfInvalidation;
|
||||
this.end = endOfInvalidation;
|
||||
}
|
||||
public synchronized void extend(WindWithConfidence<TimePoint> startOfInvalidation, TimePoint endOfInvalidation) {
|
||||
/**
|
||||
* Neither of the two arguments is allowed to be <code>null</code>.
|
||||
*/
|
||||
public void extend(WindWithConfidence<TimePoint> startOfInvalidation, TimePoint endOfInvalidation) {
|
||||
assert startOfInvalidation != null;
|
||||
assert endOfInvalidation != null;
|
||||
if (startOfInvalidation.getObject().getTimePoint().compareTo(start.getObject().getTimePoint()) < 0) {
|
||||
this.start = startOfInvalidation;
|
||||
}
|
||||
@@ -136,11 +170,54 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl
|
||||
end = endOfInvalidation;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Leaves this object unchanged. Returns a new {@link InvalidationInterval} object. If the interval defined by
|
||||
* <code>start</code> and <code>end</code> does not exceed this interval, the resulting interval will have
|
||||
* {@link #isSet()}<code>==false</code>. Otherwise, the resulting interval will contain all time ranges from
|
||||
* <code>start</code> to <code>end</code> (inclusive) that are not in this interval. In particular, if
|
||||
* <code>start</code>..<code>end</code> exceeds this interval on both ends, the resulting interval will start at
|
||||
* <code>start</code> and end at <code>end</code>. If this interval has {@link #isSet()}<code>==false</code>,
|
||||
* the resulting interval will range from <code>start</code> to <code>end</code>.
|
||||
*
|
||||
* @param start must not be <code>null</code>
|
||||
* @param end must not be <code>null</code>
|
||||
*/
|
||||
public InvalidationInterval subtract(WindWithConfidence<TimePoint> start, TimePoint end) {
|
||||
assert start != null;
|
||||
assert end != null;
|
||||
final InvalidationInterval result = new InvalidationInterval();
|
||||
if (!isSet()) {
|
||||
result.set(start, end);
|
||||
} else {
|
||||
final WindWithConfidence<TimePoint> newStart;
|
||||
final TimePoint startTimePoint = getStart().getObject().getTimePoint();
|
||||
if (start.getObject().getTimePoint().before(startTimePoint)) {
|
||||
newStart = start;
|
||||
} else {
|
||||
// don't go beyond the end of time, avoiding overflow
|
||||
newStart = getDummyFixWithConfidence(getEnd().asMillis()==Long.MAX_VALUE?getEnd():getEnd().plus(1));
|
||||
}
|
||||
final TimePoint newEnd;
|
||||
if (end.after(getEnd())) {
|
||||
newEnd = end;
|
||||
} else {
|
||||
// avoid underflow
|
||||
newEnd = startTimePoint.asMillis()==0?startTimePoint:startTimePoint.minus(1);
|
||||
}
|
||||
if (!newStart.getObject().getTimePoint().after(newEnd)) {
|
||||
result.set(newStart, newEnd);
|
||||
} // else, the resulting interval remains unset
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link #scheduleCacheRefresh(WindWithConfidence, TimePoint)} synchronizes on this object before changing it
|
||||
* and when actually invalidating the cache.
|
||||
* {@link #scheduleCacheRefresh(WindWithConfidence, TimePoint)} synchronizes on this object before changing it and
|
||||
* when actually invalidating the cache. When a querying thread holds at least the read lock of {@link #cacheLock}
|
||||
* and the refresh interval it {@link InvalidationInterval#isSet() is set}, an invalidation is currently running or
|
||||
* has been scheduled.
|
||||
*/
|
||||
private final InvalidationInterval scheduledRefreshInterval;
|
||||
|
||||
@@ -171,8 +248,11 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl
|
||||
return o1.getObject().getTimePoint().compareTo(o2.getObject().getTimePoint());
|
||||
}
|
||||
});
|
||||
cacheLock = new NamedReentrantReadWriteLock(TrackBasedEstimationWindTrackImpl.class.getSimpleName()+" for race "+
|
||||
trackedRace.getRace().getName(), /* fair */ false);
|
||||
cacheLock = new NamedReentrantReadWriteLock(TrackBasedEstimationWindTrackImpl.class.getSimpleName()
|
||||
+ " cacheLock for race " + trackedRace.getRace().getName(), /* fair */false);
|
||||
scheduledRefreshIntervalLock = new NamedReentrantReadWriteLock(
|
||||
TrackBasedEstimationWindTrackImpl.class.getSimpleName() + " scheduledRefreshIntervalLock for race "
|
||||
+ trackedRace.getRace().getName(), /* fair */false);
|
||||
virtualInternalRawFixes = new EstimatedWindFixesAsNavigableSet(trackedRace);
|
||||
weigher = ConfidenceFactory.INSTANCE
|
||||
.createHyperbolicTimeDifferenceWeigher(getMillisecondsOverWhichToAverageWind());
|
||||
@@ -183,9 +263,11 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronizes serialization on this object to avoid the cache being updated while being written.
|
||||
* Uses the {@link #cacheLock} and {@link #scheduledRefreshIntervalLock} to make serialization on this object thread
|
||||
* safe by avoiding the cache being updated while being written.
|
||||
*/
|
||||
private synchronized void writeObject(ObjectOutputStream s) throws IOException {
|
||||
private void writeObject(ObjectOutputStream s) throws IOException {
|
||||
LockUtil.lockForRead(scheduledRefreshIntervalLock);
|
||||
LockUtil.lockForRead(cacheLock);
|
||||
lockForRead();
|
||||
try {
|
||||
@@ -193,6 +275,7 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl
|
||||
} finally {
|
||||
unlockAfterRead();
|
||||
LockUtil.unlockAfterRead(cacheLock);
|
||||
LockUtil.unlockAfterRead(scheduledRefreshIntervalLock);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,29 +310,22 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl
|
||||
return cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* The caller must <em>not</em> hold the read lock of {@link #cacheLock} when calling this method (unless the caller
|
||||
* also holds the corresponding write lock) because this method will acquire the corresponding write lock, and
|
||||
* upgrading from a read lock to a write lock is not possible.
|
||||
*/
|
||||
protected void cache(TimePoint timePoint, WindWithConfidence<TimePoint> fix) {
|
||||
// can't use lockForWrite() here because caching can happen while holding the read lock, and the lock can't be
|
||||
// upgraded. But lockForRead() and synchronization will do the job because all invalidations lock the write lock,
|
||||
// and all contains() checks and get() calls use synchronization too.
|
||||
LockUtil.lockForWrite(cacheLock);
|
||||
try {
|
||||
// synchronization necessary to protect writeObject from ConcurrentModificationException
|
||||
synchronized (this) {
|
||||
if (fix == null) {
|
||||
synchronized (timePointsWithCachedNullResult) {
|
||||
timePointsWithCachedNullResult.add(timePoint);
|
||||
}
|
||||
synchronized (timePointsWithCachedNullResultFastContains) {
|
||||
timePointsWithCachedNullResultFastContains.add(timePoint);
|
||||
}
|
||||
} else {
|
||||
LockUtil.lockForWrite(cacheLock);
|
||||
try {
|
||||
cache.add(fix);
|
||||
} finally {
|
||||
LockUtil.unlockAfterWrite(cacheLock);
|
||||
}
|
||||
}
|
||||
if (fix == null) {
|
||||
timePointsWithCachedNullResult.add(timePoint);
|
||||
timePointsWithCachedNullResultFastContains.add(timePoint);
|
||||
} else {
|
||||
cache.add(fix);
|
||||
}
|
||||
} finally {
|
||||
LockUtil.unlockAfterWrite(cacheLock);
|
||||
@@ -263,19 +339,29 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl
|
||||
* will not be massively delayed by having to re-calculate the estimation over and over again.
|
||||
*/
|
||||
private void scheduleCacheRefresh(WindWithConfidence<TimePoint> startOfInvalidation, TimePoint endOfInvalidation) {
|
||||
LockUtil.lockForWrite(cacheLock);
|
||||
final boolean alreadyContained;
|
||||
LockUtil.lockForRead(scheduledRefreshIntervalLock);
|
||||
try {
|
||||
if (!scheduledRefreshInterval.isSet()) {
|
||||
// according to the invariant this implies [1]==null
|
||||
scheduledRefreshInterval.set(startOfInvalidation, endOfInvalidation);
|
||||
startSchedulerForCacheRefresh();
|
||||
} else {
|
||||
// this means that an invalidation is already scheduled; as long as we're synchronized on scheduledInvalidationInterval
|
||||
// we can safely extend the interval; the invalidation won't start before we release the lock
|
||||
scheduledRefreshInterval.extend(startOfInvalidation, endOfInvalidation);
|
||||
}
|
||||
alreadyContained = scheduledRefreshInterval.contains(startOfInvalidation.getObject().getTimePoint(), endOfInvalidation);
|
||||
} finally {
|
||||
LockUtil.unlockAfterWrite(cacheLock);
|
||||
LockUtil.unlockAfterRead(scheduledRefreshIntervalLock);
|
||||
}
|
||||
if (!alreadyContained) {
|
||||
LockUtil.lockForWrite(scheduledRefreshIntervalLock);
|
||||
try {
|
||||
if (!scheduledRefreshInterval.isSet()) {
|
||||
// according to the invariant this implies [1]==null
|
||||
scheduledRefreshInterval.set(startOfInvalidation, endOfInvalidation);
|
||||
startSchedulerForCacheRefresh();
|
||||
} else {
|
||||
// this means that an invalidation or incremental refresh is already scheduled; as long as we're
|
||||
// synchronized on scheduledInvalidationInterval we can safely extend the interval; the invalidation
|
||||
// won't start before we release the lock
|
||||
scheduledRefreshInterval.extend(startOfInvalidation, endOfInvalidation);
|
||||
}
|
||||
} finally {
|
||||
LockUtil.unlockAfterWrite(scheduledRefreshIntervalLock);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,6 +371,7 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl
|
||||
* running.
|
||||
*/
|
||||
private void invalidateCache() {
|
||||
LockUtil.lockForWrite(scheduledRefreshIntervalLock);
|
||||
LockUtil.lockForWrite(cacheLock);
|
||||
try {
|
||||
Iterator<WindWithConfidence<TimePoint>> iter = (scheduledRefreshInterval.getStart() == null ? getCachedFixes()
|
||||
@@ -312,6 +399,7 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl
|
||||
scheduledRefreshInterval.clear();
|
||||
} finally {
|
||||
LockUtil.unlockAfterWrite(cacheLock);
|
||||
LockUtil.unlockAfterWrite(scheduledRefreshIntervalLock);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,27 +409,33 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl
|
||||
private void refreshCacheIncrementally() {
|
||||
Set<WindWithConfidence<TimePoint>> windFixesToRecalculate = new HashSet<WindWithConfidence<TimePoint>>();
|
||||
Set<TimePoint> cachedNullResultsToRecalculate = new HashSet<TimePoint>();
|
||||
final WindWithConfidence<TimePoint> refreshIntervalStart;
|
||||
final TimePoint refreshIntervalEnd;
|
||||
LockUtil.lockForRead(scheduledRefreshIntervalLock);
|
||||
LockUtil.lockForRead(cacheLock);
|
||||
try {
|
||||
Iterator<WindWithConfidence<TimePoint>> iter = (scheduledRefreshInterval.getStart() == null ? getCachedFixes()
|
||||
: getCachedFixes().tailSet(scheduledRefreshInterval.getStart(), /* inclusive */true)).iterator();
|
||||
Iterator<TimePoint> nullIter = (scheduledRefreshInterval.getStart() == null ? timePointsWithCachedNullResult
|
||||
: timePointsWithCachedNullResult.tailSet(scheduledRefreshInterval.getStart().getObject().getTimePoint(), /* inclusive */
|
||||
refreshIntervalStart = scheduledRefreshInterval.getStart();
|
||||
Iterator<WindWithConfidence<TimePoint>> iter = (refreshIntervalStart == null ? getCachedFixes()
|
||||
: getCachedFixes().tailSet(refreshIntervalStart, /* inclusive */true)).iterator();
|
||||
Iterator<TimePoint> nullIter = (refreshIntervalStart == null ? timePointsWithCachedNullResult
|
||||
: timePointsWithCachedNullResult.tailSet(refreshIntervalStart.getObject().getTimePoint(), /* inclusive */
|
||||
true)).iterator();
|
||||
WindWithConfidence<TimePoint> nextFixToRecalculate = null;
|
||||
refreshIntervalEnd = scheduledRefreshInterval.getEnd();
|
||||
while (iter.hasNext() &&
|
||||
((nextFixToRecalculate = iter.next()).getObject().getTimePoint().compareTo(scheduledRefreshInterval.getEnd()) < 0) ||
|
||||
scheduledRefreshInterval.getEnd() == null) {
|
||||
((nextFixToRecalculate = iter.next()).getObject().getTimePoint().compareTo(refreshIntervalEnd) < 0) ||
|
||||
refreshIntervalEnd == null) {
|
||||
windFixesToRecalculate.add(nextFixToRecalculate);
|
||||
}
|
||||
TimePoint nextNullResultToRecalculate = null;
|
||||
while (nullIter.hasNext() &&
|
||||
((nextNullResultToRecalculate = nullIter.next()).compareTo(scheduledRefreshInterval.getEnd()) < 0) ||
|
||||
scheduledRefreshInterval.getEnd() == null) {
|
||||
((nextNullResultToRecalculate = nullIter.next()).compareTo(refreshIntervalEnd) < 0) ||
|
||||
refreshIntervalEnd == null) {
|
||||
cachedNullResultsToRecalculate.add(nextNullResultToRecalculate);
|
||||
}
|
||||
} finally {
|
||||
LockUtil.unlockAfterRead(cacheLock);
|
||||
LockUtil.unlockAfterRead(scheduledRefreshIntervalLock);
|
||||
}
|
||||
Set<TimePoint> nullRemovals = new HashSet<TimePoint>();
|
||||
Set<TimePoint> nullInsertions = new HashSet<TimePoint>();
|
||||
@@ -381,10 +475,22 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl
|
||||
for (Map.Entry<TimePoint, WindWithConfidence<TimePoint>> cacheInsertion : cacheInsertions.entrySet()) {
|
||||
cache(cacheInsertion.getKey(), cacheInsertion.getValue());
|
||||
}
|
||||
scheduledRefreshInterval.clear();
|
||||
} finally {
|
||||
LockUtil.unlockAfterWrite(cacheLock);
|
||||
}
|
||||
LockUtil.lockForWrite(scheduledRefreshIntervalLock);
|
||||
try {
|
||||
// now remove the interval for which the cache was refreshed; note that in between the interval may have been extended;
|
||||
// for the extended part of the interval that hasn't been refreshed during this run, a new refresh needs to be
|
||||
// requested
|
||||
InvalidationInterval remainingRefreshInterval = scheduledRefreshInterval.subtract(refreshIntervalStart, refreshIntervalEnd);
|
||||
scheduledRefreshInterval.clear();
|
||||
if (remainingRefreshInterval.isSet()) {
|
||||
scheduleCacheRefresh(remainingRefreshInterval.getStart(), remainingRefreshInterval.getEnd());
|
||||
}
|
||||
} finally {
|
||||
LockUtil.unlockAfterWrite(scheduledRefreshIntervalLock);
|
||||
}
|
||||
}
|
||||
|
||||
private void startSchedulerForCacheRefresh() {
|
||||
@@ -452,15 +558,18 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl
|
||||
return result;
|
||||
}
|
||||
|
||||
private WindWithConfidence<TimePoint> getDummyFixWithConfidence(TimePoint timePoint) {
|
||||
private static WindWithConfidence<TimePoint> getDummyFixWithConfidence(TimePoint timePoint) {
|
||||
return new WindWithConfidenceImpl<TimePoint>(new WindImpl(null, timePoint, defaultSpeedWithBearing), 0,
|
||||
timePoint, /* useSpeed */false);
|
||||
}
|
||||
|
||||
private boolean nullResultCacheContains(TimePoint timePoint) {
|
||||
assertReadLock();
|
||||
synchronized (timePointsWithCachedNullResultFastContains) {
|
||||
LockUtil.lockForRead(cacheLock);
|
||||
try {
|
||||
return timePointsWithCachedNullResultFastContains.contains(timePoint);
|
||||
} finally {
|
||||
LockUtil.unlockAfterRead(cacheLock);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -52,10 +52,6 @@ public abstract class VirtualWindFixesAsNavigableSet extends AbstractUnmodifiabl
|
||||
*/
|
||||
abstract protected Wind getWind(Position p, TimePoint timePoint);
|
||||
|
||||
public long getResolutionInMilliseconds() {
|
||||
return resolutionInMilliseconds;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param from expected to be an integer multiple of {@link #resolutionInMilliseconds} or <code>null</code>
|
||||
* @param to expected to be an integer multiple of {@link #resolutionInMilliseconds} or <code>null</code>
|
||||
@@ -71,6 +67,10 @@ public abstract class VirtualWindFixesAsNavigableSet extends AbstractUnmodifiabl
|
||||
this.resolutionInMilliseconds = resolutionInMilliseconds;
|
||||
}
|
||||
|
||||
public long getResolutionInMilliseconds() {
|
||||
return resolutionInMilliseconds;
|
||||
}
|
||||
|
||||
protected WindTrack getTrack() {
|
||||
return track;
|
||||
}
|
||||
|
||||
+8
-4
@@ -132,10 +132,14 @@ public class CompetitorEditDialog extends DataEntryDialog<CompetitorDTO> {
|
||||
@Override
|
||||
protected CompetitorDTO getResult() {
|
||||
Color color;
|
||||
try {
|
||||
color = new RGBColor(displayColorTextBox.getText());
|
||||
} catch (IllegalArgumentException iae) {
|
||||
color = new InvalidColor(iae);
|
||||
if (displayColorTextBox.getText() == null || displayColorTextBox.getText().isEmpty()) {
|
||||
color = null;
|
||||
} else {
|
||||
try {
|
||||
color = new RGBColor(displayColorTextBox.getText());
|
||||
} catch (IllegalArgumentException iae) {
|
||||
color = new InvalidColor(iae);
|
||||
}
|
||||
}
|
||||
CompetitorDTO result = new CompetitorDTOImpl(name.getText(), color,
|
||||
/* twoLetterIsoCountryCode */ null,
|
||||
|
||||
+1
@@ -373,6 +373,7 @@ public interface StringMessages extends Messages {
|
||||
String scoringSchemeLowPointSystem();
|
||||
String scoringSchemeHighPointSystem();
|
||||
String scoringSchemeLowPointWinnerGetsZero();
|
||||
String scoringSchemeWinnerGetsSix();
|
||||
String scoringSystem();
|
||||
String createFlexibleLeaderboard();
|
||||
String createRegattaLeaderboard();
|
||||
|
||||
+1
@@ -402,6 +402,7 @@ scoringSchemeHighPointSystem=High Point System
|
||||
scoringSchemeHighPointEssOverall=High Point System ESS Overall
|
||||
scoringSchemeHighPointLastBreaksTie=High Point System, Last Race Breaks Tie
|
||||
scoringSchemeLowPointWinnerGetsZero=Low Point System, Winner Gets Zero
|
||||
scoringSchemeWinnerGetsSix=High Point System, Winner Gets 6 Points
|
||||
scoringSystem=Scoring system
|
||||
createFlexibleLeaderboard=Create flexible leaderboard
|
||||
createRegattaLeaderboard=Create regatta leaderboard
|
||||
|
||||
+1
@@ -403,6 +403,7 @@ scoringSchemeHighPointSystem=High Point System
|
||||
scoringSchemeHighPointEssOverall=High Point System ESS Gesamtwertung
|
||||
scoringSchemeHighPointLastBreaksTie=High Point System, letztes Rennen entscheidet bei Gleichstand
|
||||
scoringSchemeLowPointWinnerGetsZero=Low Point System, Gewinner erhält 0 Punkte
|
||||
scoringSchemeWinnerGetsSix=High Point System, Gewinner erhält 6 Punkte
|
||||
scoringSystem=Bepunktungssystem
|
||||
createFlexibleLeaderboard=Flexible Rangliste anlegen
|
||||
createRegattaLeaderboard=Rangliste für Regatta anlegen
|
||||
|
||||
+2
@@ -20,6 +20,8 @@ public class ScoringSchemeTypeFormatter {
|
||||
return stringMessages.scoringSchemeHighPointFirstGetsTen();
|
||||
case LOW_POINT_WINNER_GETS_ZERO:
|
||||
return stringMessages.scoringSchemeLowPointWinnerGetsZero();
|
||||
case HIGH_POINT_WINNER_GETS_SIX:
|
||||
return stringMessages.scoringSchemeWinnerGetsSix();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
+1
-2
@@ -6,7 +6,6 @@ import java.util.Set;
|
||||
import org.json.simple.JSONArray;
|
||||
import org.json.simple.JSONObject;
|
||||
|
||||
import com.sap.sailing.domain.base.Person;
|
||||
import com.sap.sailing.domain.base.SharedDomainFactory;
|
||||
import com.sap.sailing.domain.base.impl.DynamicPerson;
|
||||
import com.sap.sailing.domain.base.impl.DynamicTeam;
|
||||
@@ -32,7 +31,7 @@ public class TeamJsonDeserializer implements JsonDeserializer<DynamicTeam> {
|
||||
public DynamicTeam deserialize(JSONObject object)
|
||||
throws JsonDeserializationException {
|
||||
String name = (String) object.get(TeamJsonSerializer.FIELD_NAME);
|
||||
Person coach = personDeserializer.deserialize((JSONObject) object
|
||||
DynamicPerson coach = personDeserializer.deserialize((JSONObject) object
|
||||
.get(TeamJsonSerializer.FIELD_COACH));
|
||||
Set<DynamicPerson> sailors = new HashSet<DynamicPerson>();
|
||||
|
||||
|
||||
+17
-18
@@ -37,7 +37,6 @@ import com.sap.sailing.domain.base.CourseArea;
|
||||
import com.sap.sailing.domain.base.DomainFactory;
|
||||
import com.sap.sailing.domain.base.Event;
|
||||
import com.sap.sailing.domain.base.Fleet;
|
||||
import com.sap.sailing.domain.base.Person;
|
||||
import com.sap.sailing.domain.base.RaceColumn;
|
||||
import com.sap.sailing.domain.base.Regatta;
|
||||
import com.sap.sailing.domain.base.Series;
|
||||
@@ -193,7 +192,7 @@ public class MasterDataImportTest {
|
||||
Set<DynamicPerson> sailors = new HashSet<DynamicPerson>();
|
||||
sailors.add(new PersonImpl("Froderik Poterson", new NationalityImpl("GER"), new Date(645487200000L),
|
||||
"Oberhoschy"));
|
||||
Person coach = new PersonImpl("Lennart Hensler", new NationalityImpl("GER"), new Date(645487200000L),
|
||||
DynamicPerson coach = new PersonImpl("Lennart Hensler", new NationalityImpl("GER"), new Date(645487200000L),
|
||||
"Der Lennart halt");
|
||||
DynamicTeam team = new TeamImpl("Pros", sailors, coach);
|
||||
BoatClass boatClass = new BoatClassImpl("H16", true);
|
||||
@@ -204,7 +203,7 @@ public class MasterDataImportTest {
|
||||
Set<DynamicPerson> sailors2 = new HashSet<DynamicPerson>();
|
||||
sailors2.add(new PersonImpl("Angela Merkel", new NationalityImpl("GER"), new Date(645487200000L),
|
||||
"segelt auch mit"));
|
||||
Person coach2 = new PersonImpl("Peer Steinbrueck", new NationalityImpl("GER"), new Date(645487200000L),
|
||||
DynamicPerson coach2 = new PersonImpl("Peer Steinbrueck", new NationalityImpl("GER"), new Date(645487200000L),
|
||||
"Bester Coach");
|
||||
DynamicTeam team2 = new TeamImpl("Noobs", sailors2, coach2);
|
||||
DynamicBoat boat2 = new BoatImpl("LahmeEnte", boatClass, "GER1337");
|
||||
@@ -402,7 +401,7 @@ public class MasterDataImportTest {
|
||||
Set<DynamicPerson> sailors = new HashSet<DynamicPerson>();
|
||||
sailors.add(new PersonImpl("Froderik Poterson", new NationalityImpl("GER"), new Date(645487200000L),
|
||||
"Oberhoschy"));
|
||||
Person coach = new PersonImpl("Lennart Hensler", new NationalityImpl("GER"), new Date(645487200000L),
|
||||
DynamicPerson coach = new PersonImpl("Lennart Hensler", new NationalityImpl("GER"), new Date(645487200000L),
|
||||
"Der Lennart halt");
|
||||
DynamicTeam team = new TeamImpl("Pros", sailors, coach);
|
||||
BoatClass boatClass = new BoatClassImpl("H16", true);
|
||||
@@ -412,7 +411,7 @@ public class MasterDataImportTest {
|
||||
UUID competitor2UUID = UUID.randomUUID();
|
||||
Set<DynamicPerson> sailors2 = new HashSet<DynamicPerson>();
|
||||
sailors2.add(new PersonImpl("Test Mustermann", new NationalityImpl("GER"), new Date(645487200000L), "desc"));
|
||||
Person coach2 = new PersonImpl("Max Test", new NationalityImpl("GER"), new Date(645487200000L), "desc");
|
||||
DynamicPerson coach2 = new PersonImpl("Max Test", new NationalityImpl("GER"), new Date(645487200000L), "desc");
|
||||
DynamicTeam team2 = new TeamImpl("Pros2", sailors2, coach2);
|
||||
DynamicBoat boat2 = new BoatImpl("FastBoat", boatClass, "GER70133");
|
||||
CompetitorImpl competitor2 = new CompetitorImpl(competitor2UUID, "Froderik", Color.RED, team2, boat2);
|
||||
@@ -678,7 +677,7 @@ public class MasterDataImportTest {
|
||||
Set<DynamicPerson> sailors = new HashSet<DynamicPerson>();
|
||||
sailors.add(new PersonImpl("Froderik Poterson", new NationalityImpl("GER"), new Date(645487200000L),
|
||||
"Oberhoschy"));
|
||||
Person coach = new PersonImpl("Lennart Hensler", new NationalityImpl("GER"), new Date(645487200000L),
|
||||
DynamicPerson coach = new PersonImpl("Lennart Hensler", new NationalityImpl("GER"), new Date(645487200000L),
|
||||
"Der Lennart halt");
|
||||
DynamicTeam team = new TeamImpl("Pros", sailors, coach);
|
||||
BoatClass boatClass = new BoatClassImpl("H16", true);
|
||||
@@ -689,7 +688,7 @@ public class MasterDataImportTest {
|
||||
UUID competitor2UUID = UUID.randomUUID();
|
||||
Set<DynamicPerson> sailors2 = new HashSet<DynamicPerson>();
|
||||
sailors2.add(new PersonImpl("Test Mustermann", new NationalityImpl("GER"), new Date(645487200000L), "desc"));
|
||||
Person coach2 = new PersonImpl("Max Test", new NationalityImpl("GER"), new Date(645487200000L), "desc");
|
||||
DynamicPerson coach2 = new PersonImpl("Max Test", new NationalityImpl("GER"), new Date(645487200000L), "desc");
|
||||
DynamicTeam team2 = new TeamImpl("Pros2", sailors2, coach2);
|
||||
DynamicBoat boat2 = new BoatImpl("FastBoat", boatClass, "GER70133");
|
||||
Competitor competitor2 = sourceDomainFactory.getCompetitorStore().getOrCreateCompetitor(competitor2UUID,
|
||||
@@ -833,7 +832,7 @@ public class MasterDataImportTest {
|
||||
Set<DynamicPerson> sailors = new HashSet<DynamicPerson>();
|
||||
sailors.add(new PersonImpl("Froderik Poterson", new NationalityImpl("GER"), new Date(645487200000L),
|
||||
"Oberhoschy"));
|
||||
Person coach = new PersonImpl("Lennart Hensler", new NationalityImpl("GER"), new Date(645487200000L),
|
||||
DynamicPerson coach = new PersonImpl("Lennart Hensler", new NationalityImpl("GER"), new Date(645487200000L),
|
||||
"Der Lennart halt");
|
||||
DynamicTeam team = new TeamImpl("Pros", sailors, coach);
|
||||
BoatClass boatClass = new BoatClassImpl("H16", true);
|
||||
@@ -844,7 +843,7 @@ public class MasterDataImportTest {
|
||||
Set<DynamicPerson> sailors2 = new HashSet<DynamicPerson>();
|
||||
sailors2.add(new PersonImpl("Angela Merkel", new NationalityImpl("GER"), new Date(645487200000L),
|
||||
"segelt auch mit"));
|
||||
Person coach2 = new PersonImpl("Peer Steinbrueck", new NationalityImpl("GER"), new Date(645487200000L),
|
||||
DynamicPerson coach2 = new PersonImpl("Peer Steinbrueck", new NationalityImpl("GER"), new Date(645487200000L),
|
||||
"Bester Coach");
|
||||
DynamicTeam team2 = new TeamImpl("Noobs", sailors2, coach2);
|
||||
DynamicBoat boat2 = new BoatImpl("LahmeEnte", boatClass, "GER1337");
|
||||
@@ -1029,7 +1028,7 @@ public class MasterDataImportTest {
|
||||
Set<DynamicPerson> sailors = new HashSet<DynamicPerson>();
|
||||
sailors.add(new PersonImpl("Froderik Poterson", new NationalityImpl("GER"), new Date(645487200000L),
|
||||
"Oberhoschy"));
|
||||
Person coach = new PersonImpl("Lennart Hensler", new NationalityImpl("GER"), new Date(645487200000L),
|
||||
DynamicPerson coach = new PersonImpl("Lennart Hensler", new NationalityImpl("GER"), new Date(645487200000L),
|
||||
"Der Lennart halt");
|
||||
DynamicTeam team = new TeamImpl("Pros", sailors, coach);
|
||||
BoatClass boatClass = new BoatClassImpl("H16", true);
|
||||
@@ -1040,7 +1039,7 @@ public class MasterDataImportTest {
|
||||
Set<DynamicPerson> sailors2 = new HashSet<DynamicPerson>();
|
||||
sailors2.add(new PersonImpl("Angela Merkel", new NationalityImpl("GER"), new Date(645487200000L),
|
||||
"segelt auch mit"));
|
||||
Person coach2 = new PersonImpl("Peer Steinbrueck", new NationalityImpl("GER"), new Date(645487200000L),
|
||||
DynamicPerson coach2 = new PersonImpl("Peer Steinbrueck", new NationalityImpl("GER"), new Date(645487200000L),
|
||||
"Bester Coach");
|
||||
DynamicTeam team2 = new TeamImpl("Noobs", sailors2, coach2);
|
||||
DynamicBoat boat2 = new BoatImpl("LahmeEnte", boatClass, "GER1337");
|
||||
@@ -1127,7 +1126,7 @@ public class MasterDataImportTest {
|
||||
Set<DynamicPerson> sailorsToOverride = new HashSet<DynamicPerson>();
|
||||
sailorsToOverride.add(new PersonImpl("Froderik Poterson", new NationalityImpl("GER"), new Date(
|
||||
645487200000L), "Oberhoschy"));
|
||||
Person coachToOverride = new PersonImpl("Lennart Hensler", new NationalityImpl("GER"), new Date(
|
||||
DynamicPerson coachToOverride = new PersonImpl("Lennart Hensler", new NationalityImpl("GER"), new Date(
|
||||
645487200000L), "Der Lennart halt");
|
||||
DynamicTeam teamToOverride = new TeamImpl("Pros", sailorsToOverride, coachToOverride);
|
||||
BoatClass boatClassToOverride = new BoatClassImpl("H16", true);
|
||||
@@ -1298,7 +1297,7 @@ public class MasterDataImportTest {
|
||||
Set<DynamicPerson> sailors = new HashSet<DynamicPerson>();
|
||||
sailors.add(new PersonImpl("Froderik Poterson", new NationalityImpl("GER"), new Date(645487200000L),
|
||||
"Oberhoschy"));
|
||||
Person coach = new PersonImpl("Lennart Hensler", new NationalityImpl("GER"), new Date(645487200000L),
|
||||
DynamicPerson coach = new PersonImpl("Lennart Hensler", new NationalityImpl("GER"), new Date(645487200000L),
|
||||
"Der Lennart halt");
|
||||
DynamicTeam team = new TeamImpl("Pros", sailors, coach);
|
||||
BoatClass boatClass = new BoatClassImpl("H16", true);
|
||||
@@ -1309,7 +1308,7 @@ public class MasterDataImportTest {
|
||||
Set<DynamicPerson> sailors2 = new HashSet<DynamicPerson>();
|
||||
sailors2.add(new PersonImpl("Angela Merkel", new NationalityImpl("GER"), new Date(645487200000L),
|
||||
"segelt auch mit"));
|
||||
Person coach2 = new PersonImpl("Peer Steinbrueck", new NationalityImpl("GER"), new Date(645487200000L),
|
||||
DynamicPerson coach2 = new PersonImpl("Peer Steinbrueck", new NationalityImpl("GER"), new Date(645487200000L),
|
||||
"Bester Coach");
|
||||
DynamicTeam team2 = new TeamImpl("Noobs", sailors2, coach2);
|
||||
DynamicBoat boat2 = new BoatImpl("LahmeEnte", boatClass, "GER1337");
|
||||
@@ -1431,7 +1430,7 @@ public class MasterDataImportTest {
|
||||
Set<DynamicPerson> sailors = new HashSet<DynamicPerson>();
|
||||
sailors.add(new PersonImpl("Froderik Poterson", new NationalityImpl("GER"), new Date(645487200000L),
|
||||
"Oberhoschy"));
|
||||
Person coach = new PersonImpl("Lennart Hensler", new NationalityImpl("GER"), new Date(645487200000L),
|
||||
DynamicPerson coach = new PersonImpl("Lennart Hensler", new NationalityImpl("GER"), new Date(645487200000L),
|
||||
"Der Lennart halt");
|
||||
DynamicTeam team = new TeamImpl("Pros", sailors, coach);
|
||||
BoatClass boatClass = new BoatClassImpl("H16", true);
|
||||
@@ -1442,7 +1441,7 @@ public class MasterDataImportTest {
|
||||
Set<DynamicPerson> sailors2 = new HashSet<DynamicPerson>();
|
||||
sailors2.add(new PersonImpl("Angela Merkel", new NationalityImpl("GER"), new Date(645487200000L),
|
||||
"segelt auch mit"));
|
||||
Person coach2 = new PersonImpl("Peer Steinbrueck", new NationalityImpl("GER"), new Date(645487200000L),
|
||||
DynamicPerson coach2 = new PersonImpl("Peer Steinbrueck", new NationalityImpl("GER"), new Date(645487200000L),
|
||||
"Bester Coach");
|
||||
DynamicTeam team2 = new TeamImpl("Noobs", sailors2, coach2);
|
||||
DynamicBoat boat2 = new BoatImpl("LahmeEnte", boatClass, "GER1337");
|
||||
@@ -1638,7 +1637,7 @@ public class MasterDataImportTest {
|
||||
Set<DynamicPerson> sailors = new HashSet<DynamicPerson>();
|
||||
sailors.add(new PersonImpl("Froderik Poterson", new NationalityImpl("GER"), new Date(645487200000L),
|
||||
"Oberhoschy"));
|
||||
Person coach = new PersonImpl("Lennart Hensler", new NationalityImpl("GER"), new Date(645487200000L),
|
||||
DynamicPerson coach = new PersonImpl("Lennart Hensler", new NationalityImpl("GER"), new Date(645487200000L),
|
||||
"Der Lennart halt");
|
||||
DynamicTeam team = new TeamImpl("Pros", sailors, coach);
|
||||
BoatClass boatClass = new BoatClassImpl("H16", true);
|
||||
@@ -1649,7 +1648,7 @@ public class MasterDataImportTest {
|
||||
Set<DynamicPerson> sailors2 = new HashSet<DynamicPerson>();
|
||||
sailors2.add(new PersonImpl("Angela Merkel", new NationalityImpl("GER"), new Date(645487200000L),
|
||||
"segelt auch mit"));
|
||||
Person coach2 = new PersonImpl("Peer Steinbrueck", new NationalityImpl("GER"), new Date(645487200000L),
|
||||
DynamicPerson coach2 = new PersonImpl("Peer Steinbrueck", new NationalityImpl("GER"), new Date(645487200000L),
|
||||
"Bester Coach");
|
||||
DynamicTeam team2 = new TeamImpl("Noobs", sailors2, coach2);
|
||||
DynamicBoat boat2 = new BoatImpl("LahmeEnte", boatClass, "GER1337");
|
||||
|
||||
+3
-1
@@ -29,7 +29,9 @@ public class UpdateCompetitor extends AbstractRacingEventServiceOperation<Compet
|
||||
|
||||
@Override
|
||||
public Competitor internalApplyTo(RacingEventService toState) throws Exception {
|
||||
return toState.getBaseDomainFactory().getCompetitorStore().updateCompetitor(idAsString, newName, newDisplayColor, newSailId, newNationality);
|
||||
Competitor result = toState.getBaseDomainFactory().getCompetitorStore()
|
||||
.updateCompetitor(idAsString, newName, newDisplayColor, newSailId, newNationality);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -95,7 +95,30 @@
|
||||
<a href='http://www.extremesailingseries.com' target='_blank'>Official website</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class='raceSection'>
|
||||
|
||||
<div class='raceSection'>
|
||||
<h5 class='date'>
|
||||
2013 Season
|
||||
</h5>
|
||||
<div class='raceTimepoint'></div>
|
||||
<a href='http://bundesliga2013.sapsailing.com' target='_blank'>
|
||||
<ul class='racePanel'>
|
||||
<li class='raceName'>
|
||||
German Sailing League / Bundesliga 2013
|
||||
</li>
|
||||
<li class='raceLocation'>
|
||||
Germany
|
||||
</li>
|
||||
</ul>
|
||||
</a>
|
||||
<div class='raceDescription'>
|
||||
<p>German Sailing League / Bundesliga 2013</p>
|
||||
<a href='http://bundesliga2013.sapsailing.com' target='_blank'>SAP Sailing Analytics</a><br/>
|
||||
<a href='http://segelbundesliga.de' target='_blank'>Official website</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='raceSection'>
|
||||
<h5 class='date'>
|
||||
Sep 19-22
|
||||
</h5>
|
||||
@@ -115,7 +138,7 @@
|
||||
<a href='http://505idm2013.sapsailing.com' target='_blank'>SAP Sailing Analytics</a><br/>
|
||||
<a href='http://505idm2013.de' target='_blank'>Official website</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='raceSection'>
|
||||
<h5 class='date'>
|
||||
July 19 - 28
|
||||
|
||||
@@ -38,6 +38,12 @@
|
||||
<h2 class="articleSubheadline">May 2014</h2>
|
||||
|
||||
<ul class="bulletList">
|
||||
<li>A hint regarding the "distance to start line" metrics: please keep in mind that currently this
|
||||
measurement is calculated as the distance of the GPS tracker from the line. Unless the tracker
|
||||
is mounted near the bow (or at the bow sprit, if any), the distance calculated this way will
|
||||
be greater than what counts for crossing the line. Ordering, comparing and aggregating over this
|
||||
figure still makes sense as long as you remember this detail.
|
||||
</li>
|
||||
<li>Introduced a new leaderboard entry "XTE +/-" featuring a signed cross-track error which shows
|
||||
for a leg and also aggregated for an entire race on which side of the leg a competitor stayed on
|
||||
average. Currently, the "side" is defined as viewing in the leg's direction, so "left" for a downwind
|
||||
|
||||
Reference in new issue
Block a user