mirror of
https://github.com/eclipse-sailing-analytics/sailing-analytics.git
synced 2026-10-20 19:20:04 +00:00
bug 4087: using IControl ID + (1/2) for marks' ID; replace control points by existing ones for same set of marks
Change-Id: Icec17f89224aa0bef70d681362613bda849a42c2
This commit is contained in:
1 parent
90ac3aff2e
commit
8180a65da5
8 files changed
+138
-22
No files matched your search
+1
-1
@@ -52,7 +52,7 @@ public interface SharedDomainFactory extends CompetitorFactory {
|
||||
Mark getOrCreateMark(String toStringRepresentationOfID, String name, MarkType type, String color, String shape, String pattern);
|
||||
|
||||
/**
|
||||
* @param name also uses the name as the gate's ID; if you have a real ID, use {@link #createGate(Serializable, Mark, Mark, String)} instead
|
||||
* @param name also uses the name as the gate's ID; if you have a real ID, use {@link #createControlPointWithTwoMarks(Serializable, Mark, Mark, String)} instead
|
||||
*/
|
||||
ControlPointWithTwoMarks createControlPointWithTwoMarks(Mark left, Mark right, String name);
|
||||
|
||||
|
||||
+29
-5
@@ -53,6 +53,8 @@ import com.tractrac.model.lib.api.event.CreateModelException;
|
||||
import com.tractrac.model.lib.api.event.ICompetitor;
|
||||
import com.tractrac.model.lib.api.event.IEvent;
|
||||
import com.tractrac.model.lib.api.event.IRace;
|
||||
import com.tractrac.model.lib.api.route.IControl;
|
||||
import com.tractrac.model.lib.api.route.IControlPoint;
|
||||
import com.tractrac.subscription.lib.api.IEventSubscriber;
|
||||
import com.tractrac.subscription.lib.api.IRaceSubscriber;
|
||||
import com.tractrac.subscription.lib.api.SubscriberInitializationException;
|
||||
@@ -186,26 +188,26 @@ public interface DomainFactory {
|
||||
* notification was sent, and that the {@link RaceDefinition} is already
|
||||
* {@link com.sap.sailing.domain.base.Regatta#getAllRaces() known} by its
|
||||
* containing {@link com.sap.sailing.domain.base.Regatta}.
|
||||
*
|
||||
* @param raceDefinitionSetToUpdate
|
||||
* @param raceDefinitionSetToUpdate
|
||||
* if not <code>null</code>, after creating the
|
||||
* {@link TrackedRace}, the {@link RaceDefinition} is
|
||||
* {@link DynamicRaceDefinitionSet#addRaceDefinition(RaceDefinition, DynamicTrackedRace)
|
||||
* added} to that object.
|
||||
* @param runBeforeExposingRace
|
||||
* @param runBeforeExposingRace
|
||||
* if not {@code null} then this consumer will be passed the
|
||||
* {@link DynamicTrackedRace} if it was actually created by this
|
||||
* call. This happens while still in the
|
||||
* {@code synchronized(raceCache)} block, therefore before calls
|
||||
* waiting for the race (e.g.,
|
||||
* {@link #getAndWaitForRaceDefinition(UUID)}) return the race.
|
||||
* @param tractracRace TODO
|
||||
*/
|
||||
DynamicTrackedRace getOrCreateRaceDefinitionAndTrackedRace(DynamicTrackedRegatta trackedRegatta, UUID raceId,
|
||||
String raceName, Iterable<com.sap.sailing.domain.base.Competitor> competitors, BoatClass boatClass,
|
||||
Map<Competitor, Boat> competitorBoats, Course course, Iterable<Sideline> sidelines, WindStore windStore,
|
||||
long delayToLiveInMillis, long millisecondsOverWhichToAverageWind,
|
||||
DynamicRaceDefinitionSet raceDefinitionSetToUpdate, URI courseDesignUpdateURI, UUID tracTracEventUuid,
|
||||
String tracTracUsername, String tracTracPassword, boolean ignoreTracTracMarkPassings, RaceLogResolver raceLogResolver, Consumer<DynamicTrackedRace> runBeforeExposingRace);
|
||||
String tracTracUsername, String tracTracPassword, boolean ignoreTracTracMarkPassings, RaceLogResolver raceLogResolver, Consumer<DynamicTrackedRace> runBeforeExposingRace, IRace tractracRace);
|
||||
|
||||
/**
|
||||
* The record may be for a single mark or a gate. If for a gate, the
|
||||
@@ -308,6 +310,28 @@ public interface DomainFactory {
|
||||
* the start time and whether a race was aborted.
|
||||
*/
|
||||
void addTracTracUpdateHandlers(URI tracTracUpdateURI, UUID tracTracEventUuid, String tracTracUsername,
|
||||
String tracTracPassword, RaceDefinition raceDefinition, DynamicTrackedRace trackedRace);
|
||||
String tracTracPassword, RaceDefinition raceDefinition, DynamicTrackedRace trackedRace, IRace tractracRace);
|
||||
|
||||
/**
|
||||
* Since TracAPI 3.6.1 the TracAPI provides a course area name for {@link IRace} objects. Furthermore, the
|
||||
* {@link IControl} control points can now tell their {@link IControl#getCourseArea() course area}. This allows
|
||||
* us to fetch the {@link IControl}s for a specific course area, thereby, e.g., restricting the marks of an
|
||||
* event that we offer to the Race Manager app's course designer to those available on the course area.
|
||||
*/
|
||||
Iterable<IControl> getControlsForCourseArea(IEvent tracTracEvent, String tracTracCourseAreaName);
|
||||
|
||||
/**
|
||||
* Since TracAPI 3.6.1 the TracAPI provides a course area name for {@link IRace} objects. Furthermore, the
|
||||
* {@link IControl} control points can now tell their {@link IControl#getCourseArea() course area}. This allows
|
||||
* us to fetch the {@link IControlPoint}s for a specific course area, thereby, e.g., restricting the marks of an
|
||||
* event that we offer to the Race Manager app's course designer to those available on the course area.
|
||||
*/
|
||||
Iterable<IControlPoint> getControlPointsForCourseArea(IEvent tracTracEvent, String tracTracCourseAreaName);
|
||||
|
||||
/**
|
||||
* Looks for an {@link IControl} in the {@code candidates} that contains two {@link IControlPoint}s
|
||||
* that map to the {@code first} and {@code second} mark.
|
||||
*/
|
||||
IControl getExistingControlWithTwoMarks(Iterable<IControl> candidates, Mark first, Mark second);
|
||||
|
||||
}
|
||||
+44
-4
@@ -11,10 +11,12 @@ import java.text.ParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
@@ -90,6 +92,8 @@ import com.tractrac.model.lib.api.event.ICompetitorClass;
|
||||
import com.tractrac.model.lib.api.event.IEvent;
|
||||
import com.tractrac.model.lib.api.event.IRace;
|
||||
import com.tractrac.model.lib.api.event.IRaceCompetitor;
|
||||
import com.tractrac.model.lib.api.route.IControl;
|
||||
import com.tractrac.model.lib.api.route.IControlPoint;
|
||||
import com.tractrac.subscription.lib.api.IEventSubscriber;
|
||||
import com.tractrac.subscription.lib.api.IRaceSubscriber;
|
||||
import com.tractrac.subscription.lib.api.SubscriberInitializationException;
|
||||
@@ -514,7 +518,7 @@ public class DomainFactoryImpl implements DomainFactory {
|
||||
long delayToLiveInMillis, long millisecondsOverWhichToAverageWind,
|
||||
DynamicRaceDefinitionSet raceDefinitionSetToUpdate, URI tracTracUpdateURI, UUID tracTracEventUuid,
|
||||
String tracTracUsername, String tracTracPassword, boolean ignoreTracTracMarkPassings, RaceLogResolver raceLogResolver,
|
||||
Consumer<DynamicTrackedRace> runBeforeExposingRace) {
|
||||
Consumer<DynamicTrackedRace> runBeforeExposingRace, IRace tractracRace) {
|
||||
synchronized (raceCache) {
|
||||
RaceDefinition raceDefinition = raceCache.get(raceId);
|
||||
if (raceDefinition == null) {
|
||||
@@ -537,7 +541,7 @@ public class DomainFactoryImpl implements DomainFactory {
|
||||
runBeforeExposingRace.accept(trackedRace);
|
||||
}
|
||||
addTracTracUpdateHandlers(tracTracUpdateURI, tracTracEventUuid, tracTracUsername, tracTracPassword,
|
||||
raceDefinition, trackedRace);
|
||||
raceDefinition, trackedRace, tractracRace);
|
||||
raceCache.put(raceId, raceDefinition);
|
||||
raceCache.notifyAll();
|
||||
} else {
|
||||
@@ -559,12 +563,48 @@ public class DomainFactoryImpl implements DomainFactory {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<IControlPoint> getControlPointsForCourseArea(IEvent tracTracEvent, String tracTracCourseAreaName) {
|
||||
final Set<IControlPoint> result = new HashSet<>();
|
||||
for (final IControl control : getControlsForCourseArea(tracTracEvent, tracTracCourseAreaName)) {
|
||||
result.addAll(control.getControlPoints());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<IControl> getControlsForCourseArea(IEvent tracTracEvent, String tracTracCourseAreaName) {
|
||||
final Set<IControl> result = new HashSet<>();
|
||||
for (final IControl control : tracTracEvent.getControls()) {
|
||||
if (Util.equalsWithNull(control.getCourseArea(), tracTracCourseAreaName)) {
|
||||
result.add(control);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IControl getExistingControlWithTwoMarks(Iterable<IControl> candidates, Mark first, Mark second) {
|
||||
final Set<Mark> pairOfMarksToFind = new HashSet<>();
|
||||
pairOfMarksToFind.add(first);
|
||||
pairOfMarksToFind.add(second);
|
||||
for (final IControl control : candidates) {
|
||||
TracTracControlPoint cp = new ControlPointAdapter(control);
|
||||
Set<Mark> marksInExistingControlPoint = new HashSet<>();
|
||||
Util.addAll(getOrCreateControlPoint(cp).getMarks(), marksInExistingControlPoint);
|
||||
if (marksInExistingControlPoint.equals(pairOfMarksToFind)) {
|
||||
return control;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addTracTracUpdateHandlers(URI tracTracUpdateURI, UUID tracTracEventUuid, String tracTracUsername,
|
||||
String tracTracPassword, RaceDefinition raceDefinition, DynamicTrackedRace trackedRace) {
|
||||
String tracTracPassword, RaceDefinition raceDefinition, DynamicTrackedRace trackedRace, IRace tractracRace) {
|
||||
TracTracCourseDesignUpdateHandler courseDesignHandler = new TracTracCourseDesignUpdateHandler(
|
||||
tracTracUpdateURI, tracTracUsername, tracTracPassword, tracTracEventUuid,
|
||||
raceDefinition.getId());
|
||||
raceDefinition.getId(), tractracRace, this);
|
||||
trackedRace.addCourseDesignChangedListener(courseDesignHandler);
|
||||
TracTracStartTimeUpdateHandler startTimeHandler = new TracTracStartTimeUpdateHandler(
|
||||
tracTracUpdateURI, tracTracUsername, tracTracPassword, tracTracEventUuid,
|
||||
|
||||
+2
-2
@@ -154,9 +154,9 @@ public class MetadataParserImpl implements MetadataParser {
|
||||
if (name2 == null) {
|
||||
name2 = controlPointName + " (2)";
|
||||
}
|
||||
final Serializable id1 = mark1UUID == null ? name1 : UUID.fromString(mark1UUID);
|
||||
final Serializable id1 = mark1UUID == null ? controlPoint.getId().toString()+" (1)" : UUID.fromString(mark1UUID);
|
||||
ControlPointMetaData mark1Metadata = new ControlPointMetaDataImpl(name1, type1, color1, shape1, pattern1, id1);
|
||||
final Serializable id2 = mark2UUID == null ? name2 : UUID.fromString(mark2UUID);
|
||||
final Serializable id2 = mark2UUID == null ? controlPoint.getId().toString()+" (2)" : UUID.fromString(mark2UUID);
|
||||
ControlPointMetaData mark2Metadata = new ControlPointMetaDataImpl(name2, type2, color2, shape2, pattern2, id2);
|
||||
result = Arrays.asList(new ControlPointMetaData[] { mark1Metadata, mark2Metadata });
|
||||
} else {
|
||||
|
||||
+21
-2
@@ -15,6 +15,7 @@ import com.sap.sailing.domain.base.BoatClass;
|
||||
import com.sap.sailing.domain.base.Competitor;
|
||||
import com.sap.sailing.domain.base.ControlPoint;
|
||||
import com.sap.sailing.domain.base.Course;
|
||||
import com.sap.sailing.domain.base.Mark;
|
||||
import com.sap.sailing.domain.base.RaceDefinition;
|
||||
import com.sap.sailing.domain.base.Regatta;
|
||||
import com.sap.sailing.domain.base.Sideline;
|
||||
@@ -146,6 +147,7 @@ public class RaceCourseReceiver extends AbstractReceiverWithQueue<IControlRoute,
|
||||
getDomainFactory().updateCourseWaypoints(existingRaceDefinitionForRace.getCourse(), ttControlPoints);
|
||||
if (getTrackedRegatta().getExistingTrackedRace(existingRaceDefinitionForRace) == null) {
|
||||
trackedRace = createTrackedRace(existingRaceDefinitionForRace, sidelines, tr->updateRaceTimes(tractracRace, tr));
|
||||
addAllMarksFromCourseArea(trackedRace);
|
||||
}
|
||||
} catch (PatchFailedException e) {
|
||||
logger.log(Level.SEVERE, "Internal error updating race course "+course+": "+e.getMessage());
|
||||
@@ -160,13 +162,29 @@ public class RaceCourseReceiver extends AbstractReceiverWithQueue<IControlRoute,
|
||||
getTrackedRegatta(), tractracRace.getId(), tractracRace.getName(), competitorsAndDominantBoatClass.getA(),
|
||||
competitorsAndDominantBoatClass.getB(), competitorBoats, course, sidelines, windStore, delayToLiveInMillis,
|
||||
millisecondsOverWhichToAverageWind, raceDefinitionSetToUpdate, tracTracUpdateURI,
|
||||
getTracTracEvent().getId(), tracTracUsername, tracTracPassword, useInternalMarkPassingAlgorithm, raceLogResolver, tr->updateRaceTimes(tractracRace, tr));
|
||||
getTracTracEvent().getId(), tracTracUsername, tracTracPassword, useInternalMarkPassingAlgorithm, raceLogResolver, tr->updateRaceTimes(tractracRace, tr), tractracRace);
|
||||
addAllMarksFromCourseArea(trackedRace);
|
||||
if (getSimulator() != null) {
|
||||
getSimulator().setTrackedRace(trackedRace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For all marks associated with the course area of the {@link #tractracRace}'s {@link IRace#getCourseArea() course area} (if any; this
|
||||
* was introduced with the TracAPI version 3.6.1) a mark track is created in the {@link TrackedRace}. This will let the {@link TrackedRace#getMarks}
|
||||
* method return those, helping clients asking for "available" marks.
|
||||
*/
|
||||
private void addAllMarksFromCourseArea(DynamicTrackedRace trackedRace) {
|
||||
for (final IControl tractracControlPoint : getDomainFactory().getControlsForCourseArea(getTracTracEvent(), tractracRace.getCourseArea())) {
|
||||
final TracTracControlPoint ttcp = new ControlPointAdapter(tractracControlPoint);
|
||||
final ControlPoint cp = getDomainFactory().getOrCreateControlPoint(ttcp);
|
||||
for (final Mark mark : cp.getMarks()) {
|
||||
trackedRace.getOrCreateTrack(mark);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateRaceTimes(IRace tractracRace, DynamicTrackedRace trackedRace) {
|
||||
final int liveDelayInSeconds = tractracRace.getLiveDelay();
|
||||
long delayInMillis = liveDelayInSeconds * 1000;
|
||||
@@ -225,7 +243,8 @@ public class RaceCourseReceiver extends AbstractReceiverWithQueue<IControlRoute,
|
||||
if (runAfterCreatingTrackedRace != null) {
|
||||
runAfterCreatingTrackedRace.accept(trackedRace);
|
||||
}
|
||||
getDomainFactory().addTracTracUpdateHandlers(tracTracUpdateURI, getTracTracEvent().getId(), tracTracUsername, tracTracPassword, race, trackedRace);
|
||||
getDomainFactory().addTracTracUpdateHandlers(tracTracUpdateURI, getTracTracEvent().getId(), tracTracUsername, tracTracPassword, race, trackedRace,
|
||||
tractracRace);
|
||||
return trackedRace;
|
||||
}
|
||||
|
||||
|
||||
+41
-4
@@ -6,13 +6,18 @@ import java.net.HttpURLConnection;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.util.Iterator;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import org.json.simple.JSONObject;
|
||||
import org.json.simple.parser.ParseException;
|
||||
|
||||
import com.sap.sailing.domain.base.CourseBase;
|
||||
import com.sap.sailing.domain.base.Mark;
|
||||
import com.sap.sailing.domain.base.Waypoint;
|
||||
import com.sap.sailing.domain.base.impl.CourseDataImpl;
|
||||
import com.sap.sailing.domain.tracking.CourseDesignChangedListener;
|
||||
import com.sap.sailing.domain.tractracadapter.DomainFactory;
|
||||
import com.sap.sailing.server.gateway.serialization.JsonSerializer;
|
||||
import com.sap.sailing.server.gateway.serialization.coursedata.impl.ControlPointJsonSerializer;
|
||||
import com.sap.sailing.server.gateway.serialization.coursedata.impl.CourseBaseJsonSerializer;
|
||||
@@ -20,16 +25,23 @@ import com.sap.sailing.server.gateway.serialization.coursedata.impl.CourseJsonSe
|
||||
import com.sap.sailing.server.gateway.serialization.coursedata.impl.GateJsonSerializer;
|
||||
import com.sap.sailing.server.gateway.serialization.coursedata.impl.MarkJsonSerializer;
|
||||
import com.sap.sailing.server.gateway.serialization.coursedata.impl.WaypointJsonSerializer;
|
||||
import com.sap.sse.common.Util;
|
||||
import com.tractrac.model.lib.api.event.IRace;
|
||||
import com.tractrac.model.lib.api.route.IControl;
|
||||
|
||||
public class TracTracCourseDesignUpdateHandler extends UpdateHandler implements CourseDesignChangedListener {
|
||||
|
||||
private final static String ACTION = "update_course";
|
||||
|
||||
private final static Logger logger = Logger.getLogger(TracTracCourseDesignUpdateHandler.class.getName());
|
||||
private JsonSerializer<CourseBase> courseSerializer;
|
||||
private final JsonSerializer<CourseBase> courseSerializer;
|
||||
private final IRace tractracRace;
|
||||
private final DomainFactory domainFactory;
|
||||
|
||||
public TracTracCourseDesignUpdateHandler(URI updateURI, String tracTracUsername, String tracTracPassword, Serializable tracTracEventId, Serializable raceId) {
|
||||
public TracTracCourseDesignUpdateHandler(URI updateURI, String tracTracUsername, String tracTracPassword, Serializable tracTracEventId, Serializable raceId, IRace tractracRace, DomainFactory domainFactory) {
|
||||
super(updateURI, ACTION, tracTracUsername, tracTracPassword, tracTracEventId, raceId);
|
||||
this.domainFactory = domainFactory;
|
||||
this.tractracRace = tractracRace;
|
||||
this.courseSerializer = new CourseJsonSerializer(
|
||||
new CourseBaseJsonSerializer(
|
||||
new WaypointJsonSerializer(
|
||||
@@ -39,12 +51,13 @@ public class TracTracCourseDesignUpdateHandler extends UpdateHandler implements
|
||||
}
|
||||
|
||||
@Override
|
||||
public void courseDesignChanged(CourseBase newCourseDesign) throws MalformedURLException, IOException {
|
||||
public void courseDesignChanged(final CourseBase newCourseDesign) throws MalformedURLException, IOException {
|
||||
if (!isActive()) {
|
||||
logger.info("Not sending course update to TracTrac because no URL has been provided.");
|
||||
return;
|
||||
}
|
||||
JSONObject serializedCourseDesign = courseSerializer.serialize(newCourseDesign);
|
||||
final CourseBase newCourseDesignWithExistingControlPoints = replaceControlPointsByMatchingExistingControlPoints(newCourseDesign);
|
||||
JSONObject serializedCourseDesign = courseSerializer.serialize(newCourseDesignWithExistingControlPoints);
|
||||
String payload = serializedCourseDesign.toJSONString();
|
||||
URL currentCourseDesignURL = buildUpdateURL();
|
||||
logger.info("Using " + currentCourseDesignURL.toString() + " for the course update!");
|
||||
@@ -66,4 +79,28 @@ public class TracTracCourseDesignUpdateHandler extends UpdateHandler implements
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private CourseBase replaceControlPointsByMatchingExistingControlPoints(CourseBase courseDesign) {
|
||||
final Iterable<IControl> candidates = domainFactory.getControlsForCourseArea(tractracRace.getEvent(), tractracRace.getCourseArea());
|
||||
final CourseBase result = new CourseDataImpl(courseDesign.getName());
|
||||
int zeroBasedPosition = 0;
|
||||
boolean changed = false;
|
||||
for (final Waypoint waypoint : courseDesign.getWaypoints()) {
|
||||
if (Util.size(waypoint.getMarks()) > 1) {
|
||||
final Iterator<Mark> markIter = waypoint.getMarks().iterator();
|
||||
final Mark first = markIter.next();
|
||||
final Mark second = markIter.next();
|
||||
final IControl existingControl = domainFactory.getExistingControlWithTwoMarks(candidates, first, second);
|
||||
if (existingControl == null) {
|
||||
result.addWaypoint(zeroBasedPosition++, waypoint);
|
||||
} else {
|
||||
domainFactory.getBaseDomainFactory().createWaypoint(domainFactory.getOrCreateControlPoint(new ControlPointAdapter(existingControl)),
|
||||
waypoint.getPassingInstructions());
|
||||
}
|
||||
} else {
|
||||
result.addWaypoint(zeroBasedPosition++, waypoint);
|
||||
}
|
||||
}
|
||||
return changed ? result : courseDesign;
|
||||
}
|
||||
}
|
||||
-3
@@ -26,14 +26,11 @@ public class GateDeserializer implements JsonDeserializer<ControlPointWithTwoMar
|
||||
|
||||
@Override
|
||||
public ControlPointWithTwoMarks deserialize(JSONObject object) throws JsonDeserializationException {
|
||||
|
||||
JSONObject jsonLeftMark = (JSONObject) object.get(GateJsonSerializer.FIELD_LEFT);
|
||||
JSONObject jsonRightMark = (JSONObject) object.get(GateJsonSerializer.FIELD_RIGHT);
|
||||
Mark leftMark = markDeserializer.deserialize(jsonLeftMark);
|
||||
Mark rightMark = markDeserializer.deserialize(jsonRightMark);
|
||||
|
||||
String gateName = (String) object.get(BaseControlPointJsonSerializer.FIELD_NAME);
|
||||
|
||||
ControlPointWithTwoMarks controlPoint = factory.createControlPointWithTwoMarks(leftMark, rightMark, gateName);
|
||||
return controlPoint;
|
||||
}
|
||||
|
||||
-1
@@ -34,5 +34,4 @@ public class MarkDeserializer implements JsonDeserializer<Mark> {
|
||||
Mark mark = factory.getOrCreateMark(id, name, type, color, shape, pattern);
|
||||
return mark;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user