package com.sap.sailing.server.simulation; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.TreeMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.logging.Logger; import com.sap.sailing.domain.base.BoatClass; import com.sap.sailing.domain.base.Competitor; import com.sap.sailing.domain.base.Course; import com.sap.sailing.domain.base.Leg; import com.sap.sailing.domain.base.Mark; import com.sap.sailing.domain.base.Regatta; import com.sap.sailing.domain.base.Waypoint; import com.sap.sailing.domain.common.LegIdentifier; import com.sap.sailing.domain.common.LegIdentifierImpl; import com.sap.sailing.domain.common.LegType; import com.sap.sailing.domain.common.NoWindException; import com.sap.sailing.domain.common.PathType; import com.sap.sailing.domain.common.Position; import com.sap.sailing.domain.common.RaceIdentifier; import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; import com.sap.sailing.domain.common.Wind; import com.sap.sailing.domain.common.WindSource; import com.sap.sailing.domain.common.tracking.GPSFix; import com.sap.sailing.domain.common.tracking.GPSFixMoving; import com.sap.sailing.domain.polars.PolarDataService; import com.sap.sailing.domain.tracking.DynamicTrackedRegatta; import com.sap.sailing.domain.tracking.MarkPassing; import com.sap.sailing.domain.tracking.RaceListener; import com.sap.sailing.domain.tracking.TrackedLeg; import com.sap.sailing.domain.tracking.TrackedRace; import com.sap.sailing.domain.tracking.impl.AbstractRaceChangeListener; import com.sap.sailing.server.RacingEventService; import com.sap.sailing.simulator.Path; import com.sap.sailing.simulator.PolarDiagram; import com.sap.sailing.simulator.SimulationParameters; import com.sap.sailing.simulator.SimulationResults; import com.sap.sailing.simulator.Simulator; import com.sap.sailing.simulator.impl.PolarDiagramGPS; import com.sap.sailing.simulator.impl.SimulationParametersImpl; import com.sap.sailing.simulator.impl.SimulatorImpl; import com.sap.sailing.simulator.impl.SparseSimulationDataException; import com.sap.sailing.simulator.util.SailingSimulatorConstants; import com.sap.sailing.simulator.windfield.WindFieldGenerator; import com.sap.sailing.simulator.windfield.impl.WindFieldTrackedRaceImpl; import com.sap.sse.common.Duration; import com.sap.sse.common.TimePoint; import com.sap.sse.common.impl.MillisecondsDurationImpl; import com.sap.sse.common.impl.MillisecondsTimePoint; import com.sap.sse.util.SmartFutureCache; public class SimulationServiceImpl implements SimulationService { private static final Logger logger = Logger.getLogger(SimulationService.class.getName()); final private ScheduledExecutorService executor; final private SmartFutureCache cache; final private RacingEventService racingEventService; final private ScheduledExecutorService scheduler; final private HashMap raceListeners; final private HashMap legListeners; final private long WAIT_MILLIS = 20000; // milliseconds to wait until earliest cache-update for simulation public SimulationServiceImpl(ScheduledExecutorService executor, RacingEventService racingEventService) { this.executor = executor; this.scheduler = executor; this.racingEventService = racingEventService; if (racingEventService != null) { this.raceListeners = new HashMap(); this.legListeners = new HashMap(); this.cache = new SmartFutureCache( new SmartFutureCache.AbstractCacheUpdater() { @Override public SimulationResults computeCacheUpdate(LegIdentifier key, SmartFutureCache.EmptyUpdateInterval updateInterval) throws Exception { logger.info("Simulation Started: \"" + key.toString() + "\""); SimulationResults results = computeSimulationResults(key); logger.info("Simulation Finished: \"" + key.toString() + "\", Results-Version: "+ (results==null?0:results.getVersion().asMillis())); return results; } }, "SmartFutureCache.simulationService (" + racingEventService.toString() + ")"); } else { this.raceListeners = null; this.legListeners = null; this.cache = null; } } private class SimulationRaceListener implements RaceListener { @Override public void raceAdded(TrackedRace trackedRace) { } @Override public void raceRemoved(TrackedRace trackedRace) { RaceIdentifier raceIdentifier = trackedRace.getRaceIdentifier(); LegChangeListener listener = legListeners.get(raceIdentifier); if (listener != null) { trackedRace.removeListener(listener); } int legNumber = 1; Iterator iterator = trackedRace.getTrackedLegs().iterator(); while (iterator.hasNext()) { LegIdentifier key = new LegIdentifierImpl((RegattaAndRaceIdentifier)raceIdentifier, ""+legNumber); SimulationResults entry = cache.get(key, false); if (entry != null) { cache.remove(key); } legNumber++; iterator.next(); } legListeners.remove(raceIdentifier); } } private class LegChangeListener extends AbstractRaceChangeListener { private final TrackedRace trackedRace; private LegIdentifier legIdentifier; private final ScheduledExecutorService scheduler; private boolean covered; public LegChangeListener(TrackedRace trackedRace, ScheduledExecutorService scheduler) { this.trackedRace = trackedRace; this.legIdentifier = null; this.scheduler = scheduler; this.covered = false; } public boolean isLive() { return trackedRace.isLive(MillisecondsTimePoint.now()); } public void setLegIdentifier(LegIdentifier legIdentifier) { if (this.legIdentifier != null) { if (!this.legIdentifier.equals(legIdentifier)) { this.legIdentifier = legIdentifier; this.covered = false; } } else { this.legIdentifier = legIdentifier; } } @Override protected void defaultAction() { if ((!this.covered)&&(legIdentifier!=null)) { this.covered = true; LegIdentifier tmpLegIdentifier = new LegIdentifierImpl(legIdentifier, legIdentifier.getLegName()); scheduler.schedule(() -> triggerUpdate(tmpLegIdentifier), WAIT_MILLIS, TimeUnit.MILLISECONDS); } } private void triggerUpdate(LegIdentifier legIdentifier) { if (this.legIdentifier.equals(legIdentifier)) { this.covered = false; } logger.info("Simulation Scheduled Update Triggered: \"" + legIdentifier.toString() + "\""); cache.triggerUpdate(legIdentifier, null); } @Override public void startOfRaceChanged(TimePoint oldStartOfRace, TimePoint newStartOfRace) { // relevant for simulation: start of leg 1 changes; requires update of leg 1 // TODO: update leg 1 } @Override public void finishedTimeChanged(TimePoint oldFinishedTime, TimePoint newFinishedTime) { // relevant for simulation? for last leg? // TODO: update last leg? } @Override public void windSourcesToExcludeChanged(Iterable windSourcesToExclude) { // relevant for simulation: update all legs when wind changes overall // TODO: update all legs } @Override public void startOfTrackingChanged(TimePoint oldStartOfTracking, TimePoint newStartOfTracking) { // irrelevant for simulation } @Override public void endOfTrackingChanged(TimePoint oldEndOfTracking, TimePoint newEndOfTracking) { // irrelevant for simulation } @Override public void startTimeReceivedChanged(TimePoint startTimeReceived) { // irrelevant for simulation: should be covered by startOfRaceChanged() } @Override public void markPositionChanged(GPSFix fix, Mark mark, boolean firstInTrack) { // relevant for simulation // TODO: identify influenced legs and update these legs } @Override public void windDataReceived(Wind wind, WindSource windSource) { // relevant for simulation: update legs influenced by wind // TODO: update influenced legs; "live" update current leg defaultAction(); } @Override public void windDataRemoved(Wind wind, WindSource windSource) { // relevant for simulation: update legs influenced by wind // TODO: update influenced legs; "live" update current leg defaultAction(); } @Override public void windAveragingChanged(long oldMillisecondsOverWhichToAverage, long newMillisecondsOverWhichToAverage) { // relevant for simulation: update legs influenced by wind // TODO: update influenced legs; "live" update current leg defaultAction(); } @Override public void competitorPositionChanged(GPSFixMoving fix, Competitor item) { // irrelevant for simulation: covered by wind estimation } @Override public void markPassingReceived(Competitor competitor, Map oldMarkPassings, Iterable markPassings) { // relevant for simulation: update start- and end-times of legs // TODO: update influenced legs; "live" update current leg } @Override public void speedAveragingChanged(long oldMillisecondsOverWhichToAverage, long newMillisecondsOverWhichToAverage) { // irrelevant for simulation } @Override public void delayToLiveChanged(long delayToLiveInMillis) { // irrelevant for simulation } @Override public void waypointAdded(int zeroBasedIndex, Waypoint waypointThatGotAdded) { // relevant for simulation: update legs influenced waypoint // TODO: update influenced legs; "live" update current leg } @Override public void waypointRemoved(int zeroBasedIndex, Waypoint waypointThatGotRemoved) { // relevant for simulation: update legs influenced waypoint // TODO: update influenced legs; "live" update current leg } } @Override public long getSimulationResultsVersion(LegIdentifier legIdentifier) { SimulationResults result = cache.get(legIdentifier, false); long version = (result == null ? 0 : result.getVersion().asMillis()); logger.fine("Simulation Results-Version: " + version); return version; } @Override public SimulationResults getSimulationResults(LegIdentifier legIdentifier) { SimulationResults result = cache.get(legIdentifier, false); if (result == null) { logger.fine("Simulation Get: Cache Empty: \"" + legIdentifier.toString() + "\""); if (!raceListeners.containsKey(legIdentifier.getRegattaName())) { Regatta regatta = racingEventService.getRegattaByName(legIdentifier.getRegattaName()); DynamicTrackedRegatta trackedRegatta = racingEventService.getTrackedRegatta(regatta); SimulationRaceListener raceListener = new SimulationRaceListener(); raceListeners.put(legIdentifier.getRegattaName(), raceListener); trackedRegatta.addRaceListener(raceListener); } if (!legListeners.containsKey(legIdentifier.getRaceIdentifier())) { TrackedRace trackedRace = racingEventService.getTrackedRace(legIdentifier); if (trackedRace != null) { LegChangeListener listener = new LegChangeListener(trackedRace, scheduler); if (listener.isLive()) { listener.setLegIdentifier(legIdentifier); } legListeners.put(legIdentifier.getRaceIdentifier(), listener); trackedRace.addListener(listener); } } else { LegChangeListener listener = legListeners.get(legIdentifier.getRaceIdentifier()); if (listener.isLive()) { listener.setLegIdentifier(legIdentifier); } } logger.info("Simulation Get: Update Triggered: \"" + legIdentifier.toString() + "\""); cache.triggerUpdate(legIdentifier, null); result = cache.get(legIdentifier, true); // take first simulation result that becomes available } if (result == null) { logger.fine("Simulation Get: Null-Result: \"" + legIdentifier.toString() + "\""); } return result; } private List getLinePositions(Waypoint wayPoint, TimePoint at, TrackedRace trackedRace) { List line = new ArrayList(); if (wayPoint != null) { for (Mark lineMark : wayPoint.getMarks()) { Position estimatedMarkPosition = trackedRace.getOrCreateTrack(lineMark).getEstimatedPosition(at, /* extrapolate */ false); if (estimatedMarkPosition != null) { line.add(estimatedMarkPosition); } } } return line; } public SimulationResults computeSimulationResults(LegIdentifier legIdentifier) throws InterruptedException, ExecutionException { TimePoint simulationStartTime = MillisecondsTimePoint.now(); SimulationResults result = null; TrackedRace trackedRace = racingEventService.getTrackedRace(legIdentifier); if (trackedRace != null) { int legNumber = legIdentifier.getLegNumber(); Course raceCourse = trackedRace.getRace().getCourse(); Leg leg = raceCourse.getLegs().get(legNumber); // get previous mark or start line as start-position Waypoint fromWaypoint = leg.getFrom(); // get next mark as end-position Waypoint toWaypoint = leg.getTo(); TimePoint startTimePoint = null; TimePoint endTimePoint = null; MarkPassing markPassing; Iterator markPassingIterator = trackedRace.getMarkPassingsInOrder(fromWaypoint).iterator(); if (markPassingIterator.hasNext()) { markPassing = markPassingIterator.next(); } else { markPassing = null; } if (markPassing != null) { startTimePoint = markPassing.getTimePoint(); } markPassingIterator = trackedRace.getMarkPassingsInOrder(toWaypoint).iterator(); if (markPassingIterator.hasNext()) { markPassing = markPassingIterator.next(); } else { markPassing = null; } if (markPassing != null) { endTimePoint = markPassing.getTimePoint(); } long legDuration = 0; if ((startTimePoint != null) && (endTimePoint != null)) { legDuration = endTimePoint.asMillis() - startTimePoint.asMillis(); } Position startPosition = null; List startLine = null; Position endPosition = null; List endLine = null; if (startTimePoint != null) { startPosition = trackedRace.getApproximatePosition(fromWaypoint, startTimePoint); List line = this.getLinePositions(fromWaypoint, startTimePoint, trackedRace); if (line.size() == 2) { startLine = line; } } if (endTimePoint != null) { endPosition = trackedRace.getApproximatePosition(toWaypoint, endTimePoint); List line = this.getLinePositions(toWaypoint, endTimePoint, trackedRace); if (line.size() == 2) { endLine = line; } } else if (startTimePoint != null) { endPosition = trackedRace.getApproximatePosition(toWaypoint, startTimePoint); } // determine legtype upwind/downwind/reaching LegType legType = null; if (startTimePoint != null) { try { legType = trackedRace.getTrackedLeg(leg).getLegType(startTimePoint); } catch (NoWindException e) { return null; } } else { return null; } // get windfield WindFieldGenerator windField = new WindFieldTrackedRaceImpl(trackedRace); Duration timeStep = new MillisecondsDurationImpl(15 * 1000); windField.generate(startTimePoint, null, timeStep); // prepare simulation-parameters List course = new ArrayList(); course.add(startPosition); course.add(endPosition); BoatClass boatClass = trackedRace.getRace().getBoatClass(); PolarDataService polarDataService = racingEventService.getPolarDataService(); PolarDiagram polarDiagram; try { polarDiagram = new PolarDiagramGPS(boatClass, polarDataService); } catch (SparseSimulationDataException e) { polarDiagram = null; // TODO: raise a UI message, to inform user about missing polar data resulting in unability to simulate } Map paths = null; if (polarDiagram != null) { double simuStepSeconds = startPosition.getDistance(endPosition).getNauticalMiles() / ((PolarDiagramGPS) polarDiagram).getAvgSpeed() * 3600 / 100; Duration simuStep = new MillisecondsDurationImpl(Math.round(simuStepSeconds) * 1000); SimulationParameters simulationPars = new SimulationParametersImpl(course, startLine, endLine, polarDiagram, windField, simuStep, SailingSimulatorConstants.ModeEvent, true, true, legType); paths = getAllPathsEvenTimed(simulationPars, timeStep.asMillis()); } // prepare simulator-results result = new SimulationResults(startTimePoint.asDate(), timeStep.asMillis(), legDuration, startPosition, endPosition, paths, null, simulationStartTime); } return result; } public Map getAllPaths(SimulationParameters simulationParameters) throws InterruptedException, ExecutionException { Simulator simulator = new SimulatorImpl(simulationParameters); Map result = new HashMap(); Future taskOmniscient = null; if (simulationParameters.showOmniscient()) { // schedule omniscient task taskOmniscient = executor.submit(() -> simulator.getPath(PathType.OMNISCIENT)); } Future task1TurnerLeft = null; Future task1TurnerRight = null; if (simulationParameters.getLegType() != LegType.REACHING) { // schedule 1-turner tasks task1TurnerLeft = executor.submit(() -> simulator.getPath(PathType.ONE_TURNER_LEFT)); task1TurnerRight = executor.submit(() -> simulator.getPath(PathType.ONE_TURNER_RIGHT)); } Future taskOpportunistLeft = null; Future taskOpportunistRight = null; if (simulationParameters.showOpportunist()) { // schedule opportunist tasks (which depend on 1-turner results) taskOpportunistLeft = executor.submit(() -> simulator.getPath(PathType.OPPORTUNIST_LEFT)); taskOpportunistRight = executor.submit(() -> simulator.getPath(PathType.OPPORTUNIST_RIGHT)); } Path path1TurnerLeft = null; Path path1TurnerRight = null; if (simulationParameters.getLegType() != LegType.REACHING) { // collect 1-turner results path1TurnerLeft = task1TurnerLeft.get(); result.put(PathType.ONE_TURNER_LEFT, path1TurnerLeft); path1TurnerRight = task1TurnerRight.get(); result.put(PathType.ONE_TURNER_RIGHT, path1TurnerRight); } Path pathOpportunistLeft = null; Path pathOpportunistRight = null; if (simulationParameters.showOpportunist()) { // collect opportunist results pathOpportunistLeft = taskOpportunistLeft.get(); if (path1TurnerLeft != null) { if (!path1TurnerLeft.getAlgorithmTimedOut() && (pathOpportunistLeft.getTurnCount() == 1)) { pathOpportunistLeft = path1TurnerLeft; } } result.put(PathType.OPPORTUNIST_LEFT, pathOpportunistLeft); pathOpportunistRight = taskOpportunistRight.get(); if (path1TurnerRight != null) { if (!path1TurnerRight.getAlgorithmTimedOut() && (pathOpportunistRight.getTurnCount() == 1)) { pathOpportunistRight = path1TurnerRight; } } result.put(PathType.OPPORTUNIST_RIGHT, pathOpportunistRight); } if (simulationParameters.showOmniscient()) { // collect omniscient result (last, since usually slowest calculation) Path pathOmniscient = taskOmniscient.get(); if (path1TurnerLeft != null) { if (!path1TurnerLeft.getAlgorithmTimedOut() && (pathOmniscient.getFinalTime().after(path1TurnerLeft.getFinalTime()))) { pathOmniscient = path1TurnerLeft; } } if (path1TurnerRight != null) { if (!path1TurnerRight.getAlgorithmTimedOut() && (pathOmniscient.getFinalTime().after(path1TurnerRight.getFinalTime()))) { pathOmniscient = path1TurnerRight; } } if (pathOpportunistLeft != null) { if (!pathOpportunistLeft.getAlgorithmTimedOut() && (pathOmniscient.getFinalTime().after(pathOpportunistLeft.getFinalTime()))) { pathOmniscient = pathOpportunistLeft; } } if (pathOpportunistRight != null) { if (!pathOpportunistRight.getAlgorithmTimedOut() && (pathOmniscient.getFinalTime().after(pathOpportunistRight.getFinalTime()))) { pathOmniscient = pathOpportunistRight; } } result.put(PathType.OMNISCIENT, pathOmniscient); } // return combined result return result; } public Map getAllPathsEvenTimed(SimulationParameters simuPars, long millisecondsStep) throws InterruptedException, ExecutionException { Map allTimedPaths = new TreeMap(); Map allPaths = this.getAllPaths(simuPars); for (Entry entry : allPaths.entrySet()) { PathType pathType = entry.getKey(); Path pathValue = entry.getValue(); if (pathValue != null) { allTimedPaths.put(pathType, pathValue.getEvenTimedPath(millisecondsStep)); } } return allTimedPaths; } }