diff --git a/java/com.sap.sailing.domain.common/META-INF/MANIFEST.MF b/java/com.sap.sailing.domain.common/META-INF/MANIFEST.MF index 366d8cd72b9..b6f50c6f473 100644 --- a/java/com.sap.sailing.domain.common/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.domain.common/META-INF/MANIFEST.MF @@ -17,17 +17,19 @@ Export-Package: com.sap.sailing.domain.common, com.sap.sailing.domain.common.quadtree, com.sap.sailing.domain.common.quadtree.impl;x-friends:="com.sap.sailing.domain.test", com.sap.sailing.domain.common.racelog, - com.sap.sailing.domain.common.sharding, com.sap.sailing.domain.common.racelog.tracking, com.sap.sailing.domain.common.racelog.utils, com.sap.sailing.domain.common.scalablevalue.impl, com.sap.sailing.domain.common.security, com.sap.sailing.domain.common.sensordata, + com.sap.sailing.domain.common.sharding, + com.sap.sailing.domain.common.tagging, com.sap.sailing.domain.common.trackfiles, com.sap.sailing.domain.common.tracking, com.sap.sailing.domain.common.tracking.impl, com.sap.sailing.domain.common.windfinder Require-Bundle: com.sap.sse.common, com.sap.sse.security.common, - com.sap.sse.datamining.annotations;bundle-version="1.0.0" + com.sap.sse.datamining.annotations;bundle-version="1.0.0", + org.json.simple;bundle-version="1.1.0" Automatic-Module-Name: com.sap.sailing.domain.common diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/TagDTO.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/TagDTO.java new file mode 100644 index 00000000000..39dc7489cad --- /dev/null +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/TagDTO.java @@ -0,0 +1,296 @@ +package com.sap.sailing.domain.common.dto; + +import java.io.Serializable; + +import com.sap.sse.common.TimePoint; + +/** + * Tags are used to link input of users in form of "tags" (extended comments) to a specific race. These tags consists of + * {@link #tag title}, optional {@link #comment}, optional {@link imageURL image URL} and a {@link #visibleForPublic + * visibility status}. They can be set to be visible only for the author or visible for everybody. Private tags will be + * stored in the user store, public tags will be stored as event in the racelog. Not every user has write access to the + * racelog therefor the default visibility should be private. The {@link com.sap.sailing.server.tagging.TaggingService + * TaggingService} is able to perform all CRUD operations on tags and also checks for permissions.
+ *
+ * Tags should be unique for the key consisting of: {@link #tag title}, {@link #comment}, {@link imageURL image URL}, + * {@link #visibleForPublic visibility status}, {@link #username} and {@link #raceTimepoint}. NOT part of + * the key are {@link #createdAt} and {@link #revokedAt}. + */ +public class TagDTO implements Serializable { + + private static final long serialVersionUID = 3907411584518452300L; + + public final static String TAG_URL_PARAMETER = "tag"; + + private String tag, comment, username, imageURL, resizedImageURL; + private boolean visibleForPublic; + /** + * By default every tag should have set the attributes {@link #raceTimepoint} and {@link #createdAt}.
+ * raceTimepoint: absolute timepoint of race when the tag got created
+ * createdAt: actual absolute creation timepoint of tag
+ *
+ * The racelog does not allow to delete events. Instead these events can only be revoked which is implemented as a + * new revocation event. This applies also for tags so it is not possible to "delete" public tags. To mark a tag as + * revoked, set the attribute {@link #revokedAt} to a value which does not equal null. So every tag + * where {@link #revokedAt} equals null can be seen as valid and NOT revoked. + */ + private TimePoint raceTimepoint, createdAt, revokedAt; + + /** + * Required by GWT, do NOT use this constructor! Use the other constructors instead to allow information + * transfer. + */ + @Deprecated + TagDTO() { + } + + /** + * Creates tag which is not revoked and therefor valid. + * + * @param tag + * title of tag + * @param comment + * comment of tag, may be null as comment is optional + * @param imageURL + * image URL of tag, may be null as image is optional + * @param resizedImageURL + * resized image URL of tag, may be null as image is optional + * @param visibleForPublic + * should be true if everybody should see this tag, otherwise false + * @param username + * name of user who created the tag + * @param raceTimepoint + * timepoint of race where tag ot created + * @param createdAt + * timepoint where username created the tag + */ + public TagDTO(String tag, String comment, String imageURL, String resizedImageURL, boolean visibleForPublic, + String username, TimePoint raceTimepoint, TimePoint createdAt) { + this(tag, comment, imageURL, resizedImageURL, visibleForPublic, username, raceTimepoint, createdAt, null); + } + + /** + * Creates tag which may be revoked. + * + * @param tag + * title of tag + * @param comment + * comment of tag, may be null as comment is optional + * @param imageURL + * image URL of tag, may be null as image is optional + * @param resizedImageURL + * resized image URL of tag, may be null as image is optional + * @param visibleForPublic + * should be true if everbidy should see this tag, otherwise false + * @param username + * name of user who created the tag + * @param raceTimepoint + * timepoint of race where tag ot created + * @param createdAt + * timepoint where username created the tag + * @param revokedAt + * timepoint where tag got revoked, may be null if tag is not revoked + */ + public TagDTO(String tag, String comment, String imageURL, String resizedImageURL, boolean visibleForPublic, + String username, TimePoint raceTimepoint, TimePoint createdAt, TimePoint revokedAt) { + this.tag = tag; + this.comment = comment; + this.imageURL = imageURL; + this.resizedImageURL = resizedImageURL; + this.visibleForPublic = visibleForPublic; + this.username = username; + this.raceTimepoint = raceTimepoint; + this.createdAt = createdAt; + this.revokedAt = revokedAt; + } + + /** + * Returns title of tag. + * + * @return title of tag + */ + public String getTag() { + return tag; + } + + /** + * Returns optional comment of tag. + * + * @return comment of tag, may be null + */ + public String getComment() { + return comment; + } + + /** + * Returns optional image URL of tag. + * + * @return image URLs of tag, may be null + */ + public String getImageURL() { + return imageURL; + } + + /** + * Returns optional image URL of tag. + * + * @return resized image URL of tag, may be null + */ + public String getResizedImageURL() { + return resizedImageURL; + } + + /** + * Returns visibilty of tag. + * + * @return true if everybody should see the tag, otherwise false + */ + public boolean isVisibleForPublic() { + return visibleForPublic; + } + + /** + * Returns name of user who created this tag. + * + * @return username + */ + public String getUsername() { + return username; + } + + /** + * Returns logical timepoint of race where tag was created. + * + * @return logical timepoint + */ + public TimePoint getRaceTimepoint() { + return raceTimepoint; + } + + /** + * Returns creation timestamp of tag. + * + * @return creation timestamp + */ + public TimePoint getCreatedAt() { + return createdAt; + } + + /** + * Returns revokaction timestamp if tag is already revoked. + * + * @return timepoint if tag is already revoked, otherwise null + */ + public TimePoint getRevokedAt() { + return revokedAt; + } + + /** + * Compares attributes {@link #tag}, {@link #comment}, {@link #imageURL}, {@link #resizedImageURL}, + * {@link #visibleForPublic}, {@link #username} and {@link #raceTimepoint}, but NOT attributes + * {@link #createdAt} and {@link #revokedAt}. + */ + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + TagDTO other = (TagDTO) obj; + if (tag == null) { + if (other.tag != null) + return false; + } else if (!tag.equals(other.tag)) + return false; + if (comment == null) { + if (other.comment != null) + return false; + } else if (!comment.equals(other.comment)) + return false; + if (imageURL == null) { + if (other.imageURL != null) + return false; + } else if (!imageURL.equals(other.imageURL)) + return false; + if (resizedImageURL == null) { + if (other.resizedImageURL != null) + return false; + } else if (!resizedImageURL.equals(other.resizedImageURL)) + return false; + if (isVisibleForPublic() != other.isVisibleForPublic()) { + return false; + } + if (username == null) { + if (other.username != null) + return false; + } else if (!username.equals(other.username)) { + return false; + } + if (raceTimepoint == null) { + if (other.raceTimepoint != null) + return false; + } else if (!raceTimepoint.equals(other.raceTimepoint)) + return false; + return true; + } + + /** + * Compares {@link TagDTO} with given attributes. + * + * @return true if all parameters match the key attributes of {@link TagDTO}, otherwise + * false + */ + public boolean equals(String tag, String comment, String imageURL, String resizedImageURL, boolean visibleForPublic, + String username, TimePoint raceTimepoint) { + if (this.comment == null) { + if (comment != null) + return false; + } else if (!this.comment.equals(comment)) + return false; + if (this.imageURL == null) { + if (imageURL != null) + return false; + } else if (!this.imageURL.equals(imageURL)) + return false; + if (this.resizedImageURL == null) { + if (resizedImageURL != null) + return false; + } else if (!this.resizedImageURL.equals(resizedImageURL)) + return false; + if (this.raceTimepoint == null) { + if (raceTimepoint != null) + return false; + } else if (!this.raceTimepoint.equals(raceTimepoint)) + return false; + if (this.tag == null) { + if (tag != null) + return false; + } else if (!this.tag.equals(tag)) + return false; + if (this.username == null) { + if (username != null) + return false; + } else if (!this.username.equals(username)) { + return false; + } + if (isVisibleForPublic() != visibleForPublic) { + return false; + } + if (this.raceTimepoint == null) { + if (raceTimepoint != null) + return false; + } else if (!this.raceTimepoint.equals(raceTimepoint)) { + return false; + } + return true; + } + + @Override + public String toString() { + return "TagDTO [tag=" + tag + ", comment=" + comment + ", imageURL=" + imageURL + ", resizedImageURL=" + + resizedImageURL + ", visibleForPublic=" + visibleForPublic + ", username=" + username + + ", raceTimepoint=" + raceTimepoint + ", createdAt=" + createdAt + ", revokedAt=" + revokedAt + "]"; + } +} \ No newline at end of file diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/tagging/RaceLogNotFoundException.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/tagging/RaceLogNotFoundException.java new file mode 100644 index 00000000000..392b8a5f695 --- /dev/null +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/tagging/RaceLogNotFoundException.java @@ -0,0 +1,14 @@ +package com.sap.sailing.domain.common.tagging; + +public class RaceLogNotFoundException extends Exception { + + private static final long serialVersionUID = -1935079125197798550L; + + public RaceLogNotFoundException() { + + } + + public RaceLogNotFoundException(String message) { + super(message); + } +} diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/tagging/ServiceNotFoundException.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/tagging/ServiceNotFoundException.java new file mode 100644 index 00000000000..3ca538d89d8 --- /dev/null +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/tagging/ServiceNotFoundException.java @@ -0,0 +1,14 @@ +package com.sap.sailing.domain.common.tagging; + +public class ServiceNotFoundException extends Exception { + + private static final long serialVersionUID = 4354757061181985766L; + + public ServiceNotFoundException() { + + } + + public ServiceNotFoundException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/tagging/TagAlreadyExistsException.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/tagging/TagAlreadyExistsException.java new file mode 100644 index 00000000000..f51e7ceaba3 --- /dev/null +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/tagging/TagAlreadyExistsException.java @@ -0,0 +1,14 @@ +package com.sap.sailing.domain.common.tagging; + +public class TagAlreadyExistsException extends Exception { + + private static final long serialVersionUID = -3908869597780348971L; + + public TagAlreadyExistsException() { + + } + + public TagAlreadyExistsException(String message) { + super(message); + } +} diff --git a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/DomainObjectFactoryImpl.java b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/DomainObjectFactoryImpl.java index 1c89c0f6538..5c61d99ea84 100644 --- a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/DomainObjectFactoryImpl.java +++ b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/DomainObjectFactoryImpl.java @@ -62,6 +62,7 @@ import com.sap.sailing.domain.abstractlog.race.RaceLogStartOfTrackingEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogStartProcedureChangedEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogStartTimeEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogSuppressedMarkPassingsEvent; +import com.sap.sailing.domain.abstractlog.race.RaceLogTagEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogWindFixEvent; import com.sap.sailing.domain.abstractlog.race.SimpleRaceLogIdentifier; import com.sap.sailing.domain.abstractlog.race.impl.CompetitorResultImpl; @@ -84,6 +85,7 @@ import com.sap.sailing.domain.abstractlog.race.impl.RaceLogStartOfTrackingEventI import com.sap.sailing.domain.abstractlog.race.impl.RaceLogStartProcedureChangedEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogStartTimeEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogSuppressedMarkPassingsEventImpl; +import com.sap.sailing.domain.abstractlog.race.impl.RaceLogTagEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogWindFixEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.SimpleRaceLogIdentifierImpl; import com.sap.sailing.domain.abstractlog.race.scoring.AdditionalScoringInformationType; @@ -1591,7 +1593,9 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory { } else if (eventClass.equals(RaceLogUseCompetitorsFromRaceLogEvent.class.getSimpleName())) { resultEvent = loadRaceLogUseCompetitorsFromRaceLogEvent(createdAt, author, logicalTimePoint, id, passId, competitors, dbObject); - } else { + } else if (eventClass.equals(RaceLogTagEvent.class.getSimpleName())) { + resultEvent = loadRaceLogTagEvent(createdAt, author, logicalTimePoint, id, passId, dbObject); + }else { throw new IllegalStateException(String.format("Unknown RaceLogEvent type %s", eventClass)); } return new Pair<>(resultEvent, dbObjectForUpdate); @@ -1766,6 +1770,16 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory { .valueOf(dbObject.get(FieldNames.RACE_LOG_START_PROCEDURE_TYPE.name()).toString()); return new RaceLogStartProcedureChangedEventImpl(createdAt, logicalTimePoint, author, id, passId, type); } + + private RaceLogEvent loadRaceLogTagEvent(TimePoint createdAt, AbstractLogEventAuthor author, + TimePoint logicalTimePoint, Serializable id, Integer passId, + DBObject dbObject) { + String tag = (String) dbObject.get(FieldNames.RACE_LOG_TAG.name()); + String comment = (String) dbObject.get(FieldNames.RACE_LOG_COMMENT.name()); + String imageUrl = (String) dbObject.get(FieldNames.RACE_LOG_IMAGE_URL.name()); + String resizedImageURL = (String) dbObject.get(FieldNames.RACE_LOG_RESIZED_IMAGE_URL.name()); + return new RaceLogTagEventImpl(tag, comment, imageUrl, resizedImageURL, createdAt, logicalTimePoint, author, id, passId); + } private RaceLogEvent loadRaceLogGateLineOpeningTimeEvent(TimePoint createdAt, AbstractLogEventAuthor author, TimePoint logicalTimePoint, Serializable id, Integer passId, List competitors, diff --git a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/FieldNames.java b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/FieldNames.java index b557b018195..929e9bde534 100644 --- a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/FieldNames.java +++ b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/FieldNames.java @@ -105,7 +105,7 @@ public enum FieldNames { RACE_LOG_REVOKED_EVENT_SHORT_INFO, RACE_LOG_REVOKED_REASON, RACE_LOG_DEVICE_MAPPING_EVENT_ID, RACE_LOG_CLOSING_TIMEPOINT, TIMEPOINT_OF_FIXED_MARKPASSING, INDEX_OF_PASSED_WAYPOINT,INDEX_OF_FIRST_SUPPRESSED_WAYPOINT, MARK, RACE_LOG_COMPETITOR_ID, RACE_LOG_BOAT_ID, RACE_LOG_DEPDENDENT_ON_FLEET, RACE_LOG_DEPDENDENT_ON_RACECOLUMN, RACE_LOG_DEPDENDENT_ON_REGATTALIKE, - RACE_LOG_START_TIME_DIFFERENCE_IN_MS, RACE_LOG_FINISHING_TIME_AS_MILLIS, + RACE_LOG_START_TIME_DIFFERENCE_IN_MS, RACE_LOG_FINISHING_TIME_AS_MILLIS, RACE_LOG_TAG, RACE_LOG_COMMENT, RACE_LOG_IMAGE_URL, RACE_LOG_RESIZED_IMAGE_URL, /** @deprecated Replaced by {@link #REGATTA_LOG_FROM}; auto-migration from race log device mappings to regatta log is implemented */ @Deprecated RACE_LOG_FROM, /** @deprecated Replaced by {@link #REGATTA_LOG_TO}; auto-migration from race log device mappings to regatta log is implemented */ @Deprecated RACE_LOG_TO, /** @deprecated Replaced by a regatta log entry with type {@link #REGATTA_LOG_MARK} */ @Deprecated RACE_LOG_MARK, diff --git a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoObjectFactoryImpl.java b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoObjectFactoryImpl.java index 408e806b9fc..04534b5f7c5 100644 --- a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoObjectFactoryImpl.java +++ b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoObjectFactoryImpl.java @@ -46,6 +46,7 @@ import com.sap.sailing.domain.abstractlog.race.RaceLogStartOfTrackingEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogStartProcedureChangedEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogStartTimeEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogSuppressedMarkPassingsEvent; +import com.sap.sailing.domain.abstractlog.race.RaceLogTagEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogWindFixEvent; import com.sap.sailing.domain.abstractlog.race.scoring.RaceLogAdditionalScoringInformationEvent; import com.sap.sailing.domain.abstractlog.race.tracking.RaceLogDenoteForTrackingEvent; @@ -1087,6 +1088,24 @@ public class MongoObjectFactoryImpl implements MongoObjectFactory { result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogUseCompetitorsFromRaceLogEvent(event)); return result; } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogTagEvent event) { + BasicDBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogTagEvent(event)); + return result; + } + + public DBObject storeRaceLogTagEvent(RaceLogTagEvent event) { + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(event, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogTagEvent.class.getSimpleName()); + result.put(FieldNames.RACE_LOG_TAG.name(), event.getTag()); + result.put(FieldNames.RACE_LOG_COMMENT.name(), event.getComment()); + result.put(FieldNames.RACE_LOG_IMAGE_URL.name(), event.getImageURL()); + result.put(FieldNames.RACE_LOG_RESIZED_IMAGE_URL.name(), event.getResizedImageURL()); + return result; + } public DBObject storeRaceLogUseCompetitorsFromRaceLogEvent(RaceLogUseCompetitorsFromRaceLogEvent event) { DBObject result = new BasicDBObject(); @@ -1776,5 +1795,4 @@ public class MongoObjectFactoryImpl implements MongoObjectFactory { e.printStackTrace(); } } - } diff --git a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoRaceLogStoreVisitor.java b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoRaceLogStoreVisitor.java index 4ba962744a7..11cb40b051c 100644 --- a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoRaceLogStoreVisitor.java +++ b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoRaceLogStoreVisitor.java @@ -21,6 +21,7 @@ import com.sap.sailing.domain.abstractlog.race.RaceLogStartOfTrackingEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogStartProcedureChangedEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogStartTimeEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogSuppressedMarkPassingsEvent; +import com.sap.sailing.domain.abstractlog.race.RaceLogTagEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogWindFixEvent; import com.sap.sailing.domain.abstractlog.race.scoring.RaceLogAdditionalScoringInformationEvent; import com.sap.sailing.domain.abstractlog.race.tracking.RaceLogDenoteForTrackingEvent; @@ -180,4 +181,10 @@ public class MongoRaceLogStoreVisitor implements RaceLogEventVisitor { DBObject object = mongoObjectFactory.storeRaceLogEntry(raceLogIdentifier, event); mongoObjectFactory.storeRaceLogEventEvent(object); } + + @Override + public void visit(RaceLogTagEvent event) { + DBObject object = mongoObjectFactory.storeRaceLogEntry(raceLogIdentifier, event); + mongoObjectFactory.storeRaceLogEventEvent(object); + } } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/RaceLogEventVisitor.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/RaceLogEventVisitor.java index f51bda94195..a26c060ac4c 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/RaceLogEventVisitor.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/RaceLogEventVisitor.java @@ -53,4 +53,6 @@ public interface RaceLogEventVisitor { public void visit(RaceLogEndOfTrackingEvent event); public void visit(RaceLogUseCompetitorsFromRaceLogEvent event); + + public void visit(RaceLogTagEvent event); } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/RaceLogTagEvent.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/RaceLogTagEvent.java new file mode 100644 index 00000000000..d77d91aa4c6 --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/RaceLogTagEvent.java @@ -0,0 +1,63 @@ +package com.sap.sailing.domain.abstractlog.race; + +import com.sap.sailing.domain.abstractlog.Revokable; +import com.sap.sse.common.TimePoint; + +/** + * Tags can be added by users to mark an important situation at a race. Tags consist of a title, optional comment and + * optional image URL. + */ +public interface RaceLogTagEvent extends RaceLogEvent, Revokable { + + /** + * Returns title of tag. The title should be short and summarize the tag. + * + * @return title of tag + */ + String getTag(); + + /** + * Returns name of user who created the tag. Equals name of {@link #getAuthor() author} of {@link RaceLogEvent}. + * + * @return username of author + */ + String getUsername(); + + /** + * Returns optional comment. Comments should add additional information to the title. + * + * @return comment of tag, may be null as comments are optional + */ + String getComment(); + + /** + * Returns optional image URL. + * + * @return image URL of tag, may be null as images are optional + */ + String getImageURL(); + + /** + * Returns optional resized image URL. + * + * @return resized image URL of tag, may be null as images are optional and images might not be resized + */ + String getResizedImageURL(); + + /** + * Marks this {@link RaceLogTagEvent} as revoked. Used to differentiate between revoked and non-revoked + * {@link RaceLogTagEvent tag events}. + * + * @param revokedAt + * {@link RaceLogRevokeEvent#getCreatedAt() timepoint of creation} of {@link RaceLogRevokeEvent} + */ + void markAsRevoked(TimePoint revokedAt); + + /** + * Returns timepoint of creation of {@link RaceLogRevokeEvent}. + * + * @return {@link RaceLogRevokeEvent#getCreatedAt() timepoint of creation} of {@link RaceLogRevokeEvent} if + * {@link RaceLogTagEvent tag} is {@link #markAsRevoked() revoked}, otherwise null + */ + TimePoint getRevokedAt(); +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/analyzing/impl/TagFinder.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/analyzing/impl/TagFinder.java new file mode 100644 index 00000000000..83c11b04def --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/analyzing/impl/TagFinder.java @@ -0,0 +1,75 @@ +package com.sap.sailing.domain.abstractlog.race.analyzing.impl; + +import java.util.ArrayList; +import java.util.List; + +import com.sap.sailing.domain.abstractlog.race.RaceLog; +import com.sap.sailing.domain.abstractlog.race.RaceLogEvent; +import com.sap.sailing.domain.abstractlog.race.RaceLogRevokeEvent; +import com.sap.sailing.domain.abstractlog.race.RaceLogTagEvent; + +/** + * Scans {@link RaceLog} for {@link RaceLogTagEvent RaceLogTagEvents} and {@link RaceLogRevokeEvent + * RaceLogRevokeEvents}. + */ +public class TagFinder extends RaceLogAnalyzer> { + + private final RaceLog raceLog; + private final boolean onlyUnrevokedEvents; + + /** + * Creates instance of {@link TagFinder} which searches the given raceLog for {@link RaceLogTagEvent + * RaceLogTagEvents} and their {@link RaceLogRevokeEvent revocations}. + * + * @param raceLog + * racelog to scan for {@link RaceLogTagEvent RaceLogTagEvents} + */ + public TagFinder(RaceLog raceLog) { + this(raceLog, false); + } + + /** + * Creates instance of {@link TagFinder} which searches the given raceLog for {@link RaceLogTagEvent + * RaceLogTagEvents} and for their {@link RaceLogRevokeEvent revocations} if onlyUnrevokedEvents is set + * to false. + * + * @param raceLog + * racelog to scan for {@link RaceLogTagEvent RaceLogTagEvents} + * @param onlyUnrevokedEvents + * true to search only for unrevoked event, otherwise false + */ + public TagFinder(RaceLog raceLog, boolean onlyUnrevokedEvents) { + super(raceLog); + this.raceLog = raceLog; + this.onlyUnrevokedEvents = onlyUnrevokedEvents; + } + + /** + * Scans {@link RaceLog} for {@link RaceLogTagEvent RaceLogTagEvents} and {@link RaceLogRevokeEvent + * RaceLogRevokeEvents}. If a {@link RaceLogTagEvent} is revoked, the event will be + * {@link RaceLogTagEvent#markAsRevoked() marked as revoked}. + */ + @Override + protected List performAnalysis() { + Iterable raceLogEvents = onlyUnrevokedEvents ? raceLog.getUnrevokedEvents() : getAllEvents(); + List result = new ArrayList<>(); + for (RaceLogEvent raceLogEvent : raceLogEvents) { + if (raceLogEvent instanceof RaceLogTagEvent) { + // tag event is not revoked (at least until this point of time at scanning race log) + result.add((RaceLogTagEvent) raceLogEvent); + } else if (raceLogEvent instanceof RaceLogRevokeEvent) { + // tag event got revoked => update it in result list or add it to result as revoked tag event + RaceLogRevokeEvent revokeEvent = (RaceLogRevokeEvent) raceLogEvent; + RaceLogEvent revokedEvent = raceLog.getEventById(revokeEvent.getRevokedEventId()); + if (revokedEvent != null && revokedEvent instanceof RaceLogTagEvent) { + RaceLogTagEvent revokedTagEvent = (RaceLogTagEvent) revokedEvent; + int index = result.indexOf(revokedTagEvent); + if (index >= 0) { + result.get(index).markAsRevoked(revokeEvent.getCreatedAt()); + } + } + } + } + return result; + } +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/impl/AbstractRaceLogChangedVisitor.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/impl/AbstractRaceLogChangedVisitor.java index 9662e231f16..89e0c66cb50 100755 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/impl/AbstractRaceLogChangedVisitor.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/impl/AbstractRaceLogChangedVisitor.java @@ -19,6 +19,7 @@ import com.sap.sailing.domain.abstractlog.race.RaceLogStartOfTrackingEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogStartProcedureChangedEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogStartTimeEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogSuppressedMarkPassingsEvent; +import com.sap.sailing.domain.abstractlog.race.RaceLogTagEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogWindFixEvent; import com.sap.sailing.domain.abstractlog.race.scoring.RaceLogAdditionalScoringInformationEvent; import com.sap.sailing.domain.abstractlog.race.tracking.RaceLogDenoteForTrackingEvent; @@ -138,4 +139,9 @@ public abstract class AbstractRaceLogChangedVisitor implements RaceLogEventVisit public void visit(RaceLogUseCompetitorsFromRaceLogEvent event) { notifyListenerAboutEventAdded(event); } + + @Override + public void visit(RaceLogTagEvent event) { + notifyListenerAboutEventAdded(event); + } } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/impl/BaseRaceLogEventVisitor.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/impl/BaseRaceLogEventVisitor.java index 8f35eb16aba..dd36294e008 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/impl/BaseRaceLogEventVisitor.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/impl/BaseRaceLogEventVisitor.java @@ -18,6 +18,7 @@ import com.sap.sailing.domain.abstractlog.race.RaceLogStartOfTrackingEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogStartProcedureChangedEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogStartTimeEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogSuppressedMarkPassingsEvent; +import com.sap.sailing.domain.abstractlog.race.RaceLogTagEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogWindFixEvent; import com.sap.sailing.domain.abstractlog.race.scoring.RaceLogAdditionalScoringInformationEvent; import com.sap.sailing.domain.abstractlog.race.tracking.RaceLogDenoteForTrackingEvent; @@ -145,4 +146,8 @@ public abstract class BaseRaceLogEventVisitor implements RaceLogEventVisitor { } -} + @Override + public void visit(RaceLogTagEvent event) { + //TODO maybe add overridding visit methods for subclasses of this class D067890 + } +} \ No newline at end of file diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/impl/RaceLogTagEventImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/impl/RaceLogTagEventImpl.java new file mode 100644 index 00000000000..442d3c0f799 --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/impl/RaceLogTagEventImpl.java @@ -0,0 +1,152 @@ +package com.sap.sailing.domain.abstractlog.race.impl; + +import java.io.Serializable; + +import com.sap.sailing.domain.abstractlog.AbstractLogEventAuthor; +import com.sap.sailing.domain.abstractlog.race.RaceLogEventVisitor; +import com.sap.sailing.domain.abstractlog.race.RaceLogTagEvent; +import com.sap.sse.common.TimePoint; + +/** + * Default implementation of {@link RaceLogTagEvent}. + */ +public class RaceLogTagEventImpl extends RaceLogEventImpl implements RaceLogTagEvent { + + private static final long serialVersionUID = 7213518902555323432L; + + private final String tag, comment, username, imageURL, resizedImageURL; + private TimePoint revokedAt; + + /** + * Creates {@link RaceLogTagEvent} with all required information. + */ + public RaceLogTagEventImpl(String tag, String comment, String imageURL, String resizedImageURL, TimePoint createdAt, + TimePoint logicalTimePoint, AbstractLogEventAuthor author, Serializable id, int passId) { + super(createdAt, logicalTimePoint, author, id, passId); + this.tag = tag; + this.comment = comment; + this.imageURL = imageURL; + this.resizedImageURL = resizedImageURL; + username = author.getName(); + revokedAt = null; + } + + /** + * Creates {@link RaceLogTagEvent} without required serializable id by generating it. + */ + public RaceLogTagEventImpl(String tag, String comment, String imageURL, String resizedImageURL, TimePoint createdAt, + TimePoint logicalTimePoint, AbstractLogEventAuthor author, int passId) { + this(tag, comment, imageURL, resizedImageURL, createdAt, logicalTimePoint, author, randId(), passId); + } + + /** + * Creates {@link RaceLogTagEvent} with minimal required information. + */ + public RaceLogTagEventImpl(String tag, String comment, String imageURL, String resizedImageURL, TimePoint logicalTimePoint, + AbstractLogEventAuthor author, int passId) { + this(tag, comment, imageURL, resizedImageURL, now(), logicalTimePoint, author, randId(), passId); + } + + @Override + public String getTag() { + return tag; + } + + @Override + public String getComment() { + return comment; + } + + @Override + public String getUsername() { + return username; + } + + /** + * Only sets revokedAt if tag is not already revoked and given {@link TimePoint} is non-null. + */ + @Override + public void markAsRevoked(TimePoint revokedAt) { + if (this.revokedAt == null && revokedAt != null) { + this.revokedAt = revokedAt; + } + } + + @Override + public TimePoint getRevokedAt() { + return revokedAt; + } + + @Override + public void accept(RaceLogEventVisitor visitor) { + visitor.visit(this); + } + + @Override + public String getShortInfo() { + return "tag=" + tag + ", comment=" + comment; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + RaceLogTagEventImpl other = (RaceLogTagEventImpl) obj; + if (comment == null) { + if (other.comment != null) + return false; + } else if (!comment.equals(other.comment)) + return false; + if (imageURL == null) { + if (other.imageURL != null) + return false; + } else if (!imageURL.equals(other.imageURL)) + return false; + if (resizedImageURL == null) { + if (other.resizedImageURL != null) + return false; + } else if (!resizedImageURL.equals(other.resizedImageURL)) + return false; + if (tag == null) { + if (other.tag != null) + return false; + } else if (!tag.equals(other.tag)) + return false; + if (username == null) { + if (other.username != null) + return false; + } else if (!username.equals(other.username)) + return false; + if (getLogicalTimePoint() == null) { + if (other.getLogicalTimePoint() != null) + return false; + } else if (!getLogicalTimePoint().equals(other.getLogicalTimePoint())) + return false; + if (getCreatedAt() == null) { + if (other.getCreatedAt() != null) + return false; + } else if (!getCreatedAt().equals(other.getCreatedAt())) + return false; + return true; + } + + @Override + public String toString() { + return "RaceLogTagEvent [tag=" + tag + ", comment=" + comment + ", imageURL=" + imageURL + ", resizedImageURL=" + resizedImageURL + ", username=" + + username + ", revokedAt=" + revokedAt + "]"; + } + + @Override + public String getImageURL() { + return imageURL; + } + + @Override + public String getResizedImageURL() { + return resizedImageURL; + } +} \ No newline at end of file diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/RaceState.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/RaceState.java index 6df0c3f9ca0..710cafe3700 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/RaceState.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/RaceState.java @@ -194,7 +194,7 @@ public interface RaceState extends ReadonlyRaceState { * an already existing one. */ void setAdditionalScoringInformationEnabled(TimePoint creationTimePoint, boolean enable, AdditionalScoringInformationType informationType); - + boolean isAdditionalScoringInformationEnabled(AdditionalScoringInformationType informationType); /** diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/RaceStateChangedListener.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/RaceStateChangedListener.java index 81cd4038a1f..d2b3b276790 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/RaceStateChangedListener.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/RaceStateChangedListener.java @@ -17,5 +17,6 @@ public interface RaceStateChangedListener { void onFinishingPositionsConfirmed(ReadonlyRaceState state); void onCourseDesignChanged(ReadonlyRaceState state); void onWindFixChanged(ReadonlyRaceState state); + void onTagEventsChanged(ReadonlyRaceState state); } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/ReadonlyRaceState.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/ReadonlyRaceState.java index 3b1649bfd03..b2dc8b0e980 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/ReadonlyRaceState.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/ReadonlyRaceState.java @@ -4,6 +4,7 @@ import com.sap.sailing.domain.abstractlog.race.CompetitorResults; import com.sap.sailing.domain.abstractlog.race.RaceLog; import com.sap.sailing.domain.abstractlog.race.RaceLogEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogStartTimeEvent; +import com.sap.sailing.domain.abstractlog.race.RaceLogTagEvent; import com.sap.sailing.domain.abstractlog.race.analyzing.impl.RaceLogAnalyzer; import com.sap.sailing.domain.abstractlog.race.analyzing.impl.StartTimeFinderResult; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogEventComparator; @@ -147,4 +148,8 @@ public interface ReadonlyRaceState extends RaceStateEventProcessor { */ Wind getWindFix(); + /** + * Returns an iterable of tag events which may be empty but never null. + */ + Iterable getTagEvents(); } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/impl/BaseRaceStateChangedListener.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/impl/BaseRaceStateChangedListener.java index cac2cac05f8..5d7db3578c0 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/impl/BaseRaceStateChangedListener.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/impl/BaseRaceStateChangedListener.java @@ -63,4 +63,9 @@ public abstract class BaseRaceStateChangedListener implements RaceStateChangedLi } + @Override + public void onTagEventsChanged(ReadonlyRaceState state) { + + } + } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/impl/RaceStateChangedListeners.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/impl/RaceStateChangedListeners.java index 5b51541827c..c03f3845e01 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/impl/RaceStateChangedListeners.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/impl/RaceStateChangedListeners.java @@ -111,4 +111,11 @@ public class RaceStateChangedListeners extends HashSet listener.onWindFixChanged(state); } } + + @Override + public void onTagEventsChanged(ReadonlyRaceState state) { + for (RaceStateChangedListener listener : getWorkingCopyOfListeners()) { + listener.onTagEventsChanged(state); + } + } } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/impl/RaceStateImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/impl/RaceStateImpl.java index 7d09c0c9ad5..5fee951c675 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/impl/RaceStateImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/impl/RaceStateImpl.java @@ -242,5 +242,4 @@ public class RaceStateImpl extends ReadonlyRaceStateImpl implements RaceState { public void forceUpdate() { super.update(); } - -} +} \ No newline at end of file diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/impl/ReadonlyRaceStateImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/impl/ReadonlyRaceStateImpl.java index accf15e0227..f73ac581202 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/impl/ReadonlyRaceStateImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/impl/ReadonlyRaceStateImpl.java @@ -13,6 +13,7 @@ import com.sap.sailing.domain.abstractlog.race.RaceLogDependentStartTimeEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogPassChangeEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogStartTimeEvent; +import com.sap.sailing.domain.abstractlog.race.RaceLogTagEvent; import com.sap.sailing.domain.abstractlog.race.SimpleRaceLogIdentifier; import com.sap.sailing.domain.abstractlog.race.analyzing.impl.ConfirmedFinishPositioningListFinder; import com.sap.sailing.domain.abstractlog.race.analyzing.impl.FinishPositioningListFinder; @@ -27,6 +28,7 @@ import com.sap.sailing.domain.abstractlog.race.analyzing.impl.RaceStatusAnalyzer import com.sap.sailing.domain.abstractlog.race.analyzing.impl.RacingProcedureTypeAnalyzer; import com.sap.sailing.domain.abstractlog.race.analyzing.impl.StartTimeFinder; import com.sap.sailing.domain.abstractlog.race.analyzing.impl.StartTimeFinderResult; +import com.sap.sailing.domain.abstractlog.race.analyzing.impl.TagFinder; import com.sap.sailing.domain.abstractlog.race.impl.WeakRaceLogChangedVisitor; import com.sap.sailing.domain.abstractlog.race.state.RaceStateChangedListener; import com.sap.sailing.domain.abstractlog.race.state.RaceStateEvent; @@ -167,6 +169,8 @@ public class ReadonlyRaceStateImpl implements ReadonlyRaceState, RaceLogChangedL private final LastPublishedCourseDesignFinder courseDesignerAnalyzer; private final LastWindFixFinder lastWindFixAnalyzer; + + private final TagFinder tagAnalyzer; /** * The cached racing procedure type. If no racing procedure type specification is found in the underlying race log, @@ -197,6 +201,8 @@ public class ReadonlyRaceStateImpl implements ReadonlyRaceState, RaceLogChangedL */ private Iterable timePointsWhenRaceStatusMayChange; + private Iterable cachedTagEvents; + private int cachedPassId; private StartTimeFinderResult cachedStartTimeFinderResult; private TimePoint cachedFinishingTime; @@ -246,6 +252,7 @@ public class ReadonlyRaceStateImpl implements ReadonlyRaceState, RaceLogChangedL this.confirmedFinishPositioningListAnalyzer = new ConfirmedFinishPositioningListFinder(raceLog); this.courseDesignerAnalyzer = new LastPublishedCourseDesignFinder(raceLog, /* onlyCoursesWithValidWaypointList */ false); this.lastWindFixAnalyzer = new LastWindFixFinder(raceLog); + this.tagAnalyzer = new TagFinder(raceLog); this.raceStateToObserveListener = new BaseRaceStateChangedListener() { @Override @@ -420,6 +427,11 @@ public class ReadonlyRaceStateImpl implements ReadonlyRaceState, RaceLogChangedL return cachedWindFix; } + @Override + public Iterable getTagEvents() { + return cachedTagEvents; + } + @Override public RegattaConfiguration getConfiguration() { return procedureFactory.getConfiguration(); @@ -562,6 +574,11 @@ public class ReadonlyRaceStateImpl implements ReadonlyRaceState, RaceLogChangedL cachedWindFix = windFix; changedListeners.onWindFixChanged(this); } + Iterable tagEvents = tagAnalyzer.analyze(); + if (!Util.equalsWithNull(cachedTagEvents, tagEvents)) { + cachedTagEvents = tagEvents; + changedListeners.onTagEventsChanged(this); + } } /** diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/RaceColumnRaceLogReplicator.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/RaceColumnRaceLogReplicator.java index 26c2a045b4c..82427674879 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/RaceColumnRaceLogReplicator.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/RaceColumnRaceLogReplicator.java @@ -21,6 +21,7 @@ import com.sap.sailing.domain.abstractlog.race.RaceLogStartOfTrackingEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogStartProcedureChangedEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogStartTimeEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogSuppressedMarkPassingsEvent; +import com.sap.sailing.domain.abstractlog.race.RaceLogTagEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogWindFixEvent; import com.sap.sailing.domain.abstractlog.race.scoring.RaceLogAdditionalScoringInformationEvent; import com.sap.sailing.domain.abstractlog.race.tracking.RaceLogDenoteForTrackingEvent; @@ -162,4 +163,9 @@ public class RaceColumnRaceLogReplicator implements RaceLogEventVisitor, Seriali public void visit(RaceLogUseCompetitorsFromRaceLogEvent event) { notifyOnAdd(event); } + + @Override + public void visit(RaceLogTagEvent event) { + notifyOnAdd(event); + } } diff --git a/java/com.sap.sailing.gwt.ui/.settings/org.eclipse.core.resources.prefs b/java/com.sap.sailing.gwt.ui/.settings/org.eclipse.core.resources.prefs index 7c2cbcf679b..1a1b620212e 100755 --- a/java/com.sap.sailing.gwt.ui/.settings/org.eclipse.core.resources.prefs +++ b/java/com.sap.sailing.gwt.ui/.settings/org.eclipse.core.resources.prefs @@ -18,5 +18,6 @@ encoding//src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properti encoding//src/main/java/com/sap/sailing/gwt/ui/client/media/MediaPlayerManagerComponent.java=utf-8 encoding//src/main/java/com/sap/sailing/gwt/ui/leaderboard/CompetitorFilterPanel.java=utf-8 encoding//src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardEntryPoint.java=utf-8 +encoding//src/main/java/com/sap/sailing/gwt/ui/leaderboard/TagFilterPanel.java=utf-8 encoding//src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPanel.java=utf-8 encoding//src/main/java/com/sap/sailing/gwt/ui/spectator/LeaderboardGroupPanel.java=utf-8 diff --git a/java/com.sap.sailing.gwt.ui/RaceBoard.css b/java/com.sap.sailing.gwt.ui/RaceBoard.css index 38eecd7a2b7..7ee5fde83db 100755 --- a/java/com.sap.sailing.gwt.ui/RaceBoard.css +++ b/java/com.sap.sailing.gwt.ui/RaceBoard.css @@ -594,7 +594,9 @@ img.openColumn { background-position: 5% 50%, 95% 50% } -.gwt-SplitLayoutPanel-EastToggleButton-Open-leaderboard { +.gwt-SplitLayoutPanel-EastToggleButton-Open-leaderboard, +.gwt-SplitLayoutPanel-WestToggleButton-Open-leaderboard + { background-color: white; background-image: url(images/viewicons/@1x/img/SAP_RV_Leaderboard_GHOSTED.png), @@ -604,7 +606,8 @@ img.openColumn { background-position: 5px center, calc(100% - 6px) center; } -.gwt-SplitLayoutPanel-EastToggleButton-Closed-leaderboard { +.gwt-SplitLayoutPanel-EastToggleButton-Closed-leaderboard, +.gwt-SplitLayoutPanel-WestToggleButton-Closed-leaderboard { background-color: white; background-image: url(images/viewicons/@1x/img/SAP_RV_Leaderboard_INACTIVE.png), @@ -614,6 +617,26 @@ img.openColumn { background-position: 5px center, calc(100% - 6px) center; } +.gwt-SplitLayoutPanel-WestToggleButton-Open-tags { + background-color: white; + background-image: + url(images/viewicons/@1x/img/SAP_RV_Map_GHOSTED.png), + url(images/arrows_bottom_black_small.png); + background-repeat: no-repeat, no-repeat; + background-position: 4% 50%, 96% 50%; + background-position: 5px center, calc(100% - 6px) center; +} + +.gwt-SplitLayoutPanel-WestToggleButton-Closed-tags { + background-color: white; + background-image: + url(images/viewicons/@1x/img/SAP_RV_Map_INACTIVE.png), + url(images/arrows_top_black_small.png); + background-repeat: no-repeat, no-repeat; + background-position: 4% 50%, 96% 50%; + background-position: 5px center, calc(100% - 6px) center; +} + .gwt-SplitLayoutPanel-NorthSouthToggleButton-Closed-media, .gwt-SplitLayoutPanel-NorthSouthToggleButton-Open-media { background-color: white; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/whatsnew/resources/SailingAnalyticsNotes.html b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/whatsnew/resources/SailingAnalyticsNotes.html index be534fbc260..646e110cfef 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/whatsnew/resources/SailingAnalyticsNotes.html +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/whatsnew/resources/SailingAnalyticsNotes.html @@ -10,6 +10,15 @@ across a distributed server and event landscape. +
September 2018
+
    +
  • In RaceBoard.html is a new feature available which allows users to create tags consisting of a title + and optional comment and/or image per race. These tags can be saved visible only for the creating user or for everbody + (requiring special permissions) and can be used to mark interesting situations during a race. Tag buttons allow users to + save frequently used tags as a preset which can be used quckily and do not require repeating input of the same tag. + Those tags and their comments are also used in order to find the containing leaderboard via the global search.
  • +
+
August 2018
  • The windstreamlets are now colored dynamically according to the wind fields maximum and minimum wind speed.
  • diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/raceboard/RaceBoardPerspectiveOwnSettings.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/raceboard/RaceBoardPerspectiveOwnSettings.java index e9480181516..c63f89957ae 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/raceboard/RaceBoardPerspectiveOwnSettings.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/raceboard/RaceBoardPerspectiveOwnSettings.java @@ -1,5 +1,6 @@ package com.sap.sailing.gwt.settings.client.raceboard; +import com.sap.sailing.domain.common.dto.TagDTO; import com.sap.sailing.gwt.settings.client.settingtypes.DurationSetting; import com.sap.sse.common.Duration; import com.sap.sse.common.impl.MillisecondsDurationImpl; @@ -23,6 +24,7 @@ public class RaceBoardPerspectiveOwnSettings extends AbstractGenericSerializable private transient StringSetting activeCompetitorsFilterSetName; private transient BooleanSetting canReplayDuringLiveRaces; private transient DurationSetting initialDurationAfterRaceStartInReplay; + private transient StringSetting jumpToTag; public static final String PARAM_VIEW_MODE = "viewMode"; public static final String PARAM_VIEW_SHOW_LEADERBOARD = "viewShowLeaderboard"; @@ -33,6 +35,7 @@ public class RaceBoardPerspectiveOwnSettings extends AbstractGenericSerializable public static final String PARAM_VIEW_CHART_SUPPORT_ENABLED = "viewChartSupportEnabled"; public static final String PARAM_CAN_REPLAY_DURING_LIVE_RACES = "canReplayDuringLiveRaces"; public static final String PARAM_TIME_AFTER_RACE_START_AS_HOURS_COLON_MILLIS_COLON_SECONDS = "t"; + public static final String PARAM_JUMP_TO_TAG = TagDTO.TAG_URL_PARAMETER; public RaceBoardPerspectiveOwnSettings() { } @@ -52,11 +55,12 @@ public class RaceBoardPerspectiveOwnSettings extends AbstractGenericSerializable this.activeCompetitorsFilterSetName = new StringSetting("activeCompetitorsFilterSetName", this, null); this.canReplayDuringLiveRaces = new BooleanSetting("canReplayDuringLiveRaces", this, false); this.initialDurationAfterRaceStartInReplay = new DurationSetting(PARAM_TIME_AFTER_RACE_START_AS_HOURS_COLON_MILLIS_COLON_SECONDS, this, null); + this.jumpToTag = new StringSetting(PARAM_JUMP_TO_TAG, this, null); } public RaceBoardPerspectiveOwnSettings(String activeCompetitorsFilterSetName, Boolean showLeaderboard, Boolean showWindChart, Boolean showCompetitorsChart, Boolean canReplayDuringLiveRaces, - Duration initialDurationAfterRaceStartInReplay) { + Duration initialDurationAfterRaceStartInReplay, String jumpToTag) { this.activeCompetitorsFilterSetName.setValue(activeCompetitorsFilterSetName); this.showLeaderboard.setValue(showLeaderboard); this.showWindChart.setValue(showWindChart); @@ -84,10 +88,14 @@ public class RaceBoardPerspectiveOwnSettings extends AbstractGenericSerializable public boolean isCanReplayDuringLiveRaces() { return canReplayDuringLiveRaces.getValue(); } + + public String getJumpToTag() { + return jumpToTag.getValue(); + } public static RaceBoardPerspectiveOwnSettings readSettingsFromURL(boolean defaultForViewShowLeaderboard, boolean defaultForViewShowWindchart, boolean defaultForViewShowCompetitorsChart, - String defaultForViewCompetitorFilter, boolean defaultForCanReplayDuringLiveRaces) { + String defaultForViewCompetitorFilter, boolean defaultForCanReplayDuringLiveRaces, String defaultForJumpToTag) { final boolean showLeaderboard = GwtHttpRequestUtils.getBooleanParameter(PARAM_VIEW_SHOW_LEADERBOARD, defaultForViewShowLeaderboard /* default */); final boolean showWindChart = GwtHttpRequestUtils.getBooleanParameter(PARAM_VIEW_SHOW_WINDCHART, defaultForViewShowWindchart /* default */); final boolean showCompetitorsChart = GwtHttpRequestUtils.getBooleanParameter(PARAM_VIEW_SHOW_COMPETITORSCHART, defaultForViewShowCompetitorsChart /* default */); @@ -95,8 +103,9 @@ public class RaceBoardPerspectiveOwnSettings extends AbstractGenericSerializable final boolean canReplayWhileLiveIsPossible = GwtHttpRequestUtils.getBooleanParameter(PARAM_CAN_REPLAY_DURING_LIVE_RACES, defaultForCanReplayDuringLiveRaces /* default */); final Duration initialDurationAfterRaceStartInReplay = parseDuration(GwtHttpRequestUtils.getStringParameter( PARAM_TIME_AFTER_RACE_START_AS_HOURS_COLON_MILLIS_COLON_SECONDS, null /* default */)); + final String jumpToTag = GwtHttpRequestUtils.getStringParameter(PARAM_JUMP_TO_TAG, defaultForJumpToTag /* default */); return new RaceBoardPerspectiveOwnSettings(activeCompetitorsFilterSetName, showLeaderboard, showWindChart, - showCompetitorsChart, canReplayWhileLiveIsPossible, initialDurationAfterRaceStartInReplay); + showCompetitorsChart, canReplayWhileLiveIsPossible, initialDurationAfterRaceStartInReplay, jumpToTag); } public Duration getInitialDurationAfterRaceStartInReplay() { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/actions/GetRaceTimesInfoAction.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/actions/GetRaceTimesInfoAction.java index bb23803f5f8..f7053f250e0 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/actions/GetRaceTimesInfoAction.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/actions/GetRaceTimesInfoAction.java @@ -2,24 +2,36 @@ package com.sap.sailing.gwt.ui.actions; import java.util.Collection; import java.util.List; +import java.util.Map; import com.google.gwt.user.client.rpc.AsyncCallback; import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.shared.RaceTimesInfoDTO; +import com.sap.sse.common.TimePoint; import com.sap.sse.gwt.client.async.AsyncAction; public class GetRaceTimesInfoAction implements AsyncAction> { private final SailingServiceAsync sailingService; private final Collection raceIdentifiers; + private final Map latestReceivedTagTimes; public GetRaceTimesInfoAction(SailingServiceAsync sailingService, Collection raceIdentifiers) { + this(sailingService, raceIdentifiers, null); + } + + public GetRaceTimesInfoAction(SailingServiceAsync sailingService, Collection raceIdentifiers, Map latestReceivedTagTimes) { this.sailingService = sailingService; this.raceIdentifiers = raceIdentifiers; + this.latestReceivedTagTimes = latestReceivedTagTimes; } @Override public void execute(AsyncCallback> callback) { - sailingService.getRaceTimesInfos(raceIdentifiers, callback); + if (latestReceivedTagTimes == null) { + sailingService.getRaceTimesInfos(raceIdentifiers, callback); + } else { + sailingService.getRaceTimesInfosIncludingTags(raceIdentifiers, latestReceivedTagTimes, callback); + } } } \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RaceTimesInfoProvider.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RaceTimesInfoProvider.java index 1bbc5c7302f..392594e2662 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RaceTimesInfoProvider.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RaceTimesInfoProvider.java @@ -19,6 +19,7 @@ import com.sap.sailing.domain.common.dto.LeaderboardDTO; import com.sap.sailing.domain.common.dto.RaceColumnDTO; import com.sap.sailing.gwt.ui.actions.GetRaceTimesInfoAction; import com.sap.sailing.gwt.ui.shared.RaceTimesInfoDTO; +import com.sap.sse.common.TimePoint; import com.sap.sse.gwt.client.ErrorReporter; import com.sap.sse.gwt.client.async.AsyncActionsExecutor; @@ -34,6 +35,29 @@ public class RaceTimesInfoProvider { private final HashMap raceTimesInfos; private final Set listeners; private boolean terminated = false; + + /** + * If set to true, {@link com.sap.sailing.domain.common.dto.TagDTO tags} will also be requested from server by this + * {@link RaceTimesInfoProvider}. This is only a temporarily workaround to save a new repeating request which is + * only used for tags. This attribute will be set by {@link com.sap.sailing.gwt.ui.raceboard.tagging.TaggingPanel + * TaggingPanel} by calling methods {@link #enableTagRequests()} and {@link #disableTagRequests()} and should not + * affect any data of the other attributes of {@link RaceTimesInfoDTO}. + */ + // TODO: remove this workaround as soon as bug 4736 is resolved + private boolean requestTags = false; + /** + * Used to save timepoints of the client for differential transmission of + * {@link com.sap.sailing.domain.common.dto.TagDTO tags} from the server. The {@link RaceTimesInfoProvider} will + * only request {@link com.sap.sailing.domain.common.dto.TagDTO tags} since the saved timepoint of the requested + * race. The saved timepoint does equal the latest {@link com.sap.sailing.domain.common.dto.TagDTO#createdAt + * createdAt} or {@link com.sap.sailing.domain.common.dto.TagDTO#revokedAt revokedAt} timepoint of all already + * received tags of the current client. This allows the server to filter all tags only for updates for this specific + * client and therefor only updates will be transmitted over network. The saved timepoint can be null + * or 0 to request all tags from server.
    + * Timepoints can be updated via method call {@link #setLatestReceivedTagTime(RegattaAndRaceIdentifier, TimePoint)} + * and should be updated everytime the client receives a change of the tags to use minimal network bandwidth. + */ + private Map latestReceivedTagTimes; /** * The raceIdentifiers has to be non-null, but can be empty. @@ -47,6 +71,7 @@ public class RaceTimesInfoProvider { this.requestIntervalInMillis = requestIntervalInMillis; raceTimesInfos = new HashMap(); listeners = new HashSet(); + latestReceivedTagTimes = new HashMap(); RepeatingCommand command = new RepeatingCommand() { @Override public boolean execute() { @@ -82,7 +107,7 @@ public class RaceTimesInfoProvider { raceIdentifiers.add(raceIdentifier); if (forceTimesInfoRequest) { final long clientTimeWhenRequestWasSent = System.currentTimeMillis(); - sailingService.getRaceTimesInfo(raceIdentifier, new AsyncCallback() { + AsyncCallback callback = new AsyncCallback() { @Override public void onFailure(Throwable caught) { errorReporter.reportError("Error trying to obtain the time infos for race " @@ -97,14 +122,25 @@ public class RaceTimesInfoProvider { notifyListeners(clientTimeWhenRequestWasSent, raceTimesInfo.currentServerTime, clientTimeWhenResponseWasReceived); } } - }); + }; + if (requestTags) { + sailingService.getRaceTimesInfoIncludingTags(raceIdentifier, latestReceivedTagTimes.get(raceIdentifier), callback); + } else { + sailingService.getRaceTimesInfo(raceIdentifier, callback); + } } } private void readTimesInfos() { if (!raceIdentifiers.isEmpty()) { final long clientTimeWhenRequestWasSent = System.currentTimeMillis(); - GetRaceTimesInfoAction getRaceTimesInfoAction = new GetRaceTimesInfoAction(sailingService, raceIdentifiers); + GetRaceTimesInfoAction getRaceTimesInfoAction; + if (requestTags) { + getRaceTimesInfoAction = new GetRaceTimesInfoAction(sailingService, raceIdentifiers, latestReceivedTagTimes); + } else { + getRaceTimesInfoAction = new GetRaceTimesInfoAction(sailingService, raceIdentifiers); + } + asyncActionsExecutor.execute(getRaceTimesInfoAction, new AsyncCallback>() { @Override public void onFailure(Throwable caught) { @@ -226,4 +262,48 @@ public class RaceTimesInfoProvider { raceTimesInfos.clear(); } + /** + * Returns the saved timepoint for the given raceIdentifier which is used as known point of time when + * data is requested from server and {@link #requestTags} is set to true. + * + * @param raceIdentifier + * identifies race + * @return timepoint of latest received tag, may be null when no timepoint is saved + */ + public TimePoint getLatestReceivedTagTime(RegattaAndRaceIdentifier raceIdentifier) { + return latestReceivedTagTimes.get(raceIdentifier); + } + + /** + * Sets the timepoint to request {@link com.sap.sailing.domain.common.dto.TagDTO tags} from when request is send to + * {@link SailingService}. latestReceivedTagTime may be null or 0 to request + * all tags. + * + * @param raceIdentifier + * identifies race + * @param latestReceivedTagTime + * timepoint to request {@link com.sap.sailing.domain.common.dto.TagDTO tags} from. Should be + * {@link com.sap.sailing.domain.common.dto.TagDTO#createdAt createdAt} or + * {@link com.sap.sailing.domain.common.dto.TagDTO#revokedAt revokedAt} timepoint of tags of the latests + * known tag to optimize network bandwidth. + */ + public void setLatestReceivedTagTime(RegattaAndRaceIdentifier raceIdentifier, TimePoint latestReceivedTagTime) { + latestReceivedTagTimes.put(raceIdentifier, latestReceivedTagTime); + } + + /** + * Enables the repeating request to receive also {@link com.sap.sailing.domain.common.dto.TagDTO tags} from + * {@link SailingService}. + */ + public void enableTagRequests() { + requestTags = true; + } + + /** + * Disables the repeating request to receive {@link com.sap.sailing.domain.common.dto.TagDTO tags} from + * {@link SailingService}. + */ + public void disableTagRequests() { + requestTags = false; + } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java index 64ff5065f37..c09d02b9c9c 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java @@ -10,6 +10,9 @@ import java.util.UUID; import com.google.gwt.user.client.rpc.RemoteService; import com.sap.sailing.domain.abstractlog.Revokable; +import com.sap.sailing.domain.abstractlog.race.RaceLog; +import com.sap.sailing.domain.abstractlog.race.RaceLogTagEvent; +import com.sap.sailing.domain.abstractlog.race.state.ReadonlyRaceState; import com.sap.sailing.domain.abstractlog.race.tracking.RaceLogDenoteForTrackingEvent; import com.sap.sailing.domain.base.RaceColumn; import com.sap.sailing.domain.common.CompetitorDescriptor; @@ -46,6 +49,7 @@ import com.sap.sailing.domain.common.dto.RaceColumnDTO; import com.sap.sailing.domain.common.dto.RaceColumnInSeriesDTO; import com.sap.sailing.domain.common.dto.RaceDTO; import com.sap.sailing.domain.common.dto.RegattaCreationParametersDTO; +import com.sap.sailing.domain.common.dto.TagDTO; import com.sap.sailing.domain.common.racelog.RacingProcedureType; import com.sap.sailing.domain.common.racelog.tracking.CompetitorRegistrationOnRaceLogDisabledException; import com.sap.sailing.domain.common.racelog.tracking.DoesNotHaveRegattaLogException; @@ -126,6 +130,8 @@ import com.sap.sse.gwt.client.media.ImageDTO; import com.sap.sse.gwt.client.media.ImageResizingTaskDTO; import com.sap.sse.gwt.client.media.VideoDTO; import com.sap.sse.gwt.client.replication.RemoteReplicationService; +import com.sap.sse.security.UserStore; +import com.sap.sse.security.ui.shared.SuccessInfo; /** * The client side stub for the RPC service. Usually, when a null date is passed to @@ -193,9 +199,25 @@ public interface SailingService extends RemoteService, FileStorageManagementGwtS boolean extrapolate) throws NoWindException; RaceTimesInfoDTO getRaceTimesInfo(RegattaAndRaceIdentifier raceIdentifier); - + + /** + * Returns {@link RaceTimesInfoDTO race times info} for specified race (raceIdentifier) including + * {@link RaceLogTagEvent tag events} since received timestamp (searchSince). Loads tags from + * {@link ReadonlyRaceState cache} instead of scanning the whole {@link RaceLog} every request. + */ + RaceTimesInfoDTO getRaceTimesInfoIncludingTags(RegattaAndRaceIdentifier raceIdentifier, TimePoint searchSince); + List getRaceTimesInfos(Collection raceIdentifiers); - + + /** + * Collects besides {@link RaceTimesInfoDTO race times infos} public {@link RaceLogTagEvent tag events} from + * {@link ReadonlyRaceState cache} and compares the createdAt timepoint to the received + * searchSince timepoint. Returns {@link RaceTimesInfoDTO race times infos} including + * {@link RaceLogTagEvent public tag events} since the latest client-side received tag. + */ + List getRaceTimesInfosIncludingTags(Collection raceIdentifiers, + Map searchSinceMap); + CoursePositionsDTO getCoursePositions(RegattaAndRaceIdentifier raceIdentifier, Date date); RaceCourseDTO getRaceCourse(RegattaAndRaceIdentifier raceIdentifier, Date date); @@ -552,7 +574,117 @@ public interface SailingService extends RemoteService, FileStorageManagementGwtS * @throws NotFoundException */ void addCourseDefinitionToRaceLog(String leaderboardName, String raceColumnName, String fleetName, List> course) throws NotFoundException; - + + /** + * Adds public tag as {@link RaceLogTagEvent} to {@link RaceLog} and private tag to + * {@link com.sap.sse.security.UserStore UserStore}. + * + * @param leaderboardName + * required to identify {@link RaceLog}, must NOT be null + * @param raceColumnName + * required to identify {@link RaceLog}, must NOT be null + * @param fleetName + * required to identify {@link RaceLog}, must NOT be null + * @param tag + * title of tag, must NOT be null + * @param comment + * optional comment of tag + * @param imageURLs + * optional image URLs of tag + * @param visibleForPublic + * when set to true tag will be saved as public tag (visible for every user), when set to + * false tag will be saved as private tag (visible only for creator) + * @param raceTimepoint + * timepoint in race where user created tag, must NOT be null + * @return successful {@link SuccessInfo} if tag was added successfully, otherwise + * non-successful {@link SuccessInfo} + */ + SuccessInfo addTag(String leaderboardName, String raceColumnName, String fleetName, String tag, String comment, + String imageURL, String resizedImageURL, boolean visibleForPublic, TimePoint raceTimepoint); + + /** + * Removes public {@link TagDTO tag} from {@link RaceLog} and private {@link TagDTO tag} from {@link UserStore}. + * + * @param leaderboardName + * required to identify {@link RaceLog}, must NOT be null + * @param raceColumnName + * required to identify {@link RaceLog}, must NOT be null + * @param fleetName + * required to identify {@link RaceLog}, must NOT be null + * @param tag + * tag to remove + * @return successful {@link SuccessInfo} if tag was removed successfully, otherwise + * non-successful {@link SuccessInfo} + */ + SuccessInfo removeTag(String leaderboardName, String raceColumnName, String fleetName, TagDTO tag); + + /** + * Updates given tagToUpdate with the given attributes tag, comment, + * imageURL and visibleForPublic. Tags are not really updated, instead public tags are + * revoked/private tags removed first and then the new tags gets saved depending on the new value + * visibleForPublic. + * + * @param leaderboardName + * required to identify {@link RaceLog}, must NOT be null + * @param raceColumnName + * required to identify {@link RaceLog}, must NOT be null + * @param fleetName + * required to identify {@link RaceLog}, must NOT be null + * @param tagToUpdate + * tag to be updated + * @param tag + * new tag title + * @param comment + * new comment + * @param imageURL + * new image url + * @param visibleForPublic + * new visibility status + * @return successful {@link SuccessInfo} if tag was updated successfully, otherwise + * non-successful {@link SuccessInfo} + */ + SuccessInfo updateTag(String leaderboardName, String raceColumnName, String fleetName, TagDTO tagToUpdate, + String tag, String comment, String imageURL, String resizedImageURL, boolean visibleForPublic); + + /** + * Returns all public and private tags of specified race and current user. + * + * @param leaderboardName + * required to identify {@link RaceLog}, must NOT be null + * @param raceColumnName + * required to identify {@link RaceLog}, must NOT be null + * @param fleetName + * required to identify {@link RaceLog}, must NOT be null + * @return list of {@link TagDTO tags} + */ + List getAllTags(String leaderboardName, String raceColumnName, String fleetName); + + /** + * Returns all public {@link TagDTO tags} of specified race. + * + * @param leaderboardName + * required to identify {@link RaceLog}, must NOT be null + * @param raceColumnName + * required to identify {@link RaceLog}, must NOT be null + * @param fleetName + * required to identify {@link RaceLog}, must NOT be null + * @return list of public {@link TagDTO tags} + */ + List getPublicTags(String leaderboardName, String raceColumnName, String fleetName); + + /** + * Returns all private {@link TagDTO tags} of specified race and current user. + * + * @param leaderboardName + * required to identify {@link RaceLog}, must NOT be null + * @param raceColumnName + * required to identify {@link RaceLog}, must NOT be null + * @param fleetName + * required to identify {@link RaceLog}, must NOT be null + * @return list of private {@link TagDTO tags} + */ + List getPrivateTags(String leaderboardName, String raceColumnName, String fleetName); + RaceCourseDTO getLastCourseDefinitionInRaceLog(String leaderboardName, String raceColumnName, String fleetName) throws NotFoundException; /** diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java index a82d82fa795..305a7177748 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java @@ -42,6 +42,7 @@ import com.sap.sailing.domain.common.dto.RaceColumnDTO; import com.sap.sailing.domain.common.dto.RaceColumnInSeriesDTO; import com.sap.sailing.domain.common.dto.RaceDTO; import com.sap.sailing.domain.common.dto.RegattaCreationParametersDTO; +import com.sap.sailing.domain.common.dto.TagDTO; import com.sap.sailing.domain.common.racelog.RacingProcedureType; import com.sap.sailing.domain.common.tracking.impl.PreciseCompactGPSFixMovingImpl.PreciseCompactPosition; import com.sap.sailing.domain.common.windfinder.SpotDTO; @@ -113,6 +114,7 @@ import com.sap.sse.gwt.client.media.VideoDTO; import com.sap.sse.gwt.client.replication.RemoteReplicationServiceAsync; import com.sap.sse.pairinglist.PairingList; import com.sap.sse.pairinglist.PairingListTemplate; +import com.sap.sse.security.ui.shared.SuccessInfo; /** * The async counterpart of {@link SailingService} @@ -238,9 +240,15 @@ public interface SailingServiceAsync extends ServerInfoRetriever, FileStorageMan void getRaceTimesInfo(RegattaAndRaceIdentifier raceIdentifier, AsyncCallback callback); + void getRaceTimesInfoIncludingTags(RegattaAndRaceIdentifier raceIdentifier, TimePoint searchSince, + AsyncCallback callback); + void getRaceTimesInfos(Collection raceIdentifiers, AsyncCallback> callback); + void getRaceTimesInfosIncludingTags(Collection raceIdentifiers, + Map searchSinceMap, AsyncCallback> callback); + void getCoursePositions(RegattaAndRaceIdentifier raceIdentifier, Date date, AsyncCallback asyncCallback); @@ -736,6 +744,25 @@ public interface SailingServiceAsync extends ServerInfoRetriever, FileStorageMan void addCourseDefinitionToRaceLog(String leaderboardName, String raceColumnName, String fleetName, List> course, AsyncCallback callback); + void addTag(String leaderboardName, String raceColumnName, String fleetName, String tag, String comment, + String imageURL, String resizedImageURL, boolean visibleForPublic, TimePoint raceTimepoint, + AsyncCallback asyncCallback); + + void updateTag(String leaderboardName, String raceColumnName, String fleetName, TagDTO tagToUpdate, String tag, + String comment, String imageURL, String resizedImageURL, boolean visibleForPublic, AsyncCallback asyncCallback); + + void removeTag(String leaderboardName, String raceColumnName, String fleetName, TagDTO tag, + AsyncCallback asyncCallback); + + void getAllTags(String leaderboardName, String raceColumnName, String fleetName, + AsyncCallback> asyncCallback); + + void getPublicTags(String leaderboardName, String raceColumnName, String fleetName, + AsyncCallback> asyncCallback); + + void getPrivateTags(String leaderboardName, String raceColumnName, String fleetName, + AsyncCallback> asyncCallback); + void getLastCourseDefinitionInRaceLog(String leaderboardName, String raceColumnName, String fleetName, AsyncCallback callback); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java index 2fa0bfd16af..cf35cf20fe0 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java @@ -2110,4 +2110,66 @@ public interface StringMessages extends com.sap.sse.gwt.client.StringMessages, String errorUpdatingOwnership(String nameOfOwnedObject); String soundOff(); String turnOffAllSoundChannels(); + String tag(); + String tagButtons(); + String tagPanel(); + String tagCreated(String author, String time); + String tagNoTagsFound(); + String tagAddTag(); + String tagEditTag(); + String tagDeleteTag(); + String tagShareTag(); + String tagAddTags(); + String tagSavedSuccessfully(); + String tagNotSaved(); + String tagNotSavedReason(String reason); + String tagAlreadyExists(); + String tagNotLoaded(); + String tagNotLoggedIn(); + String tagNotRemoved(); + String tagRemovedSuccessfully(); + String tagFilterExplanation(); + String tagSelectedTags(); + String tagsFilter(); + String tagSearchTags(); + String tagLabelTag(); + String tagLabelComment(); + String tagLabelImage(); + String tagLabelAction(); + String tagLabelVisibility(); + String tagCreateTag(); + String tagEditCustomTagButtons(); + String tagAddCustomTagButton(); + String tagNotSpecified(); + String tagEditFilter(); + String tagEditCustomTagButton(); + String tagSaveCustomTagButton(); + String tagPreview(); + String tagVisibleForPublicCheckBox(); + String tagVisibility(); + String tagConfirmDeletionHeading(); + String tagConfirmDeletion(String tag); + String tagButtonConfirmDeletionHeading(); + String tagButtonConfirmDeletion(String tagButton); + String tagDiscardChangesHeading(); + String tagDiscardChanges(); + String tagFilterNotSavable(); + String tagCurrentFilter(); + String tagCreateFilterNotLoggedIn(); + String tagCreateFilterHint(); + String tagButtonNotSavable(); + String tagNotEditable(); + String tagPublicModificationPermissionMissing(); + String tagNotFound(); + String tagInvalidURL(); + String tagSharedDialog(); + String tagSharedURL(); + String tagCopyToClipBoard(); + String tagBrowseAndUploadImage(); + String tagCopiedLinkSuccessfully(); + String tagCopiedLinkNotSuccessfully(); + String confirm(); + String cancel(); + String close(); + String username(); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties index 48d00f71549..c6bcaed56b2 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties @@ -2106,4 +2106,66 @@ automaticResizeNeedsStorageService=Automatic Resizing needs a working FileStorag changeOwnership=Change ownership errorUpdatingOwnership=Error updating ownership of object {0} soundOff=Sound off -turnOffAllSoundChannels=Turn off all sound channels \ No newline at end of file +turnOffAllSoundChannels=Turn off all sound channels +tag=Tag +tagButtons=Tag-Buttons +tagPanel=Tags +tagCreated=({1}, created by {0}) +tagNoTagsFound=No tags found! +tagAddTag=Add Tag +tagEditTag=Edit Tag +tagDeleteTag=Delete Tag +tagShareTag=Share Tag +tagAddTags=Add Tags +tagSavedSuccessfully=Saved tag successfully +tagNotSaved=Could not save tag! +tagNotSavedReason=Could not save tag! Reason: {0} +tagAlreadyExists=Tag already exists +tagNotLoaded=Could not load tags! +tagNotLoggedIn=Please log in to add new tags! +tagNotRemoved=Could not remove tag! +tagRemovedSuccessfully=Removed tag successfully +tagFilterExplanation=The filter will show all tags matching the following criterias. Filters are case sensitive! +tagSelectedTags=Selected Tags +tagsFilter=Tag Filter +tagSearchTags=Search +tagLabelTag=Tag +tagLabelComment=Comment +tagLabelImage=Image +tagLabelAction=Action +tagLabelVisibility=Visibility +tagCreateTag=Create Tag +tagEditCustomTagButtons=Edit Tag-Buttons +tagAddCustomTagButton=Add Tag-Button +tagNotSpecified=Tag must not be empty! +tagEditFilter=Edit Filter +tagEditCustomTagButton=Edit Tag-Button +tagSaveCustomTagButton=Save +tagPreview=Tag Preview: +tagVisibleForPublicCheckBox=Visible for public +tagVisibility=Visibility +tagConfirmDeletionHeading=Delete Tag +tagConfirmDeletion=This will delete the tag "{0}". Do you really want to continue? +tagButtonConfirmDeletionHeading=Delete Tag-Button +tagButtonConfirmDeletion=This will delete the tag-button "{0}". Do you really want to continue? +tagDiscardChangesHeading=Discard changes +tagDiscardChanges=Are you sure you want to discard your changes? +tagFilterNotSavable=Could not save the filter settings on the server! +tagCurrentFilter=Current Filter: +tagCreateFilterNotLoggedIn=You are not logged in so filters will not be saved! +tagCreateFilterHint=Create a filter set to filter the list of tags. +tagButtonNotSavable=Could not save tag-button on the server! +tagNotEditable=You may not edit this tag! +tagPublicModificationPermissionMissing=You are not allowed to create public tags but you are allowed to create private tags instead. +tagNotFound=Tag could not be found! +tagInvalidURL=Invalid URL parameter +tagSharedDialog=Shared tag +tagSharedURL=You can share this tag by passing the following URL: +tagCopyToClipBoard=Copy to clipboard +tagBrowseAndUploadImage=Browse and upload +tagCopiedLinkSuccessfully=Successfully copied link to clipboard! +tagCopiedLinkNotSuccessfully=Your browser does not support copying to clipboard, please copy link manually! +username=User name +confirm=Confirm +cancel=Cancel +close=Close diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties index d409021226b..6705263c988 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties @@ -2101,4 +2101,66 @@ automaticResizeNeedsStorageService=Die automatische Skalierung benötigt einen f changeOwnership=Besitzer wechseln errorUpdatingOwnership=Fehler beim Setzen des Eigners für Objekt {0} soundOff=Ton aus -turnOffAllSoundChannels=Alle Tonkanäle abschalten \ No newline at end of file +turnOffAllSoundChannels=Alle Tonkanäle abschalten +tag=Tag +tagButtons=Tag-Buttons +tagPanel=Tags +tagCreated=({1}, erstellt von {0}) +tagNoTagsFound=Keine Tags gefunden! +tagAddTag=Tag hinzufügen +tagEditTag=Tag bearbeiten +tagDeleteTag=Tag löschen +tagShareTag=Tag teilen +tagAddTags=Tags hinzufügen +tagSavedSuccessfully=Tag erfolgreich gespeichert +tagNotSaved=Tag konnte nicht gespeichert werden! +tagNotSavedReason=Tag konnte nicht gespeichert werden! Grund: {0} +tagAlreadyExists=Tag existiert bereits +tagNotLoaded=Tags konnten nicht geladen werden! +tagNotLoggedIn=Bitte logge dich ein, um einem Rennen Tags hinzufügen zu können! +tagNotRemoved=Tag konnte nicht entfernt werden! +tagRemovedSuccessfully=Tag erfolgreich entfernt +tagFilterExplanation=Der Filter zeigt alle Tags an, welche die nachfolgenden Kriterien erfüllen. Groß- und Kleinschreibung werden beachtet! +tagSelectedTags=Ausgewählte Tags +tagsFilter=Tag Filter +tagSearchTags=Suchen +tagLabelTag=Tag +tagLabelComment=Kommentar +tagLabelImage=Bild +tagLabelAction=Aktion +tagLabelVisibility=Sichtbarkeit +tagCreateTag=Tag hinzufügen +tagEditCustomTagButtons=Tag-Buttons bearbeiten +tagAddCustomTagButton=Tag-Button hinzufügen +tagNotSpecified=Tag darf nicht leer sein! +tagEditFilter=Bearbeite Filter +tagEditCustomTagButton=Bearbeite Tag-Button +tagSaveCustomTagButton=Speichern +tagPreview=Tag Vorschau: +tagVisibleForPublicCheckBox=Für alle sichtbar +tagVisibility=Sichtbarkeit +tagConfirmDeletionHeading=Tag löschen +tagConfirmDeletion=Dies wird den Tag "{0}" endgültig löschen. Willst du wirklich fortfahren? +tagButtonConfirmDeletionHeading=Tag-Button löschen +tagButtonConfirmDeletion=Dies wird den Tag-Button "{0}" endgütltig löschen. Willst du wirklich fortfahren? +tagDiscardChangesHeading=Änderungen verwerfen +tagDiscardChanges=Willst du wirklich alle Änderungen verwerfen? +tagFilterNotSavable=Die Tag-Filter konnten nicht auf dem Server gespeichert werden! +tagCurrentFilter=Aktueller Filter: +tagCreateFilterNotLoggedIn=Bitte einloggen um Filter speichern zu können! +tagCreateFilterHint=Lege einen Filter an, um die sichtbaren Tags zu reduzieren. +tagButtonNotSavable=Tag-Button konnten nicht auf dem Server gespeichert werden! +tagNotEditable=Du darfst diesen Tag nicht bearbeiten! +tagPublicModificationPermissionMissing=Du darfst nur private, jedoch keine öffentlichen Tags erstellen. +tagNotFound=Tag wurde nicht gefunden! +tagInvalidURL=URL Parameter nicht gültig +tagSharedDialog=Geteilter Tag +tagSharedURL=Du kannst diesen Tag durch die Weitergabe der folgenden URL teilen: +tagCopyToClipBoard=Kopiere in die Zwischenablage +tagBrowseAndUploadImage=Bild auswählen und senden +tagCopiedLinkSuccessfully=Link erfolgreich in die Zwischenablage kopiert! +tagCopiedLinkNotSuccessfully=Dein Browser unterstüzt leider kein Kopieren in die Zwischenablage, bitte kopiere den Link manuell! +username=Benutzername +confirm=Fortfahren +cancel=Abbrechen +close=Schließen diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/filter/AbstractTagTextFilterUIFactory.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/filter/AbstractTagTextFilterUIFactory.java new file mode 100644 index 00000000000..856f09b1262 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/filter/AbstractTagTextFilterUIFactory.java @@ -0,0 +1,38 @@ +package com.sap.sailing.gwt.ui.client.shared.filter; + +import java.util.ArrayList; +import java.util.List; + +import com.google.gwt.user.client.ui.ListBox; +import com.sap.sailing.domain.common.dto.TagDTO; +import com.sap.sse.common.filter.TextFilter; +import com.sap.sse.common.filter.TextOperator; +import com.sap.sse.gwt.client.dialog.DataEntryDialog; + +public abstract class AbstractTagTextFilterUIFactory implements FilterUIFactory { + + protected List supportedOperators; + protected TextOperator.Operators defaultOperator; + protected TextFilter tagTextFilter; + + public AbstractTagTextFilterUIFactory(TextFilter tagTextFilter, TextOperator.Operators defaultOperator) { + this.tagTextFilter = tagTextFilter; + this.defaultOperator = defaultOperator; + supportedOperators = new ArrayList(); + } + + protected ListBox createOperatorSelectionListBox(DataEntryDialog dataEntryDialog) { + ListBox operatorsListBox = dataEntryDialog.createListBox(false); + int i = 0; + for (TextOperator.Operators op : supportedOperators) { + operatorsListBox.addItem(FilterOperatorsFormatter.format(op), op.name()); + if (tagTextFilter.getOperator() != null && tagTextFilter.getOperator().getName().equals(op.name())) { + operatorsListBox.setSelectedIndex(i); + } else if (defaultOperator != null && defaultOperator.equals(op)) { + operatorsListBox.setSelectedIndex(i); + } + i++; + } + return operatorsListBox; + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/filter/AbstractTagsFilterSetDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/filter/AbstractTagsFilterSetDialog.java new file mode 100644 index 00000000000..feaad61c069 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/filter/AbstractTagsFilterSetDialog.java @@ -0,0 +1,277 @@ +package com.sap.sailing.gwt.ui.client.shared.filter; + +import java.util.ArrayList; +import java.util.List; + +import com.google.gwt.event.dom.client.ChangeEvent; +import com.google.gwt.event.dom.client.ChangeHandler; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.FocusWidget; +import com.google.gwt.user.client.ui.Grid; +import com.google.gwt.user.client.ui.HasVerticalAlignment; +import com.google.gwt.user.client.ui.HorizontalPanel; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.ListBox; +import com.google.gwt.user.client.ui.TextBox; +import com.google.gwt.user.client.ui.VerticalPanel; +import com.google.gwt.user.client.ui.Widget; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.domain.common.dto.TagDTO; +import com.sap.sse.common.filter.Filter; +import com.sap.sse.common.filter.FilterSet; +import com.sap.sse.gwt.client.dialog.DataEntryDialog; + +/** + * Used to create or edit a {@link FilterSet} for {@link TagDTO}s + */ +public abstract class AbstractTagsFilterSetDialog extends DataEntryDialog>> { + private final FilterSet> tagsFilterSet; + private final StringMessages stringMessages; + private final List availableTagFilterNames; + + private ListBox filterListBox; + private final Button addFilterButton; + + protected TextBox filterSetNameTextBox; + private Grid tagsFiltersGrid; + private Label tagsFiltersGridHeadline; + private Label tagsFiltersGridFooter; + private VerticalPanel mainPanel; + + private final List filterEditWidgets; + private final List> filterUIFactories; + private final List