From 11583c87a411ba48311f61008d36d83ae3bf24c6 Mon Sep 17 00:00:00 2001 From: "Christopher Ronnewinkel (D036654)" Date: Fri, 18 Jul 2014 18:22:59 +0200 Subject: [PATCH 01/20] tune streamlet-parameters for spatial-wind: fix alpha vlaue-range, simplify linewidth, motionscale; --- .../gwt/ui/simulator/streamlets/SimulatorField.java | 2 +- .../simulator/streamlets/WindInfoForRaceVectorField.java | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/streamlets/SimulatorField.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/streamlets/SimulatorField.java index 55461318fee..30f545edcc0 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/streamlets/SimulatorField.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/streamlets/SimulatorField.java @@ -220,7 +220,7 @@ public class SimulatorField implements VectorField { private String[] createColorsForSpeeds() { String[] colors = new String[256]; double alphaMin = 0.0; - double alphaMax = 1.0; + double alphaMax = 0.9; int greyValue = 255; for (int i = 0; i < 256; i++) { colors[i] = "rgba(" + (greyValue) + "," + (greyValue) + "," + (greyValue) + "," diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/streamlets/WindInfoForRaceVectorField.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/streamlets/WindInfoForRaceVectorField.java index 80efe2d3dbb..6ac8207570d 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/streamlets/WindInfoForRaceVectorField.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/streamlets/WindInfoForRaceVectorField.java @@ -156,7 +156,7 @@ public class WindInfoForRaceVectorField implements VectorField, AverageLatitudeP public double getMotionScale(int zoomLevel) { // This implementation is copied from SimulatorField, hoping it does something useful in combination with // the Swarm implementation. - return 0.07 * Math.pow(1.6, Math.min(1.0, 6.0 - zoomLevel)); + return 0.05 * Math.pow(1.6, Math.min(1.0, 6.0 - zoomLevel)); } @Override @@ -170,7 +170,7 @@ public class WindInfoForRaceVectorField implements VectorField, AverageLatitudeP */ @Override public double getLineWidth(double speed) { - return 3.*speed/MAX_WIND_SPEED_IN_KNOTS; + return 1.2; } @Override @@ -180,7 +180,7 @@ public class WindInfoForRaceVectorField implements VectorField, AverageLatitudeP @Override public double getParticleFactor() { - return 1.0; + return 0.5; } /** @@ -188,7 +188,7 @@ public class WindInfoForRaceVectorField implements VectorField, AverageLatitudeP */ @Override public String getColor(double speed) { - return "rgba(255,255,255,"+Math.min(255, Math.round(255.*speed/MAX_WIND_SPEED_IN_KNOTS))+")"; + return "rgba(255,255,255,"+Math.min(1.0, 0.4+0.8*speed/MAX_WIND_SPEED_IN_KNOTS)+")"; } } From 26b9004cf4aca87f22cb591262b007a006966d6c Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Wed, 23 Jul 2014 09:50:07 +0000 Subject: [PATCH 02/20] Updated home (markdown) --- home.md | 1 + 1 file changed, 1 insertion(+) diff --git a/home.md b/home.md index 3b92ad2e70a..64e2970f0fe 100644 --- a/home.md +++ b/home.md @@ -15,6 +15,7 @@ Like businesses, sailors need the latest information to make strategic decisions * [[EC2 Backup Strategy|wiki/amazon-ec2-backup-strategy]] * [[TracTrac|wiki/tractrac-lifecycle]] * [[Analytics on a stick|wiki/analytics-on-a-stick]] + * [[Operating Igtimi WindBots/windbot-operations]] * [[Monitoring Apache and RabbitMQ|wiki/monitoring-apache-and-rabbitmq]] * [[JMX Support|wiki/jmx]] * Information for Developers From 09b01ef894008efdab867ef1fbbef173a630d629 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Wed, 23 Jul 2014 10:05:36 +0000 Subject: [PATCH 03/20] Created windbot operations (markdown) --- Operating-Igtimi-WindBots/windbot-operations.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 Operating-Igtimi-WindBots/windbot-operations.md diff --git a/Operating-Igtimi-WindBots/windbot-operations.md b/Operating-Igtimi-WindBots/windbot-operations.md new file mode 100644 index 00000000000..a7acb6b583f --- /dev/null +++ b/Operating-Igtimi-WindBots/windbot-operations.md @@ -0,0 +1,13 @@ +# Operating Igtimi WindBots + +For wind measurements we're trying to consistently use Igtimi WindBot equipment. The AdminConsole has a tab "Igtimi Accounts" where accounts for the Igtimi YachtBot web site can be managed. WindBots associated with any of these accounts will feed their wind readings, when active, into races being tracked. Operations of the WindBots mainly needs to take care of charging the devices, turning them on when they leave the harbor and turning them off when they leave the race course. This can also be delegated to race officials carrying these devices on their vessels. + +The devices have three LEDs whose status works as follows: + +1. The three LED’s are “power”, “connection”, and “GPS” in order from top to bottom. +2. All the lights should be on when operating normally. +3. Slow flashing is normal while booting. +4. Fast flashing means that function can’t start or has failed - e.g. SIM not inserted. i.e. you shouldn’t see this +5. During sync, the “power” light blips and the “connection” LED should stay on until the sync has finished. + +More information on running the WindBots can be found [here](https://igtimi.desk.com/customer/portal/articles/1494533-yachtbot-tracker-basic-operation?b_id=1690). \ No newline at end of file From 66a3d1f68703428ad2dad64204569cc04236febc Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Wed, 23 Jul 2014 18:17:58 +0200 Subject: [PATCH 04/20] added a test case ensuring that empty leg details transition properly to non-empty through incremental leaderboard diffing --- .../test/LeaderboardDTODiffingTest.java | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/common/test/LeaderboardDTODiffingTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/common/test/LeaderboardDTODiffingTest.java index ba5e9886d66..b9b1a1c3e74 100755 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/common/test/LeaderboardDTODiffingTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/common/test/LeaderboardDTODiffingTest.java @@ -14,8 +14,11 @@ import java.io.ObjectInputStream; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; import org.junit.Before; import org.junit.Test; @@ -72,6 +75,39 @@ public class LeaderboardDTODiffingTest { } return null; } + + @Test + public void testEmptyLegDetailsGetFilled() throws FileNotFoundException, IOException, ClassNotFoundException { + // remove legDetails for R9 for all competitors from previousVersion; remember which ones did contain data + Set hadLegDetailsInR9 = new HashSet<>(); + Map newRows = new HashMap<>(); + for (CompetitorDTO competitor : previousVersion.rows.keySet()) { + LeaderboardRowDTO competitorRow = new LeaderboardRowDTO(); + cloner.clone(newVersion.rows.get(competitor), competitorRow); + Map newFieldsByRaceColumnName = new HashMap<>(); + for (Entry e : competitorRow.fieldsByRaceColumnName.entrySet()) { + LeaderboardEntryDTO newEntry = new LeaderboardEntryDTO(); + cloner.clone(e.getValue(), newEntry); + newFieldsByRaceColumnName.put(e.getKey(), newEntry); + } + competitorRow.fieldsByRaceColumnName = newFieldsByRaceColumnName; + newRows.put(competitor, competitorRow); // use a copy of the row so we can null out legDetails for R9 in previousVersion only + LeaderboardRowDTO previousRow = previousVersion.rows.get(competitor); + assertNotSame(previousRow, competitorRow); + if (previousRow.fieldsByRaceColumnName.get("R9").legDetails != null) { + hadLegDetailsInR9.add(competitor); + previousRow.fieldsByRaceColumnName.get("R9").legDetails = null; + assertNotNull(competitorRow.fieldsByRaceColumnName.get("R9").legDetails != null); + } + } + newVersion.rows = newRows; + newVersion.strip(previousVersion); // should contain the leg details now + LeaderboardDTO applied = newVersion.getLeaderboardDTO(previousVersion); + // now applied must contain the leg details + for (CompetitorDTO competitor : hadLegDetailsInR9) { + assertNotNull(applied.rows.get(competitor).fieldsByRaceColumnName.get("R9").legDetails); + } + } @Test public void testLeaderboardSuccessfullyRead() { From 080be4d5becdd53caea0ba052497700ffb38ace0 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Wed, 23 Jul 2014 21:35:05 +0200 Subject: [PATCH 05/20] bug 2088: avoiding NPE when drawing advantage line with fixes lacking speed/bearing --- .../com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java index 53d16cdb0c1..86a2bb40188 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java @@ -883,7 +883,7 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe } lastBoatFix = getBoatFix(visibleLeaderInfo.getB(), date); } - if (isVisibleLeaderInfoComplete && isLegTypeKnown && lastBoatFix != null) { + if (isVisibleLeaderInfoComplete && isLegTypeKnown && lastBoatFix != null && lastBoatFix.speedWithBearing != null) { LegInfoDTO legInfoDTO = lastRaceTimesInfo.getLegInfos().get(visibleLeaderInfo.getA() - 1); double advantageLineLengthInKm = 1.0; // TODO this should probably rather scale with the visible // area of the map; bug 616 From 26fff28560c823b257ccc7f2fc90cbd108c4b4d4 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Wed, 23 Jul 2014 22:41:54 +0200 Subject: [PATCH 06/20] bug 2086: avoid NPE by no trying to correct declination with a null position when the Wind fix wouldn't be constructed anyway --- .../shared/IgtimiWindReceiver.java | 77 ++++++++++--------- 1 file changed, 41 insertions(+), 36 deletions(-) diff --git a/java/com.sap.sailing.domain.igtimiadapter/src/com/sap/sailing/domain/igtimiadapter/shared/IgtimiWindReceiver.java b/java/com.sap.sailing.domain.igtimiadapter/src/com/sap/sailing/domain/igtimiadapter/shared/IgtimiWindReceiver.java index f0f4b765eb9..c0b114ed3ac 100755 --- a/java/com.sap.sailing.domain.igtimiadapter/src/com/sap/sailing/domain/igtimiadapter/shared/IgtimiWindReceiver.java +++ b/java/com.sap.sailing.domain.igtimiadapter/src/com/sap/sailing/domain/igtimiadapter/shared/IgtimiWindReceiver.java @@ -185,42 +185,47 @@ public class IgtimiWindReceiver implements BulkFixReceiver { Speed aws = getAWS(timePoint, awsPair); com.sap.sse.common.Util.Pair gpsPair = getSurroundingFixes(getGpsTrack(deviceSerialNumber), timePoint); Position pos = getPosition(timePoint, gpsPair); - com.sap.sse.common.Util.Pair hdgPair = getSurroundingFixes(getHdgTrack(deviceSerialNumber), timePoint); - com.sap.sse.common.Util.Pair hdgmPair = getSurroundingFixes(getHdgmTrack(deviceSerialNumber), timePoint); - Bearing heading = getHeading(timePoint, hdgPair, hdgmPair, pos); - if (awa != null && aws != null && pos != null && heading != null) { - Bearing apparentWindDirection = heading.add(awa); - SpeedWithBearing apparentWindSpeedWithDirection = new KnotSpeedWithBearingImpl(aws.getKnots(), apparentWindDirection); - /* - * Hint from Brent Russell from Igtimi, at 2013-12-05 on the question whether to use GpsLatLong to improve - * precision of boat speed / coarse over SOG/COG measurements: - * - * "Personally I would use COG/SOG exclusively, and if unhappy with the result add a small amount of - * smoothing and consider dropping samples as outliers if they cause a SOG discontinuity. The latter might - * happen as a satellite is dropped/acquired - and I'd expect to see a time correlated position jump as - * well. Probably not though a direction/speed correlation :) - * - * All our GPS systems are using Doppler to calculate COG/SOG and this should be the most accurate measure. - * I don't believe that delta position really adds any more "truth" to the measurement of physical reality, - * if that makes sense. I'd trust d-p even less at low speeds, where you'll see the most disagreement. Also - * there is a significant quantisation noise error in the d-p calculations from the GPS resolution too, so - * you'd have to smooth it before averaging - possibly in a speed dependent way. - * - * Remember that Doppler COG/SOG is using the same raw satellite measurements that are being used to - * calculate position, just the algorithm is different. I suspect that merging the two might be, in - * practice, just a slightly indirect way of averaging. If you like the central limit theorem in action over - * the set of algorithms! - * - * So again, my personal preference would be to work with the data that should be the most accurate - * (COG/SOG) and consider algorithms that handle smoothing of that data best." - */ - com.sap.sse.common.Util.Pair sogPair = getSurroundingFixes(getSogTrack(deviceSerialNumber), timePoint); - Speed sog = getSOG(timePoint, sogPair); - com.sap.sse.common.Util.Pair cogPair = getSurroundingFixes(getCogTrack(deviceSerialNumber), timePoint); - Bearing cog = getCOG(timePoint, cogPair); - SpeedWithBearing sogCog = new KnotSpeedWithBearingImpl(sog.getKnots(), cog); - SpeedWithBearing trueWindSpeedAndDirection = apparentWindSpeedWithDirection.add(sogCog); - result = new WindImpl(pos, timePoint, trueWindSpeedAndDirection); + if (pos != null) { + com.sap.sse.common.Util.Pair hdgPair = getSurroundingFixes(getHdgTrack(deviceSerialNumber), timePoint); + com.sap.sse.common.Util.Pair hdgmPair = getSurroundingFixes(getHdgmTrack(deviceSerialNumber), timePoint); + Bearing heading = getHeading(timePoint, hdgPair, hdgmPair, pos); + if (awa != null && aws != null && heading != null) { + Bearing apparentWindDirection = heading.add(awa); + SpeedWithBearing apparentWindSpeedWithDirection = new KnotSpeedWithBearingImpl(aws.getKnots(), apparentWindDirection); + /* + * Hint from Brent Russell from Igtimi, at 2013-12-05 on the question whether to use GpsLatLong to + * improve precision of boat speed / coarse over SOG/COG measurements: + * + * "Personally I would use COG/SOG exclusively, and if unhappy with the result add a small amount of + * smoothing and consider dropping samples as outliers if they cause a SOG discontinuity. The latter + * might happen as a satellite is dropped/acquired - and I'd expect to see a time correlated position + * jump as well. Probably not though a direction/speed correlation :) + * + * All our GPS systems are using Doppler to calculate COG/SOG and this should be the most accurate + * measure. I don't believe that delta position really adds any more "truth" to the measurement of + * physical reality, if that makes sense. I'd trust d-p even less at low speeds, where you'll see the + * most disagreement. Also there is a significant quantisation noise error in the d-p calculations from + * the GPS resolution too, so you'd have to smooth it before averaging - possibly in a speed dependent + * way. + * + * Remember that Doppler COG/SOG is using the same raw satellite measurements that are being used to + * calculate position, just the algorithm is different. I suspect that merging the two might be, in + * practice, just a slightly indirect way of averaging. If you like the central limit theorem in action + * over the set of algorithms! + * + * So again, my personal preference would be to work with the data that should be the most accurate + * (COG/SOG) and consider algorithms that handle smoothing of that data best." + */ + com.sap.sse.common.Util.Pair sogPair = getSurroundingFixes(getSogTrack(deviceSerialNumber), timePoint); + Speed sog = getSOG(timePoint, sogPair); + com.sap.sse.common.Util.Pair cogPair = getSurroundingFixes(getCogTrack(deviceSerialNumber), timePoint); + Bearing cog = getCOG(timePoint, cogPair); + SpeedWithBearing sogCog = new KnotSpeedWithBearingImpl(sog.getKnots(), cog); + SpeedWithBearing trueWindSpeedAndDirection = apparentWindSpeedWithDirection.add(sogCog); + result = new WindImpl(pos, timePoint, trueWindSpeedAndDirection); + } else { + result = null; + } } else { result = null; } From 34923cda7542d5f427984b89f632ffa0d3176891 Mon Sep 17 00:00:00 2001 From: "Christopher Ronnewinkel (D036654)" Date: Wed, 23 Jul 2014 22:50:17 +0200 Subject: [PATCH 07/20] refactor Boundary to Grid with extension to a curved grid that is constructed from great circles; ensure symmetry along race course middle line; --- .../gwt/ui/server/SimulatorServiceImpl.java | 15 +- .../test/RectangularBoundaryTest.java | 8 +- .../sailing/simulator/test/SimulatorTest.java | 6 +- .../sailing/simulator/test/TreeGrowTest.java | 6 +- .../test/WindFieldGeneratorTest.java | 16 +- .../com/sap/sailing/simulator/Boundary.java | 46 ---- .../src/com/sap/sailing/simulator/Grid.java | 54 +++++ .../simulator/SimulationParameters.java | 2 +- .../sailing/simulator/impl/CurvedGrid.java | 218 ++++++++++++++++++ .../impl/PathGenerator1TurnerLeftDirect.java | 6 +- .../impl/PathGenerator1TurnerRightDirect.java | 6 +- .../simulator/impl/PathGeneratorDijkstra.java | 6 +- .../impl/PathGeneratorDynProgForward.java | 6 +- .../impl/PathGeneratorTreeGrowWind3.java | 4 +- ...ularBoundary.java => RectangularGrid.java} | 177 ++++---------- .../simulator/impl/SailingSimulatorImpl.java | 6 +- .../impl/SimulationParametersImpl.java | 6 +- .../simulator/windfield/WindField.java | 4 +- .../windfield/WindFieldGenerator.java | 4 +- .../windfield/WindFieldGeneratorFactory.java | 4 +- .../impl/WindFieldGeneratorBlastImpl.java | 4 +- .../impl/WindFieldGeneratorCombined.java | 6 +- .../impl/WindFieldGeneratorFactoryImpl.java | 4 +- .../impl/WindFieldGeneratorImpl.java | 14 +- .../impl/WindFieldGeneratorMeasured.java | 4 +- .../WindFieldGeneratorOscillationImpl.java | 4 +- 26 files changed, 392 insertions(+), 244 deletions(-) delete mode 100644 java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/Boundary.java create mode 100644 java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/Grid.java create mode 100644 java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/CurvedGrid.java rename java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/{RectangularBoundary.java => RectangularGrid.java} (51%) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SimulatorServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SimulatorServiceImpl.java index 0620b7f5082..3ba765dc2fa 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SimulatorServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SimulatorServiceImpl.java @@ -55,6 +55,7 @@ import com.sap.sailing.gwt.ui.simulator.windpattern.WindPatternDisplayManager; import com.sap.sailing.gwt.ui.simulator.windpattern.WindPatternNotFoundException; import com.sap.sailing.gwt.ui.simulator.windpattern.WindPatternSetting; import com.sap.sailing.simulator.BoatClassProperties; +import com.sap.sailing.simulator.Grid; import com.sap.sailing.simulator.Path; import com.sap.sailing.simulator.PolarDiagram; import com.sap.sailing.simulator.SailingSimulator; @@ -62,11 +63,11 @@ import com.sap.sailing.simulator.SimulationParameters; import com.sap.sailing.simulator.TimedPosition; import com.sap.sailing.simulator.TimedPositionWithSpeed; import com.sap.sailing.simulator.impl.ConfigurationManager; +import com.sap.sailing.simulator.impl.CurvedGrid; import com.sap.sailing.simulator.impl.PathGenerator1Turner; import com.sap.sailing.simulator.impl.PathImpl; import com.sap.sailing.simulator.impl.PolarDiagramCSV; import com.sap.sailing.simulator.impl.ReadingConfigurationFileStatus; -import com.sap.sailing.simulator.impl.RectangularBoundary; import com.sap.sailing.simulator.impl.SailingSimulatorImpl; import com.sap.sailing.simulator.impl.SimulationParametersImpl; import com.sap.sailing.simulator.impl.TimedPositionImpl; @@ -185,9 +186,7 @@ public class SimulatorServiceImpl extends RemoteServiceServlet implements Simula course.add(nw); course.add(se); - RectangularBoundary bd = new RectangularBoundary(nw, se, 0.1); - // List lattice = bd.extractLattice(params.getxRes(), - // params.getyRes()); + Grid bd = new CurvedGrid(nw, se); controlParameters.resetBlastRandomStream = params.isKeepState(); retreiveWindControlParameters(pattern); @@ -201,7 +200,7 @@ public class SimulatorServiceImpl extends RemoteServiceServlet implements Simula throw new WindPatternNotFoundException("Please select a valid wind pattern."); } - Position[][] grid = bd.extractGrid(params.getxRes(), params.getyRes(), params.getBorderY(), params.getBorderX()); + Position[][] grid = bd.generatePositions(params.getxRes(), params.getyRes(), params.getBorderY(), params.getBorderX()); wf.setPositionGrid(grid); TimePoint startTime = new MillisecondsTimePoint(params.getStartTime().getTime()); @@ -549,9 +548,9 @@ public class SimulatorServiceImpl extends RemoteServiceServlet implements Simula SailingSimulator sailingSimulator = new SailingSimulatorImpl(simulationParameters); Path gpsWind = sailingSimulator.getLegGPSTrack(SimulatorServiceUtils.toSimulatorUISelection(requestData.selection)); - RectangularBoundary rectangularBoundry = new RectangularBoundary(oldMovedPosition, newMovedPosition, 0.1); - this.controlParameters.baseWindBearing += rectangularBoundry.getSouth().getDegrees(); - WindFieldGeneratorMeasured windFieldGenerator = new WindFieldGeneratorMeasured(rectangularBoundry, this.controlParameters); + Grid grid = new CurvedGrid(oldMovedPosition, newMovedPosition); + this.controlParameters.baseWindBearing += grid.getSouth().getDegrees(); + WindFieldGeneratorMeasured windFieldGenerator = new WindFieldGeneratorMeasured(grid, this.controlParameters); windFieldGenerator.setGPSWind(gpsWind); TimePoint startTime = new MillisecondsTimePoint(requestData.oldMovedPointTimePoint); diff --git a/java/com.sap.sailing.simulator.test/src/com/sap/sailing/simulator/test/RectangularBoundaryTest.java b/java/com.sap.sailing.simulator.test/src/com/sap/sailing/simulator/test/RectangularBoundaryTest.java index b5c6ca5b64c..05c8dd9191e 100644 --- a/java/com.sap.sailing.simulator.test/src/com/sap/sailing/simulator/test/RectangularBoundaryTest.java +++ b/java/com.sap.sailing.simulator.test/src/com/sap/sailing/simulator/test/RectangularBoundaryTest.java @@ -6,8 +6,8 @@ import org.junit.Test; import com.sap.sailing.domain.common.Position; import com.sap.sailing.domain.common.impl.DegreePosition; -import com.sap.sailing.simulator.Boundary; -import com.sap.sailing.simulator.impl.RectangularBoundary; +import com.sap.sailing.simulator.Grid; +import com.sap.sailing.simulator.impl.RectangularGrid; public class RectangularBoundaryTest { @@ -16,8 +16,8 @@ public class RectangularBoundaryTest { Position p1 = new DegreePosition(25.661333, -90.752563); Position p2 = new DegreePosition(24.522137, -90.774536); - Boundary b = new RectangularBoundary(p1, p2, 0.1); - Position[][] grid = b.extractGrid(20,20,0,0); + Grid b = new RectangularGrid(p1, p2); + Position[][] grid = b.generatePositions(20,20,0,0); assertEquals("Number of lattice points",400,grid.length*grid[0].length); } diff --git a/java/com.sap.sailing.simulator.test/src/com/sap/sailing/simulator/test/SimulatorTest.java b/java/com.sap.sailing.simulator.test/src/com/sap/sailing/simulator/test/SimulatorTest.java index a29bb6039c2..16fd9731d41 100644 --- a/java/com.sap.sailing.simulator.test/src/com/sap/sailing/simulator/test/SimulatorTest.java +++ b/java/com.sap.sailing.simulator.test/src/com/sap/sailing/simulator/test/SimulatorTest.java @@ -20,7 +20,7 @@ import com.sap.sailing.simulator.Path; import com.sap.sailing.simulator.PolarDiagram; import com.sap.sailing.simulator.SimulationParameters; import com.sap.sailing.simulator.impl.PolarDiagram49STG; -import com.sap.sailing.simulator.impl.RectangularBoundary; +import com.sap.sailing.simulator.impl.RectangularGrid; import com.sap.sailing.simulator.impl.SailingSimulatorImpl; import com.sap.sailing.simulator.impl.SimulationParametersImpl; import com.sap.sailing.simulator.util.SailingSimulatorConstants; @@ -43,8 +43,8 @@ public class SimulatorTest { course.add(end); PolarDiagram pd = new PolarDiagram49STG();//PolarDiagram49.CreateStandard49(); - RectangularBoundary bd = new RectangularBoundary(start, end, 0.1); - Position[][] positions = bd.extractGrid(10, 10, 0, 0); + RectangularGrid bd = new RectangularGrid(start, end); + Position[][] positions = bd.generatePositions(10, 10, 0, 0); Bearing windBear = end.getBearingGreatCircle(start); WindControlParameters windParameters = new WindControlParameters(12.0, windBear.getDegrees()); WindFieldGenerator wf = new WindFieldGeneratorBlastImpl(bd, windParameters); diff --git a/java/com.sap.sailing.simulator.test/src/com/sap/sailing/simulator/test/TreeGrowTest.java b/java/com.sap.sailing.simulator.test/src/com/sap/sailing/simulator/test/TreeGrowTest.java index 9070c7b8565..4f50acfdbd8 100644 --- a/java/com.sap.sailing.simulator.test/src/com/sap/sailing/simulator/test/TreeGrowTest.java +++ b/java/com.sap.sailing.simulator.test/src/com/sap/sailing/simulator/test/TreeGrowTest.java @@ -22,7 +22,7 @@ import com.sap.sailing.simulator.PolarDiagram; import com.sap.sailing.simulator.SimulationParameters; import com.sap.sailing.simulator.impl.PathGeneratorTreeGrowWind3; import com.sap.sailing.simulator.impl.PolarDiagram49STG; -import com.sap.sailing.simulator.impl.RectangularBoundary; +import com.sap.sailing.simulator.impl.RectangularGrid; import com.sap.sailing.simulator.impl.SimulationParametersImpl; import com.sap.sailing.simulator.util.SailingSimulatorConstants; import com.sap.sailing.simulator.windfield.WindControlParameters; @@ -44,8 +44,8 @@ public class TreeGrowTest { course.add(start); course.add(end); PolarDiagram pd = new PolarDiagram49STG();//PolarDiagram49.CreateStandard49(); - RectangularBoundary bd = new RectangularBoundary(start, end, 0.1); - Position[][] positions = bd.extractGrid(10, 10, 0, 0); + RectangularGrid bd = new RectangularGrid(start, end); + Position[][] positions = bd.generatePositions(10, 10, 0, 0); //RectangularBoundary new_bd = new RectangularBoundary(start, end, 0.1); //Speed knotSpeed = new KnotSpeedImpl(8); WindControlParameters windParameters = new WindControlParameters(12, start.getBearingGreatCircle(end).reverse().getDegrees()); diff --git a/java/com.sap.sailing.simulator.test/src/com/sap/sailing/simulator/test/WindFieldGeneratorTest.java b/java/com.sap.sailing.simulator.test/src/com/sap/sailing/simulator/test/WindFieldGeneratorTest.java index 742748b1292..42ea29dc9b3 100644 --- a/java/com.sap.sailing.simulator.test/src/com/sap/sailing/simulator/test/WindFieldGeneratorTest.java +++ b/java/com.sap.sailing.simulator.test/src/com/sap/sailing/simulator/test/WindFieldGeneratorTest.java @@ -23,7 +23,7 @@ import com.sap.sailing.domain.common.impl.DegreePosition; import com.sap.sailing.domain.common.impl.MillisecondsDurationImpl; import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; import com.sap.sailing.domain.tracking.Wind; -import com.sap.sailing.simulator.impl.RectangularBoundary; +import com.sap.sailing.simulator.impl.RectangularGrid; import com.sap.sailing.simulator.impl.TimedPositionWithSpeedImpl; import com.sap.sailing.simulator.windfield.WindControlParameters; import com.sap.sailing.simulator.windfield.impl.WindFieldGeneratorBlastImpl; @@ -51,11 +51,11 @@ public class WindFieldGeneratorTest { course.add(end); WindControlParameters windParameters = new WindControlParameters(3, 180); - RectangularBoundary bd = new RectangularBoundary(start, end, 0.1); + RectangularGrid bd = new RectangularGrid(start, end); WindFieldGeneratorImpl wf = new WindFieldGeneratorBlastImpl(bd, windParameters); int hSteps = 10; int vSteps = 5; - Position[][] positions = bd.extractGrid(hSteps, vSteps, 0, 0); + Position[][] positions = bd.generatePositions(hSteps, vSteps, 0, 0); assert (positions.length*positions[0].length == hSteps * vSteps); int index = 0; for (int i = 0; i < positions.length; ++i) { @@ -63,7 +63,7 @@ public class WindFieldGeneratorTest { logger.info("P" + ++index + ":" + positions[i][j]); } } - wf.setPositionGrid(bd.extractGrid(hSteps, vSteps, 0, 0)); + wf.setPositionGrid(bd.generatePositions(hSteps, vSteps, 0, 0)); Position[][] positionGrid = wf.getPositionGrid(); assertNotNull("Position Grid is not null", positionGrid); assertEquals("Position Grid Number of Rows", vSteps, positionGrid.length); @@ -91,13 +91,13 @@ public class WindFieldGeneratorTest { windParameters.frequency = 0.375; windParameters.amplitude = 20.0; - RectangularBoundary bd = new RectangularBoundary(start, end, 0.1); + RectangularGrid bd = new RectangularGrid(start, end); WindFieldGeneratorOscillationImpl wf = new WindFieldGeneratorOscillationImpl(bd, windParameters); int hSteps = 30; int vSteps = 15; - wf.setPositionGrid(bd.extractGrid(hSteps, vSteps, 0, 0)); + wf.setPositionGrid(bd.generatePositions(hSteps, vSteps, 0, 0)); Position[][] positionGrid = wf.getPositionGrid(); TimePoint startTime = new MillisecondsTimePoint(0); Duration timeStep = new MillisecondsDurationImpl(30 * 1000); @@ -235,12 +235,12 @@ public class WindFieldGeneratorTest { windParameters.blastWindSpeed = 0.0; windParameters.blastWindSpeedVar = 0.0; - RectangularBoundary bd = new RectangularBoundary(start, end, 0.1); + RectangularGrid bd = new RectangularGrid(start, end); WindFieldGeneratorCombined wf = new WindFieldGeneratorCombined(bd, windParameters); int hSteps = 30; int vSteps = 15; - wf.setPositionGrid(bd.extractGrid(hSteps, vSteps, 0, 0)); + wf.setPositionGrid(bd.generatePositions(hSteps, vSteps, 0, 0)); Position[][] positionGrid = wf.getPositionGrid(); TimePoint startTime = new MillisecondsTimePoint(0); Duration timeStep = new MillisecondsDurationImpl(30 * 1000); diff --git a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/Boundary.java b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/Boundary.java deleted file mode 100644 index d89f2dab8bc..00000000000 --- a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/Boundary.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.sap.sailing.simulator; - -import java.io.Serializable; -import java.util.Map; - -import com.sap.sailing.domain.common.Bearing; -import com.sap.sailing.domain.common.Distance; -import com.sap.sailing.domain.common.Position; -import com.sap.sailing.domain.common.impl.DegreeBearingImpl; -import com.sap.sse.common.Util; - -public interface Boundary extends Serializable { - - static Bearing TRUENORTH = new DegreeBearingImpl(0); - static Bearing TRUESOUTH = new DegreeBearingImpl(180); - static Bearing TRUEEAST = new DegreeBearingImpl(90); - static Bearing TRUEWEST = new DegreeBearingImpl(270); - - double getTolerance(); - - Map getCorners(); - - boolean isWithinBoundaries(Position P); - - //List extractLattice(int hPoints, int vPoints); - Position[][] extractGrid(int hPoints, int vPoints, int borderY, int borderX); - //List extractLattice(Distance hStep, Distance vstep); - public Util.Pair getGridIndex(Position x); - - public int getResY(); - public int getResX(); - public int getBorderY(); - public int getBorderX(); - - Bearing getNorth(); - Bearing getSouth(); - Bearing getEast(); - Bearing getWest(); - - Distance getHeight(); - Distance getWidth(); - - Map getRelativeCoordinates(Position p); - Position getRelativePoint(double x, double y); - -} diff --git a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/Grid.java b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/Grid.java new file mode 100644 index 00000000000..4d6ccb2a4a1 --- /dev/null +++ b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/Grid.java @@ -0,0 +1,54 @@ +package com.sap.sailing.simulator; + +import java.io.Serializable; +import java.util.Map; + +import com.sap.sailing.domain.common.Bearing; +import com.sap.sailing.domain.common.Distance; +import com.sap.sailing.domain.common.Position; +import com.sap.sailing.domain.common.impl.DegreeBearingImpl; +import com.sap.sse.common.Util; + +/** + * The Grid interface defines access to a set of GPS-positions which form a lattice of supporting positions for + * representing a vectorfield or performing optimization. + * + * @author Christopher Ronnewinkel (D036654) + * + */ +public interface Grid extends Serializable { + + static Bearing relativeNorth = new DegreeBearingImpl(0); + static Bearing relativeSouth = new DegreeBearingImpl(180); + static Bearing relativeEast = new DegreeBearingImpl(90); + static Bearing relativeWest = new DegreeBearingImpl(270); + + Map getCorners(); + + boolean inBounds(Position P); + + Position[][] generatePositions(int hPoints, int vPoints, int borderY, int borderX); + + public Util.Pair getIndex(Position x); + + public int getResY(); + + public int getResX(); + + public int getBorderY(); + + public int getBorderX(); + + Bearing getNorth(); + + Bearing getSouth(); + + Bearing getEast(); + + Bearing getWest(); + + Distance getHeight(); + + Distance getWidth(); + +} diff --git a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/SimulationParameters.java b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/SimulationParameters.java index 768b28e05d8..29d23fa9afb 100644 --- a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/SimulationParameters.java +++ b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/SimulationParameters.java @@ -18,7 +18,7 @@ public interface SimulationParameters { WindFieldGenerator getWindField(); - Boundary getBoundaries(); + Grid getGrid(); Map getSettings(); diff --git a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/CurvedGrid.java b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/CurvedGrid.java new file mode 100644 index 00000000000..2020c299a82 --- /dev/null +++ b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/CurvedGrid.java @@ -0,0 +1,218 @@ +package com.sap.sailing.simulator.impl; + +import java.util.HashMap; +import java.util.Map; + +import com.sap.sailing.domain.common.Bearing; +import com.sap.sailing.domain.common.Distance; +import com.sap.sailing.domain.common.Position; +import com.sap.sailing.simulator.Grid; +import com.sap.sse.common.Util; + +/** + * Implements the {@link Grid} interface by providing a grid of GPS-positions based on great circles and the + * corresponding index-calculation to associate an arbitrary GPS-position with the closest grid-GPS-position. + * + * Since the grid is constructed in spherical coordinates, we call it the curved grid. It stays perfectly symmetric no + * matter along which bearing to North it is aligned with. + * + * @author Christopher Ronnewinkel (D036654) + * + */ +public class CurvedGrid implements Grid { + + private static final long serialVersionUID = 3598121983120213464L; + private Position rcStart; // start position of race course + private Position rcEnd; // end position of race course + private int vPoints; // number of vertical steps + private int hPoints; // number of horizontal steps + private int borderY; + private int borderX; + + private Position gridNorthWest; + private Position gridSouthEast; + private Position gridSouthWest; + private Position gridNorthEast; + + private Bearing gridNorth; + private Bearing gridSouth; + private Bearing gridEast; + private Bearing gridWest; + + private Distance gridWidth; + private double xscale = 1.5; + private Distance gridHeight; + + public CurvedGrid(Position p1, Position p2) { + + rcStart = p1; + rcEnd = p2; + + gridNorth = p1.getBearingGreatCircle(p2); + gridSouth = gridNorth.reverse(); + gridEast = gridNorth.add(relativeEast); + gridWest = gridNorth.add(relativeWest); + + gridHeight = p1.getDistance(p2); + gridWidth = gridHeight.scale(2); + + gridNorthWest = p2.translateGreatCircle(gridWest, gridHeight); + gridNorthEast = p2.translateGreatCircle(gridEast, gridHeight); + + gridSouthWest = p1.translateGreatCircle(gridWest, gridHeight); + gridSouthEast = p1.translateGreatCircle(gridEast, gridHeight); + + } + + @Override + public Map getCorners() { + + Map map = new HashMap(); + map.put("NorthWest", gridNorthWest); + map.put("SouthWest", gridSouthWest); + map.put("SouthEast", gridSouthEast); + map.put("NorthEast", gridNorthEast); + + return map; + + } + + @Override + public boolean inBounds(Position p) { + + Position northProjection = p.projectToLineThrough(gridNorthWest, getEast()); + Position southProjection = p.projectToLineThrough(gridSouthWest, getEast()); + Position westProjection = p.projectToLineThrough(gridNorthWest, getNorth()); + Position eastProjection = p.projectToLineThrough(gridNorthEast, getNorth()); + + Distance northSouth = northProjection.getDistance(southProjection); + Distance eastWest = eastProjection.getDistance(westProjection); + + return (northSouth.compareTo(p.getDistance(northProjection)) >= 0) + && (northSouth.compareTo(p.getDistance(southProjection)) >= 0) + && (eastWest.compareTo(p.getDistance(eastProjection)) >= 0) + && (eastWest.compareTo(p.getDistance(westProjection)) >= 0); + + } + + @Override + public Position[][] generatePositions(int hPoints, int vPoints, int borderY, int borderX) { + + this.vPoints = vPoints; + this.hPoints = hPoints; + + this.borderY = borderY; + this.borderX = borderX; + + Distance vStep = gridHeight.scale(1.0 / (vPoints - 1)); + Distance hStep = gridHeight.scale(xscale / (hPoints - 1)); + + Position[][] grid = new Position[vPoints + 2 * borderY][hPoints + 2 * borderX]; + Position pv; + for (int i = -borderY; i < (vPoints + borderY); i++) { + + if (i == 0) { + pv = rcStart; + } else if (i == vPoints - 1) { + pv = rcEnd; + } else { + pv = rcStart.translateGreatCircle(gridNorth, vStep.scale(i)); + } + + int j = 0; + // left side + while (j < (hPoints + 2 * borderX) / 2) { + grid[i + borderY][j] = pv.translateGreatCircle(gridWest, + hStep.scale((hPoints + 2 * borderX - 1) / 2. - j)); + j++; + } + + // middle + if ((hPoints + 2 * borderX) % 2 == 1) { + grid[i + borderY][j] = pv; + j++; + } + + // right side + while (j < (hPoints + 2 * borderX)) { + grid[i + borderY][j] = pv.translateGreatCircle(gridEast, + hStep.scale(j - (hPoints + 2 * borderX - 1) / 2.)); + j++; + } + } + + return grid; + } + + public Util.Pair getIndex(Position x) { + + double vFlt = 0; + double hFlt = 0; + + if (!x.equals(rcStart)) { + Position h = x.projectToLineThrough(rcStart, gridNorth); + vFlt = vPoints * rcStart.getDistance(h).getMeters() / gridHeight.getMeters(); + int sign = +1; + if (Math.abs(h.getBearingGreatCircle(x).getDifferenceTo(gridWest).getDegrees()) < 90.0) { + sign = -1; + } + hFlt = sign * (hPoints - 1) * x.getDistance(h).getMeters() / gridHeight.getMeters() / xscale; + } + + int vIdx = Math.min(Math.max(-this.borderY, (int) Math.round(vFlt) + this.borderY), vPoints - 1 + this.borderY); + int hIdx = Math.min(Math.max(-this.borderX, (int) Math.round((hPoints - 1) / 2. + hFlt)), hPoints - 1 + this.borderX); + + return new Util.Pair(vIdx, hIdx); + } + + @Override + public int getResY() { + return this.vPoints; + } + + @Override + public int getResX() { + return this.hPoints; + } + + @Override + public int getBorderY() { + return this.borderY; + } + + @Override + public int getBorderX() { + return this.borderX; + } + + @Override + public Bearing getNorth() { + return gridNorth; + } + + @Override + public Bearing getSouth() { + return gridSouth; + } + + @Override + public Bearing getEast() { + return gridEast; + } + + @Override + public Bearing getWest() { + return gridWest; + } + + @Override + public Distance getWidth() { + return gridWidth; + } + + @Override + public Distance getHeight() { + return gridHeight; + } + +} diff --git a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PathGenerator1TurnerLeftDirect.java b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PathGenerator1TurnerLeftDirect.java index 6a7b4e88990..cea23d98d76 100644 --- a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PathGenerator1TurnerLeftDirect.java +++ b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PathGenerator1TurnerLeftDirect.java @@ -9,7 +9,7 @@ import com.sap.sailing.domain.common.Speed; import com.sap.sailing.domain.common.SpeedWithBearing; import com.sap.sailing.domain.common.TimePoint; import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; -import com.sap.sailing.simulator.Boundary; +import com.sap.sailing.simulator.Grid; import com.sap.sailing.simulator.Path; import com.sap.sailing.simulator.PolarDiagram; import com.sap.sailing.simulator.SimulationParameters; @@ -26,7 +26,7 @@ public class PathGenerator1TurnerLeftDirect extends PathGeneratorBase { public Path getPath() { // retrieve simulation parameters - Boundary boundary = new RectangularBoundary(this.parameters.getCourse().get(0), this.parameters + Grid boundary = new RectangularGrid(this.parameters.getCourse().get(0), this.parameters .getCourse().get(1));// simulationParameters.getBoundaries(); WindFieldGenerator windField = this.parameters.getWindField(); PolarDiagram polarDiagram = this.parameters.getBoatPolarDiagram(); @@ -102,7 +102,7 @@ public class PathGenerator1TurnerLeftDirect extends PathGeneratorBase { // System.out.println("out of time"); break; } - if (!boundary.isWithinBoundaries(currentPosition)) { + if (!boundary.inBounds(currentPosition)) { // outOfBounds = true; // System.out.println("out of bounds"); break; diff --git a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PathGenerator1TurnerRightDirect.java b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PathGenerator1TurnerRightDirect.java index 44225dbe694..837e71af5e4 100644 --- a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PathGenerator1TurnerRightDirect.java +++ b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PathGenerator1TurnerRightDirect.java @@ -9,7 +9,7 @@ import com.sap.sailing.domain.common.Speed; import com.sap.sailing.domain.common.SpeedWithBearing; import com.sap.sailing.domain.common.TimePoint; import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; -import com.sap.sailing.simulator.Boundary; +import com.sap.sailing.simulator.Grid; import com.sap.sailing.simulator.Path; import com.sap.sailing.simulator.PolarDiagram; import com.sap.sailing.simulator.SimulationParameters; @@ -26,7 +26,7 @@ public class PathGenerator1TurnerRightDirect extends PathGeneratorBase { public Path getPath() { // retrieve simulation parameters - Boundary boundary = new RectangularBoundary(this.parameters.getCourse().get(0), this.parameters + Grid boundary = new RectangularGrid(this.parameters.getCourse().get(0), this.parameters .getCourse().get(1));// simulationParameters.getBoundaries(); WindFieldGenerator windField = this.parameters.getWindField(); PolarDiagram polarDiagram = this.parameters.getBoatPolarDiagram(); @@ -102,7 +102,7 @@ public class PathGenerator1TurnerRightDirect extends PathGeneratorBase { // System.out.println("out of time"); break; } - if (!boundary.isWithinBoundaries(currentPosition)) { + if (!boundary.inBounds(currentPosition)) { // outOfBounds = true; // System.out.println("out of bounds"); break; diff --git a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PathGeneratorDijkstra.java b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PathGeneratorDijkstra.java index 3e17816a5cb..58d92730a11 100644 --- a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PathGeneratorDijkstra.java +++ b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PathGeneratorDijkstra.java @@ -14,7 +14,7 @@ import com.sap.sailing.domain.common.Speed; import com.sap.sailing.domain.common.SpeedWithBearing; import com.sap.sailing.domain.common.TimePoint; import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; -import com.sap.sailing.simulator.Boundary; +import com.sap.sailing.simulator.Grid; import com.sap.sailing.simulator.Path; import com.sap.sailing.simulator.PolarDiagram; import com.sap.sailing.simulator.SimulationParameters; @@ -34,7 +34,7 @@ public class PathGeneratorDijkstra extends PathGeneratorBase { public Path getPath() { // retrieve simulation parameters - Boundary boundary = new RectangularBoundary(this.parameters.getCourse().get(0), this.parameters + Grid boundary = new RectangularGrid(this.parameters.getCourse().get(0), this.parameters .getCourse().get(1));// simulationParameters.getBoundaries(); WindFieldGenerator windField = this.parameters.getWindField(); PolarDiagram polarDiagram = this.parameters.getBoatPolarDiagram(); @@ -49,7 +49,7 @@ public class PathGeneratorDijkstra extends PathGeneratorBase { int gridv = this.parameters.getProperty("Djikstra.gridv[int]").intValue(); // number of vertical grid steps int gridh = this.parameters.getProperty("Djikstra.gridh[int]").intValue(); // number of horizontal grid // steps - Position[][] sailGrid = boundary.extractGrid(gridh, gridv, 0, 0); + Position[][] sailGrid = boundary.generatePositions(gridh, gridv, 0, 0); // create adjacency graph including start and end Map> graph = new HashMap>(); diff --git a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PathGeneratorDynProgForward.java b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PathGeneratorDynProgForward.java index f633e2dbe33..f74afafc7d8 100644 --- a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PathGeneratorDynProgForward.java +++ b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PathGeneratorDynProgForward.java @@ -11,7 +11,7 @@ import com.sap.sailing.domain.common.Speed; import com.sap.sailing.domain.common.SpeedWithBearing; import com.sap.sailing.domain.common.TimePoint; import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; -import com.sap.sailing.simulator.Boundary; +import com.sap.sailing.simulator.Grid; import com.sap.sailing.simulator.Path; import com.sap.sailing.simulator.PolarDiagram; import com.sap.sailing.simulator.SimulationParameters; @@ -130,7 +130,7 @@ public class PathGeneratorDynProgForward extends PathGeneratorBase { public Path getPath() { // retrieve simulation parameters - Boundary boundary = new RectangularBoundary(this.parameters.getCourse().get(0), this.parameters + Grid boundary = new RectangularGrid(this.parameters.getCourse().get(0), this.parameters .getCourse().get(1));// simulationParameters.getBoundaries(); WindFieldGenerator windField = this.parameters.getWindField(); PolarDiagram polarDiagram = this.parameters.getBoatPolarDiagram(); @@ -162,7 +162,7 @@ public class PathGeneratorDynProgForward extends PathGeneratorBase { } // generate grid positions using sgridh and sgridv - Position[][] sailGrid = boundary.extractGrid(spatialGridsizeHorizontal, spatialGridsizeVertical, 0, 0); + Position[][] sailGrid = boundary.generatePositions(spatialGridsizeHorizontal, spatialGridsizeVertical, 0, 0); // optimization grid indexes: // vertical steps: 0, ..., v-1 diff --git a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PathGeneratorTreeGrowWind3.java b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PathGeneratorTreeGrowWind3.java index c5a903d8802..485d42209dd 100644 --- a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PathGeneratorTreeGrowWind3.java +++ b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PathGeneratorTreeGrowWind3.java @@ -219,7 +219,7 @@ public class PathGeneratorTreeGrowWind3 extends PathGeneratorBase { if (Math.abs(bearHeightSide) > 170.0) { vrtSide = (this.upwindLeg ? +1.0 : -1.0); } - double vrtDist = vrtSide*Math.round(posHeight.getDistance(posEnd).getMeters()*100.0)/100.0; + double vrtDist = vrtSide*Math.round(posHeight.getDistance(posEnd).getMeters()*200.0)/200.0; /*if (vrtDist > tgtHeight) { // scale last step so that vrtDist ~ tgtHeight @@ -244,7 +244,7 @@ public class PathGeneratorTreeGrowWind3 extends PathGeneratorBase { } // calculate horizontal distance as distance of height-position to current position //double hrzDist = Math.round(posSide*posHeight.getDistance(pathPos.getPosition()).getMeters()*100.0)/100.0; - double hrzDist = Math.round(posSide*posHeightTrgt.getDistance(pathPos.getPosition()).getMeters()*100.0)/100.0; + double hrzDist = Math.round(posSide*posHeightTrgt.getDistance(pathPos.getPosition()).getMeters()*200.0)/200.0; //System.out.println(""+hrzDist+", "+vrtDist+", "+pathPos.getPosition().getLatDeg()+", "+pathPos.getPosition().getLngDeg()+", "+posHeight.getLatDeg()+", "+posHeight.getLngDeg()); diff --git a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/RectangularBoundary.java b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/RectangularGrid.java similarity index 51% rename from java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/RectangularBoundary.java rename to java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/RectangularGrid.java index 4dd22f70006..013c3d96a56 100644 --- a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/RectangularBoundary.java +++ b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/RectangularGrid.java @@ -7,10 +7,23 @@ import com.sap.sailing.domain.common.Bearing; import com.sap.sailing.domain.common.Distance; import com.sap.sailing.domain.common.Position; import com.sap.sailing.domain.common.impl.DegreePosition; -import com.sap.sailing.simulator.Boundary; +import com.sap.sailing.simulator.Grid; import com.sap.sse.common.Util; -public class RectangularBoundary implements Boundary { +/** + * Implements the {@link Grid} interface by providing a grid of GPS-positions based on the plane approximation of the + * earth and the corresponding index-calculation to associate an arbitrary GPS-position with the closest + * grid-GPS-position. + * + * Since the grid has rectangular shape in the plane approximation, we call it rectangular grid. The plane approximation + * allows for a simple index calculation based on CPU-light operations. If not aligned with North the grid gets slightly + * distorted leading to asymmetries which may influence the accuracy of path calculations or optimizations. + * Still, for typical race course diameters of up to 5 nautical miles, it is a very good and fast approximation. + * + * @author Christopher Ronnewinkel (D036654) + * + */ +public class RectangularGrid implements Grid { private static final long serialVersionUID = 3598121983120213464L; private Position rcStart; // start position of race course @@ -24,7 +37,7 @@ public class RectangularBoundary implements Boundary { private int borderX; private Position northWest; - //private Position southEast; + // private Position southEast; private Position southWest; private Position northEast; @@ -43,20 +56,17 @@ public class RectangularBoundary implements Boundary { double tolerance; - public RectangularBoundary(Position p1, Position p2, double tlr) { - - // System.out.println("Start:"+p1); - // System.out.println("End :"+p2); + public RectangularGrid(Position p1, Position p2) { rcStart = p1; rcEnd = p2; - tolerance = tlr; + tolerance = 0.1; north = p1.getBearingGreatCircle(p2); south = north.reverse(); - east = north.add(TRUEEAST); - west = north.add(TRUEWEST); + east = north.add(relativeEast); + west = north.add(relativeWest); appHeight = p1.getDistance(p2); appWidth = appHeight.scale(2); @@ -65,40 +75,21 @@ public class RectangularBoundary implements Boundary { appNorthWest = p2.translateGreatCircle(west, appHeight); appNorthEast = p2.translateGreatCircle(east, appHeight); - // the following test shows, that numerics appear to be instable: - // translating appNorthEast 2xEast should be appNorthWest, but numerically its NOT - // Position tstNorthEast = appNorthWest.translateGreatCircle(east, appHeight).translateGreatCircle(east, - // appHeight); - // appSouthEast = appNorthEast.translateGreatCircle(south, appHeight); - // appSouthWest = appNorthWest.translateGreatCircle(south, appHeight); - appSouthWest = p1.translateGreatCircle(west, appHeight); appSouthEast = p1.translateGreatCircle(east, appHeight); - // System.out.println("southWest:"+appSouthWest); - // System.out.println("southEast:"+appSouthEast); - // System.out.println("northWest:"+appNorthWest); - // System.out.println("northEast:"+appNorthEast); - // System.out.println("testnEast:"+tstNorthEast); - Distance diag = appNorthWest.getDistance(appSouthEast); Bearing diag1 = appSouthEast.getBearingGreatCircle(appNorthWest); northWest = appNorthWest.translateGreatCircle(diag1, diag.scale(tolerance)); - diag1 = diag1.reverse(); - //southEast = appSouthEast.translateGreatCircle(diag1, diag.scale(tolerance)); + // diag1 = diag1.reverse(); + // southEast = appSouthEast.translateGreatCircle(diag1, diag.scale(tolerance)); Bearing diag2 = appSouthWest.getBearingGreatCircle(appNorthEast); northEast = appNorthEast.translateGreatCircle(diag2, diag.scale(tolerance)); diag2 = diag2.reverse(); southWest = appSouthWest.translateGreatCircle(diag2, diag.scale(tolerance)); } - public RectangularBoundary(Position p1, Position p2) { - - this(p1, p2, 0.1); - - } - @Override public Map getCorners() { @@ -113,7 +104,7 @@ public class RectangularBoundary implements Boundary { } @Override - public boolean isWithinBoundaries(Position p) { + public boolean inBounds(Position p) { Position northProjection = p.projectToLineThrough(northWest, getEast()); Position southProjection = p.projectToLineThrough(southWest, getEast()); @@ -131,14 +122,14 @@ public class RectangularBoundary implements Boundary { } @Override - public Position[][] extractGrid(int hPoints, int vPoints, int borderY, int borderX) { + public Position[][] generatePositions(int hPoints, int vPoints, int borderY, int borderX) { this.vPoints = vPoints; this.hPoints = hPoints; this.borderY = borderY; this.borderX = borderX; - + double xscale = 1.5; double alat = (rcEnd.getLatDeg() + rcStart.getLatDeg()) / 2.; @@ -170,11 +161,9 @@ public class RectangularBoundary implements Boundary { nHor[0] = nscHor[0]; nHor[1] = nscHor[1] / lngScale; - // System.out.println("LngScale:"+lngScale+", Diff.Vrt:"+dVrt); - Position[][] grid = new Position[vPoints+2*borderY][hPoints+2*borderX]; + Position[][] grid = new Position[vPoints + 2 * borderY][hPoints + 2 * borderX]; Position pv; - for (int i = -borderY; i < (vPoints+borderY); i++) { - // System.out.print("nrow "+i+": "); + for (int i = -borderY; i < (vPoints + borderY); i++) { if (i == 0) { pv = rcStart; @@ -187,41 +176,32 @@ public class RectangularBoundary implements Boundary { int j = 0; // left side - while (j < (hPoints+2*borderX) / 2) { - grid[i+borderY][j] = new DegreePosition(pv.getLatDeg() - ((hPoints + 2*borderX - 1) / 2. - j) / (hPoints - 1) * nHor[0] - * xscale * lscVrt, pv.getLngDeg() - ((hPoints + 2*borderX - 1) / 2. - j) / (hPoints - 1) * nHor[1] * xscale - * lscVrt); - // System.out.print(""+grid[i][j]+"("+((hPoints-1)/2.-j)+"), "); + while (j < (hPoints + 2 * borderX) / 2) { + grid[i + borderY][j] = new DegreePosition(pv.getLatDeg() - ((hPoints + 2 * borderX - 1) / 2. - j) + / (hPoints - 1) * nHor[0] * xscale * lscVrt, pv.getLngDeg() + - ((hPoints + 2 * borderX - 1) / 2. - j) / (hPoints - 1) * nHor[1] * xscale * lscVrt); j++; } // middle - if ((hPoints+2*borderX) % 2 == 1) { - grid[i+borderY][j] = pv; - // System.out.print(""+grid[i][j]+", "); + if ((hPoints + 2 * borderX) % 2 == 1) { + grid[i + borderY][j] = pv; j++; } // right side - while (j < (hPoints+2*borderX)) { - grid[i+borderY][j] = new DegreePosition(pv.getLatDeg() + (j - (hPoints + 2*borderX - 1) / 2.) / (hPoints - 1) * nHor[0] - * xscale * lscVrt, pv.getLngDeg() + (j - (hPoints + 2*borderX - 1) / 2.) / (hPoints - 1) * nHor[1] * xscale - * lscVrt); - // System.out.print(""+grid[i][j]+"("+(j-(hPoints-1)/2.)+"), "); + while (j < (hPoints + 2 * borderX)) { + grid[i + borderY][j] = new DegreePosition(pv.getLatDeg() + (j - (hPoints + 2 * borderX - 1) / 2.) + / (hPoints - 1) * nHor[0] * xscale * lscVrt, pv.getLngDeg() + + (j - (hPoints + 2 * borderX - 1) / 2.) / (hPoints - 1) * nHor[1] * xscale * lscVrt); j++; } - // System.out.println(); } - /* - * System.out.println("Grid Index Test:"); for (int i =0; i getGridIndex(Position x) { + public Util.Pair getIndex(Position x) { double[] scX = new double[2]; scX[0] = x.getLatDeg() - rcStart.getLatDeg(); @@ -229,64 +209,35 @@ public class RectangularBoundary implements Boundary { double sPrd = scX[0] * nvHor[0] + scX[1] * nvHor[1]; - int vIdx = Math.min(Math.max(-this.borderY, (int) Math.round(scX[0] * nvVrt[0] + scX[1] * nvVrt[1])), vPoints - 1 + this.borderY); - int hIdx = Math.min(Math.max(-this.borderX, (int) Math.round(sPrd + (hPoints - 1) / 2.)), hPoints - 1 + this.borderX); + int vIdx = Math.min(Math.max(-this.borderY, (int) Math.round(scX[0] * nvVrt[0] + scX[1] * nvVrt[1])), vPoints + - 1 + this.borderY); + int hIdx = Math.min(Math.max(-this.borderX, (int) Math.round(sPrd + (hPoints - 1) / 2.)), hPoints - 1 + + this.borderX); - // System.out.println("getGridIndex: "+vIdx+","+hIdx+"("+vPoints+","+hPoints+")"); return new Util.Pair(vIdx, hIdx); } - /* - * @Override public List extractLattice(int hPoints, int vPoints) { - * - * Position[][] grid = extractGrid(hPoints, vPoints); List lst = new ArrayList(); for(Position[] - * line : grid) { for(Position p : line) { lst.add(p); } } return lst; } - * - * @Override //may not return a rectangular lattice! public List extractLattice(Distance hStep, Distance - * vStep) { - * - * Position startPoint = appSouthWest; - * - * Bearing vBearing = getNorth(); Bearing hBearing = getEast(); boolean hMode = true; - * - * List lst = new ArrayList(); - * - * Position current = startPoint; lst.add(current); Position next; - * - * while (true) { - * - * if (hMode) next = current.translateGreatCircle(hBearing, hStep); else next = - * current.translateGreatCircle(vBearing, vStep); - * - * if (isWithinBoundaries(next)) { current = next; lst.add(current); if (!hMode) { hMode = true; hBearing = - * hBearing.reverse(); } } else { if(hMode) hMode = false; else break; } - * - * } - * - * return lst; } - */ - @Override public int getResY() { - return this.vPoints; + return this.vPoints; } - + @Override public int getResX() { - return this.hPoints; + return this.hPoints; } - + @Override public int getBorderY() { - return this.borderY; + return this.borderY; } - + @Override public int getBorderX() { - return this.borderX; + return this.borderX; } - + @Override public Bearing getNorth() { return north; @@ -317,32 +268,4 @@ public class RectangularBoundary implements Boundary { return appHeight; } - @Override - public Map getRelativeCoordinates(Position p) { - if (!isWithinBoundaries(p)) - return null; - - Map map = new HashMap(); - Position px = p.projectToLineThrough(appSouthWest, getEast()); - Position py = p.projectToLineThrough(appSouthWest, getNorth()); - - map.put("X", px.getDistance(appSouthWest).getMeters() / getWidth().getMeters()); - map.put("Y", py.getDistance(appSouthWest).getMeters() / getHeight().getMeters()); - return map; - } - - @Override - public Position getRelativePoint(double x, double y) { - - Position point = appSouthWest; - point = point.translateGreatCircle(getEast(), getWidth().scale(x)); - point = point.translateGreatCircle(getNorth(), getHeight().scale(y)); - return point; - } - - @Override - public double getTolerance() { - return tolerance; - } - } diff --git a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/SailingSimulatorImpl.java b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/SailingSimulatorImpl.java index 6144fdffa4a..17288df9988 100644 --- a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/SailingSimulatorImpl.java +++ b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/SailingSimulatorImpl.java @@ -12,7 +12,7 @@ import com.sap.sailing.domain.common.Position; import com.sap.sailing.domain.common.impl.DegreeBearingImpl; import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl; import com.sap.sailing.domain.common.impl.MeterDistance; -import com.sap.sailing.simulator.Boundary; +import com.sap.sailing.simulator.Grid; import com.sap.sailing.simulator.Path; import com.sap.sailing.simulator.RaceProperties; import com.sap.sailing.simulator.SailingSimulator; @@ -79,7 +79,7 @@ public class SailingSimulatorImpl implements SailingSimulator { LOGGER.info("showOpportunist: "+this.simulationParameters.showOpportunist()); if (gridArea != null) { - Boundary bd = new RectangularBoundary(gridArea[0], gridArea[1], 0.1); + Grid bd = new CurvedGrid(gridArea[0], gridArea[1]); // set base wind bearing wf.getWindParameters().baseWindBearing += bd.getSouth().getDegrees(); @@ -103,7 +103,7 @@ public class SailingSimulatorImpl implements SailingSimulator { } wf.setBoundary(bd); - Position[][] positionGrid = bd.extractGrid(gridRes[0], gridRes[1], gridRes[2], gridRes[3]); + Position[][] positionGrid = bd.generatePositions(gridRes[0], gridRes[1], gridRes[2], gridRes[3]); wf.setPositionGrid(positionGrid); wf.generate(wf.getStartTime(), wf.getEndTime(), wf.getTimeStep()); } diff --git a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/SimulationParametersImpl.java b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/SimulationParametersImpl.java index 522fb79f62b..cc791cb732f 100644 --- a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/SimulationParametersImpl.java +++ b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/SimulationParametersImpl.java @@ -5,7 +5,7 @@ import java.util.List; import java.util.Map; import com.sap.sailing.domain.common.Position; -import com.sap.sailing.simulator.Boundary; +import com.sap.sailing.simulator.Grid; import com.sap.sailing.simulator.PolarDiagram; import com.sap.sailing.simulator.SimulationParameters; import com.sap.sailing.simulator.windfield.WindFieldGenerator; @@ -64,8 +64,8 @@ public class SimulationParametersImpl implements SimulationParameters { } @Override - public Boundary getBoundaries() { - return windField.getBoundaries(); + public Grid getGrid() { + return windField.getGrid(); } @Override diff --git a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/WindField.java b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/WindField.java index 7ff6505ced7..4025fce637f 100644 --- a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/WindField.java +++ b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/WindField.java @@ -3,7 +3,7 @@ package com.sap.sailing.simulator.windfield; import java.io.Serializable; import com.sap.sailing.domain.tracking.Wind; -import com.sap.sailing.simulator.Boundary; +import com.sap.sailing.simulator.Grid; import com.sap.sailing.simulator.Path; import com.sap.sailing.simulator.TimedPosition; @@ -11,7 +11,7 @@ public interface WindField extends Serializable { public Wind getWind(TimedPosition coordinates); - public Boundary getBoundaries(); + public Grid getGrid(); public Path getLine(TimedPosition seed, boolean forward); diff --git a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/WindFieldGenerator.java b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/WindFieldGenerator.java index 450b417995a..45748eb3cab 100644 --- a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/WindFieldGenerator.java +++ b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/WindFieldGenerator.java @@ -7,13 +7,13 @@ import java.io.Serializable; import com.sap.sailing.domain.common.Duration; import com.sap.sailing.domain.common.Position; import com.sap.sailing.domain.common.TimePoint; -import com.sap.sailing.simulator.Boundary; +import com.sap.sailing.simulator.Grid; public interface WindFieldGenerator extends WindField, Serializable { public WindControlParameters getWindParameters(); - public void setBoundary(Boundary boundary); + public void setBoundary(Grid boundary); public void setPositionGrid(Position[][] positions); diff --git a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/WindFieldGeneratorFactory.java b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/WindFieldGeneratorFactory.java index ce293021260..6fdaa686138 100644 --- a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/WindFieldGeneratorFactory.java +++ b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/WindFieldGeneratorFactory.java @@ -1,12 +1,12 @@ package com.sap.sailing.simulator.windfield; -import com.sap.sailing.simulator.Boundary; +import com.sap.sailing.simulator.Grid; import com.sap.sailing.simulator.windfield.impl.WindFieldGeneratorFactoryImpl; public interface WindFieldGeneratorFactory { static WindFieldGeneratorFactory INSTANCE = new WindFieldGeneratorFactoryImpl(); - public WindFieldGenerator createWindFieldGenerator(String patternName, Boundary boundary, + public WindFieldGenerator createWindFieldGenerator(String patternName, Grid boundary, WindControlParameters windParameters); } diff --git a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/impl/WindFieldGeneratorBlastImpl.java b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/impl/WindFieldGeneratorBlastImpl.java index 23d74cfffd8..6952aa2f325 100644 --- a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/impl/WindFieldGeneratorBlastImpl.java +++ b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/impl/WindFieldGeneratorBlastImpl.java @@ -14,7 +14,7 @@ import com.sap.sailing.domain.common.impl.DegreeBearingImpl; import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl; import com.sap.sailing.domain.tracking.Wind; import com.sap.sailing.domain.tracking.impl.WindImpl; -import com.sap.sailing.simulator.Boundary; +import com.sap.sailing.simulator.Grid; import com.sap.sailing.simulator.TimedPosition; import com.sap.sailing.simulator.windfield.WindControlParameters; import com.sap.sailing.simulator.windfield.WindFieldGenerator; @@ -44,7 +44,7 @@ public class WindFieldGeneratorBlastImpl extends WindFieldGeneratorImpl implemen private static Logger logger = Logger.getLogger(WindFieldGeneratorBlastImpl.class.getName()); - public WindFieldGeneratorBlastImpl(Boundary boundary, WindControlParameters windParameters) { + public WindFieldGeneratorBlastImpl(Grid boundary, WindControlParameters windParameters) { super(boundary, windParameters); } diff --git a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/impl/WindFieldGeneratorCombined.java b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/impl/WindFieldGeneratorCombined.java index 863891c471d..9e384c819a6 100644 --- a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/impl/WindFieldGeneratorCombined.java +++ b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/impl/WindFieldGeneratorCombined.java @@ -7,7 +7,7 @@ import com.sap.sailing.domain.common.TimePoint; import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl; import com.sap.sailing.domain.tracking.Wind; import com.sap.sailing.domain.tracking.impl.WindImpl; -import com.sap.sailing.simulator.Boundary; +import com.sap.sailing.simulator.Grid; import com.sap.sailing.simulator.TimedPosition; import com.sap.sailing.simulator.windfield.WindControlParameters; import com.sap.sailing.simulator.windfield.WindFieldGenerator; @@ -18,14 +18,14 @@ public class WindFieldGeneratorCombined extends WindFieldGeneratorImpl implement private WindFieldGeneratorBlastImpl blastGen; private WindFieldGeneratorOscillationImpl oscillationGen; - public WindFieldGeneratorCombined(Boundary boundary, WindControlParameters windParameters) { + public WindFieldGeneratorCombined(Grid boundary, WindControlParameters windParameters) { super(boundary, windParameters); blastGen = new WindFieldGeneratorBlastImpl(boundary, windParameters); oscillationGen = new WindFieldGeneratorOscillationImpl(boundary, windParameters); } @Override - public void setBoundary(Boundary boundary) { + public void setBoundary(Grid boundary) { super.setBoundary(boundary); blastGen.setBoundary(boundary); diff --git a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/impl/WindFieldGeneratorFactoryImpl.java b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/impl/WindFieldGeneratorFactoryImpl.java index d51cafe9d85..c20a53e8849 100644 --- a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/impl/WindFieldGeneratorFactoryImpl.java +++ b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/impl/WindFieldGeneratorFactoryImpl.java @@ -1,7 +1,7 @@ package com.sap.sailing.simulator.windfield.impl; -import com.sap.sailing.simulator.Boundary; +import com.sap.sailing.simulator.Grid; import com.sap.sailing.simulator.windfield.WindControlParameters; import com.sap.sailing.simulator.windfield.WindFieldGenerator; import com.sap.sailing.simulator.windfield.WindFieldGeneratorFactory; @@ -9,7 +9,7 @@ import com.sap.sailing.simulator.windfield.WindFieldGeneratorFactory; public class WindFieldGeneratorFactoryImpl implements WindFieldGeneratorFactory { @Override - public WindFieldGenerator createWindFieldGenerator(String patternName, Boundary boundary, + public WindFieldGenerator createWindFieldGenerator(String patternName, Grid boundary, WindControlParameters windParameters) { if (patternName.equals("BLASTS")) { return new WindFieldGeneratorBlastImpl(boundary, windParameters); diff --git a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/impl/WindFieldGeneratorImpl.java b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/impl/WindFieldGeneratorImpl.java index f1d1e5ac3c7..c696bf8b115 100644 --- a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/impl/WindFieldGeneratorImpl.java +++ b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/impl/WindFieldGeneratorImpl.java @@ -13,7 +13,7 @@ import com.sap.sailing.domain.common.impl.DegreeBearingImpl; import com.sap.sailing.domain.common.impl.KnotSpeedImpl; import com.sap.sailing.domain.tracking.Wind; import com.sap.sailing.domain.tracking.impl.WindImpl; -import com.sap.sailing.simulator.Boundary; +import com.sap.sailing.simulator.Grid; import com.sap.sailing.simulator.Path; import com.sap.sailing.simulator.TimedPosition; import com.sap.sailing.simulator.TimedPositionWithSpeed; @@ -27,7 +27,7 @@ import com.sap.sse.common.Util; public abstract class WindFieldGeneratorImpl implements WindFieldGenerator { private static final long serialVersionUID = -6366698648104363491L; - protected Boundary boundary; + protected Grid boundary; protected WindControlParameters windParameters; protected Position[][] positions; @@ -46,7 +46,7 @@ public abstract class WindFieldGeneratorImpl implements WindFieldGenerator { private static Logger logger = Logger.getLogger("com.sap.sailing.windfield"); - public WindFieldGeneratorImpl(Boundary boundary, WindControlParameters windParameters) { + public WindFieldGeneratorImpl(Grid boundary, WindControlParameters windParameters) { this.boundary = boundary; this.windParameters = windParameters; this.positions = null; @@ -58,7 +58,7 @@ public abstract class WindFieldGeneratorImpl implements WindFieldGenerator { } @Override - public void setBoundary(Boundary boundary) { + public void setBoundary(Grid boundary) { this.boundary = boundary; } @@ -77,7 +77,7 @@ public abstract class WindFieldGeneratorImpl implements WindFieldGenerator { } @Override - public Boundary getBoundaries() { + public Grid getGrid() { return boundary; } @@ -97,7 +97,7 @@ public abstract class WindFieldGeneratorImpl implements WindFieldGenerator { LinkedList path = new LinkedList(); path.add(new TimedPositionWithSpeedImpl(currentTime, currentPosition, null)); - while(boundary.isWithinBoundaries(currentPosition)) { + while(boundary.inBounds(currentPosition)) { Wind currentWind = this.getWind(new TimedPositionImpl(startTime, currentPosition)); TimePoint middleTime = currentTime.plus(timeStep/2); Position middlePosition; @@ -138,7 +138,7 @@ public abstract class WindFieldGeneratorImpl implements WindFieldGenerator { } public Util.Pair getPositionIndex(Position p) { - Util.Pair gIdx = boundary.getGridIndex(p); + Util.Pair gIdx = boundary.getIndex(p); if ((gIdx.getA() != null) && (gIdx.getB() != null)) { return gIdx; } else { diff --git a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/impl/WindFieldGeneratorMeasured.java b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/impl/WindFieldGeneratorMeasured.java index a8cdba604eb..f7ad8712628 100644 --- a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/impl/WindFieldGeneratorMeasured.java +++ b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/impl/WindFieldGeneratorMeasured.java @@ -11,7 +11,7 @@ import com.sap.sailing.domain.common.impl.DegreeBearingImpl; import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl; import com.sap.sailing.domain.tracking.Wind; import com.sap.sailing.domain.tracking.impl.WindImpl; -import com.sap.sailing.simulator.Boundary; +import com.sap.sailing.simulator.Grid; import com.sap.sailing.simulator.Path; import com.sap.sailing.simulator.TimedPosition; import com.sap.sailing.simulator.TimedPositionWithSpeed; @@ -30,7 +30,7 @@ public class WindFieldGeneratorMeasured extends WindFieldGeneratorImpl implement // super(); // } - public WindFieldGeneratorMeasured(Boundary boundary, WindControlParameters windParameters) { + public WindFieldGeneratorMeasured(Grid boundary, WindControlParameters windParameters) { super(boundary, windParameters); } diff --git a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/impl/WindFieldGeneratorOscillationImpl.java b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/impl/WindFieldGeneratorOscillationImpl.java index 012a949084f..67b2a1f53c7 100644 --- a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/impl/WindFieldGeneratorOscillationImpl.java +++ b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/impl/WindFieldGeneratorOscillationImpl.java @@ -14,7 +14,7 @@ import com.sap.sailing.domain.common.impl.KnotSpeedImpl; import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl; import com.sap.sailing.domain.tracking.Wind; import com.sap.sailing.domain.tracking.impl.WindImpl; -import com.sap.sailing.simulator.Boundary; +import com.sap.sailing.simulator.Grid; import com.sap.sailing.simulator.TimedPosition; import com.sap.sailing.simulator.windfield.WindControlParameters; import com.sap.sailing.simulator.windfield.WindFieldGenerator; @@ -32,7 +32,7 @@ public class WindFieldGeneratorOscillationImpl extends WindFieldGeneratorImpl im private static Logger logger = Logger.getLogger(WindFieldGeneratorOscillationImpl.class.getName()); - public WindFieldGeneratorOscillationImpl(Boundary boundary, WindControlParameters windParameters) { + public WindFieldGeneratorOscillationImpl(Grid boundary, WindControlParameters windParameters) { super(boundary, windParameters); } From 26b8c8affb71d3750c6990d1a508060fa3cc4acf Mon Sep 17 00:00:00 2001 From: "Christopher Ronnewinkel (D036654)" Date: Thu, 24 Jul 2014 01:38:37 +0200 Subject: [PATCH 08/20] fix artefactual-looking optimization results due to bad sorting of candidates; round to millimeters in path generation (ensure stable numerics); --- .../src/com/sap/sailing/simulator/impl/PathCandidate.java | 4 ++-- .../simulator/impl/PathGeneratorTreeGrowWind3.java | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PathCandidate.java b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PathCandidate.java index ee33d92f8e4..734a6e919d6 100644 --- a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PathCandidate.java +++ b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PathCandidate.java @@ -25,14 +25,14 @@ public class PathCandidate implements Comparable { // sort descending by length, width, height public int compareTo(PathCandidate other) { if (this.path.length() == other.path.length()) { - if (this.vrt == other.vrt) { + if (this.trn == other.trn) { if (Math.abs(this.hrz) == Math.abs(other.hrz)) { return 0; } else { return (Math.abs(this.hrz) < Math.abs(other.hrz) ? -1 : +1); } } else { - return (this.vrt > other.vrt ? -1 : +1); + return (this.trn < other.trn ? -1 : +1); } } else { return (this.path.length() < other.path.length() ? -1 : +1); diff --git a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PathGeneratorTreeGrowWind3.java b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PathGeneratorTreeGrowWind3.java index 485d42209dd..6e80083d4d2 100644 --- a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PathGeneratorTreeGrowWind3.java +++ b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PathGeneratorTreeGrowWind3.java @@ -219,7 +219,7 @@ public class PathGeneratorTreeGrowWind3 extends PathGeneratorBase { if (Math.abs(bearHeightSide) > 170.0) { vrtSide = (this.upwindLeg ? +1.0 : -1.0); } - double vrtDist = vrtSide*Math.round(posHeight.getDistance(posEnd).getMeters()*200.0)/200.0; + double vrtDist = vrtSide*Math.round(posHeight.getDistance(posEnd).getMeters()*1000.0)/1000.0; /*if (vrtDist > tgtHeight) { // scale last step so that vrtDist ~ tgtHeight @@ -244,7 +244,7 @@ public class PathGeneratorTreeGrowWind3 extends PathGeneratorBase { } // calculate horizontal distance as distance of height-position to current position //double hrzDist = Math.round(posSide*posHeight.getDistance(pathPos.getPosition()).getMeters()*100.0)/100.0; - double hrzDist = Math.round(posSide*posHeightTrgt.getDistance(pathPos.getPosition()).getMeters()*200.0)/200.0; + double hrzDist = Math.round(posSide*posHeightTrgt.getDistance(pathPos.getPosition()).getMeters()*1000.0)/1000.0; //System.out.println(""+hrzDist+", "+vrtDist+", "+pathPos.getPosition().getLatDeg()+", "+pathPos.getPosition().getLngDeg()+", "+posHeight.getLatDeg()+", "+posHeight.getLngDeg()); @@ -555,7 +555,7 @@ public class PathGeneratorTreeGrowWind3 extends PathGeneratorBase { tstPosition = this.getStep(new TimedPositionImpl(startTime, startPos), timeStep, turnLoss, true, 'R').getA(); double tstDist2 = startPos.getDistance(tstPosition.getPosition()).getMeters(); - double hrzBinSize = (tstDist1 + tstDist2)/3.0; // horizontal bin size in meters + double hrzBinSize = (tstDist1 + tstDist2)/6.0; // horizontal bin size in meters if (debugMsgOn) { System.out.println("Horizontal Bin Size: "+hrzBinSize); } @@ -609,7 +609,7 @@ public class PathGeneratorTreeGrowWind3 extends PathGeneratorBase { if ((curPath.vrt > 0.0)) { //logger.info("\ntPath: " + curPath.path + "\n Time: " + (Math.round((curPath.pos.getTimePoint().asMillis()-startTime.asMillis())/1000.0/60.0*10.0)/10.0)+", Height: "+curPath.vrt+" of "+(Math.round(startPos.getDistance(endPos).getMeters()*100.0)/100.0)+", Dist: "+curPath.hrz+"m ~ "+(Math.round(curPath.pos.getPosition().getDistance(endPos).getMeters()*100.0)/100.0)+"m"); int curBin = (int)Math.round(Math.floor( (curPath.hrz + hrzBinSize/2.0) / hrzBinSize )); - if ((Math.abs(curBin) <= 1)) { + if ((Math.abs(curBin) <= 2)) { reachedEnd = true; trgPaths.add(curPath); // add path to list of target-paths } From a4c27b078305152cb8116ab621b016fdfb972f29 Mon Sep 17 00:00:00 2001 From: "Christopher Ronnewinkel (D036654)" Date: Thu, 24 Jul 2014 02:32:27 +0200 Subject: [PATCH 09/20] sanitize default parameters for blasts; --- .../windpattern/impl/WindPatternDisplayManagerImpl.java | 2 +- .../simulator/windfield/WindControlParameters.java | 2 +- .../windfield/impl/WindFieldGeneratorBlastImpl.java | 8 +++----- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/windpattern/impl/WindPatternDisplayManagerImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/windpattern/impl/WindPatternDisplayManagerImpl.java index 82450651445..ec2296132f3 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/windpattern/impl/WindPatternDisplayManagerImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/windpattern/impl/WindPatternDisplayManagerImpl.java @@ -79,7 +79,7 @@ public class WindPatternDisplayManagerImpl implements WindPatternDisplayManager 1, 1, 10); display.addSetting(maxBlastSize); WindPatternSetting blastWindSpeed = new WindPatternSettingSliderBar("blastWindSpeed", - "Average Speed (%)", 0, 200, 5, 120, 10); + "Average Speed (%)", 0, 200, 5, 100, 10); display.addSetting(blastWindSpeed); WindPatternSetting blastWindSpeedVar = new WindPatternSettingSliderBar("blastWindSpeedVar", "Speed Variance (%)", 1e-4, 100, 5, 10, 10); diff --git a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/WindControlParameters.java b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/WindControlParameters.java index e5204649103..26e15352a5c 100644 --- a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/WindControlParameters.java +++ b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/WindControlParameters.java @@ -140,7 +140,7 @@ public class WindControlParameters implements Serializable { blastProbability = 25.0; maxBlastSize = 1.0; - blastWindSpeed = 120.0; + blastWindSpeed = 100.0; blastWindSpeedVar = 10.0; if (blastRandomStreamManager == null || !resetBlastRandomStream) { diff --git a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/impl/WindFieldGeneratorBlastImpl.java b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/impl/WindFieldGeneratorBlastImpl.java index 6952aa2f325..af35125b379 100644 --- a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/impl/WindFieldGeneratorBlastImpl.java +++ b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/impl/WindFieldGeneratorBlastImpl.java @@ -32,7 +32,7 @@ public class WindFieldGeneratorBlastImpl extends WindFieldGeneratorImpl implemen private double blastSizeProbability = 70; private double blastEdgeProbability = 30; private double blastBearingMean = 0; - private double blastBearingVar = 8; + private double blastBearingVar = 6; private double defaultWindSpeed = 0; private double defaultWindBearing = 0; private SpeedWithBearing defaultSpeedWithBearing; @@ -164,8 +164,7 @@ public class WindFieldGeneratorBlastImpl extends WindFieldGeneratorImpl implemen //System.out.println("blast speed mean: "+bSpeedMean+" var: "+bSpeedVar); //return NormalGen.nextDouble(new LFSR113("BlastSpeedStream"), bSpeedMean, bSpeedVar); RandomStream speedStream = windParameters.getBlastRandomStreamManager().getRandomStream(BlastRandomSeedManagerImpl.BlastStream.SPEED.name()); - return NormalGen.nextDouble(speedStream, bSpeedMean, bSpeedVar); - + return Math.max(0.5*bSpeedMean, Math.min(1.5*bSpeedMean, NormalGen.nextDouble(speedStream, bSpeedMean, bSpeedVar))); } private int getBlastSize() { @@ -177,8 +176,7 @@ public class WindFieldGeneratorBlastImpl extends WindFieldGeneratorImpl implemen private double getBlastAngle() { //return NormalGen.nextDouble(new LFSR113("BlastAngleStream"), blastBearingMean, blastBearingVar); RandomStream bearingStream = windParameters.getBlastRandomStreamManager().getRandomStream(BlastRandomSeedManagerImpl.BlastStream.BEARING.name()); - return NormalGen.nextDouble(bearingStream, blastBearingMean, blastBearingVar); - + return Math.max(-1.5*blastBearingVar, Math.min(1.5*blastBearingVar, NormalGen.nextDouble(bearingStream, blastBearingMean, blastBearingVar))); } private SpeedWithBearing getSpeedWithBearing(TimedPosition timedPosition) { From 58caa12def2cdf3a2c1044789b971fb6874374db Mon Sep 17 00:00:00 2001 From: "Steffen Wagner (d059727)" Date: Thu, 24 Jul 2014 13:15:54 +0200 Subject: [PATCH 10/20] added 1st version of a section for sap cube replication setup --- wiki/server-replication.md | 72 +++++++++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/wiki/server-replication.md b/wiki/server-replication.md index 148ae51fbe3..583f99aed1a 100644 --- a/wiki/server-replication.md +++ b/wiki/server-replication.md @@ -90,4 +90,74 @@ Fortunately, the dangerous-to-parallelize operations seem to be the ones that oc These encompass leaderboard and leaderboard group creation and manipulation, including score corrections. In a replica these operations must be executed in the same sequence they were executed in on the master. It probably encompasses the creation of a tracked race and its course, we well as the updates to a race course with waypoint additions and mark passings because mark passing events relate to the waypoints. Paralllel operations -These encompass the receiving of raw sensor data. The order in which this data is re-played doesn't matter. The data is time-stamped, and its history is not rewritten (as maybe the case for the mark passings or for the race tracking start/finish times). Applying these operations to a replica is idempotent, so serializing an initial state doesn't need to be synchronized with the receiving of such data. Duplicate application of these operations to a server doesn't do any harm. \ No newline at end of file +These encompass the receiving of raw sensor data. The order in which this data is re-played doesn't matter. The data is time-stamped, and its history is not rewritten (as maybe the case for the mark passings or for the race tracking start/finish times). Applying these operations to a replica is idempotent, so serializing an initial state doesn't need to be synchronized with the receiving of such data. Duplicate application of these operations to a server doesn't do any harm. + +### Replication in SAP Cube +This section will describe, how a working local replica can be set up at the SAP Cube. +The setup at the Travemünder Woche 2014 was done by Steffen Wagner as it follows: + +LTE router --> gigabit switch --> cube computers (dhcp) && local replica machine (with e.g. Ubuntu 14.04 LTS & a version of the analytics suite installed) + +Steps to get everything working: +* The LTE router (192.168.1.1) should have DHCP and NS activated. Dhcp was within range XXX.XXX.1.2 - XXX.XXX.1.49 and our local replica machine had a static ip address of XXX.XXX.1.50. Please make sure, that the ip address is outside of the dhcp range. For a better setup we should think of using the local replica server for dhcp and ns services. +* Setup networking (to your needs) on the local replica machine as follows: +/etc/networking/interfaces +
+auto eth0
+iface eth0 inet static
+	address 192.168.1.50
+	netmask 255.255.255.0
+	gateway 192.168.1.1
+
+/etc/hosts +
+127.0.0.1		localhost
+127.0.0.1		tw2014 tw2014.sapsailing.com
+192.168.1.50	tw2014 tw2014.sapsailing.com
+
+* For rewriting the URLs and Ports to the fitting Spectator URL when entering the URL we need a Apache installed (apt-get install apache2) and set up like on our sapsailing.com server. Please check 000-macros.conf, 001-events.conf and 000-main.conf and set it up as you need for the event. They need to be in the config dir of apache (/etc/apache2/mods-enabled and /etc/apache2/sites-enabled) +* For better remote administration of the local replica, set up and autossh tunnel to trac@sapsailing.com and put it into rc.local for autostarting, e.g. as the following: +
+autossh -M 20009 -f -N -L 1337:127.0.0.1:22 -i SSHKEY trac@sapsailing.com
+
+* Set up automatic replication in env.sh of the analystics server and start the server (not with root!) with startscript ~/servers/server/start +* On all Cube computers add the folling batch file to the windows task scheduling for a check every minute. The script switches hosts to local replica if available. Before the script can work, you have to edit host file manully and add the required hostnames: e.g. 192.168.1.50 tw2014.sapsailing.com. You also need to replace find and replace to the required ip addresses. +
+@echo off
+@setlocal enableextensions enabledelayedexpansion
+
+REM Set a variable for the Windows hosts file location
+set "hostpath=%systemroot%\system32\drivers\etc"
+set "hostfile=hosts"
+
+REM Make the hosts file writable
+attrib -r -s -h "%hostpath%\%hostfile%"
+
+ping -n 1 -w 1000 -l 2000 192.168.1.50
+REM if local replica is available
+IF %ERRORLEVEL% == 0 (
+	REM set the string you wish to find
+	set find=54.229.94.254
+	REM set the string you wish to replace with
+	set replace=192.168.1.50
+
+REM if local replica is not available
+) ELSE (
+	REM set ring you wish to find
+	set find=192.168.1.50
+	REM set the string you wish to replace with
+	set replace=54.229.94.254
+)	
+
+for /f "delims=" %%a in ('type "%hostpath%\%hostfile%"') do (
+set "string=%%a"
+set "string=!string:%find%=%replace%!"
+>> "newfile.txt" echo !string!
+)
+
+move /y "newfile.txt" "%hostpath%\%hostfile%"
+REM Make the hosts file un-writable - not necessary.
+REM attrib +r "%hostpath%\%hostfile%"
+echo hostfile done.
+
+* If server should be shutted down, then first stop replication manually in the backend and then call ~/servers/server/stop. If everything works fine, then the Cube computers should recognize, that the replica machine is not available anymore and then switch to the real sapsailing host. From 76d65681ee3cf7d1c0a18b85e4c336911f9fd92e Mon Sep 17 00:00:00 2001 From: "Steffen Wagner (d059727)" Date: Thu, 24 Jul 2014 13:18:05 +0200 Subject: [PATCH 11/20] Revert "added 1st version of a section for sap cube replication setup" This reverts commit 58caa12def2cdf3a2c1044789b971fb6874374db. --- wiki/server-replication.md | 72 +------------------------------------- 1 file changed, 1 insertion(+), 71 deletions(-) diff --git a/wiki/server-replication.md b/wiki/server-replication.md index 583f99aed1a..148ae51fbe3 100644 --- a/wiki/server-replication.md +++ b/wiki/server-replication.md @@ -90,74 +90,4 @@ Fortunately, the dangerous-to-parallelize operations seem to be the ones that oc These encompass leaderboard and leaderboard group creation and manipulation, including score corrections. In a replica these operations must be executed in the same sequence they were executed in on the master. It probably encompasses the creation of a tracked race and its course, we well as the updates to a race course with waypoint additions and mark passings because mark passing events relate to the waypoints. Paralllel operations -These encompass the receiving of raw sensor data. The order in which this data is re-played doesn't matter. The data is time-stamped, and its history is not rewritten (as maybe the case for the mark passings or for the race tracking start/finish times). Applying these operations to a replica is idempotent, so serializing an initial state doesn't need to be synchronized with the receiving of such data. Duplicate application of these operations to a server doesn't do any harm. - -### Replication in SAP Cube -This section will describe, how a working local replica can be set up at the SAP Cube. -The setup at the Travemünder Woche 2014 was done by Steffen Wagner as it follows: - -LTE router --> gigabit switch --> cube computers (dhcp) && local replica machine (with e.g. Ubuntu 14.04 LTS & a version of the analytics suite installed) - -Steps to get everything working: -* The LTE router (192.168.1.1) should have DHCP and NS activated. Dhcp was within range XXX.XXX.1.2 - XXX.XXX.1.49 and our local replica machine had a static ip address of XXX.XXX.1.50. Please make sure, that the ip address is outside of the dhcp range. For a better setup we should think of using the local replica server for dhcp and ns services. -* Setup networking (to your needs) on the local replica machine as follows: -/etc/networking/interfaces -
-auto eth0
-iface eth0 inet static
-	address 192.168.1.50
-	netmask 255.255.255.0
-	gateway 192.168.1.1
-
-/etc/hosts -
-127.0.0.1		localhost
-127.0.0.1		tw2014 tw2014.sapsailing.com
-192.168.1.50	tw2014 tw2014.sapsailing.com
-
-* For rewriting the URLs and Ports to the fitting Spectator URL when entering the URL we need a Apache installed (apt-get install apache2) and set up like on our sapsailing.com server. Please check 000-macros.conf, 001-events.conf and 000-main.conf and set it up as you need for the event. They need to be in the config dir of apache (/etc/apache2/mods-enabled and /etc/apache2/sites-enabled) -* For better remote administration of the local replica, set up and autossh tunnel to trac@sapsailing.com and put it into rc.local for autostarting, e.g. as the following: -
-autossh -M 20009 -f -N -L 1337:127.0.0.1:22 -i SSHKEY trac@sapsailing.com
-
-* Set up automatic replication in env.sh of the analystics server and start the server (not with root!) with startscript ~/servers/server/start -* On all Cube computers add the folling batch file to the windows task scheduling for a check every minute. The script switches hosts to local replica if available. Before the script can work, you have to edit host file manully and add the required hostnames: e.g. 192.168.1.50 tw2014.sapsailing.com. You also need to replace find and replace to the required ip addresses. -
-@echo off
-@setlocal enableextensions enabledelayedexpansion
-
-REM Set a variable for the Windows hosts file location
-set "hostpath=%systemroot%\system32\drivers\etc"
-set "hostfile=hosts"
-
-REM Make the hosts file writable
-attrib -r -s -h "%hostpath%\%hostfile%"
-
-ping -n 1 -w 1000 -l 2000 192.168.1.50
-REM if local replica is available
-IF %ERRORLEVEL% == 0 (
-	REM set the string you wish to find
-	set find=54.229.94.254
-	REM set the string you wish to replace with
-	set replace=192.168.1.50
-
-REM if local replica is not available
-) ELSE (
-	REM set ring you wish to find
-	set find=192.168.1.50
-	REM set the string you wish to replace with
-	set replace=54.229.94.254
-)	
-
-for /f "delims=" %%a in ('type "%hostpath%\%hostfile%"') do (
-set "string=%%a"
-set "string=!string:%find%=%replace%!"
->> "newfile.txt" echo !string!
-)
-
-move /y "newfile.txt" "%hostpath%\%hostfile%"
-REM Make the hosts file un-writable - not necessary.
-REM attrib +r "%hostpath%\%hostfile%"
-echo hostfile done.
-
-* If server should be shutted down, then first stop replication manually in the backend and then call ~/servers/server/stop. If everything works fine, then the Cube computers should recognize, that the replica machine is not available anymore and then switch to the real sapsailing host. +These encompass the receiving of raw sensor data. The order in which this data is re-played doesn't matter. The data is time-stamped, and its history is not rewritten (as maybe the case for the mark passings or for the race tracking start/finish times). Applying these operations to a replica is idempotent, so serializing an initial state doesn't need to be synchronized with the receiving of such data. Duplicate application of these operations to a server doesn't do any harm. \ No newline at end of file From 836323863e3c00ca1b0aeaf809b6d2ad7d0d0a4b Mon Sep 17 00:00:00 2001 From: Steffen Tobias Wagner Date: Thu, 24 Jul 2014 11:33:00 +0000 Subject: [PATCH 12/20] Added section for setting up replication for SAP Cube --- wiki/server-replication.md | 252 +++++++++++++++++++++++-------------- 1 file changed, 159 insertions(+), 93 deletions(-) diff --git a/wiki/server-replication.md b/wiki/server-replication.md index 148ae51fbe3..9b80a143f05 100644 --- a/wiki/server-replication.md +++ b/wiki/server-replication.md @@ -1,93 +1,159 @@ -# Replicating an SAP Sailing Analytics Server - -[[_TOC_]] - -### Requirements -Scaling to many (>100) concurrent users following a live sailing race requires providing more than one OSGi server instance. To an extent this is cause by an aspect of our architecture that decides how clients access server data. We use the Google Web Toolkit's (GWT) Remote Procedure Call (RPC) to let the clients request data from the server. These are handled as HTTP POST requests with a servlet composing the response message. This approach makes caching servlet responses to similar requests by a generic HTTP caching / proxy infrastructure difficult. - -Using more than one server instance is additionally motivated by the ManyPlayers client architecture. ManyPlayers wants to use our code base but run it on their premises and only be assured that once their instance connects to our "master" instance then all relevant state will be replicated to their instance. - -### Server State -The server's state at the current time roughly consists of the following: - -* list of races with their tracking data loaded by the server -* a race's state consists of - * RaceDefinition object with name, boat class and event association - * Course object with Waypoints and ControlPoints -* a tracked race's state consisting of - * wind tracks - * raw boat position tracks - * mark position tracks - * mark passings - * race tracking start/finish times -* list of leaderboard groups with their names and descriptions -* list of leaderboards and their association with leaderboard groups - * leaderboard names, columns and the columns' associated tracked races, if any - * discarding rules - * competitor renamings - * explicit score corrections - * disqualification / "max-points" reasons (DSQ, OCS, DNS, DNF, DND, ...) - -### Reliability -When a server is configured to be a replica of another "master" server, all updates to the master server must be replicated as quickly as possible to the replica until the replica permanently disconnects from the master server. If the connection breaks temporarily and is restored at a later point in time, all updates that occurred since the connection broke need to be updated to the client. - -### Connectivity -Replicated instances may be able to only connect to the master through an HTTP connection. In this case, the master server needs to push updates through an HTTP connection reliably. - -### Single Master, Multiple Replicas -For Kiel Week 2012 we want to support a single master server with multiple replicated copies, so-called "replica instances." The replicas are configured for their master node. This configuration doesn't change over the replica's life time. If the master dies, it need to be started again. Replicas then need to recover and re-sync once the new master instance is alive again. - -### Concurrency Challenges -Different types of updates to the master server can happen concurrently. Tracking data for multiple races is being received in multiple threads which can execute truly concurrently on a multi-core architecture. Wind data may be received in parallel to receiving other tracking data. Leaderboard and LeaderboardGroup configurations may happen concurrently to all other types of changes. New races may start to get tracked concurrently to the many other server operations. Do we need to ensure that all these events whose effects need to be replicated have to be serialized in an order that is kept the same for all replica to ensure equal resulting states in all replica? What would serializing all these many concurrent events mean to the server's internal concurrency which is important to improve performance? - -### Thoughts on Implementation -HTTP push protocols are implemented, e.g., by the Atmosphere framework and by our own, proprietary AbstractHttpPostServlet in the com.sap.sailing.server package. They both use a heartbeat mechanism to re-establish the HTTP connection after it got disconnected. However, both currently don't handle reliable data transmission. If a client is disconnected and re-connects later, it may have missed a number or messages sent while it was disconnected. This would need to be overcome in order to achieve the reliabiliy required for our replication scenario. - -### Initial Load -When a replica is first set up and connected to a master server, the master server's current state at some consistent snapshot needs to be replicated to the replica and from then on be kept in sync. How can we produce such a consistent snapshot while understanding which subsequent change events to broadcast to which client? - -One approach we're currently testing is using Java Serialization to copy an initial "image" of one server to the next. There are several tricky issues with this approach: - -* listeners / observers: several classes offer listeners to be attached; for example, a DynamicTrackedRaceImpl object supports RaceChangeListener objects to attach to it. Similarly, Course objects support CourseListeners to observe the course for changes. Currently, our strategy is to serialize the listener graph together with the object graph they observe by declaring the listener interfaces as "extends Serializable." This unfortunately requires all other listener implementations, even the inner anonymous classes used by test cases, to be serializable too. On the other hand this avoids NotSerializableExceptions. -* non-serializable JDK classes: For example, TrackedRaceImpl uses java.util.Timer to delay cache invalidation. In this fortunate case, making the field transient has no negative effects because the next action that would need the cache to be invalidated will set the timer correctly, so no explicit initialization in readObject() is required. There may be other cases, though, where a readObject() implementation is needed to establish some objects in transient fields after deserialization. -* synchronization: serializing a data structure while it is being modified is a bad idea. The java.util.* collection classes are not thread safe. In those cases where concurrent modifications have to be expected, a writeObject() definition is required that synchronizes the serialization with modifying operations. For example, serializing a TrackedRace object together with its GPSFixTracks and WindTracks needs to synchronize with inbound GPSFix / Wind updates. - -### Incremental Updates -Many updates to the server's state are small compared to the overall state. For example, adding a GPS fix to a boat's track is a very small change; so is the modification of a leaderboard name or a leaderboard group's description. Such events need to be replicated incrementally. A message queuing system with persistent message queues may be an implementation option that can guarantee delivery also through HTTP connections. - -### Implications for the MongoDB Store -A replica may or may not use the same MongoDB store that the master uses. If it uses the same store, replication events must not update the database. This then also holds for the initial load procedure. On the other hand, if the replica uses its own database instance, even the initial load has to update all structures that require persistent storage into the local database. - -Using MongoDB's built-in replication requires a robust TCP connection between the MongoDB replicas. This is hard to establish in case only HTTP connections are possible between the replica and the master. - -### Use of a Message Queuing System -An MQ system such as ActiveMQ could serve as basis for the implementation of the replication mechanism. We'd have to wrap all commands in messages. To be clarified: what happens when a client subscribes to a topic? Will it receive messages sent to that topic only after it connected? Or will it also receive older messages that have been queued persistently? - -### Design -Changes applied to a server that are relevant for replication have to be applied through a single entry point at RacingEventService, using the command pattern provided by the RacingEventServiceOperation interface. This will allow future extensions towards the use of an Operation Transformation (OT) algorithm in case operations can be performed not only on a single master but throughout the replicas. In addition to the course-grained leaderboard / group creations and editing operations, all tracking data recordings need to pass through this interface in order to be replicated properly. - -### RacingEventServiceOperations and RacingEventService -Most of the state changes relevant for replication are triggered through the RacingEventService interface. A few changes, such as the recording of GPS and wind data received, are currently immediately updated into internal data structures, such as a DynamicTrackedRace and the track objects it manages, or the Course object owned by a RaceDefinition object. It would be possible to restructure things in such a way that also the tracking data are funneled through the RacingEventService interface. - -A RacingEventServiceOperation describes serializable operations that can be applied to a RacingEventService and which can be transmitted to another server instance (a replica) to be applied to another RacingEventService, eventually leading to equal states of the RacingEventService instances. - -Question: Where and how do we ensure that all modifications to a RacingEventService that are relevant for replication are actually turned into RacingEventServiceOperations and then reliably serialized to all replicas? - -We could let RacingEventServiceImpl manage the creation of the operation objects whenever a changing method is called. This would require distinguishing between an "external" and an "internal" API where the external API methods create the operations which, when applied, use the internal API methods to perform the actual updates. - -Or we restrict the externally visible API of RacingEventService to applying RacingEventServiceOperations. Then, all modifications that are relevant for replication have to go through a RacingEventServiceOperation. This is very hard to ensure because internally all APIs, such as those of Leaderboard and LeaderboardGroup and everything are easily accessible. - -Answer: We create the operations already in SailingServiceImpl and only call apply on the RacingEventService whose implementation can then notify an optional replicator of the operations that were executed. At this time it would make sense to remove the fine-grained operations which are now only used by the operation implementations from the RacingEventService interface at least. The operation implementations can cast the RacingEventService object to RacingEventServiceImpl and then access the operations again. - -### Parallel Processing of Operations -Many types of operations can potentially be processed in parallel. For example, GPS fixes for different tracked races can be inserted into their respective tracks in parallel. So can edit operations for different leaderboards. A thread pool can be used to process the operations. However, each thread in this pool may need to obtain the monitors for the objects it will modify, and the scheduler that sequentially passes the operations to the thread pool for processing needs to wait for the processing thread's acknowledgement that all monitors required have been obtained before triggering the processing of the next operation. The general problem reminds of the versioning graph as created, e.g., by a git repository. Also, Leslie Lamport's vector clocks come to mind. - -Some operations are more tricky to parallelize, particularly if we have to assume race conditions during execution. For example, if an operation creates a leaderboard and a follow-up operation adds a column to the leaderboard just created, the leaderboard creation must be complete before the column can be added. Parallelizing such operations is generally dangerous. - -Fortunately, the dangerous-to-parallelize operations seem to be the ones that occur with low frequency (leaderboard creation, score corrections, leaderboard column configuration) and have relaxed requirements regarding the lag with which they have to be replicated. We can therefore partition the data and transactions to be replicated into two categories - -### Sequential operations -These encompass leaderboard and leaderboard group creation and manipulation, including score corrections. In a replica these operations must be executed in the same sequence they were executed in on the master. It probably encompasses the creation of a tracked race and its course, we well as the updates to a race course with waypoint additions and mark passings because mark passing events relate to the waypoints. -Paralllel operations - -These encompass the receiving of raw sensor data. The order in which this data is re-played doesn't matter. The data is time-stamped, and its history is not rewritten (as maybe the case for the mark passings or for the race tracking start/finish times). Applying these operations to a replica is idempotent, so serializing an initial state doesn't need to be synchronized with the receiving of such data. Duplicate application of these operations to a server doesn't do any harm. \ No newline at end of file +# Replicating an SAP Sailing Analytics Server + +[[_TOC_]] + +### Requirements +Scaling to many (>100) concurrent users following a live sailing race requires providing more than one OSGi server instance. To an extent this is cause by an aspect of our architecture that decides how clients access server data. We use the Google Web Toolkit's (GWT) Remote Procedure Call (RPC) to let the clients request data from the server. These are handled as HTTP POST requests with a servlet composing the response message. This approach makes caching servlet responses to similar requests by a generic HTTP caching / proxy infrastructure difficult. + +Using more than one server instance is additionally motivated by the ManyPlayers client architecture. ManyPlayers wants to use our code base but run it on their premises and only be assured that once their instance connects to our "master" instance then all relevant state will be replicated to their instance. + +### Server State +The server's state at the current time roughly consists of the following: + +* list of races with their tracking data loaded by the server +* a race's state consists of + * RaceDefinition object with name, boat class and event association + * Course object with Waypoints and ControlPoints +* a tracked race's state consisting of + * wind tracks + * raw boat position tracks + * mark position tracks + * mark passings + * race tracking start/finish times +* list of leaderboard groups with their names and descriptions +* list of leaderboards and their association with leaderboard groups + * leaderboard names, columns and the columns' associated tracked races, if any + * discarding rules + * competitor renamings + * explicit score corrections + * disqualification / "max-points" reasons (DSQ, OCS, DNS, DNF, DND, ...) + +### Reliability +When a server is configured to be a replica of another "master" server, all updates to the master server must be replicated as quickly as possible to the replica until the replica permanently disconnects from the master server. If the connection breaks temporarily and is restored at a later point in time, all updates that occurred since the connection broke need to be updated to the client. + +### Connectivity +Replicated instances may be able to only connect to the master through an HTTP connection. In this case, the master server needs to push updates through an HTTP connection reliably. + +### Single Master, Multiple Replicas +For Kiel Week 2012 we want to support a single master server with multiple replicated copies, so-called "replica instances." The replicas are configured for their master node. This configuration doesn't change over the replica's life time. If the master dies, it need to be started again. Replicas then need to recover and re-sync once the new master instance is alive again. + +### Concurrency Challenges +Different types of updates to the master server can happen concurrently. Tracking data for multiple races is being received in multiple threads which can execute truly concurrently on a multi-core architecture. Wind data may be received in parallel to receiving other tracking data. Leaderboard and LeaderboardGroup configurations may happen concurrently to all other types of changes. New races may start to get tracked concurrently to the many other server operations. Do we need to ensure that all these events whose effects need to be replicated have to be serialized in an order that is kept the same for all replica to ensure equal resulting states in all replica? What would serializing all these many concurrent events mean to the server's internal concurrency which is important to improve performance? + +### Thoughts on Implementation +HTTP push protocols are implemented, e.g., by the Atmosphere framework and by our own, proprietary AbstractHttpPostServlet in the com.sap.sailing.server package. They both use a heartbeat mechanism to re-establish the HTTP connection after it got disconnected. However, both currently don't handle reliable data transmission. If a client is disconnected and re-connects later, it may have missed a number or messages sent while it was disconnected. This would need to be overcome in order to achieve the reliabiliy required for our replication scenario. + +### Initial Load +When a replica is first set up and connected to a master server, the master server's current state at some consistent snapshot needs to be replicated to the replica and from then on be kept in sync. How can we produce such a consistent snapshot while understanding which subsequent change events to broadcast to which client? + +One approach we're currently testing is using Java Serialization to copy an initial "image" of one server to the next. There are several tricky issues with this approach: + +* listeners / observers: several classes offer listeners to be attached; for example, a DynamicTrackedRaceImpl object supports RaceChangeListener objects to attach to it. Similarly, Course objects support CourseListeners to observe the course for changes. Currently, our strategy is to serialize the listener graph together with the object graph they observe by declaring the listener interfaces as "extends Serializable." This unfortunately requires all other listener implementations, even the inner anonymous classes used by test cases, to be serializable too. On the other hand this avoids NotSerializableExceptions. +* non-serializable JDK classes: For example, TrackedRaceImpl uses java.util.Timer to delay cache invalidation. In this fortunate case, making the field transient has no negative effects because the next action that would need the cache to be invalidated will set the timer correctly, so no explicit initialization in readObject() is required. There may be other cases, though, where a readObject() implementation is needed to establish some objects in transient fields after deserialization. +* synchronization: serializing a data structure while it is being modified is a bad idea. The java.util.* collection classes are not thread safe. In those cases where concurrent modifications have to be expected, a writeObject() definition is required that synchronizes the serialization with modifying operations. For example, serializing a TrackedRace object together with its GPSFixTracks and WindTracks needs to synchronize with inbound GPSFix / Wind updates. + +### Incremental Updates +Many updates to the server's state are small compared to the overall state. For example, adding a GPS fix to a boat's track is a very small change; so is the modification of a leaderboard name or a leaderboard group's description. Such events need to be replicated incrementally. A message queuing system with persistent message queues may be an implementation option that can guarantee delivery also through HTTP connections. + +### Implications for the MongoDB Store +A replica may or may not use the same MongoDB store that the master uses. If it uses the same store, replication events must not update the database. This then also holds for the initial load procedure. On the other hand, if the replica uses its own database instance, even the initial load has to update all structures that require persistent storage into the local database. + +Using MongoDB's built-in replication requires a robust TCP connection between the MongoDB replicas. This is hard to establish in case only HTTP connections are possible between the replica and the master. + +### Use of a Message Queuing System +An MQ system such as ActiveMQ could serve as basis for the implementation of the replication mechanism. We'd have to wrap all commands in messages. To be clarified: what happens when a client subscribes to a topic? Will it receive messages sent to that topic only after it connected? Or will it also receive older messages that have been queued persistently? + +### Design +Changes applied to a server that are relevant for replication have to be applied through a single entry point at RacingEventService, using the command pattern provided by the RacingEventServiceOperation interface. This will allow future extensions towards the use of an Operation Transformation (OT) algorithm in case operations can be performed not only on a single master but throughout the replicas. In addition to the course-grained leaderboard / group creations and editing operations, all tracking data recordings need to pass through this interface in order to be replicated properly. + +### RacingEventServiceOperations and RacingEventService +Most of the state changes relevant for replication are triggered through the RacingEventService interface. A few changes, such as the recording of GPS and wind data received, are currently immediately updated into internal data structures, such as a DynamicTrackedRace and the track objects it manages, or the Course object owned by a RaceDefinition object. It would be possible to restructure things in such a way that also the tracking data are funneled through the RacingEventService interface. + +A RacingEventServiceOperation describes serializable operations that can be applied to a RacingEventService and which can be transmitted to another server instance (a replica) to be applied to another RacingEventService, eventually leading to equal states of the RacingEventService instances. + +Question: Where and how do we ensure that all modifications to a RacingEventService that are relevant for replication are actually turned into RacingEventServiceOperations and then reliably serialized to all replicas? + +We could let RacingEventServiceImpl manage the creation of the operation objects whenever a changing method is called. This would require distinguishing between an "external" and an "internal" API where the external API methods create the operations which, when applied, use the internal API methods to perform the actual updates. + +Or we restrict the externally visible API of RacingEventService to applying RacingEventServiceOperations. Then, all modifications that are relevant for replication have to go through a RacingEventServiceOperation. This is very hard to ensure because internally all APIs, such as those of Leaderboard and LeaderboardGroup and everything are easily accessible. + +Answer: We create the operations already in SailingServiceImpl and only call apply on the RacingEventService whose implementation can then notify an optional replicator of the operations that were executed. At this time it would make sense to remove the fine-grained operations which are now only used by the operation implementations from the RacingEventService interface at least. The operation implementations can cast the RacingEventService object to RacingEventServiceImpl and then access the operations again. + +### Parallel Processing of Operations +Many types of operations can potentially be processed in parallel. For example, GPS fixes for different tracked races can be inserted into their respective tracks in parallel. So can edit operations for different leaderboards. A thread pool can be used to process the operations. However, each thread in this pool may need to obtain the monitors for the objects it will modify, and the scheduler that sequentially passes the operations to the thread pool for processing needs to wait for the processing thread's acknowledgement that all monitors required have been obtained before triggering the processing of the next operation. The general problem reminds of the versioning graph as created, e.g., by a git repository. Also, Leslie Lamport's vector clocks come to mind. + +Some operations are more tricky to parallelize, particularly if we have to assume race conditions during execution. For example, if an operation creates a leaderboard and a follow-up operation adds a column to the leaderboard just created, the leaderboard creation must be complete before the column can be added. Parallelizing such operations is generally dangerous. + +Fortunately, the dangerous-to-parallelize operations seem to be the ones that occur with low frequency (leaderboard creation, score corrections, leaderboard column configuration) and have relaxed requirements regarding the lag with which they have to be replicated. We can therefore partition the data and transactions to be replicated into two categories + +### Sequential operations +These encompass leaderboard and leaderboard group creation and manipulation, including score corrections. In a replica these operations must be executed in the same sequence they were executed in on the master. It probably encompasses the creation of a tracked race and its course, we well as the updates to a race course with waypoint additions and mark passings because mark passing events relate to the waypoints. +Paralllel operations + +These encompass the receiving of raw sensor data. The order in which this data is re-played doesn't matter. The data is time-stamped, and its history is not rewritten (as maybe the case for the mark passings or for the race tracking start/finish times). Applying these operations to a replica is idempotent, so serializing an initial state doesn't need to be synchronized with the receiving of such data. Duplicate application of these operations to a server doesn't do any harm. + + +### Replication in SAP Cube +This section will describe, how a working local replica can be set up at the SAP Cube. +The setup at the Travemünder Woche 2014 was done by Steffen Wagner as it follows: + +LTE router --> gigabit switch --> cube computers (dhcp) && local replica machine (with e.g. Ubuntu 14.04 LTS & a version of the analytics suite installed) + +Steps to get it working: +* The LTE router ( _192.168.1.1_ ) should have DHCP and NS activated. Dhcp was within range _XXX.XXX.1.2 - XXX.XXX.1.49_ and our local replica machine had a static ip address of _XXX.XXX.1.50_. Please make sure, that the ip address is outside of the dhcp range. For a better setup we should think of using the local replica server for dhcp and ns services. +* Setup networking (to your needs) on the local replica machine as follows: + +**/etc/networking/interfaces** +
+auto eth0
+iface eth0 inet static
+	address 192.168.1.50
+	netmask 255.255.255.0
+	gateway 192.168.1.1
+
+**/etc/hosts** +
+127.0.0.1	localhost
+127.0.0.1	tw2014 tw2014.sapsailing.com
+192.168.1.50	tw2014 tw2014.sapsailing.com
+
+* For rewriting the URLs and Ports to the fitting Spectator URL when entering the URL we need a Apache installed (apt-get install apache2) and set up like on our sapsailing.com server. Please check 000-macros.conf, 001-events.conf and 000-main.conf and set it up as you need for the event. They need to be in the config dir of apache (/etc/apache2/mods-enabled and /etc/apache2/sites-enabled) +* For better remote administration of the local replica, set up and autossh tunnel to trac@sapsailing.com and put it into rc.local for autostarting, e.g. as the following: +
+autossh -M 20009 -f -N -L 1337:127.0.0.1:22 -i SSHKEY trac@sapsailing.com
+
+* Set up automatic replication in env.sh of the analystics server and start the server (not with root!) with startscript ~/servers/server/start +* On all Cube computers add the folling batch file to the windows task scheduling for a check every minute. The script switches hosts to local replica if available. Before the script can work, you have to edit host file manully and add the required hostnames: e.g. 192.168.1.50 tw2014.sapsailing.com. You also need to replace find and replace to the required ip addresses. Please note that all this would not be needed, if we would use the replica server also for dhcp and ns, so it makes really sense to set this up. +
+@echo off
+@setlocal enableextensions enabledelayedexpansion
+REM Set a variable for the Windows hosts file location
+set "hostpath=%systemroot%\system32\drivers\etc"
+set "hostfile=hosts"
+REM Make the hosts file writable
+attrib -r -s -h "%hostpath%\%hostfile%"
+ping -n 1 -w 1000 -l 2000 192.168.1.50
+REM if local replica is available
+IF %ERRORLEVEL% == 0 (
+   REM set the string you wish to find
+   set find=54.229.94.254
+   REM set the string you wish to replace with
+   set replace=192.168.1.50
+REM if local replica is not available
+) ELSE (
+   REM set ring you wish to find
+   set find=192.168.1.50
+   REM set the string you wish to replace with
+   set replace=54.229.94.254
+)	
+for /f "delims=" %%a in ('type "%hostpath%\%hostfile%"') do (
+set "string=%%a"
+set "string=!string:%find%=%replace%!"
+>> "newfile.txt" echo !string!
+)
+move /y "newfile.txt" "%hostpath%\%hostfile%"
+REM Make the hosts file un-writable - not necessary.
+REM attrib +r "%hostpath%\%hostfile%"
+echo hostfile done.
+
+* If server should be shut down, then first stop replication manually in the backend and then call ~/servers/server/stop. If everything works fine, then the Cube computers should recognize, that the replica machine is not available anymore and then switch to the real sapsailing host. \ No newline at end of file From 22c457e5b2bd256e7202b15b1546001172e7c731 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Thu, 24 Jul 2014 16:43:54 +0200 Subject: [PATCH 13/20] upgraded to TracAPI 3.0.2; supposed to eliminate caching problems of objects coming from the clientparams.php file, such as static mark positions --- .../META-INF/MANIFEST.MF | 2 +- java/com.tractrac.clientmodule/README.txt | 17 +++++++++++++++++ .../com.tractrac.clientmodule/lib/TracAPI.jar | Bin 813796 -> 748755 bytes java/com.tractrac.clientmodule/pom.xml | 2 +- .../app/tracapi/EventListener.java | 0 .../subscription/app/tracapi/Main.java | 0 6 files changed, 19 insertions(+), 2 deletions(-) mode change 100755 => 100644 java/com.tractrac.clientmodule/README.txt mode change 100755 => 100644 java/com.tractrac.clientmodule/lib/TracAPI.jar mode change 100755 => 100644 java/com.tractrac.clientmodule/src/com/tractrac/subscription/app/tracapi/EventListener.java mode change 100755 => 100644 java/com.tractrac.clientmodule/src/com/tractrac/subscription/app/tracapi/Main.java diff --git a/java/com.tractrac.clientmodule/META-INF/MANIFEST.MF b/java/com.tractrac.clientmodule/META-INF/MANIFEST.MF index c9187b45927..aedba8362ad 100755 --- a/java/com.tractrac.clientmodule/META-INF/MANIFEST.MF +++ b/java/com.tractrac.clientmodule/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: TracTrac Client module Bundle-SymbolicName: com.tractrac.clientmodule -Bundle-Version: 3.0.1 +Bundle-Version: 3.0.2 Bundle-RequiredExecutionEnvironment: JavaSE-1.7 Bundle-ClassPath: ., lib/TracAPI.jar diff --git a/java/com.tractrac.clientmodule/README.txt b/java/com.tractrac.clientmodule/README.txt old mode 100755 new mode 100644 index a317506cdb7..98d24e49111 --- a/java/com.tractrac.clientmodule/README.txt +++ b/java/com.tractrac.clientmodule/README.txt @@ -16,6 +16,23 @@ It contains also some files: - test.sh -> script that compiles the code in the src folder, creates the test.jar library and execute the code of the example. - Manifest.txt -> manifest used to create the test.jar file +******************************************** +************* TracAPI 3.0.2 **************** +******************************************** +This is a final version. Only fixes bugs in the implementation + +1) New features + + - The IRacesListener.updateRace() event is thrown when the live delay for a race has changed. The IRace.getLiveDelay() method + returns the new value. + +2) Bugs + + - The events IRacesListener.abandonRace() and IRacesListener.startTracking() have been reviewed. Now they are always sent + when the races are updated either using the event manager or using the external JSON service "update_race_status" + - The controls are not updated when they are retrieved a second time from a parameters file. The library always returns + the first control thas was read the first time. + ******************************************** ************* TracAPI 3.0.1 **************** ******************************************** diff --git a/java/com.tractrac.clientmodule/lib/TracAPI.jar b/java/com.tractrac.clientmodule/lib/TracAPI.jar old mode 100755 new mode 100644 index f1ad42e204cfbfa5ee711656f4c545e68e7a2321..f9ddc9677453b0d85b8dce00d47c256e3e5185f3 GIT binary patch delta 82926 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±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 delta 145420 zcmZ_01z40#*Er5Dy>zFvv~-u!-QC@t0)k6RDj_TgDlHv?EZrrY7G2T}O8Z}SeV*_A z-tYJKy14GW&zw0mXU;h@_l(ouAFa>w;%KQNqYxuuU|=96JAF>U5ktOzsPKcUVu^nh zzvI*=C9bv?60e+e6H@!AdVQE(QE&1i;sy23W66_Iw49(m{0#tG=2*H-8EEWFCG=Z< z0=};9fqK1BytyU1djICTWSf;X!Qgp}(s`mS`A>JKcMk7LWzhat6L2EW=1u^(Ns0S!vV~|p6~Ja3_=AL-H}rH-#WuQ>V^LFG`Whg zE4JN;%<0ndb=8$(vewG&9Mk5Q2|rFUZ(khz@H5BuxnAF}M4W+AC8lV;7O2MH$~CgC zaP6#?IE@IKr%8{TIf|5inUmJ_={J?`F1skdP*dBJk?&}UX&hMe^I5Nk#A1nczFNI) zR6S1CkiBw3E8HdFNH`3|w?rNjxkk}aMaTHVpZJ;$fP^FuL_+#sMoeo0@ZMW7Z65%) zW?JqlP1-ULCruwneGi8u7h~Q72+|4=fV7p9`*%Tjrxc$1-xPJ4$Vq7TfVw427Ib(E zCV>R}sYIX*e=<`)g5WBGDAIlq!0D!q(EJ5FCL|?PZo!kyEy0K39kDOVuw6I~gbUr?Ig2vy;W<*Qg+W3YGWR zE!t3U(qyqVVHx8Ys~_A#iqd5&r|X~!QEFAB4iM4kHSSG^Zt1=DgxiL{|M zVvUaq376s(L-q>LP-!QqgF_HWXWB54Fn9!H{oqT}uTIf4qvA64w%=Q{%vr1_LccLd z^Sk#bCqRH92?sr5f(FH2pW7L?=hmdHID7OTrc9 zTQO473a?J6wgIC#$w;k!X@D|YMqG(s6E`O}WrdoD%eEY7AkjsUdjX5||g5x_Z-PPzf^Uahd9*e~+kTn`&D){S)RzqFr9xq#?+TdJ;T za4)q=wR2jm9Gtz0w3e{0vwt)cII#1vpR`B8(HsBWGmCE$pF;KA%HFWqmaRP?sUm2x zMr9%hebU;ouypmZykqW`BM^z^AZ`%s>g0?)=-l%snP3H?r{7 zH=#h*O5wKw1vzEnA)kSbIhD|IYGst&hT~GN|08ox7oNutF|#6#k*)11uGopu)2#wV4;o8k~YjSK0`gB#X2+)zwbxFw$JwE@;5Jig?(?Vbc`ma2Oh^K zQC*%IJsyDuKAzgN$FvlHbqJjt25Xus86L$pDdVCq5)x7n?C*a?A4shn22Gk5IcC}$ zGDKLo4?kYyIFNuU6h!a=lpGBi9#dkd9>br5RHpaOI%etu zARI74V?*?RgyQ2#lcv?9s3{!W41~$??ElCDk90?j1ZfMj2-co1u+VqJNSek&hXBxr zA^?#0svx4Y47v~Z+#t1pzi2p&n02A_#(@8ZcEg35jO9-2UAoSrit>yIi^jl=p1c5L zKnp}t_d@&<6|R8yICp9s2!d|_LWhWIgYfF6q{ZL^QHLj=4F3T)MO~-TE}R6URt6?# zZW0b;Q3pf_3Dp5%*P&?Z!ihn&dtn3LsYH3-Mpaj7kbnTE-~tg~4P6*E*sKGtTb;Cp zKcfBK){qG!5KG;itqVNmq)A(dLW1>R*flQLC~(Djs!Vg|hRdi^9?GXr+X%3i z09E*oOEL`@*?@NF;Tf^^8E&KX{3g$Jen(z2qR7NIx*Dk|$O*o~w@u}V`gI&U6ODnw zBuzq+u2Ncv_s~Zt9?a4gZzl+Nd8dgnq%rgxv%c;Qtxn`U`wvV%`E@x^QDgt>ir(4A z95t+#(o~iSe%(^0Xg!rgok(1uJAr6vv3+TN(W(ZqVg5rod4d=@_5%ive!cmZG&8iNPbsuObB(e?4l8sNz`Q6h+qgSq7hFvA*2 zSaTN1bz+Q!H^7zm+D1XGtO{K)n~((P|7;Bq4u-$~bmL zz)WhzS(Zy=Bx=3U!&dR&=^lf>vdT)5+bA?Trc?2@)*@`1j&&nTm92W=Z*+`jBob$f{l(CHq9pyn z#8e{7i=6l$PpqUqC9jsevN5)EjQOr+Ws#)rp8E*psw55^AOChsoZVrSfW?YaN+5@n zef$S`_`!>BeQCT|W&S{%Y@F#DC}GJV?I8YZHEGBe^uDpRG+-|2FIB3ev*+M@ZIh$U*Iyid~Kf@ zAg_8T;<#;5LJBDhH)&HYdTqe2&3#ud=&XzS^MeydgMKbn5&wdup<}?;p8;M|gRi#b zf$BX!rKjKUU0+2WJv4KQN|3$);jUrX(_uiCeeluf38Pr|<$ma61@Cyt& z3wu{rdjfW1P@C@=y30a;Tu0h^ZlheUS-x6a`D$8SVD3x6tx{2@NQ;T#+gm&~4x=dU zRhkea1FI#h)1x|=BXTgWvSIRr1YT~GMU(%xhDw4}Uu6iH~^ z3_slqZ^P~gyJ6_zrhhfFzGfdIxUL=JJ7|l&J9te_qqsO;6CM(_5;+(Y!;%a5v!)&u z3V3J@rpA1ExA8RWW<8U-ySy5zrxKCnp$6wTBrQ{jlBqP zsgvXA0d=IS7j;z+<^@1QhC-w3kq8cS1^Wi*n6|lb;&u^9=zlExy0C8ikxZ_{dz4<+KEZcM>eoE4+=SIs?rqXxG+z^^UpuXLJ~49JRR2I1Epz@VpGG zjUXS4Vc9UD$Q^;$I*kS^*GecMPs9QL)DYYda}&^mx_2cTi2RHv&kgA}0a4bCSBt{a zVmdFpULr`#9Y=@ooBS%0cC;nKnC8SZ-?b#g=O@UC^<<7J4%d|yE!m_2F3fY4fi z2|oXA(Jxy>4Ja)S7Z(RoWe0P*U$UnI*tbDM#v7Qw7gFU?ZpJ-Aw*h&Ig-@|F2vpeL;@|v z0&L^L<@tvvw38BGfd>b{Jo*6s!tKTLbR8_cHhi0@4xgcwa?Bw7>w;f9JscDg_Q=h8Bneh!Ko#Ct+@Lj|M%E1mwbr zK~rP^bqFhk8v+C%RW2YZXod=a>%V(ks{u$5FgPA}Bme`dsR__QNC_V}FINyI1lJvA z{w1w{XX0PE;NVI(5C%le4TK3*(FU9%sKJYAxaj|&;6oGi{P8rL0?;FLx?g@E3;ZxWRY!n42He#5<@JBGwtIjGp#fe1 z`Tto*{7)p37bJ|F_fSS-S>(EV6I4V)Sj zwxH03&m`!tlz(gri%v9!Ygee;^3AX{PzXasH zABYN~nFII?2f>I-Kwv%bpbl>Vp$MV>B?)=u5A$>3T)+iF=zsd7LnM4bxRA(vz!jXx z-e_XAuI1pk?h_t60l>N5<` zg~MU{h&<4XaX|Xtfd#<^7A{=(B%u3$)VP=VQ827y*AyU_01gM3AhSU5(STSGuXV(j zKvFG0_P!KJ2<`^J>7*4q7!yE7zfjAJ1=OA3D)fwOvAvJ8)aX_A20?;9_R14S~TKxokX8vq4jmi=RwU;jwR_YYZI zC^s6A1tAe^rva`O@js2|5SK96TrS4|uEC{(!sy(>nLuF^@cm!fxIhu8D;W?UG0d?1 zjUWPr$@X`H=0DB#4KBj}SOtbf00q$jClE5+OA3Xl{20LorpsdlUAPMOmV!>P1EK#8 zEgz(v6W9i)`TP-3^Zv*EIKqAWNenDL5OV=F5v1>V{sYH=Wbpu(;n&@f9Zqq=Tyk-+;J>P{e{5pqFC6CvfG{yvI49 zjuJp~#M@*n2jnmoL=PpB0!kq)2A}Kq9le6Awax{4zxpW>8ou^BS&jn#juEwgC%d#{ZuUHf2Urvww=1SC5=r z|KCLnY*SASt|Yvg(ozAEL0XjmesH+Y&=44i3X*Pz5CK-maX^;sfVfas72q5~Nksm* z$MVELMuUhZ!Ddm0CXfL!TJUU(ZwX|A-0J)#`w9XyfcoeHR}g~-f2}Y8iu}i`q%m;q ze_kQWy~ya$Ff*X%|G=P1mOy31tJNhs4FtwQ1oA8ygatjf2DTv@jv9cX5ISR+!(Q6~ zj}h*C=mac6NCxM-?*>H0gzI1X1(q6MRD_`_0YDEjIP89m;FO^RFM&3QZvViTpp7X& zp8p^voeorm^M%4FN|lHDytF6W>$Z1T!=NC^NR)h5 zK!eM5;O&)v$5Ko1-Np5naU`K-75s6lS5Cj;iy5PAm}Hz}KREr0E9Qvu*n>T0nXruvbT(Osjb$O*rz20#RWppm?n~> z>DH7+!h`xcYrHtT8JC!!GrT!*oM!V9=@MF&U;36kd31BEjm`K2O&wU38PW?iUf(p9 zvIj$KoNc6(&#;KeJLv|nBTaFdZRUUl{ix;I%~@V-lzNG;$F64*_cLWf{Q#1sB?IIi z8CJm`0s3DXS9sj6I|c+BsciTCVl+AISz~GSW73`aC(oQlRx(zzB(1z>>vga<`0=#d zMst8OYjOQz^A>w54p-Nrm!^3vvvfa$Pu>PYILWE&z0HXnwtt}&V=AMwoC|%}_*f>+=OA1XGD(Y1uTq_v!k=LdMUd)HRBCD-T~fXKcj_cG`!$qGf%= zC2_|lVfMhkk36=4<&{sm4TIP->V64wD)7A7Bu_f~4(ZQS?3AV$sh;>3z2dWZS@a4` zXI*x(+U;7stPkeqb4_QY%3>wjQP$bZhTL1~U3?$TLVwDaD7EqT8WD#OJj)hJ}MzMrupVlnoKpT5VM z4lOEk=~)#CP>UU%EVgN6)%kVwB1)j-oBK`Snne-hz`ijp;`(OVhuWK)#n8)pp2Nqr zTR(5&+3t<%OIO)9$sYADVWo(EgVOtz5PljV`eW2(^DvRsSq^9Li3W85ZR(NFB3g9-iH zwby$&o)65b5KFJHWxuh_jtJKq~>iDk2rLXsC3PbaHn2msKL|z4?CvHv( ztui!Nab)FEZf4{)49>AN8K6qu4&ljb1bd+5E-z~x!_?hB=jQzcqB zrkDU-p0-R{lmjNW^0!CyX!Ek^QIJE1O|FpN$+GpBKlV?JoIOcR%$~*;%*q$z>>Fto zgyc&iA3`JB*>>@7;9vL5`&v^6P?3;gasO+x0Sa6Bx&52fOu_D&Gf(gb{tVOi=@8?O zIf|UmtZ5}&gn`(Nv^0ZrMP>6nM=U(g4OE{w4`1F$-=MS$X@{sJR)?&R9gC9}jB&Bp zOpwbTPVJBQiCt%p9qe6?S1%%wcNcxy>I{q1*yEpkb<2!PSIsonOLu~X%Wgjy6)x){ zG9SYzQWj&4JODQFo|hBXASdVLaam@PRnQ1PAoSKW4~S;DX{KFl2sLhS z)iz?u63ie<38aCUgCk04%%9XTt8vMnoaQY zDmBf(?<}eis^Ize8!39hJ>M!)t&t-2F@OZ3Qm<^KGvdJd({?Y|+WgYdNi5Ayf37j* zv?w4w&w2(W*AYdXTfNjl!R$GH{B~@To7BPSC3BYjR)FV;7<7|pdoEf|P6CrL_=|;# zQ!_*Lb_mYI3nA5&koCAaosIVv;h|6Z@(zV50-uu)FDqy>&DPi~*GKc78Gj4&z>-v< z9H2mc8I7q1rW6^jSrP9JBtCAbN+iPC^s=}l<236eCNT2SuIEXjh*{|vd6Q?Wa@Ag9 zH}sTWkS0ZY(!5Z<>FT4l*W1|fNUC3Im&HdVPk!Nf2XV?iNc&o7E>i8fw?!zfN^)Rg zj2}}wyILyPgRFU>r*sg+@Ul)3&6(fePv0+Z@Y{8fQLw(LnXWkauy5i^n0TPxWZWOM zr^aKL(O-U3LF~pD^=3wM9Y#xS%gpaxBge^Wh5;NUkHJ6Z@>o>|b2pldEp~OaIKHJt6n({i z8DYFVQUCHRI|wt|4b4(Mm!Ys&SDS9rxq8_&f80TD(;TgT$J%$Mu z<}i>N+;GZnp?lo?Quv6ODm1~Q`H{!VpHAEE)GRe~rRRyMQil>I_^2sGv;2XI11*8C zp3EFeNlhHnn{4Z;w;d7-s6XLNrXVF1w6IAPJ4{t??wBt~sQy7XBm8wDZB_#-Z$HsB zD|*7WH7Di7N3=>?*Z-V&FO!`ZiUqkSbDvI=QGwvH}*=b&XW>FdO z&xTU39i2YUeky75VW{Zzl(*qqOMlT3E`&P-unIyIe*O92ol<}#i})kPj6|W2)l+U1 zz6Jqx;R~;`lKv2NLh7|OyM$%ue7TV?;!XVCxI>o$sA;Y?M810ExX&tSPE~}sXI-{> z)q%i2h)Q?*ivs`H61r!3HOIXjQ$I7WQdK!8N>Mlbo=Re1-@RtW;=Aq4mZl~Tgpkr( zQb91bsL_65(wnAr8)qtMpuKiMZ)3bnX5Olp8szk+wzRA_#%eoxotf6i5JNC0xyRdE zx?{{*{q^|lYk(h}#(Rt5Eh;O~*Tu~3ueK7vZfLiL`RRq_vCUb58FHqEp`fC3YKIU8 z{~ue%iejC@P9HvSZL=EK;#`#H6S=CAzQ&4u!uWluJibIU^0Dc-jOZ&Rf={>Z@-yj1 z`hElj>6uwJvhJKF`MU`dns}ai-a@#7V3kbS)%W z*3eK5Du}oDDdf9251_hrJA9N|ehC<#u^@GOxCJFxhWC~uVSd#MyvR%-Jh zzAC{;>ywtQiPmH&L4r}zSG#%1kLzt)1y7^e z=eXDP9V;{t^t{2}P!bfYKVN{LCgJ$%d${LN~AVn=A5e_=&}j=nX-KF-Je;#L}d z7(1Xdka~@r-~9PSj!)3KXcedsdaM>@KFh0B%k!$7^d*D6H7Fxm;DofS24@DF(`d#b z0WE{mYygM-^rDH!&X1>IxDDr8{+X@_ePkOt1dOh;Ph57)q|!>WVZNk%-PM@FV;A(H zuHytHMC{-rWD_E*lDL(J}dABp2ZvAqxifQy^oDp-uH9nn+y7HlE zq5+ta;)kTdynE`JiEfo!c={$3p98FL6KW@-{s(i5t%6RqwJ69(>SfwQfoS9n#!oim z-L{ZR#&2XyvAt3&gXn5*zWtt^uccn;r`QNN7`c;qJx8G)C>^{K7{gbe*cDHp_GUI# zXSjL}y!XZR{h-elu(PriFn>gRe?$L!>iFInpFmz^!L}H^CxM`Ut79F=#5C{$4sk82 zek@Qv_I12cI0NKr8psXqonnd?aeQ&gMQ!HgO^U_EWwY3oAE}C4hE2qVSI+)p65G{1 z@A^5RuXW4P2Idmxx$0xR@Nx8bJN8h)9%V~`Rewug=~ z9GPt3FlTb)l`t^0Q_vUDhKa@#Sjn2|&A;BSWMWJ!p1uQGP%j!N@L+}ZO)xcgr~{$^ zWZdK@f>afAlRZX*IQF)sHvsR@S07+bMI5I^MY1oIqOAG!f!QxFR6|L%u2UY&uRQ`k zC1$jqPUJd1cpMM|{wO(*7gWFNiS|r~3Hy$_ah^5wh^KPEIDeFL{HOFAy#A!9s)re& zZ-vup4Q<|YUE`l6Hj^*k#$r#%DDX)jD46ZQ_?Zf0WFZv5)FvI%#2AU#s0>Afi<{i8V zf2Ha12uBz*?z&YUlkOpdn0yYs_Rteu$L-5At)Jcj(+5gNP6Wo1Iknod2jWH&b}&Ew zBDr@70&b}^LYPa?{Oc0`;SoBJq&FaRaHYPgCb2aB-5c`&D{`xiqUD4SoLGFVdc<;8 z#wfub=i3AlAkC*!*S%~CUyJ(kR@fipwps6LJFlX&lz1b0LZwT}FQ9D<7z7P$HOoKPXH~s;z)$tGtB|V?WG)l;gEC5+i(w=cf66U4E`m)_D8} z{z@;eN&<)T((e!wS5B=RY)O`nLt1R>#xYeyB6LUJlTE3HTa7d_-kqzoKm6(bx&DUe zXjyD&_4Q@FtEh!lOSC;fRZtKP6emYEGF)MPlE>PL@F|X(vJc)gngfsO_Iqs#u!|v* zU?KJhy}K37ZyZjgkFm@#fyTqDRS&5Z*Ks7CyfHaG>tk3W?AZ2#T2-xoiIxxAmtncj@cD+ z-U*7aC(-cOrk0K*n3aYFys11FkXyAnZgdNvY$7=m%8x4?t*2tNP&u>vV_35;tQTS+ z_DIgQ5Lsd%C_m=MZev(Nvh6k@7vxb0!#+mkcDT9RqD@Z57Vb`}W#`pztxGV0;Imjk zreF87h+0fHitIXl#axP>0reu&b}G{Md-TdnI6Sm;kdTscboeze?zsez=-fxm>5hM2 z^+l`e6D8MLFiE-oe2Gnxbr8Fn=?_lbsZzfI4-A%ueI`hJ_&%@RycXF$P0w(9J>vn` zhyHe%tuK`47m;vq=RnijDTlJgG+T?3kTRb|7V%tEUzVUhuP5GZtF>%mXTQB%`E{_z zCl{w4=TcD3e;&X`Igvt6ev-3m$~ICp_zCJbHdtNyWqi3+5+^^(zLP z)*KQkcX%~y=@k+U5>1T?n@2Qr^0qC1D%hdlEG#FRgopo$y8>oCGd=#C&{6)KZJE`RU#zY?1?MdX;l8L z@w_C%YsS~@3|Eobr?2=T#i(whiN?K#8p#>Bt+T$wKJVf%0zt_?V=Qss`@~d5XxSF* zib*YO;rkyx;q+N9t>QI1i(5c+unA^}-t6pS zN50S5YBZQpOtD>r1lFlx)l8WxqolMf^h7m>RZh-$kkjn*Ph6EhY)GC~ z`dg;r*ESsJmoJ#RxVwI8nElf5rFnMa`u+7{YZj9%xy()b&2!(w(BBKs4vhc&UOYV+ zam15SlbfbyJlzg?x1+$TZUUYz5|lMjROQu;w=ML{sDGJm(?IN7SdvcWTO_C%zgC#Z z9qRAVoV+XOG5GLw{OveaW5Av6L8+}tI43*5zkJ4?7eT=I5hZp>! zlFe3%8JjE?-_iA{u)QfVDmsqyDEm2kQlqfO-LlPknkOIsNhhc9*l->!V7`^JS6pyO zEDSvK*z0gVG_SK~V*M)WTL0T?ArPY4NQ2Uib zr`BP0>Gq+~rHyj?Z-QKl`>U=S8~!#QcI7FA2Xm-4&egfvzxP20cTG1&e-}2YuN}5H zb1ZRHEan}*_4v^b9nQ(jK3r`PY#&gcyR=9;^qtmdw_HP?_oJx>e`(a!ni@p&78eVNON;t$tV?e~_?1>*vF^oT$Wvln6!g6(n=5#EC8 zE_>9)*0sx5UOcVr-#p|9y^D{L)hv=@uU5O1$ft*XM6Osp%WtD67U({H7&EC&2J+y` zjJHwnOCwOhy6thU<#|w#x7t~BW)5$naZYQwhFSGJG$$T;%O}rLQty8}Zgp6mQ z(+G3~N3hDPf4CI7RNEJ#@@_SqZ`oWX^|`!xUcg8b_p9y$Hz_#r+pmf`g;9dK^C?MY z8mUe>L6o|fYep~W4jpyf8c7UBp!eVd>@di3bM6G25GvMX_pa-kwMnZ$NoLZg}&DUM=|9fkf{QbPTc`d0sLdT2AQ z(ZZsGzOZpQYb6By=BGIkPJa>+p+a|!Uh2=UmHinnt!A8(WWy(BZHiNVG9_bO$>5|! ziAd_^6&W$+HuG?jqrK2tROz9O`_UGgeV)?ZhT1i#(nD6VL`h~e5lPUOa&F|JYe8a+ z#GB0+S7I4xoS-CW-iTZ8k#WrDAIVLPK~bE|qmR2xuqf{gmPIufDGhKl+y_?)lBS)y z0Jx5W)Ha`?y=gFW!#|_)jT!N5y$zk+tu(_yafwAinST>uZEKw0hU4|q6^mkZfuMuC z{s;Q*%J&_=eqofn_A6xnGG+;m?98z5H8MPm4xrC|b_UH>l>1ANkmU+S!f3%5!GX}b zB-PLd@z!pSz?8q8ZgTsd)dme~~G{TT~pqX0e(7Iu!ZQ>ykEfdn6f+Gh zcd(H|JaEdty8UIWDpo}GtU8nw#1O>iQ;Yb2yyAetT6($ScT6cEC zKt>>6b3T|x@CRddV&T4vWTb;Z;NYt)mOP;?Mj`xC3yXRe*B7;&JepbrlU3vvjl)czT`;**5YOIGCfC?mXr7N;VNv9lRQ zPz{OJnX^M4v|;ID3xCEKn#l|8COYzndStPSC&l8W7DglP{I8T=rtMfKVaq7bUw1!8 zHY9}R)Y#S@>lUk7i|vOYm;cdtf1a`PjZZ$ll#QksgqJ3fj%?-7;cDq~AdodDf^t)S zQ6H-TCd>c)f1gb<$vTlFxT*0p?2;)`Xtef(e^8Ycx zxtcy$|Ct?KsC~IpxftE-lL&K`uqZTW+>HG7HCWsDv5#UL%T|4M)$Fz}wb>S^S?2@U zw(;A5Nqw;e^{=~r5Mu7hVKH+J$d(`XLe;dSj+y$`t+Pbzc8!>Mwm$hYoVU+q# zhra4KkF#R6`uTnamC&3d#VdLq%;9(U@Q(_3)AAH+Dp-3Hvi0Dt)C$D^4yPl+RjR6q zq}0_K_17E-j|<=&TC0)-E<$iqRWe5iFH z{oKp%tR)B7{qM|3hlh~uYuh&kjgth>w{ZhR{Jv0&Hbx7VQyKP&`ENu_h?2~)`voT~ ze%VGX8C{8@67jjzeY;5P5pPhYTRqB@3+DX1R0vu~8K~aH9bK7vaEbd{3uW1lXPoVTTQ3P-+7z#_VH+<7P*P)l zy+&^2J7*sDlOx?0D_(oS`t*Ckt=-GAMsQhTsdY`5R67@wVRNNRn(v3l)WS}w!wg_= zd3x^eM|RJ|dr2Pkq$fMC-aWlKW{G*oeJH?D*;H$N`4M&WmT><1T|(Wc-S}#S;xO-F zW9V>tbNXuf4Jr97)>844GlSDtXWyP4O9 zY_Kb(m`|D6k4s#W=KW45FnW;q*}sDq%*nPEHiT2XpvfijDYGK|;Js<>+$YttM+8aT z@9jM;DY2uOTydKlsWvXf#Ae<;|HvY%P+n=S@m1ISW?`-%skf~e(ajP~s&qf<%b+mr74IqAdHbX8TLf6J zyZ`4*0E;oH{dm$J!z_$XCLWvzMC!X;M&()L+VApuP@FM@Ydsxo-dl1p$u6>&OzjAF z*pVo|xS~wo^G5AaR%DA`v9CUK))==*Km4(m*7z-B{KdHe+J-zzuFO5r6Pe*Jddg9ciILY1x3amJmlFcexVzz_4MVw&=+&sZ>#M z`{rln@5%Za;Mfl8ki61H>$VfJC)Ad7HF(s<9Lht>6ftIf8zb2xpEVZ`0~`+mc8EOm zQhW2XP`arqkCqG=_Y9XF?xOjeODWF|C1&Xr%SupKmW*^{T~=a6>(Lngz+ZF&e@h)S z%wMl9b)FDCyL)r`V~MvNV<*{TGuiehozGe7jpsRw&9Rbrt^l#i>7_SWb_!~gV2Lu$ z7wTz?a5+UK859S~jqi}I7Yum;T9*NU&+FSZ9>EhOU}s96@b4_|)n80s`qA@-CJk+C zeZIxljw|%i3oX^REZ(tEUdpB60#i_*hcxM1N*hM>LpNx*x5NV%w*PYwrnepDsK6xYJU3MdLc(W)Mod zP<`il2ykuF*wOJ9-j|jna6O-hWc_X9g}adwvxryLT-xZuo#e)rmwH`N0nS$NpQ1My z*s@a$nhg~wPQ#TwFVdi#dm0iw>mB%!k@kUNz2EeO#*qA}k^qa}RBu_1#)*)r0Tgr0$G$!Xpiz0Ok~QPQ7^S(N|H1_Ai3v@yp8h_&skvz#>Mqw#%{w= zki?1+r&4+Rf~IfI&7!H3hRXUG)|gL;yZYLE56XxCxSRPjt)Y*rp+U8`lPqk&q9wj^8FhA8^ zYa7~?i;Iq*te!S1DFkUh1k3C&i>fNUU^1>Ok_y9M=>1uifnG8gRCa-ELbF-wxTsN)^D}Fx~8rXU)YA>o(+kUL|L~KYI@;C^-aED(x_X@aoTOuX)}|?WGC13G1!*- zk!xug&9wS(iW5nfr;obN-=BrSTotB=Ge zt#_`X%9V&{1wRpm1?W(daXfCMdKZkBq|iwlo~+6CthH}+%|#+1+_Ii*9+kegw)H1F zM|N46Pp+q2hJpTq{3!UX9^HAFhR^WOC-RIXd6p|N{j}rq{rp)2Ulp=S<*cO{Tj3$e zmWCX)n) zPG0!%f)kwYp7oPd;~d%!D*sBZE}`E()M!{d_l{7^eXE$)KTraWct?jPsVsO#Y_;tC zq?6xy1Ulu3|08PBbsNgV&tdPnei6kUNd2aI>X1`;=aDifuloDM9-HJ0d+ysAlXoA# zn>zPmxqGq~E+I><-@2YDrqR&iDMZgM%%%lz>7g}V|I#rXwo9fp-O15IT|XTk>(f2h znx_GYIH{X3QQ)_LXR=6D$Uka0&5%_e(rVV7Dr_1P=yN^dD6F3}DXn`Uq@>ms)0N)h zw7xapM(pMtFzx)z$(Afyr`-g#h47YWqhpKbN?3pD*$fa{0vXARD-=WG21DzZS7=!C zHLih7Ge4`Pd=^^agAe(fnoJGnALruvoZB=!Su6L89|ID6!Fs#unZ0qd0Z32(02$-F z<5@R#K~86eB+)M*Y(*XJ6x;S{#GEdw)B57X`5_Mz`F1G#L%bp2h=FdiZgP%qvl zyXe5`@!_{1H88KM*07cLtI|-j zv0DL_LW2)$q6KfGov|rGC+8q?o)NSi3Bu22ao-iNHiFYtmKn1Tw+d!tXMd3VWPSEP zcy(E_$O&pPDg8%!F7Dm$+5U?>`HoWV+ed8m{s~`CA*}BQXqG8ySAO%}em#j}c9mtV z6^d%B|0MrpJ&{YS?$Bj+;-p@nkiFKBL;CD_S?g{cfYnp?_4v?}AZFIDxJuvY-v>V$ z;?{mCgYvx>%syOQ6jSCy&4e006#hh&1rV8$Me|fh3%lL6AanYpq~Uv*VvfZazyQ#9 zMU8eL(l`@B+3&B{f37!%qc@CIl0xN7fMoP72OZ62(hjvcK6Uy;q1{WOKQK0^5^LK~;+ zdVglIqipp?HVa`@cG{5_8J?_P_PT@VwbQ%CiCAJ(-#-;9wj6j)Rv**R)jFjBVvJ!R z=C`~q5?}^^)y_0 zXeW@K@wOCqTtt++<@4G_EOwf2U0P6x0*kl9CX*ugBnq2j(++wf_v;Kx9x0mfTC3GE z+NP+oC5u#M%V&Dx%MgjzliOV(ZvAiZYLutp0WX=7T78a9>s#Qme0i zrkLqAEAq{)#5 zA-U-wB1mTr>{!jxEzlTox*2wUO%&>fjNAb~7!A81@b~;P?9BMzbKv)f0<_xXDLY_fvV~6v@h{GC5AvAfgD@;o`$cRf35X7~fzgOKPB=aLXweA*kN%P5gN#=T-cd?%lEKLI)Dern~2tbPI99{?ZRl;61+Q z!|u>Tupy5kkYP7^Xdqr3$fEGgSeT01kg5XMT@w^eWK@Iz_b{kA7joQxyo&K6%fVlv zuvSaNDRj8)HH%?a*Z75z|3ALoGOVuPS08N)6nA%bcZcHcP~6?!DZ9A4Y`nO;ZQR`* ziWPT<7KcmA?|;s@=gRZ&h1n}BnM~FsYmzss9O?g5GqD5T#y$go6}-XicqPGT-!J}m z(-s-9ca#V4AK#6_H@>P0U_I}WVJm?d{JjatM+t2FowuwinEC$;+<*@LyzDxHH2C~ZWRzcsZ2S)k- zj{u?nALxjoo6g?N?Mgqw(X8e%%TUOz}Y);VfJV)Tply8nC2VlCnPHLKDfJ5-8s#ry7c<9rn9Wcp&tU z)L)f1AGs-+J&dvPcBe0Tkz|F-jI3X7zg_j7I9oq{x?g0p|KJm9pMW*^TWJ-mzetLX z_(va$fQ%6Yr^ID5p8a7{W*-&|PdGrT+(>Q^lc!@&X~r=`sBBeB-&8UQbdSJ^e`c3D zKLuK=;OmX^-~UYP-Ya)j4=!T#)9utDh&XxJ4yAr$-qN+!rZsoQYAz7)V-b%yz!Srr zquGAcv%WZJ^5nZ=g^eft-#jK^-Gs=?+@)Shv@UCU$6Lu)&VEEovu>5<9w2k;R9J2N zJbt(m77*o-)hV+>0TNx(t+@DXnlfR6s2sPEOrA#Et6FI6CT1bfq&{L!m=K?A&nkO^ zE9uF;vvBRY9yvrwr+*PE(B_BrY~^9#Ee&nIY-X89^Mt>Ga(_acR$xxnJQt@$=B$pT z6sC;;_#M7x;sj7rsUEAAsY)Se1sa+kQiD zAPjK|FvKseEZo(sp9!{*d#3?FPOFt37*`+T4dyJSRgc9AV=lKY(}SV=Ut;c5RbSQ% z422&R^FTIO3~l{U`DNcW*iD*z?_OLIFioc8U$UaG@+y@YvY>x5Mk8>*mVUVW7Qs7L zXvVVJy{+_Yq_yQ9pa@`lfEvGT$a;y#PEadIE=FPP#kYczN`aQnORM@7|2XJCe{C{U zfq4*7gy6{L{RM4rE(bvsrP4!5nO^VgPeBKs?sTsVSBTWZd=7m6K6qhl?j_bUUeweN z@n5QTH(3Sf3F6|$Pge>Cf;2#GFUq+sw&>ZF11^IsVZV-Fy~6;gHY`8mlDPJd2Vn#j zHiTXqWsn3M`yM9!wXV26zuN!uH{NTn|tbsCjNk-=}C68ehYr5O(T zB3dP?Jr`CZi9x-=we)Kz_i*Tt$yI0m@?(nptBek58Q)ty*}aDC9p&r4&CGd{-)#IX zp=JT~zkJ^RMD~B60lv0*GJ3h@JD|F?mIIs?IS-*?S7 zydN*WR)AnP z?-(ct3~+f6Q7%}}`^Eo2j{l|-TJz>_FY>{Z-d*Tl^wJa*I1&g@3^x5={{1g6SfF6l z+phi84g&+qs{}K7m;Y~zp}#s|1gSTGu_M0E^Zy9<2JVV^=l6Faf9a8d)nQ;TprJvq zKkrlO9s{F!cU%9ZN|4|bnEyN3n-sIy=*?=IpW9PEu56Odyb?D8Kko7Xpl*uRM& zHNsGVq87jc{)?j!FuCy!VQK#JKThahVes4JP(kGDV3zN?e&fLR&i}7U8+XC7{*i;; z1onN0VE#>_g1%gU>AYX|SIvL50b0HW!+9V2ZyB=d-}0{tw_xvBKHxv4vps<2{x^p= zQexm61`ZO$`~pVtF6#f87C!JVeM2qy#s2_z#}0xKfwrN*C*Bw1%{B~x1Z{5t7zrZy zpLhCyY%Ed0!QWN-_V2(u!#8{31-_XNGElMV4YC@611<`V{O0!mZ+5K-G^_-p2c9E< zlfN%6A0as8JN>@^#()nTKYec}I^+Y|{nEU1qfJoLXO2>d$+MC^jW z0KT=v+4%28`g@|4}`3 zz1gUn5IE&OHlS=#@P~JU`1eHp6Jkjm-2Gi^|JKVv9=!EkISTr3Pw2nf>vzLIfgF{< zVc&26w(|e^;Y}!X(4-nT(fgzFFIq*?2j}}MJLrwR{+-#sIPYzo2&mBaCXbRK_{O`! z+04Kt|Am5}HyiH!rykH-pg8$otHTMb?tAMDKsN9O{wd9y8#cNn7 zc*A&PQke5gle+fFB6~s>FtygYpXEK_-T`nvYc2!$UM#wQJn7+OcG+;oiNyMZW#3g4 zn?y_*Ky8hRLt&iak1m2%WXd%Qg?o>M4#QmEWx1hf(|7HFtK?59qN!Rac3+OgQ?%J8 zDe&t%<3|IsFMD`|l8@X%CqciA(%8@pC!3E-F9?8^ zCY`mN)~saSH6))3bCfU4WRHfvfSAcq7^V8N?qtp|FXgKRRt*Jgu)14`yUn4M{n~Mj zaccLC;=*td-{Q~+V-hQ$agW?umy40RLVvrf_A^r!BcHDfmYCV8Y9;c&Xla~&oOp!%b zA!pyMUs+D5zva$znJ&qek=_FgBfv^eZzPgcn<9pW5=R@IW5LpdZv&!8 zxz$}(k8VQYK5B}#s?yS8&9`xYTW8f~2%oa%3LA_m!C~=!q(`M%iU}Osv4kOR(6Fl5 z2tFp;Q>ZJ~R^nRueKC1MyW?9jZj=L`_p-JpSd4^#Q(8qfhVEo>I*b9In#|P(n1lI< z?)H2hws;X9t$vxN@lAJ-Txa3J7+(5N~mEU+dWGw}_7XvBzw# z$q+7Hq_c0+0AI=X3Fa7(oQ1bAk!#RZW4m0J@d;Non7$9cI!ak;S!5Ewz_brQS7>To zAi~MGufKW!dL2Yuk=LW#?WC;S^t>AeP3(*^Rx?(9dWxl$HF6_8{3nToC5>~H6=f43RA{|?HBlFrUP%jW z*1LZzG&*HvxjRv;MjMn&u6W#%uXEA6$E{Gh2B13?Wk(YlsI0b4V*0AKSR!Dx>j@L- zGY$CwKAEOc?Fn#Zv0XByZtza-Dg~=D%nJRCKYi9-7u;$kG}6F*;sGY#VUeEYL*-h2 zcCmeTasKu2h>Qndgcf1ur#>4Uodo~QG^WapQYui5Cy`Ow?Yx|xA;h1d%TijFMIp9JqKxT>=9~qsw8t{wUz5Vm$VSL0~zvV5ZDhqXfnw!nI(gIMaqwB9PF6GzwY%U$V zF-w~}OBde5)=;kFIW^WFy^tu~Rx`xPDeJe*NNzy1H|T?4k+4tX&*s$(XZ)fyK%B?W zmXNW+Z)g<&f!Z^z)iti~0ev;mw9i=C<3iHsA{!X+!EAUl7sbJYY#Mc5O;E&*0 zM0YU zm}|4D`%<77H!8m~Mw5}ec74`dNeOlExQDkXE+!KOpktWX>*j5o$%Im*UN`_1!WUdw zbMEu&Hz1P36Gz2fHHl3T)42X1pOcd9erQ05kM*5l>9Y8IiHC6cW5U=0>yVA*32;NE z*JJpjUO+t4j3(rcps)?TcNKkeSr2cMPV_2zgDUH4II|>;a#j6lYmO`5Y*u^k$e z>!#aNt$gNAj~J(G71y9eJuz|(9`bf=vYZg4ND{Op*&j{h5cIiM1j%&#|bOg*;v|{{%ElQk$tQ2Pod^2|RB3$_1#XGX zTfzD5%<)e+)KI5~!^0vX(yY0+UwXOPq44GDhbdVtokLVE@@6eqIyhK-iRf<(%EfWyUF%w3KdArO8&SkowlPvt{|SVWiD)~u0i+wi>z;4_x^q!hkLtC|E{ z@6bII`6qG0{3IveJhzNxu~D$e>b8Vy5W5szg8ap)mKEW1&O|dtRb^78&%=Cb_q;`NgRpq z$zm!uU<@%PvF5f00(1ienfhzp7wI>%C3U*l%W3YA2$@&;jJ6zWRNe4aetY;%Z&seA zx;gGq2Jq&JoMGB1xSl8vlbr{A+J+WU;Ex-wwVZAVJ!B3pO!HP#LoeUdh6tE?3N}1A z36%CI-Qe|M>;h!7b0fy|BtzYOP^S(gw{y_CX~PM`J}5q}1hiLp;p&Vx|G{vd*PUHa z%tk@Gl7>!d=|A-gKCL9 zeqcFeGiI1=BnMI3vgrzfP>(qPAKlwz-34sq8 z-0#Y{L?V|sA@m`^o+qpE2F3L01(UsUR(k`6pI&%Qnf;!hN9kY%N?fc^**jBQVfu?; zHhITvkd|yiv1(JFGbP${d=kjg&1?^5KxmycODt+jPX!Y z`q<8h#cb1-NwxM7X0u>t?8r|i55llWj#qA4MVflqe!>0WR7Mdei;RzG#e$7L{5?yy z-r{h`&F~r=%`dn{%2TQI*sL&pXZ?4NdgOW4Mpp&@@WCJEe{E9lr|WNT&)ENYabUg$ zLj|a~*gF}T|K|;%UmMydalYa8he^CW%e@B50+Hk#6fvwL7`Y+4jSxyKI8ynD2}@U( z3B{P~hl+)kb8TDO>4k>mg@RAQpTzsah1Zs?D!gqQ78W}eL7nOqVxF(gox@`$#L(Rj zch60~bSL<40h8Rf8P4Zj5&C?WY~KMYJ23<{1C$s7zlq>~m>7b+m~FBmvSbYWYT&Zi zbVjVb2;xAfL9!s$%GZoP>n3uvB<<}bVb!K{G?z*Cs3q67q5rJG!h}m|5=$PyKa_w+ z@8oI{I)Qr>XUMV|kIdtm4{1z3Nt(BVB_rrw;Y)CMW$ zjv};b45*-x#&AUGR z4u0fiwSBa|2|W6XOyISy&1*DH7kd z{$f$ivWgcD9j;Qp3mf7y9v3QzMteQb6ZKL5C*8FYt0RSZut@ipP@|-z)jciyV&a3U z?UBtr4NTS(s-ydo`VmVjBD7FB%vJp*O8DOjn9s;fs7*A~BJ4hP z9fi#`>x3mL+^|jn%;0b98!Yx@*{2>ww2d5J8YX#3P%GJ^RBx6=3Y0X?=ApGmSmq+y zYa0Ch@N;HD#|;|Xof|rp8sxo7qz@N5mb=6P4=y2|*F%4DDY)jGG57A*%b)KLZ)@Ga zi>REGgs=CPnQJn&pkd7bs#bp#OJFPFUOz0L1_;>> zQ^;^=3oivM+!^CVuuE7Q$9=JYhfw6|_QSskUP$C_--#_W_b_5yYRI#h5GUv&*V|6w zgAaqhDNo6Ao9G9FaG{jatB>fWuGPf~J&Kc!7?{g${b=96=AT_UTmNEZ>;)l`MUJU? z$X@n_3LVxVnRMs(gZimU@ z#TN@#u9>CEEfb?9dtC;NsnNbBniZf+&k+iso*T;Kn_h0Y35ti)lZVfw&8j09 zUa-2oAMpnr>D3i)O8i%_2(f+)v!cB*NzDdxQMk9TL3^IdQzK4Q&Y#}LP z_u|ywa6V(#tZ{?54n8~JobP%i_7qe2I?^hr3s_dwXYxJ0oC{7T73icm^EXCJe^O8l zsB^?UL*5}%sk&c3&@#*8VCd(BKz;BDs&fRht9L}T+xxvE)0~NxA?tDlj|GTxr6+)wq(=W=I&NZ|+H^-KC+}YSf`$Tym<=85CI> z2SjxzAG=y8ch)j#+bQmGZV_nP8TrXvz{}V*?qW1I^g7j%AfE@}^x8JdF{sb|s@-wZ z?x6pL9C>iUIbnkUK29pqvcKh{eHMGZe8taaG>XC3Q{N;}&!196?>?d{TWovZ@zj;v zx{HH@uRe7wDu4)4w&!J-oG)csP5L9s8?Y;&eFlW2yU(FV9EmwliJ=jIIeIAbx|-pt zDl}uzHeYazz17^KJyt8#roXSD7mN$B@l1j6rfQHtcj{LX{kVIrqD{bSBoHwcAPFh3 zV&j942`kP3Ux66J8FZB-*71<{G%s|XcI6|}xKb-Z05U0}Z_V!?m|e2o23i?c0BTnT zCNIkAy?3B-870%B)aO3^NGziBTpp;JgZeS(i(56Kr=1n|<0semAJ(Uu58MGHf=77r zG=h40(YM3Sr3a9zzb9GgmTC#@mz}=z-)RhOKr~>i7*1h@ilZMyG`5j3vL257;ECeX zi^KSQCZ>J=j4x!5L_RXojcGbEk^Bkzja@>3zlnL@oEdiBBHMog3mtIcWeGu z5;0ylzjNT`I<&=@uQCpvRBbpumTtXeadEXK-ISY%80(7Wd_p)`Cghfm-T~kT{Iu*u z#qRdcBKaN1muDHxonPI$*!~gU|1)Bgey?~1g)8@I6}MTUE;{}Yd=zhN?7;=X4)wYI zf|h;#Kqt2u!OGVwz98AWZR5B+x^L&kjkROAHVa^Qp%YjA9{RKDDcQept?XEub&WMz zqxqj^59v5k?rpC=8##N%l|lpTwa}DxSEqm6E}Q`n4<5bG0|_LP2HNPWZh2WIX=URK zO}Vi06zD0rV~ zG__l)h){p_RW`KuGz1p4%WAbsIfS6Qv43PoX2%Zkwsc41?~>QgGr|Kbj+O^DXp&X2 z+UwPimv-RWp=NPYmV!1cNM2Nu5Gd6Rlg@HVi%#12`Lbz#o5Rj;QA@leD>yvNDPg*K z^&&&+yb`K3734&>9*#M$s9(*vq!4m=VOXo}CzcxRN~0I!hf>rL3GI}v34Ib_2|%fT z0Z3mKlgP+l8T6lP`JVtZJl@W1TSu`5NT=5xg$(rpZKf?}F`{f^F2^iTB<)gw5Z=7> zX@M4hyp~UEnV2RJc;Ps{*acH)mK{_t(o%~6@5N$hCAkLB0L^Y`OC`f3@`G@FE+;V z6Bq*bm&UkD)_2bzC`RehQs>-q=o;@m`m0G93JYYr27+ ziI1VG>e!UY5QEhAyM8RUcGCc4oSjOPWThOeIOwylpZd(NFwdn!Bf^(_-MoRqd3efq zRgJNz{7f3@_)S{|P!&G@J6PU9=?C(kBs*b!XiF3J67j|xrs0k0QVU68v64yu_Q#gq zPT^xiTlcwHW!>s~NM3&F>`}oP>|fr0bgS9W6hRw4<;&bj5msi4FkV_ky{j;CbeK1( zT0sk-GI#TwZ7!QGvkwH3GeG18@k3Ue&gO^j(A}oxMVn5IfU2&f!dDs{+m5%5^ST{j zG5s9AX?kisFkS8uV|C(2=-A)HAD6HN4Te_hpDSg>$ucSE9(&lN+?@S|ACKf}9QV^!w-S5M7$?dvi^ZIb65 zay|txB|N4E0cyBV`-{7!GyJ&%6;&+G~-6K$}`kki%HX4>r)MVw{)6J}Y zZTrs3*}|TY0V>vl&PjXce7m$L_E)34GuowbN?cE#a?xRb&@V$T62Kv!bSnzE4imtk zUZr-8ICQgO)j*Q|PFGi1_E&N@`XD&y;81UHoU<5l3$P6;T2*UfoNNGQoZw^iD*Um- zYxUusZf3&0ZkpjclkTiUlPWhk)`9Ex#EQE9>Hd+t8b)QKAMTC{_<5v%a(Z4I<-U&( znY#(|wuS@Tw22Tg<@pC8a02muaStK0hS1T5);pMV+ZnFr!WdSN`UTFij-Kx(HEyui zgNz|Y0WicIQSYrrV2jn-6DK9$EQ6=6TZ=mg+62cD5;_Z`#~SR;z=$-jfCyq&KFejI zV1hE;F@5VRw~&Ucr{rm-X9gQSTERy@VHb*P}I46c;idaQA=TRJJsBV!TUzo1pNw{WqHa3y&N!( zJco@c@Yp*KAKW$^Nw|`Nn^9Y-W`_AP=reLYH-i%5@g5eM;yFmJky1^ymTcJ?276F3 zmI1{q8%T29Ypz{A3FTkt_*_Gu5ESCXVg8r`!u>_SllG8~`a+r>W<~e7zKb}di@10@ z!pdiIcyY?F_}6mCYAMOqAX@aAG=yh-&6t?%+T!OTg}+=*WV6fn!7Wm?5fZ{JdE@)3p4wInNJap zWlLy?Ayx;!RHk;Ead@@(t_%%4c{`;VS_I0n z`g{o3Wc%|#1Il~mzRM5$Q}ATSI}}KN|F+26Y>-Uc#bdXLJRmwlNueB6Pg#nJxiX1PfxaOwEPCUNmbucNV*eloPle{aCcIM31zmFaBhw^(Ar3GZ4 zqPUxDI8ACTmj*;~#b@5c=tI_rU)wjFsSHaqkKq-CxHgQ{K5<-nB#pqZ)1((8=S{V? z~-RF_vXRd zp_^wk?K3NBE$;vqOUFDf+WBNi-VeSq1Zy8P;y);^V0VR>+(Lf33k7@-I0O6si~Pe= z2kLi3$!kOJEq`TSS9veH%(qkj$uTzFSEJ;b%*{I%VI;!-BD)2Rq4U5$)pd!Ndnxo| zMxIx*+SQSsnP0W5+aUItkjQlt>$(6b5W&2%D32-M1!v6q3|slIj* zZvWV#dJ#@nyVf#o>m~t=CnIz%`=PI7G^jI7j^>v%Z0Iz!Vffg>QWOM^T!(T4I42gt z4?khRtx=|qH~?p@KOL0vl{s@g1nw5G&2}oS0*ZzS`=IYK_V-{jDC3P9%TRE&lN6^) z1AkVAr&r+aV`&eQ0WI%P;g^_K{Jh@q*$_R>t0j69Nhpf>Jzi*3;phABhL6; zT^t_jCttqN`bd3jk{+nf7MdN!xC0qTOX&^Bm)mvY?8Q^pYOHj;exOAdw<~wP5={~k zr^IIZ=oEnqMiz1ZjUz!fUGwCNY=V#mX)`s81~9g}B@k}m@VrX=^YMlK82QX_4Z*8C zc9X@YI;w!%;-7&4IB^{8ISn?PZopRO7K=yPoEO|44mMp01NV0!Ta8X(H=?ERfj}sa zd||IgDEsb%M10PE_Y<3H=MiF3gJDS%83OSkW-Qt~B_d-*xeg>xu)^}?kgBt6nL^R((u(z$Oe^ez z<%B@DXhvIBwuDu_sFh20Gi=4>NH=Q#b?MugGo;>e5LrwwX67Pp<}&bOhwI3B!Y-lU z>zpr=+2qe}lb%0rxz8&oJD*eRlgQPVXK5p>!KbjOX;#ad|#EXDqC;TdoS=%4k2wjSmh5I)gwCHABqTZpwdXI z*|al22SVHHYQkf}xj9{-GTZwBdXzh%ZY(ZCkIh+Z@iL>$c=vOa%4%VDHw$GX!`Zoe zCUl(vlFhZ?cjzb`_enf{qxrBTp(2&Z7;G{gJmGU@nJmLs_ZvqvBy&|h zr*xg1v$9JvyXyQ<;Z(9%KMH58;?-2KpD&QJPq#)e4)0NE88hRE9CErLChQT-s4|t3 zWlrh+`8e~jale8nC;?3V=QysnY#coxf;R3Kkqi5(-y^$=K|t_I++n0eqO`$=${>8!cHH zbbAXARQnPS?Ot9F^?q-h^+8A+fj$YBF{{qc?0nxv#s=qmqVPK&+2Q;&tM^&}Ov(JS zrGeFDCr4S-KEG>%6)sb`etE+G*w%wc$517LGx<#fDIw322*bfZ7KcVxK>{D$Jb~T$ z9e0oC$ZMLcVNf&dzV;ZWk&?%~%2G3T?-e!5N@+3J33S(a1G1uL_Ho^HxLmDM{PgMQ zGjY2gO+uut+>pts8|~0KmIRa&pq5xE#Mhn*{5&AZ$VS?IQX6#qBB1`vFM@{5vYD1= ztk*fgP$TfHo_*iF?Nwv3q3 z?VCtIDNYRP$eVeg(fa(y$(8VE3J^ts*F}F|h~<4) z84y3qGR&;)Xorz`K$5=ZWB=uZldWWFw!rATDs-Ni>@jrPnO}@n*ga{oj^Y7AKfj7q zy_yOJ`BCyD0w&!QiSvkb=Z2`4Uv^;dV0W7sql|{`tDzujkGatK!py}-lw0^1$2td- zGNz|Gwz0M}c3+4HQa~&=R_)r@ke?uiDw@S3IEGX*6=m> zgY{2iuaXI*KP#B!ouagw%!}1i6(4u|=pTo#8 zZ9+q(kpjUTX30M$e+jTXHF1xJvvKR0u;I$D&{{E4I29_ZPtrr=I6v^iZ{ zURl5W_N(z>oikyaBHg&|?lFnbXVK>z@Vb6mdH!t__0{`M=|j|AmA{$?C?@UJ!5QRF zkr8{T2cdU%u0*u`?P*sJ`<7ZUWB7SBBk|G~LhSB?0zvGh5H{k-U93Ngk4872PvD{g zx&giX^+9B09Sp{nr~tv1Z!9+=^IJoPNouU_-Zn?oZi!2%S^l(`81(GK$HA*hz@?9m za5v>;$tNG2eb@^wf!=X*>@8?J0btDJ^&duuYcacyzHy+bPf&L!&9=237obgmFRY4I zBg%g0t}xPG>N_|)QpDlqUa^^G3pp{P)t{RKN9cy`>Yw|dZLO5od(=kc-x5&_oeTDN z4S}{Z0&f;$8?R{cGcB|F6SH;<0MSATT8&i^M7EV?o5Gw+3!C1@KUSA*_>)IUm?aVX z)(R|5q;Ir-8k5D#sE?#l#v=2^^Hfw1d3>QIZ_kgcvzR&i5p3KtBW(e>+9s|~JjH-9 z9V$;VI5iNeJ|x3dv}USyyotb)m}6yY?>qAJHnG;U7nJcRT<*=RS*OS`0MU9=Z;dbw zA+)KO?)Oq{;5>FFv!pEz-ezK<46WffokeA$pUX#V<~1{v%8`Z7C@|g)8f2yCY!5E1 zhDF@#qP#l57GUW00uFPp9_41qQREmf6Q@pnkuEWs5In7Ur5yIPF%9pQ;*t|rsht|B z_$N6r=x1as039+aWj*--a6Gfk+fYeWk@$JM8e^@XA3m!!qS>uV$g$p6etpOZR!G?< z^Vca1xuF_|L{1m_@d{fju(q!+K`A>TM!O+E`*#Ni4^d6rC+g8Q3>JjhFc^_WjCGLO zF(zgH4#oCLVz=m@=jh0DW#NQujrKji*db9Ar&3M(ApREo&;#ZbzyZgS!oK<`5(q=$aE9Xz78n8q}p z`XBMDcohSCq>4_cPII&qhJQO#9M2-GBXOWI zuSjtUjZ=8@Ku+-RvG5IYRqarR!$)K;!0Ygnun==c zf`kX1s||IUlIT|dz+y5~_~IE)4Z(5fF@a4aS@$Y|{wNBYkq|80D<>#sHGYGT$?NIT zLm#(LLjF<~4nX=d zB}nPtp0;MD)#N*H!t^G)e1;9uFn>l!lURH!wm9!9B7c?K@xKsl$I!e43YOi$asv9J zXkLQTL#6R5XLg^g+Qj>hFTBw02TobjD8L4lF5RC*dJ5_U!Azf2`a3TP7q(|n7EDdv zn&c%fkpfOwrYp;8sV~Xt#y9T2^e?YQ7?WF9ZV^J85mqwSeePKeT8Hq2-7!Q%BmopP`XZ4xj1q<@KO#BF=USm%^R&lg*RzIT z3THOwQ61|{Y#WXt!W(xJjT}$rRc9spkTwm0Z4qZu7&=wUJeid%uf*smi!)aa81Zzm zoJjC*<-?>$XJxm(5+Q6O+4V--5!5Ve z3%8Y!xT-4T=@8E+nLAukVZ?7?CNU87Tw6H@D|Yi+T0sG)ETAR{g~`WZCk>!okGOSO z9_y9c76BN(`<6G8uKltD>g!bS2|2urQA z^5mEe7W3Z_+JigV;xlv8oikQeNBGn7W|c&{O?clz!3lW#wv|F>MbAVsOT*uZSw^7$5;Qd`t$u#A`t*+^Hk79ddsr-X>M?xcCj z*pO-?mWy^Y;oAbJBbiT+Isk*PQFTl^UgmvWbYs0O_coBvB@_-H)-6hiSXS(@AKG0w znGb}XSk2glDpu=7YpB0*O(^j(&8V|PH49?Gr95<-7V@x|R9kL7LHH1342+!^+Ps9= ziPGBX)j>A53I+_pZ*)?NF*#%A#GYKH;lF=K_FcFT5yEX@GvWw<3T3tPT8(p>_Ak{~ z-Md+ApYvw#&QnItyR>zxy_hMFRv{@3`I1py2q5#w2<`h$Che3(vGFjlaJ9o8r=;2BcHNVS5rIV|3^LKTZ=-pdFJ!}ejgdqHU^c>(-eNSjT1&F3B}Z_Xt%bt);_gt_^Pd40Wte}Yvxu5O-y!B z_31qAwgd3!W&iEZa|iX-WpJ>ywbhhOBrSC& zqvTpsFRQ< zj`#EEZxxjIl&}#Zp8!2e!Wb91HwseuCIg0fEOEeXj6Xv*OEp#kr$1u{!L3sz$T@M@ zOhxF~?S(?h&5hExX^Jy54ssLk*d!OK6>6K*tBJ4Zkg(k=q%1QW^S0F|S6y65+)E>h z6FLKQ#TEdy+uQz!G=7iJ*x!`J)khChoHlSW1?h{beph!dN;$XF{UTCsRZ4RrzF_@q z?Q~Yxv#_84v;2BL=y$rw6$J5f#LOxkmb=*ny|5PnKq`%Kw4>^vO(xmhyCkTSCNBc~ z!AWVlf~8$9Z3X@%ZOS3!d-LoD^Jl=?sQV>g?ejVF9RVZgk`t~&z4*1T`xe`$a{|%h z5GRY$=q{PXz1)l+o$qp5G#hT9=8+}T{S%-mD#1Y~Mti=uONBRP^G z3oU7!fsGOuQVis_?3shh^MgTTNmC2)vQBa+1kR4WrZou~18m4b5`>npso#Pq+T#eI zbbg&%ws$Mcf^JCzQy0x*3CU!u;Vog@Jzmi*DWd2=k3jax)z8(?CtdJ^0iKkBJEZXFR+N9dg%t^J&fc&`c z=B9kHjUeWJzS$F%L>xM8vV=s)*>aTx(ga$=OCT|?GKBz%&ADtv(-Wq8=-iE@1MQMO z6Y8BvaLv0I_Vb9O0X(L&F`K%+825|HNF@LRZ;TX@ndP5j4kq zVq_ceDqrN@lt6srE!Szimp+%uPA}4oB_nqwHoO5LLt`q267mlg_mFiBQo~GC7s3fC zwX+AM;FaG^rLxdyWx1ad5OP9Dpntamtv@845&&<#9nM1fyM0t)L%vwq0xZ6f#acvt z+>UyVxEB%SMhj{diSuAeHc0cX{bI4LV1xmyiyjFIMs@D16sIQq^inXbfg)gIr{cgn zyVG3&rr2ApQuU=2fjvajo^-|pqHHkPSw2;^u*^#&oma9&K-Sm;oIGM8M5(8nN+hn( zqMX|^SHcGNSxrsEcyUnvJ#&6yp<=!xHI|rzg&EC_ zI{ePZeMn&x{tRexI7$sTnG{8dNgU}7%vqF3q!E@(!jRoUVL>cw0RT5T*w0^KHJTiF z2%-x>HPO7yY!*5qOyW5)dgMqgLRmcfudoJR#R$LL`-3Ytn#tZ%V%ElmQn>TtDmENE zk&%oONh#{ayvoPdLNal_yylk<;X`WDNZ857P%pr!Y-d}qjSz=-sPC{Cbq5nE^Dyyn zf`c9C7}ZqQjn$`o#Rr6sno(B5$2L*UC9Lp|``H57MYTya^cVK~N}y$JD!mLOMW}5> zmn0Mm*h3Grl|oIwKeSlbl3;|F4>t58Uj8l~(n=1jfpBLO{Xop}&0Vdtwa!#qnUsI! zv9JRbGSfH5Y7%ZD^6P+s?ee8O4m-to+;n0tDCdWY3ag$wTN&!STI z%pc76n!fTE-zIm?udgXyKfr$ra%0Y10LFzcC}Qmjy3N+fdPW4m*BB5*Bi;{N+xm)% zmHUt__#^;^1X%$0gHNc~&uD2h>ehm+o-B{9FU`Y@7mO)pS^m%IPb9C(#V<({og9Z^ zzF~)v-D01(f8(l(1=Pd-e0d9YruZpdqxF;b&=UZYRZR3ROqJ9mfr2SFaQtQ0gmN#a zuh`^e!+=e7xfj~b;FYNmm6wz!p4agm&is2`e(S6BCmO))P!?1zAJVVyrDORbH^vDP zVe{?%uV02ut_1IGk$XF3UpPCBuUelN`*y{qHOd>v;{ZNgfyuAE=czY%-w+`phF{V3 zqGx2N+GGVN5^fJ|6A>ufdl%Sdmgd-)+d*t0CERc6xn}mYIk<6SD?NI!pKEinwkaj> zi{dF-6dizR+#C1H@I^VVkahNkI4tH-Dj5@wt0osCtGw?Tqh{an=;aVPKL;$zqYfDp z!GDi@Jt?vt+d zQGVl5)Dni+T+;uwWD|yiq#_$Bve2#N!Xdbcm~jfwFubK~9OqS-T_{#8TxRX7Lc677 z)3me$%`zqJm&zzMb9B5U%mKS#Wb^&uic8;4PO9iDLIhE=efw@=`U$FyneWlG6rVQ< zCP~1KeONHgsH4w1WUaOe?XrqOsu351rGKk}T1 z%F|%c)plKLCYqlU*%v6vh*`a(iOV15XMC3cZC%EvWNgGxfDb@TYtsDH;8&$sho6Qh zOtUZ4_a`_eEkpha?q{*9rouOI}P)-IvWA@(JKLB+%SM$Kz?{WgbF$X#UG=?+0AoU<8cODbi1!TFM7Dr#B- z)0-Q~_jcJTu|ra7z(#MP_g4shZgO_o!tOI#?r@6n%ZZgyL|b-Q*(BI83aK2{xRdVh z#8OH4uR~?1J}O|1r40McQdWW-I|hSzu$8)R3pOqF_1d>>U0qpjrm1ur0nV zaJp2h1rS!I2Q*IUtn5RD&s<1|oh_*o=Mc-$UW+_-LxyihA99}Yg9~yK(kZF_<_G5Y z9Hw8>7xoFYs7ip%Ea4NNLpZ`Hs46;t*J_-yZ%_UW{sWu37ciVtJPgPc8+ymcoUT3IP z7{;^C`7YXlvju973dtuo!+DCSvR}eF%M`hl+rCo*AJ6VS3hwxF#dO0q?I(kdmG*mY z-ahTr6D>m3)4>x9(PpLXVUn_#mo$6oCs5daC~8Zz5h?E{i`+lS;#GZdZfc7-R(m3B zlsGc8%mvzJwP5|^aQKSnt!Xc{XKn^%e6?UsX`Zpu}R7(suLw&J@L0a%=;j zagOq#-pYwTw1xkjjZ20+!*yd^!H7)iL+Am?K05LNu)qo7<)sRm_%9rI9zwZU6SY4{9(YCo$;$W zzC7bs?KoD&l4tC%Y4DB{h{apP)G@IJT+P$tti^x?bwLARs4dU|RAH7tZ_2xIIXcdH z^RHgOuihhro~=focM)bi!R;zq@0`dz|#ANMX$ z=Pm{;@3`kh1yB3N(7hzM(0U+G4CJcsIZN(4d&pdM?OA(-P;Nx*n$fcfs{~BLQKzYO zcv;%#M1LQ*Y=j2pI5Au2%IsP;qEouyL~c0%B|QlEr4cUq*eA-VCEIc-@6oK^EXw)f z=eSysu?8i6PRj}_riq_7@Zwnlln^lSzO%!evwE-yZb?Fr%wQ6SD!8R^?!eFR-O3Kx z%`(rZ$#d4f)b+rg$NY>qPF4}!rSVN%oKw=};IcqIX77@V*V)6r;1PT4;4)8M;#p)8 z2->}zYRXgJy{v|^xq#=lO1tM1Lh&4=KF%^6%<&v@t7mD#M6Skx??JcvUb93w@IgUr zmOcy7m!5fK&Mszuoq^UsOL1N-AAU4F@U2Za);_{o$*`RD_BRL8)_RISWh*D-EgmE$aS>p(_s58h#m z+%pRSJ}e(NEypiyN6T4WGE5rF?D-Mf%yc$B4U-ORW{tVr^b#L}9Nh<5rHO7#LTyWt zSF}D^KBytKNc0ld7*)9L{haOR0eTHiEQ`-FkMA%y!5Cac%zCr3)y88It1#v=b{w?I z9-x#NA z1tIaAnv|SNQLSn}{|H0Kdt`+BeB+R+t4XETAyrv?wdtK1zUsmAMEHGL4(6OT^HIcd z*kwd;9~wV4Tl?TU^XC3fsn*r5xlKuLVx}kbYwhEMJi$plIuY=ySsv=rHx`vfF*>U? zhWnE6Yk}WBJarkgF5{ACO~7Udm^u$U)uI$H-6{EqmX9@b+hg)FF3Mr-TJ!RQurAB; z8{V{r3r!&RcaTXdNe2SX?>Pdx27H`MWNmnP@K7_NB*^2U{AeW0S|Mpc%dp+7-NUh~ z>5|sD$mqSqe3-&8li7_F$OjU;%-H=X1xFUiQ>>DKqxq;iD(!9<`@o6Hp96#+Q45Vd zc%Lx#&DcE<$U<-lgn8^}f-*jitND7KvhUibleFBPX0SHuk1X1pmxcHNvgW}3&u_@2KRLQ^mS_z6IJ84(N51$XQ zlNGiIDxgFPoRQ@g)|8AJ#N0Mfo*r4+TyH^5IBzVN&(4`u&U3_zI_DNE=c>wyx8$+d zT1LlfkRA{Wn0~@9(DRLWf!J)8+*mX8;nHN0d}kG23VRZC~tqw(@mUiEv?2H8z%O#eSOQd1(1M$Pn*hr>+H&8s|1}pU)nGPUVTNo&!dnTSr z&5_-^?57TenPEvRc zH{brRFD7;mUJ1~S_dF#Gr<`ZY?O*!pDR}Vf==F& z1@&D|j0)G#c~h)6TzV2e@k^tlkqq}+W?oh)c+62f5mqDRohw4_Gx;NNlUO4?n)2c2 zA!|_wKMoS#Y4#ah5O2bUHlR|@Ky(}%aPq%-dEsFBh~=3;2@)CVae{QnfcwQ6!%=}p zMsT-7Hv_Ef71|QeF+pG+3Q@)da$y7$)^?34s7B?^jWyGle_qJliOpVIgSk;0@P96? z%@W>;w23{a4oJYrZnW>qRx`e5!>H)*`Z8FLAxffUU)${{LB+0?%LIK)>HTQU_DIY= zEs{b|?Ng48*!raaMZ(uM@YHtWmSKKV1<^h*SPH0!mURAIR{D;HU`pqh_Kf6Iq6iZ4 zK?j0+iI0c6@rJl&YOsGeGL}$;JCK-m8ej+_enj#+JU_yd6_|0Y&jcAE89CJD+YRn)wH;u^Ju8 zn^SU+ucKK%+*w+Nd_3YK3epsD9B4e#W!(k3lP*vlP!i~@f0LGDJ9=xHXjfSw z>t)8x704d>MxS(05kpSzEW3~E0Lc|=4ElgME5^IYAyA?<eELiM&$4v=j zbTFduJ5L##{=#xchx_X%b%_1kwqwFj(2ju%b1$u@CJXAfxVd80k6VdEV%EY*b2r&T zl@34Hs|4o7?`P#3tEldq67_e!>jtwJAiLhY8P>?_;%Pn~EJbia*rZ?F+R~!w?Yq~& zGuhFineDshvfFf-Q1!N_lkJLw!oQgXa=|;mvs#n8r`wV>gyLK7Ol^v-qRp82*i(6< z=9Uzf?y-2@)3DW(9_DYFjCMAVBkZVP-z@C5oWHA#+Va#F+fQbP+QZeqb8@bv#r`m^ z+mxeG)IEXNhzPmnKDKbxw8xf~irV2G5cHGBy0kjprdoZWaY5#uT(gWcU^JixsNRL9 z-fn2Q8uVc;2y2Y?GbCe)>gM)_EXT0=W_bGVcDxf6f35x>T^}^Vtn*ZG-gmnsn3A@& zh8TijP6_L{7YwYPAM+js$RG}~Mq#%yGfD;F4kbd=&5=Pi-u< z5JYXImh0SE*fuBDA=fp?xrZo>5zk5Ewb*f`R%X~-eD}5{;8#x(GIfWp^Z*q?&*^Zz zw)@;F)$Vdo>&x3e=XXJl#stflZ{BqM=SIRmEO~#hsG$D)>0Jph&d=Zd7d2`}N(*4P zLaRUoaqN9^dEJop%SjITDWrLFA@eRXIoVWwX}HOh=-8m#XgcnRRJNA~Oz$w4?<5;8 z-;u^b_BW5E)!tC~Ve=5WoLJs`b9A~{j(C1LDF?ps3eF&B&^N`l7z#IF@LQ0gPR8kM zPHw~P_+1YqD+wZ@3-9@gofNOpQB={2q9&$c)!CRP#O~)DTs^8gTEe)e({|GEptf|0 zZCVt(FjfBpZ(X5ya{Jqnz|0pb^-1j{%vgt?E zQ4dbDJBhRex+ZWSOZIiT4XX{#rI(!QYs|t9nlz}@fHODF!!sH08{Cs7vy3*JzI1s_ zad>~;;IWPOl?loK5fWU=u57}830Z?^m<~)cO2!CDTW+;sJX8mvc|)aewL8bIp%&Wh%= z%fI+Bu~YE1?i(zVb{GHqk6zBRdHtgQP$AgRduW8(EGXAH<;sGofc zM>#A031>Xb4=1V^uCTkd>0$yFAur8cZQ+DDTxJdmeY_m3U$#t< z2WG5kW!JsEy3S*wM&C<&a`7y3KYnixKe#h-hr^QebH`(N`T4f~<=HLx7a3`q04)tQ z<%?J2C?0xsEDNC|x6X9TvmOb5z9x8)U2?Jx4LLVuKf0uvnm=_zUnN=-cO`q8BLh3j zz2p+UgzXBwQt6K7{c~&?_ult0jjYO017JdICwkdfyw_?lOf#IM%ovjeOb+&(%1@Hv zaGiJR&%sqs@Bw?bZ&C9FQ4aJ&{SM#3zYyC0+R~IeFYwdr>k^#~Nk)?V7`YwBV9L@x zkgW z7~blOQAk>huTNKu52}#7U|BjFAc1L+6lVAu+eLM&BHcIMd%d%n)=4duOesyNWlp^& zF^^J@Lz5kFhCM?8ynADt81r+b2b#So0sbIlf*h^VO!yk*KG9y}dyib0-2wL>PomQ$ zY4#or1aR}e$Y1|}kNv-PbCI+B^(Xnt3pQxX$ow`FbW1kzpRyLSL7yg6Iu*O|I6qS7 zkWzrZOk0QiLp1kgCeBs4HE2|5os#hOjQ}jOOBbA^ZvxZ;>qg3--&aQ1F<~RbpY`ZD zfrBJvtCpb=8i>FORDJe#ToD?g-I5vO`rmP;eU2aUwD~X_+%l5u_{O~js*l?sgh)2RmqGcN2Tn=ryK#8_9E3@w5WdgQsj#=Psv89 z#yU0*L4S4uqwSU(&8#Ich*8Dw{O1m#u$XH+C+PAD$R18SfdkuA=*uW$P!){SJjwnc zG?ROa5QlM(on7<Q)q<>y;SDZ{*5f4?pyX+caO z8RoH@@6b$XKUYF_^8FFh=RGQV*gHXAq9E)vyN~M|u>nUZb18MK3bw7@FXtHT@7R(m zLh(qX>v0S*>V$QAwS2GM)V=${lyDw5VtIr84d|MObFm1!hZrNquuQ&9pm6D{P{O^Y zX;Tv3fbNjzHp)9ny~pl7Sy{zE8oZD4QDCNUB$KJRU=+sj!8d!Btck|bY{P4} zApq%Etof>v&6kgBOW0?Seio$nSDX>u!4J0R^wm8WvfwcsrW7Vxs`Hv%#qcllV^aBJ zTjjGbr^tsq&=RHmF&Z2?G3ZJh{STc4Q)FL^;zgOL9I%Uycfdvy>k8E?jmmq~Hq^M2 za`-}8#SRj{hIQ_}z893D?AR@Nn83mZqIR$-c5)E#%$NhE^K##mXgN2JYtfIv*v}M3 zl!R+X>71!3f!ifH0n~EqMJ05*@hJ8L&~S@*c$|o{rF9y;v%o;rXWPrA+PAf z1c{{l9m`x1t;zVryxb5u;(Br0I7}k09bV$M_o$C=2>zH1 zfFTw42y8M`U^nwW2ns88`m$2uHL^e=ziQ17%@GY438`ZEE=MZ+uN3NtNm>8UN zPkXvao9>^oN?SGx&f9#!@)G=x@eLgCqG_GQH=7UAsU<(NZ?DpLG%O z(zsst(9ee9z^XD3CAf;`>Gg|1gPNC`=4W8BiH^}MUrHIarF2J(+aq{>y%;Y{Ag8Un zcJ!j;D`Wdz(zY)vhUfPO8BerG;IE2)2IXqGX^6X`8yLt@D*>MOyuoK5-{DvNaJuu= z52^MEh8067LJ3^w$x{CyD)&2V5^g;i8Fr7OC59<_S%1z7^^A8))t{gxC?8dp2BTU@ zg!3^y)$?OBV`aFy`TCV@__6SK4^F9@8j|tU2lj~T^far7>a;jU+P#|yML}tv6f5DK z-uLP1fq>Rib*2X>rfKz>mQY-G_BJVRbpzn*@tMF_`0`t@A@%-m-}C>ARsZrm3EB#H z>d3t>y_9sdpj9Qq1U5Ny8EvC@x=kNHWG2e&nhZ4+_uT~pZ`d@3CUaMkJ81XFgdaZs z0_p?#nEIWZyAm$DJ$Hpjy?|;bB=F-W@7_uOq3VcorQcw zTp1w&G~?glbojN~xzn-iJ34zH9O}vO7$RK6x>ey=F-eqtfMduUS zCTGH)JCIIzTI=?|h_BV6Q5hJ|M;9+SZ_qh3P|kDeoX=A>N~^PeUZwbrjyb@8pITo! zrVh6tZcwlW`MstFSZSGaa#=K*vL?Q3`BEFrhvQ25u!#-(sR9%LrZm*+ty~-OvZdab zJOVz~+O#-yNwxzlwgYNLn^> zA^RLJ!&^}hs|Qm@`kT((At3*qnIJvokPyG!6VrF8N%8}Oh0gRSQh^HQ$PJD%qvcNj zs8o1N#kvaPjrTUFbH7+xtIpVQVX>BvlD>G6`=pTypfN{}Y8LZvt7Ol%L~9qjV#ljH zF(JYJ2mbb=iXZ zq^>2iE)A^^+Mnoo2VQ&pfmbvCygMzeU+cI1;G)dL_0zX+6%^cgG9^DVR4` zxW8^^5YMdpKVn2EJ$U&)+!Fl?Nhip5LBx6X^~%4;E;5 z$0*_lG^>>yM~8$wn)WDfp-h$c34B~Ba{==K!A9*P(gkIH%3;Ymg=pB`L_CKTZ1bV1 ztgZGHs52hJfqw`cTjImb;Vk6oB^o!%%k?*KTw+&vvyg(X1w-*E{=%L6< zO}1cEoDu=w9C1Q0pjOByQ$Q>euMWFHZ3c|J^%5fMmv$CRV#?FVjrY_jRVK$Diaoet z_-$u7uis4BDjI=br}Q0mr&m8;RulgGgY4IR%Mht*M)TZ8rvREId)$Oc+T<*}!erX5 zakdw_%_(%?>l>)T=JK&9cpQ#{k?#IF^?wny$5Z0-fHHr;LV{ZRV4*-fMSxeF;P@fJ z{}Lktr`2?=7MRez!OPXA?Lr({{Po)Yzj zewfRrx)$F+XDoa$Fp14`Gi*YJuPJTawSTM<=(cHY)Qzb7+gVA^b8%tq6!s-qemhp< z6M0^xiYXtV=n)jE#4o*!f=I3M#w_?i$nHS;gO#Kn$@Y#N#Ickj zm!wrHgPc)FT8WG6%h+LE-Ic+*R_~dKs`jD9WhCeB_rl!c#S(wV^tYyZ#<1j)U1nox zX$^iTpNm20!T6eyc!v8xZF=l#{U4BZ?|^$-E`jiT1FHR3Ay`bAq6mZ{hZfhO4r61)AWmFHrJH^GEL7S* z@WZaI#9T~~HBBA}yu39>iYD7F7#%y1=bm+MTyR;FVZh-7Zz!KjGlI57x^J+{Kmw|# zb_#ope@FjO$NhyWa>*VPsF4G#fm{7{M%CDfOZbHb|wx^7AB4ueR8k?OvplO-tx?=JS1vZrK>sh zyGXN`)lk^O8i2Go=I@u+H4#umjNEZl9YTnWj8oYC#d)Skzo!HEg;Fs%7!L`*Swr%s zZPR|GBYR+BozYlYjE>p%vYnQ3%U~=!ytDS#e^JN|w4ZJE4tX0ACg_37OUVx>(E`A9 z*nf?6&-^{GK42yd|Nmsu0Kt_3gkF)H<;wt~uNQm2Uh!Gj|d&qh71PtP=qBnp$Vy1(5!m4}8U1ch(Kq_^S}y4X+B} z>yY{YNq@8at3}v#%l&|V0NH>?PK*ZDFKL-7K!2N4&;69-HlOGi%F%J0oM^p#asrJtu zA%Y)n))W9EX$p*|pE?QHdM)!mE39jt`lq=d@MB^H-Ax0gU+V$?=l)txJd)VoYU+Gv z0Vsc~{-d|9eGU)^`6uryM~cuI00-o=@{eD)A0-5VCFFH0D*&OtWnN*?!Lp0jyLjz~ z_TR8bAnSF&)oU&Pl)(Xw{sx1aS8o8`|3xh)p#`Is|FL8c(=3=(=y4m+`C3KC4nXns z;J zy=H2j1XKIGDvUY&N0+T7cvP$&1293MNB{l7_#OeEK~W3fXR>|tkJkTE@vlT25Yh?Y z=(XzCX20rxO}tqE4+@D>z|LzT^BF+>^&;X7@Ck@;KuBq?=+pBPxF_U%k6z6XL>&a^u5> zy8+C+nB#;_U_%>lFTz#`c?X?bZ=PWx?tQpFY$-)#coIIJEQasznTiA@y6+DX+%LS? zCB5x_k8Rnap|pJx&evF_M8{$Y=paB^MNj0*gcAKq!CF}{1$(^uxc3~rx(kVZc`2&i zZrPfhw7P-e1?f5jixxagG{Xm=^hj~z`UM4t>l@G9u|P&pPcM!ik1|xPL>&Ic?Pd^WRMAAnsk(J4-H1W(4>)j+%c0W16BeHh0^`Zwjgqbaa*;Y(H zp)S?853v4L*w*l)&Jwmo>!O&SvK6-c5Z?!e$ciWMonn<$PEHoR6vL3n&HWB4O~5)Y zV&F(bZEf?~Xw{i<+$k@FDJLao_k;kpT&ml=!J+r;^=a|h(K;Y$t^u7^SX#4z)W$#R z#mHC_Y0?NOFzjP{jjyy}F7@0VOxM@r7tnY;eRY~KKAq1AtW)Rp_I$6k&h0gOzGnL; z{uON?IBA08AMC&9XVB2lZ@?q?Z~OyCl|MgR;E$XbNS+%O7qoH=3k&kR1pMEql-QL) z`Vj9i-)@44MWOM)TQvaaIG3r=##Z3+HsH_yh_Im9wLgRE3V;N{=!N+kkiahQFV_>; z*MLXmBLKDT@CxAmN{j%e2Ybzq4{}}xJLRfJz}8=Ral)${y>93oB*k9`@vq{LAf6R4 zGXxtHWYpgTANoJx?T=184t!+R|O2dY?wg{uoehb;WdvH*?@)Jbb#UI;r%NQc)u1cAfR-@J9$^pF&A ze@1j2c-(jYt?93G`L`b6I(SYjDDd~j>cGC^=W78lb!cN5-bf?#fzuB_>c}I8hKLigAJL#UwtRaX3u=+t7 z8y}H^VaKBw2i9Z=!CZ%f0gM|T#K3re)b;9Ri&P;G{BCcMXx;h(Na1PRG(HXlpu&58 zihSqbCaQ!;iyg)uD+Z%SwM zNM5(K;ZI*rEReo+xNe^6VvH8KjRc&mJ#6Uoc~GYi~4cPQ{{_<^y1 zWqU@qyg=c3c`w2PPx(-7o#m>J;<%Uo;VT1ouC$*MC|^GJ$T;|Db`YUIjt9uy#`~+I zKWcX9@y(J}s&74}`v1mwoDSeT<%9&PpCJ|UT6<3h)SW7XJ<4>nVKmdP9(}Rl2YBn?DVugp#_kAJB6(%9lbxB11o_S%M0f~^%q3Of@_N^$}40~XEqYfWE4*K;a`3t7le z8&*?5Jw&>Jma#bUFvhb8kHH6e!+0ZX93`kpDk!1I@^~MqoNMcxcR!Mi8|*%br^6=l zdWgbLt}Kmz+55!e6V@7ux3D2@7$Wq!G@o2yZBLbc{8mWZ&T~bcjQU9hdvfDmKZlo) z(uxIV|Hl%HGLSP~Wtw0ds-Y&^;pZu5n$WVK=kbE<1#N)VhK8?UztM3{8TqGbTo5a%3)cdy4 z-CrqB2F9TJwa5`xu%nXI@KqHWM-cjt7U;2>x|(GgiGenEUXh6q)|m5OTM=COaPup? zEZAbo@K08+&gvNgBtL1%ZAWWeow|s(jE|bz&ZDszno{Fh;Qw&*y!N*n6*%yG2sp1Luf#4>Kk;>PEAe|dU;E94OS+;93}yA;y3W}o>(a4CA5dIxVw z80>Nl?K&{j{|5cBw}{wf%Wawg6XF}Qlv50kelkJrkLDU@O~olxX$|{X9NbW*!}ZJG z^HT6uj(wj$(8`-v$5>W<5EnVBW;Zfi9*nXIFreDv&lor6Gyj6vKUA6?8!nUYfLlwF zx8~#}DZ*H5$rY$5Ce$@tFNPwF#=t~db7&Q-Esz5Y<6Bb`ZKZ?-+?mM|gj;mxwWFQN zp{#R6od?l|2+O%K6@`%&`GwP<>Y?@Ih<4%JRfrnG4=Q+sAM4dlJxrPaHbt7ELtKMg)^1rj6*!_UakQV^UkdvbFY9!JYbi_!*3quY zD7Qm_4UrgyQw)tJ+DU!xIl+nMk?Vb?^a+K@!(4j!^h~U};8n=JCpCgpZt(tsic2yA zSNPHb&00GBCo4lgE;Tf`9#XL>%N+SB)+g6wQ&}_wY?B72fJ)z$@~|G%L8KBbiW|BP z7bwlLuzb`M&s(1MzR@#l^9UBX6s!$Pe~iz-<`WuPm@bQb9@4SojX0L>-5i*v;vdBy zS>T@_ej+Ic1EK{SP?P}t4LT+IEorSmS>v8}b$K<;Qa~^W6TM$Yhme0FWqFfTE~%QO zVIr42T5-=AMDa`8Z_ruN`1DBj7X>pGd;DAi)yVaJg?G8mk1_*vEV+9KiF>eItteW+ z1f!y8G=>AJ4JK?3r_?xG@lx?(I&Xh(PWq?~D-3SF@njS!Ja*P$fjx`TvW&09+A-nq z{qF2rMYJn3m=7t_MtlMM?dyd+C3|{%hHAI6%tl6BTh|u2gvWAC?gsL`9MO(uuC5o9 zJZ7GJJjs|p^*xx4w2krb;&B{|U4E1Um9CzBb~wt*GN`Sq0?)ixoi2yoqMg0uH_YI_ z@W$zQ8sGd_$o8*5!C^YuuGW07aN%^LqMx2J)fEP_R3}mv&u>IM&W6o;qg54(6^Lrx zZ|9ckk^wK{0MBxO5Q{rDV7m8A1F7U0#A)nWsvz4DH#jguwj^zHdubm#Sjuw*yzE@$ z!buLRe73Y_IBFzdXSpg}gga*1G%7`2Mor9@lIe?#{Iz8yS?&>98|Z?EsRVTEHT6)X z@o7A#)^1S?>mOwy>{h6Cir|`=D(P!c9`YjbV67u*bE_3MQT&i|2+!ecBfCr`sDwI) zXEr0>qxw7GZdV-X4JTjK0_o;gAVLBAPrNNY84j0pIAUX&c15Zg`NjxGizg=Vk{rCc zcUM4{J8bk7b+>eaK8C+eih-?&Abh+3rhO-B>k`WH8uX1Rj6w&0(BpLU z|IQjkUl^f$avOlP(aUD z%>=1%03GfTl3f$#>j~&-A*0^Y<%^s!NPs-|zN1OFdB3QT?G`qg;eRugJ^owb(|8kk z(13voB&-+YG7ilUJoOb)*1i3$SGcPh>`$asYGFrccDrwbcW;NtR79wQ?l>H(qw?(F zGlV|EDRFURZI?2X&|VhO0O^|LN-hgkRU8!CH!j?)`U$B?iIJeq1%|aj!cPd)*r<0p z*0IGsfXzZbR1|88=2;%Y{;1OdvdP;=p+e{c6kQ&_<|z_Tm=L!j5O&a}8u}R~(kG-+ z2hc1E(vBRo?laX9`h`-GWu+cXUw%&5OA<6aqjs|os}x;Vt2Q?V#Jwr~v?yY?Q(9*C zsJS`mMWKq(s-%}vzPNLRG01y~arR!>O)qOg-D1U7ADE5Jb6H&%XQN@&kgttO{xdh| z=7}$D3N4TD;0r$;4HaRuy*K7YFo{!jq<%b_uw0CsXf2nd<9iG2U;Xj7EPYQy&9Ler z#dvt74M=IF+~G(Xzzc4!{?u}z2zI>?wM|W$b5dmp^G&EREfsx&?KM%l9CSerX67_# zYjM2%^x*guy3$bOCa4T-ajb8~x^78{Ywl`6Sbb-(le?;Ffpr>ah->g9x0ULhWnPF* ziU9&X-vB*Y*oFjcvK~yt@Q3KU6*B1|74;Y>_PjakHr?Guz|aU#9C~@KOmi6j&~!@~ zPx{e*yGivazB3xAe_pHAqq8jFEX_1LsWzunIBkuQA% z8OYFoW(~p|uCeyEf=%Rwe4#PcOaL{OA`J2>6rGWo7TS}6d0>Vblx&#fxq^Fv{PWhuygG6y z3f@T}gZ&@xME`p00=XMO@&l*6VD!;dCxFujQjKO4BqYej$mCfS<>CX6!?NG|MMBT2<(?jmRIFWq z#ivK0&z}2Jv`3q|YI)J;7SqgyyRqCiOj?CE?drGr;d<^ut00?O2;fUc4DftI@9{R3 zXytT6+sCUn=7j&&_OU}GpGfsFCS1^+7!Eknu4l)qe!SW%kI=;DA8HCsf-YBIpp@q> z+W4tKOF1Y_5+;U{g-;kOrD!)ltOP!e-ovb2KW+oXTU1YO(?e9S5=41l+QVW9RGf^3lu+3wmslM5}p}xnP_DL<#yg(QOrxvD>4+5SGFa zsWWVVc-||P!?r=qgm0yqudXn|H^vdY4X*Q-`M^HDiBb$G%9(q2AQ}r*Yj?pcOO+R< zCI>#Pwe5$nSP%3g!7mP<*3s#TsTrEuaGO2jI|O|9@TZiY>Xu67(nR}Fkt~1Jem9hI z;eKn8s?jU?yLjCRNvHR`RX)8VhLznO^X1zxKd1RRzFhjC=%~^8j@3GZoK8X5O5r+b zSt7)8UrfZEJ&Yj3)*j==#8zrOsfZ3uk*Yn0`sd-q->1M0EgzlUn1eg}04Sa)XIsM} z&Z2cnBt81+_lzzNN?p8wKp1Yd)9#C58l24^(rW<9deIb=8#*MepuIhn^dF%Dab)DNLLY|xah$O&w zm#_gZ9<$pC$)YWz$*Br|C_uA8b)umpJjTHx>nNK;pM(khY%8*!W&sxbsw)Yzbf67z zZv76~`pI`?SX#ZFSttup(iu(m2VuvU)@K#nz7x0y9JtPUFC(1cXS@E#{qXa+u-!9p zjy_e%CkY{30hixP4X!3*>O|%?$+h1{R!|XvfHciDEiaN4THq{^WxhL#2e&=4t1*<8 zp*!m21S%}Yr*i7jui*`G=X}8AzItJYjJv|}500N_)$6{J({amTDuCav*SQf@x-Yr~ ztT3AKjAlWHrBEfRX%M~lJ0TFpNZL}C&mNir>_8;0vkxRPA6J$0JcidWo7%51@$g}R zcaoc}_xDl;hAtblT?7gFq!e9wy>z~{3ZJVjqsuManX1EQ)pRXFe7i--bsbR>m`*-$ z=k)(x{KUeDPYP+mifUeVQyw-xI)MFDV$6P~IcE0on>2y4i||NWiMd}O8rniMMzjZh z_a`|axofeYQ;8tC!R}RSg>6dtw4T^15W3VDR+tu*4O`+f1*~aw`J4m;7-i7$bbHnz zyh-&8LTsbRO%a2!u)dAyMb8%zm$WDE2pqi|Z zzul@$Hy%nVT@rcG0zxK^ou@7`l|i7y zOZn!NRZxJYycoIZ%8Q_;QqcoN^k%$;t+sa7w?UL667BY_yQ=wVI_HHRcIby2I$0`d z{BB4uInk+~16W#8!CLz@)f~#?z~ONkhQW`n%jNXCYV!7RwBiMZ90bcKy)VDS?hmLh z%3EbN-`iijFJzX-?|rhp#g^EjIg~19O~5;l42sH$P!KM><8q&1cKM}a!J;}Ic{=to z?tiVpFckZx%SmkX8+4k_MmU2#9A<_kVHXOlvY^3*-$ymO#f>BiRvvhq1E7MBWTd0k z&Z&A)R{}KtFhG1?z*PZq2QaEuh_Dv0QXhH!C5%pg>3i~_->hb&<+S5AJ=Q@t}A_gMtm*RR>!~0qILi%=8WLYAJ-qG0>4$Tr%_ju#HVXwu*RwOhD2?6 zVRe1X=7zA;YSr)4WQzRPEUX|(*EBB>i>s)F5#(9p}7X33N(9!}$P+@hd%?W=$ zQ=f8LT-*$4AxBCOg|;@Der^hZ%(qf2$Ch!8_(tF^N|C;DpaJdKNX4GQCY7Psd1_D7 zy1L~_7{ye8Q-%Ri6uecidn_XU4wc|^T5lQo`bAF3X!w*|#3_(hd4AhGM&~6J6R10D zTMt`i)(yQ!-fo9i=;Azp^_E{FNP3;w_^Z@J*f&Y!Xjv1U1r*9eDFRJ@+0=P~>>nf7 zUztECKnGX~{L^6-DWlOD{q!c!+E~fYOudz|Hf!t(?GAn$tL-{*(<2wxMDsXbrY?y8T8Zc)B6@ zc$-4Ba90^m2z?qw`+2hGU>$*gu^R!}$mM{LE7AU}3=O8mO&MeKV4eQVj38w76TuQ! zD0Vy6j~?r6U)pceJqzmus(e2p+Cp8!NPfxDayslF6uBv2okSoxon_xb8)Ro`}X^kYCqMl)limziS% z!!yluenfc>XXCA!*N=#kp0+SkX7wkb77Z2*W#=RfsHEmKK zBaeK0f8#7esIs?ZDAvp#l`Z$4!8Zn%&*?FEgH?QS~7BtjGue#l5)Vm>nwe zUk5ag&=Kf$sPAPPA4uC$Nca$Rs>6u!lk_IspEd->_ASPgU2X*#Pd#WK2)`AnOw=iJ zPzYP>tF*h{8~7~EK?Mqs6BXYpcXKxJt>Q!gT3?@0hfj1`Ih$nRTb_k+%;7H}NHU5W zmXG0j4`2E5u&@jen-J;Zl*EqxBuiA@yIr@UR8J2(O@y~V^W2ucv8Jqy`DUS z^orAwQ7BhVM(MiUs4935uP(kTEx>;YX_8llP94;3#F2YW$^lur(<)mh*{g7>>_OcF zY^-WilGS15*Z(y|okoLwkP<=h!qyY_9zW&WO_xRyaV$~M@JCss8sgL$<84_PeqLnu zyYqC#q4(N^cHdc%BsZiJE6Hh-77C6L6`$|+KpquS0H_;J+3kZh8p!vwK>a&xa=r?`Mq0WfnQ1c z`UIKnJ5GzCSO){-CRNd(braD^hw~y9$m|NJk3nU!s^9mxF!q>{teh@490(LPe-qd4 zf)V&ckxY1m_l`{@G*(P^2V`BA(5}g$0zb7v;SdK_Hl=WlC&E_7)7i2eCZogN%>W&? zh1E@IeyW}om8pCkp><54)#Z*5iZiXQRpL#t{Iv`G{VC_eJasYY+>tuSy%-He_)nWQN7a?=i064icz zBrrxp3y&g_6|T-bGP4gnYs#RI`vQbCA~g)Om*Sy(^5}!9LtV=vRQ^h&XVNP!n(pVg znTU^3V(T=2_>D>wJ1JT3VlSwTD(@p$_Eem&#ZVc; z!h&{IU&qTc+K{QnyOQ^OgqeT*As*|~+G;g>9svD7?#BjlWll{{jsWTx{0wZIo30)w zJ(-BMhHoP?XTeUzEDFq9=Ow^juG;DB-?0UjS9{Rk_c5{~X@ARtnrpqJ1UkNp|8#yg zy(B%-q4MH$hk;=~U8{$$g~7+u)KSX{A5o>ZZZg-#JB5euY3U|Clw~jR<160EPwiik zLd6Iv(n9@qXBmb*=?fV7ex*7#tpWDme|7h9Ol+q;#~tdY+(;APj>%I=KKwSvEQ>3LtAPP_E52#V z=$Aog1?Nsir?MAJI%p*ZN2!D)xTRE0lfCjz4otI~H|%&i-w3%`FIIc_>T5$dk!kc| z=$K>;Yc3#y>5k}(kb399lm;GnRlaR%oDQAWGSSS1l)*;k2r?gmKN|E(hT_Sc3hM@3 zL`^OE$5r?skNex&9DTY-t?l1^(A~TsW*_6RR)s4@u5_xjH?yRCAFkAYk{0G2E&kYB z-9M`AxM9sonz2z)-@$R`=cGfCK^q^jCcRu;_E`9pr|m&@u+Nav-7#mth2DUY3Gwi) zNF-89vX3E$OPd4}(0@bi1DE_zK|PU&G0(F%sFHL6vg2*xbMD&<9>b4SBxklv8Y@Of zC?y6YFXypsi4ZuhUa_&B!N>CyTR*;n9 zrqkn*^d?4JK&a(C(OZlt?Iy1PR{ zLO>dnlm(=udqKJ-l#~Spq@^3AQ&K=c1Qd~X-POnM^Zfqrb$NL$Gjrz5nG-YT-1q%C zR~MK_Xx7JiE3k*zG&fH?f^J1`Zft^XQ-1!m3%Y#<{u$ETek%`Na{DZ~=TULgj)k^H zth`EK%SXQ^IW?^$3`Nb@^K+JCv*|L%f1Ot-h4jjapW9qBiFDRUFOGC&vY5?$q(wzL zPk9{WWWoot#fHPaOX&WPFF8PP9VrWATUXAbaosZuN98U{xq;3KQZ$FldEB1z&w>`4 zbvy!3aJg9ViQL34G4UgLMqOnP?OTZ9`7AMb4%Sj|3rI0E+8?&}z0h6!~ zMuyACPed$6O3J3cIH>{)A`{0U)=cXw`k>`O>^H}2lMg)y6Afzb0iNlp|}M{Leq(V9+$nMki|LW}CA>32iN&xr4Vc*UYqmf1ivfve0r#dILj})a;3wYtssy&Kx{2#~; zuO1KZXuKr@>|mdjKkAgZB)$ym5)ZJji9vC2j$Caj=H@d*uB}GB7+t7Ejmjw7#pX50 z(C0S!QZ9Q?wT8yj+zrxS53XHAW@_JWU33C}-|k?NEG*Q#S8E&efhyJ2Hoe9q!?!-Z zvUCM0C}_G>SN6@w(W71brcKVfU*A-kEddPdOE?!>z?Eztd`O4`S6XRn-=eBZtRpRm zaD13XE@}O0K5+hqBt$y&nG`B2a|Egzz7WO+?Pj_%$9#_OWbzn=eG<|fK3-BuCQ;=P z*w959<9p{IA4t21rXCm5R$A=x-hd!a?kzQ>&zThx}cNyAO7!0|IG zMJdj8s?|v7jh8I7@{3o|R$|mD$?d^tVD-@{LEZRoGY3T?y(y-5IoTt0>hR;74vS6% zNVk5LvKVXxa1{}7Fi4@}6{hwF5=;tmBy>;KJTM$e58RQb!RvjhDQ$u2LdM_Aa(PJJ z>ZTE}l(>TXCU)x$J>Bw!NR@$CTdV_z{)nPutG}eK);C_h*Kt|Lr9CNvR6s00ab5U*^HI< zkj<1Dq%xwlIY+YHtMlEB$Js}+ovZVmjZKJmMjDL91*#vA?HDdrac;-cj4W_i2xs7J z9kd>%$3NlEi@>dmtQk=F7(vS*9Y|cY?)4&)ZGin2*?j$uSXk4E=DhOaPcCQhxes$F z*#@%zv%oeK-p=Zi2mULp;ZSaZHn)XlD9c-Py?2V33Ps{$DczoGN1t$9zTuKrZW8Mk zh5P`E41zi)gjjsYsC--kMt$pyUg?`Cazqn-@lkWAT2L$_QRXtG#B$7MAWtKMusSh& zWt2%Sxlp6Bu{hbEdK>b+%dB}0J}{EBm1E=LnvQKV2Xb+gtABrauVppzw5!=~l+#hz zPc1vWUZUHg>FFA4Ynwvmr+ZnT&nX@ubjCeE=4iYP(yJJe&iBmGtvJV#yJAAt|nHH(|cyG`YmcV%kL^aDpzryklfIi{owHLx_+gY~~>%5G9O z$nbP~@){Gg3rUS&YsFjnXkDjZ4QZ1XZ*C|;gC$*=^?qtT?gX_}?R(J9kqc;kRzY$K zW`$J9}SuwJA&&BeAzU0%i`q>;^ft>DP3r}37n}(_clxi)F0RxoYiY3)>qeQ z`ZbG6MzcAk2RkKYSo3B72v4jZYtHW9K+-G26{&}!#wVmty!aO)LxMnt(E9u_7 z^{>9&&jrSfO)WlsCHCv~a>yP-elNL)^M%zVGh{pBF?%8VmEvXJ2l-6Vw73QVh&r2B z84ssU@em_#p933NV`g4=82P)@L5=rU7H^+i^Ld!h>_`Jk_P+_6z4{#b?kBZ6TbBa6 zx4C>p$1Jf`t{hvtmTz|uI9A)AZc6P~&~QTO_DJJmM|b7~6uV0``Lvq73NsD*jb>-v z@i30}K2J#GO(a!2BOUOW{IbeOZ&xw$m)<~X##iredYQyD+v1pdb+T&D-^)8-lim?F zEv60wawLc;i_>UiJf^O8?e`k$5wi-Q98QOLhM!5$*0en(%Fyvl1)ou98$VtrCcCv5 z@(m~-=w~XF9b*fswqv^Crl-oq&`8&6Nj?xVeVH2;XCLnu)j;{si@(o5C!M7w;y?hj zTiwJ;SuK=0ZW*gGWh#(OP_4x>?P4%kw`h5vw|anZ$%CvVqhJHbQAUtQSH_}KadO4v zJZR)Rc%99(kgD>X4y3_D`!=yD~zY)O$Y~_TCkyq2!*>vpTjwtM25~! z^qovd8d;Vq_m+L7dEc9k_O~{~PN&WjzEQO=Dr9AOM1v#e)hC=c$Q!>;HH?a!v7gu`l}r4pF74nYMXHsz`Nw){^9y?? zk9D6Hra)5lMna-oZ}c0kC6)s7igH_56VwWu{9Z3?60jL8`K^W|y%tW2XjerM9Go-> zC3`SEX^(4YM>3O&LZjj0L-6gA%8Pb67Hb{X613z=K%?uj-_k%1Op(}4VFeY zM*7KFLnwJu^u8b%a6)d$038k-DBZfUn(+qF#GeHWPee^P=ZDS2DP)>24#e5t^VA|n^XO!6QKBP}fI!)NkqL0U(7POMM;&#eubot-I}hh& z>i}jhA+cpU6{apB>b+#T&M(i=G4t6)H7H*;$Z-U}(b{PJ;I-piGopW$=c(1(LiY|8 zkd0aECx`KZ|M7Cs7q*GDXDJF6=H(;I;r>#API2?93RRU^D)nOvds10AS`!091#`GA zV=N-DSH#M}V4ep{WfvPC*_-vU(#lJ{7Vq4w`3?GohO9&GR{w^HqeeN#z@o zv09c4oiF8n-5Jf(v23eRnloIYAC1QPt7+`)Odltdcm;%~z}_J*NU@=ZI=DxEda?h# zfy=`fy!b+N0`F)~Yqh33muj@`loj)~L1KN!@+)S`Dp>7PrafpWjiHSNqx)h-i9PS) zbDSD^KgNo8HvjX54!hQ)h!F1hLTMjyCx4pO>QA;H_wZ-_Ok7<#>tb!_+T9i#>TReJ zC))F|eq9DF*~^>KA4xy=`L-q9pVdB0!rLHUjupOQH;aFNA`(np(8YG*5KOq%E4vNgga$(?!-dc$F$fW=%?UYUG99!78$mv*{SDAr6n3L1gx ztEwVXu+=uP(kpLw?nN#l;at=>%Wdp+U)&JY{vWmL{Yf!lbP9Ao&@Q@k$|ya%=M}hv z6G~37jW^lGi8Up#lOdw=0_H$H29Jl!V@s4BkHy}%iCsq$hw>&H4?T>|T^hLwEWMHM zG}=p0JZy+=GM*y|ADNO&y-d;GiA38y8^uBq1eeean&M_;A9*;jgiU=?{-7gE+i`a|SDd@^e*X{*rb?Y`K$4qG zX#Q-Aa{aMs*Pv_H?fL}q(o4n2{dnGyR_WBCjviHq>C<&>Cdd;Lw&i;}95#dQ#}Pq8 zQ(&>iQG##n{m7>CA5~r7(sKUQKQ#`yMv^$B{B}e@?Clv6 z#I7{eHfyP?X$t_NI45deD!0uiGhYa^w3fEKKt1MU=Q@bE#`2-%Lx1Dv)pe81u;%P$ z;WXW|HL-wwr;0aK|unD(n zsA6QM=I<#q;-;28EL)&*f9Wh{#pV&aLOgDjB%0+oKI}IqJ}LQ0%k`xPvxYx7ZENbv z(fc@dCN4uUGkbdpbIIQf{Lw*L13W%yp3ipLuDiprio(n294_jH)zv#{2V{*&1TwsU-I^eSha z)Y;fUad-HvG$SeQQ;rZ$+a_j#k&W)NauJi%sg5)p1N*1dBDk$24mBK0XuvwTK0StQ z8I0&6EZZl-XO{#)Jx*;;)p=eBeB-c588qqNZoE3^k<6|&HXbbWf+%hA7#Ju&t&U^W zO7i^fP9i#d`!2ecAlk1!mBQ^*(MD`M$T|Od^Vuc|Xs z@i<14H}ASPiwExP;%vIlER+rVPdsQmYbCw1+#l1;oX^8B3semm>?(Wd>SMoe8?3l0 zwxf@B1DYiM!ON*dFX|l|T?tMD`sk^iv8LVz#&P@Rw<`xWN$wJpOf{nZv}_Vz95Aw2 zcl${gp`OEC%}Mk6BrOKg!#8+rNMBN@3{r3oKK)o%RQAIEIlsE*Cw$8Or8P^u0G)isIC+WN`FZ2OJ#_6oUT&yt>6d$cofHx49=M&x2?TW-oy5&x zR^*%nO9$j*a6Ec28FEJsk_T(&CI0#~=;Q{slg^-y9P7$k;;94@IJ}@A`H%q(><4e| zBf^t6xL1}4o7;FuPe@UI;3Kc9htjcjCK0Z`rw+YU_LApXC%J-c(xSjn8C)6;9QM!( zyx>#%-~c-8kDXK{{bc=oXJh7c1c3 zm|bQV{)QhO$*2fEugDHCPq}+vg^RQ#VYZ*X4jBVq^*(_rV5w6zFeYp=K8wP(^O>Dz zu0N9%5zsT9MzYJTD6$t+Hysf*{{`5TEsa`#CfrNR6i%`=Wa)ZsE4wkX?MXp zyp^Mspv>D`*w-69(2y^SCeNI}v;}9A#@{{Y+)8dw@1J8ihuj&hE&cXd=$3QzEoJ*I z6Ysr@!CU8ZD!P5&S(OjuaTeKSgDL$i2^P;Sou#bi#B`LcoP%X98jrVvd64XQ(#ut4 zh!xOyPEW~sGRjAy?d%+c#>TfTi%$I)B;>Z91xO9PPYZ}may&D1E>|Rl;G&M4|6`EAts?0vSR`{k&x=I8R?Os!|RFH9}BeC*G-Z!1o!tNENI~2W3Z}L)b zlr{`gYidI?1k6WzC`hQx1@3S#5Htv@lQGC+ z#$$C|jO$gY)N)MZy83ITru;>o2R%hM$|VH2C^S)}`2eq72Tj51sBe+aqo1a%e!oz} zHnAH##AUryufrF`Qi^X5>)>H(7M~~SS69<$k|aL{^~(!DNmrh&Fo3g~IC9d9I)2vm z;N$bb?ga=0r*jn&V_e+f=_$RxD2}oqi?msMaax7VtWwu7%3x-PFB53@WkWM6SR>*K z$K*yJcc-%rJ&l|ao59^QJkPKeZ?Hc<>Dx<2MfR;q|2JA&lENKvmCIAIgt2qAVp)8~ z*oTV_y~KXj5NTjq{1RBGgR1fYpwYl!2U?IQFy0I}~7kWK+}YOqz>-lg`ds@OXG**Ce6yyoXRx zhTT^zHC{Bow1xg*>9D%4=bf0cnhi<^kZ6Nml5#hwP2upYchB)n6@^;7@Rtnyp{lF8 zmG!BIK9#f>_aB>s&xdF*`r}9S#KpTUniaYFCqFSKoTG-K6S~*kr`IOK`KBX){Pu0h z4X~C?DUh{cbTgjji#xOa#|X!DS_$em6ag$wA;U%;*D`&|v0w#~6!k57Y@);c1$)8) z_n)@JijDWhv&)yeQ&5g4w4E~UQ|UWMwDR|CxkfuzJ`NTQ1ly1$ywz31UKo3om{~5l z!2XGej=i~XM6aU|ug|X0s){ZJRg#kxqa7E;NBKZ*CtySq zcR4qs^zaP31Mn*s(t3mNh3~JVLAM8~yVO}}<`ZUX^o~Kk^mA@Cm})rtB(4Yc5bfwa zIp^cqeeE|I6>rNHct&xZAFC5Fi_IREetM+Bw1Qfn6^Swj+0eWMJ&1gaDj#r4h z9q5`^_T?ABhBN1%@nhrOnv|d`=((RhUF8b{N&KgJ;5WWMx#*^^TE^I}#L@HvNU{rL zDo(#h6nrGhI#Bj&_YFyrEx1>5ZXoflH}nQ8{8;8E<8)0*wnYW!dBWk_>HC<|4WHZ; zXRt5Oh`PRS-}8E1F>7v0k-!}OLTH^&X+Hx*Xm_xMLbjIePH-(Kg`%&mCZk&9YIReT z=HmS2*c5zD`^imQDnMW*utyxF)m_oZ9ILpF_^kpzQ4!ajfgvX`>=c%C#>9R+D0tS*Uo5U-1i=6D^L&R5teL>OSCrUGJlQBOg95LL-s#aB&it-9Y zUU5vnu`lj!QEL#NqQs2ehG3XjqK&?EG0v{46TNvmCu?JCoG5D2|NbS0iVB|;eUkH` z$v`GngITj(QlwRYQBdmJ*1q8k zjUc|47Oo?gg-M3`;Zu_F*^(DVwFMm(4vtu`n^DhWnub3FFo88#u}oscDcec{$xLOahAT||GW_E_}phb&;PlH?%=+#@`$ZFe8xSkuc?JPsj#i6=ejJ>`VH59m`%T= z#~01e{v7EE_1nnfr=%;^c2KG(vU^sAxuKU*t)*TXX`Bfvl-%nwc&oK(exK0K^4Mgf zB%;2NRjx~su*wp%ME4=f=&(%1V-O4HR!nt5z%LG^5^!enE44ywaGQI-XD&h*W-ufhIJ z-BS*SY80Jd5TS=)`N($UVa-^Uo-N|h#0W6xr(6T}!(x}^s>n-ENoa+?*Z_=4o3I+=`*6se9sw4Rzz8znv?^pv$z43BRW zm;3{AKW?W#o6Rx?xt|_q?rR+PR@y>WwBVdQhDA*kXRIdx$Tk`hn1XB<>b1$m)As~Z z89fWk-p`g@z)g9y<(9&WVJRvJ$GxfsB`SPFu>ZW~s+GO< zg3rnOXFqlT2yQ58X7>zsWasw0-pg`H%cU0-)N=hOb=V+{VPMR|Hnc8OqkHwJ8t5#* zRTaqKdcjlery;w|sFhJ2x*>FlRobePvV=aT7W81OTQ}i}T0~=^JQdm!+YTHLRX@%aa`+tG5bY&R>!}Oota~9{ ze$hY{SMW~8Oh;lN`tF5o@{{xL_SW)Ze1kkasnq9a<(+-gjZ<3_Cm7MRlqPDu)?x!BNbjM~bu4<_4^-Mv5D2jmujf3UqM@e?D==`?Ri_(~laa*I|Fqc@zBTbbp3tWkSX8hggDRf`{X{ET*xJ8%Nu&0I}?x2oObP5Z3;^Fk;>fg|h z?i2q=O0i17^O4j^fiAKV1f}-KL_|ngzz0io{Oj6QSw$6W9cM*++wuPfu6^-Ll9wP!hB*i|IB- zk{bOdKgF!oI6dhrel%YUdxH_r+50E|5Cp|LnKZXpjRh-BL$sQOP8n*Gt{Bi(#T^n} zeQjW-h5;;?;C>Iu>7ZNKZ;XQ+A8Mi0P4MV#vGK#iRBsnPrBXa7j(vt>EKSMpL-p=(Zwt6=*3S%WH$`)@4{hD)2NUh#>y$D{J)_+i zJSWyE+LAJoYrsb0Mq^$pmqq@n{`@Xj9bCSe*a`xzb(vSR1N>bra_?rftTiazX|2^S zQ(k)l>9;P!^a=ecN+6@T?{H_kOf@fxIyJv;=-02fAOs8N^c0(G&}7FkcJv)T z=Ol60$k?6%y-DzI5?QP=9IA$0X`7K#8ShbqX{9K5Wn8t}=Sw9zAz|eLA#)g@FGnqN zjI@ueCVo3=tjV)&y(9`9K1u+HUsd=l&E$*8+zKD_$V-z61eTI2feWH_l>aNTa$pDwwuYz^LZTo$d=MCqsURN3d%uMSvx&mx|~ zvk*%@3m_K}lOOyzqu+!Q2L|7Jz1Z;LHM+~AyDzn0ynbZp(yp*Jj*s-d6xex0fkHni zJ0a^h#A?T!Q=>D?x{OZocJRHlS;Jt+cQ%$TyuE1Sec~oCVRutp(}sYXD?!7{)<3gdLuI>2;OksAPiw;FVjQm9*`7MC~oi9LlM=-tY(77RSjnca?*s9PS7-~vlF}f-O1m)jcDA5aa zS~%>%r0h6G1zfzhyFMqofs+R>Roa5@}Y_hxCd)Y+7x;O!ruSWTnxzFeM}I9RjUeZ zvGkF_8pjqnpO)*e2;J<=?UM(R5y^6kRR^d89oH)##{Ga-j@B8cna(|BSH>v7*jvpuKTfUn6-GKn`{ni+6)hCq4K?V)j zCCw?9wOvTs1XeDsddbuoMvlVFm~`>D`q5%0mKCo+AKX%`QRSH8n^oL(iLj~61NbuJ z??#JtR=kU{$aM3~rYRc_vq1~fwW)X5peA-5R;S%g9g%k>-Cvx7=A5BLKozWpbW$cvLz%d$sB>HMF zGvfz<9@zjwgAAYmJI^PLcFV{a4##WVSblrRDP7T;Zb95GE9oDZRO-}DuxyoRRXrYk z#M#cv>N@7?R=S7r>GjHsXmsSIn@=Y$z6}Dar0uEa1W}c>Y*O?r4t_N68otj7kHlU- zv*=`sY^~%SZF##8WU6{|R|B`v@kg>s2l)GA<)DXcfgZUl)M92dS2R?muN8_rxHsp5 z3_^SWRzDVQa0Y;|^K;lDr(@K<77c^95(E8rkvl>LSQA~0jvJhCj2iIO8S)7=3-Hwo z@IkCm0AzmvtWr?`Boui3H}0_0cO;Mh17OvO0^o-a?Q;}>2>i!wHh>bUf(j^wPf_SG z27nncS#6zR3|@SIA7Uns`oR9|Ap)EsX4Ypb*q>A609W`dg33_=q~Tan&@e7QBw|v- zR8d02&M>i|ygYzW#L{>x4p{$HEp$@?(1j3#)kB*p2>?KcqyR&7aGQRSAQoRRvD28D zh#)hT05HN-aR4pU_a49>UJwd1!2`ayLSga}2v8}Q9hA@(PXHm@Xei8RURHR0ux8d{ z|4~i}9W4iFAr{l*a@ZoqHwz?yXx0G`a~vK`;H32d02(C!4;_Yo^;`589ePp+z(lOO zFkr3!VLgNE0zy8941Yg1yNSsR;P~PiC00Ko1zDoYB<&PK{$%Ou{1r96tS4Z_C%$VgXSk#zo z0hk~<;Is|4Nd6VkZ3loLfs8~A3~2<{CGgK?(!cg6qmGn|P7CF=)35JS6u03ePqoPP(= z7%YhFEhZjRWC(zbVEkJF6g&)IL@;1wC4LE0La#;ug$VoM2%~?R34>z0g`quejsu{- z?1Y1T{^6lP$0q>4(LrH>+YsK6{~0xN08EJP0&IZl&H!%yQ~D1NnlKAsfZGCv4Ms_X zRsZ9uKXMSRdB7gLf}@y!Mgxoqg;l@;FZFNJAh6zf1+ViLu;yQjla>JKh)Q7akK7Pk zm0wVnD*$G=YDsa-za$V0gbrr==n4QIyr@1W9becI-&L4gC;xU5T0Ed$Jd+vEyo6D5 z+rhcumCL2AOg0zYIXgQ!J2^S)b#lb2ag%yyv@8p)#EJY$Q#TtHNYP|z-$fQYUGfk0 zVfheGNjD*{6Q05Ujl3v6T~m>n&9{%SGIQ_eHTIL(sODIOMA7Hay{f9>gZ>Q}Fm5!{ zxloQwXLo#rrIP}hLs#chbi1Pl1GQ&fH9W^Vmkc5jm1#viN_@DRVbOxwb>|PB9L#Ik z`X-}d>qYT`V6XMo#I=^k?q+Idodu|RC+Y!_JmmyPWqm_K_H@2k67*$9T@ByRDC45p z*j&kH7J9X@VwT+C?-0&q#?YCJfO*chyG8LI2V8YRRm~#w{KBeVX7S+%_*dGeph|Nz z4zL$u%Emu7ixcx1C_xQu*-yO!oKxBw9+$}<<(p2AgjEuwQ1+w9{Fqv`5-zcY1By?HM=5Dl5=TPryA7%-Ck^CkzIzD3+$wfX4c@! z27T#>iuZ+ib)P2uih{k$ln0r9mXs-frjCj5`A)6Y+gEC8-R83j=I^2BDvXE8%{33T z=@scbCd3{vhMC|cEXJ%dHw%x(x%p10j0Wh)5>*pMeyV$sTQM@^5}o*IqvKYUi6M$h z-%OlQYIs_PHOl!*fm3K)ChV(GTARV^IxpJGV-ugS$ORFJh%xGw6HLsPSY2AOm5$%v zKvC8T+0)L^S8v3{sp*;bse+mWYYOUaEP2;4KkY7Kn?GpCJZVcl$S$Ymx+^JrCMCM^ z+5H~)`eC%>zL#F(=ZyAOTaQ=8-!&Fg8>ff>DlJ`aw|k$i{ff7;$-1ORVX%Np_Fp_@ zi2FLg0sMagE{cG>LM}->HnvvFkbYdO;FmA1316|#v0JC&&NQ&33|!N`_V6*;s3lNP6`|w5iUt9pBX&2B9HvkS7{4eI4UvxN>E{se8 zVSa^z2qW1t;yrJeYyJQ0UI-iv7q)Pb{)O^{ zWAwsF?|?uo$lCeud|=WECX7|6%^p^|}`L+b^)|MBu9|=|3g$ zA)`20xX@U1;7^1te_{|R6lSSBqP{=D;&-uMI-wdkz$1jS|D-ddP!B>NCVV>z-*NtG z5VVdGXo$!I|0;or;X)4au!ta*48S!+PoMv`%Z3+rciTT%Lm?v~AtHsq==cyYAr=4* z!VNuO1co36)}J5*7r}$RVg&}lO@L(#icn8p;5DLl`1Ta;;$JmDt3-ea2u4`bAD8^8 z0vj4D4fKJJljgg?WyJRI*Y*zHAR<-ZBw{Nrrv&kRwYP^MFj120Aj+qP}l&>K=kOJbnb7J1kfgTpb)~Kzg$l;n0gT}AQht6pPYjf z0%HK7NsobYhTKm{E5to>&k|6)SdAV6EVKmK|VUMMIVI1d;9M?W<2Iq)}(_@5My6cS2>g$`NG z`JIgZ9f~kI15`g3Xou*bKfR2Qx1q+u$fU-?grXG#D-fOWk5O=iFabs=Mk&w?F_iue zMHm+YvP_Ld0)e)j|hxdai8>RtKp zUum+Fu{TJw$mymHxY@p)BNb``x2iw;dsH zxGu&Eg9K)d(TmU6K-13LP|CIMGmxU*Wq3C2|16r<=QH+rp2I2Txbw0`ooXh1U!pHM z(Q-M7CI=r{tepZX>P-&{cYSHbvliHwG`s@al#abZgaq6V6r1jR5P0d!>{vGarro+w zwP3FC)!i>W$`qVsyyj|os!at{;D$T=IPYg0-*DGm+6q44*kE>J+NCsp-1VIFT-7(L zlxUbLHEolJ>0t{HEp;PYp_b3|3r5&o-d2(qF%)J`JUP-Pj`X-~r!Z9jcgI2kaNe!};YFri4pA(p9dOd8ktdYcbSrbg0+>2|OIDm8Z` zE9pc&hROOGFNuf7vYuBu^^%fUoMI#E+cGjuoJS}QJ-rkdTs5?}y)#hhPa-18bSxzG zwbL+Sg1+wcS~It#{U`IGfpP`&m?UN)TZ`;u$;xQz71k_FOA>=DW4@A2Fp49IrQs>1 zmB}f9NAJS0Z4d^|+Z;RWuhRFnrF2u8^z#-fX(Pw0_0-NWJ#Uv?KRR`n1>HGs1@Wcg zQZa^|Cj)t-`wR~m(@)3ZQNipJ^~G-sH#uYo_c%Mn^1K+M2@_||RC~@T$e+ocuuR-5 z%-JKnz8WaLInMCB7!)#n3@+Fk!Urd*(Gs2(ueQ8zA{?38nU+SGjjPFe^eBmRpR+9ehU>J4p+>SP?3Nwk;mhz# z@yTC^B`1A9nm>*_?NfaV!cTY!#lO<0jG-iy`#!YaSf(^<8P%ug2dzQ7={B-B13oxU07heyQgnT>Iop%)6nn zP+wCr)6IHy8`@bJU)XN(9lbeO)_cmZ@w!&6$FGVq^Uj)PYHUAPoSA>n{g7!=YtOAL zm}LI!>66;84EfP-6ORtaG7kJL`#tB@c0>8*6a#nNfV%e) zb+?KA6G#nMi7rGzHsn3QvWg+`)(dte%^60X13qR-Ps{ut1nk6ussw)c4b|*v?MSxY z^x9w4K+g{;==2Am@WDa}BgCTBYq~0g=h>&%V4ZrVIglR}~sC6w1q)oaoHV z(N&B+c_DNus%6ml4X@^&{+;g6gcme zzWo{d%O~GV6Q(lR1*{Wr{OV4F&~}-hE4`yqyBMaS{t16V;dZOgj{!hJI>G+$;oZMt z3_7Ha28#mX{RW7*0A9i?_Yo;4GZMRT7^U3cXv-t)zhdP}CT@Z;LJd=k@Xxzaf&bR>qp@Lt59Po@B3X|PH zD1x8D{1Kyumiz$r!x>Q5Tc9+e8w%rK2Q@Ge!Phq+R>bM*-%>jKAQFhp9he^?u|a|G zqVSd$-U4YL)_1T#PzyYe6r!%bCc#P(Lr)1n_;7ba(a1qvh^+$5Fa=2MEsz{C!G#5Y z-k|~MA|7A;QzP^p3rPJxRzP9;exDxy$*n1xg+Uew`~U4k_)vzv76JJq+6fy662IH| zdoS^OJm>Sm8r2~IibELx|I$(-4GKeW;ZJM+973T%9PWY`;Nud$jr>a=RRCc?ChmgJ z!CG-keE8Cm@4B?B3Ck3NtN=MhZa*)h}nt`!JkzxSjToV-PA8^ z$P@A%^YfKyCzp&;qDh>X(y0~OOQ|C1TZbZYaf|8jthdG|-4=_=jD6Nc6q|&eIyt&6 zu$9`73`AtakJIY~s&{~?QgqZlD4h=c1P{gF1h!hT4pSvv&mUb3)J~qzl96(ePMNZb zE9N1QohMx4l02Ae3>3ET$E+7HJBpX)>GDc{jAH%*`>f#WD+5`j!9+ZMO$&b8EbNhXqzmY^z9xDJz~_c^^WGDa@S+Eu#4cC-m{4)WlW1tr-NOi z#UNd*%HR2(PB=V8Dg7E4;X-JB-mDg>8j_+QuNL%Dr^JiK968VBHG2w&iPC-Hss5x? zvRZ}Qh0|3>)Wvkoz^9vE*(;mhq-bfo_-}Mssy&FFb!vm@-=@O&SFm*ZMYQ*0*Usf6wX~_-H+5sfOn0IkCD|tk{rc z_{fQ9Xynq~s@RQoq~^wb8-7l-7fKhmi)gSD7XsT%rfQzM99y3Fa|$GCZ+r zA^$Nc|JYgNOC<4yP=e$42Jus~G~!rU+fQPbI;Wvo+a5Jhb9L@0$LL~R60fM_1?PoC zKN!-osmq>O+7R(~l%aP`t~O3m0O^5A;^Z)U=<$(0U<~GEw?C(^t@{W3>w7={9o9 z7dN;RzDttE#zvi%X2;Kwr&^@r*(pU@Aw=zPxf`HR#wM;E*!lIzd$}qC?af2Gtk=o% zT@X91aks~(_>l?Ms8MmZLLSm4IW*UXi&LLhFMg^@TK+mmYpl3i;{`iqd5nbj-`MA& z2$F?QyLv^C!ml4`g`2dH&;St5pKHCKus!*G#FT|y@P*((It8!*kPj-qAG~h(!99$172Z~BP`tg(gU?r2O-WgU=i?t^guuAf({Yu%s;Xa zun-m&6wd&JxF`#DY{m(pHwGad>A)8FEqbYqY{!sI_RHYkr7C>#;0igsboZeVh3 zkUJuDG+ixT=J8C3VX$-kV9 zE{R0|-GqS55P<{MubdDoN!Xq_!5H?jj|s`oHCK2hMP)M6YQ zHz71lQwiP7!H+BfeAyHaki*TPJT9N+-XOhHTH-L)LtesQQMLFGtMStOa>X1n_vD+* zJDr5vfHb#M4Ee0;W)^Mqi3);-k@0C#Bg44=3M z1SLhi^-7Apbn&NKtt|r4;hz$as2EXCulKc6#(EXh1naIIL!et{kCzkhT2F2Dz<2we zQgV0|#6}AlZp3KH6mOPLx;W@_25~xgygYo_i$Re{wqfUQ+VlPtgM>}klA(k8s5f!a{)RsiK(ZK>Ro4J zs=@5Z+EGkrSoTwnLioZ#s2dR!^ES!}FNsUdiPJAA^qAaysZulr9B9FO#XafYBrhZJ zB2XQd_1!Ahith4%`=q;`i2Z!Bqm;ma5Bxxzn=%mxEpSRrP~`5@3mL)6&KwUpIWHXv z>V+2Y%(nNP`WeA7eJtveObi;F z&DsAfN}l2NASb+db)zW-d?3>O1XIe9D@Dy<5`=f>MIQE>(M6l{N1EKziL~RT6_vct z%Q~DFCBD8^l_C{nbQaWZ;`qX7CEF@5U1r^v#vxNt9DUw=^&?=a0I-Os6d&PKW*8iX zTvz_AG(~+I$zq2WX;O3jUTYzSB{;Y3#gIh-LETN=3$C$B({yoHRxpa>NH4bZlC7OU z(Z0)3y{|Xy8zI)i;VbEch}&--Ca)z?(XjH?(XjH5Zr^?JxTiSzWu%X-n$GK z1FClIy{S`GYu1``)$*49eDEzMA~Q>+U(V?T?u*(LEPiKP8x4%A^kC&>T7>SdTLS-} z4cOY|jtiGJ;|@k!)~?8lz#yT~w%Ln>As_+lJ!iKvCe;AF{3_}A0r#z9m#~JO^zE1- zcb+DRL*ziF{n_<_a)fIG<(k#rFB8yZq&oZUCjEP!!K`8I(5NTQnd`AhFL(X0K|uva zEGu53Ll1Z0*#*1SJzT0yHt}~{QI6`Yg>0A`l zKMOvEA4K?I6~0$oB2{bIxqU?eMDGkIIjAHZWP07gpB)TB`9h6_pW*eDr1(-}pbl^n z(;Y<`E4F=D6D?z`Uyp0e+G_7dRq1i9Jdx?oO|Bzugh;rKWrTV>@FRiG+^tUoe^3@~ zu?HJ5>sYFa$Q?yd-Py;xO#{*KIeDOqDgEYo(;m07@8NEQ(U4no(7Ry<=xiw1?g1^r z_9v~Y$u4jMo2a$L_nq~=R!z0nKjkzprfeRycGCchH((pYaW4_!1SY4&qJ7x#BIVZRt55>y4M-5&XV&|h0Cw-m6RlNQcuFrv z5Q)So`)un;YO)6_;`iYXQGHa1IFv z>w;N=7pj=f=t`+HZ82auV2s24$}G_0vkQ9mbyS!!PQir+=9(uyjqT}@oT`y#9K`TQ?sI$VD;S=jikFbNU z^SNM@{t)TNp#xbJ*&Z=iB6hTi8CL0WDSv9?F4AOmsWtA_*K4nAat9k1sFb@ox;v(; zKq$Y8y1DVx0Zh>HV4P&$yVZWB7iyM6eJeFuG_3wf#j|spZ&H2Nw8M!Zf2YeNp?`QE#U9qVPqOE%(e@N&fYLEvi zwTW#cfKcR^JcETSA+n;|x;~0wMZ`=QrwVgx#D@Ed;w!lXcCE5r9 zw+b18Kwuqs4Hfw3^%{|Hq4xh`??3L9|G5skQT`bU{_n&8vhL>42v9cmjz)$KMvjTm zf*|;R_BFrrJ#GQ30RL0PsPyMIB_l^AGb>w5qrZXg`KsDVsA^c=vV2QO3nA1{Ur!)& z0z9J{LI}T`itw|3w*tW^osp>tm?s~m!xaMv7QT1gHQY8#e3u@T_Lx7Cw((efehZ9P z;4|S+=Zl$G`lUY!GQU9TZ4WwIJJ`ZuL3yS_lP%=8^Tv^Xy}DejI!>Q-BQS z5c2T~jh3$M0$GD$OJ~Ql3LkWBTBxu*?9WD;8ajLxjb?j_8vWws@${OEWk=35yT4dk z(wS9h3;2kbnFsb0FeB&X>%y_gdkHWPL)i~lo5YJy_d%e~0&$8|<|ArgCat;W2tW%6!jZt*U5KEmcH4=*XWdUB0+xpYQJYV0O z?_Pd#b)szOCK(xuZ(*#Yx2;=-e^`yaQA3dYHTcZ=bxq)#?!dxzq;Y+SP(Z%SQEpmv zwUEeq2Fqwom2ryIyK0+BMSN+P7Nddsw}9GrBa>8B{keMB<^3=#?ED;lHAjFgB)>}> zfwxqni-lsUVixoo?q~$-42}(+J9m^$z{x{Tvi6IoF2@rBe4_5VP?m$u09e4A=7@8D z5oY`s8oBO78OhGu*6Q2%34h8+MqP9%z*vmAv%}ErIo901^PTr7yusrC7>J&CZvpd#9ToqgVh;H?UH+ZtkGzcDvf+Zxc>!4>t3=Y+-{iwt#MCopA~dFCW-= z%aM_{yOKz_=jB9&=b_jZrS3kjgjx7Vq_FL*1?ICSFgh`_YNRJ)Zcyt{S+1|A0x?BjXQI{<9l5MI7O z9s+!Bw4q8Wk>qW!*q)YQ_8CQOjc~VHfau_+Wdhy1BapXPD?5TqP@=VBe(^Ah0{zJV z_E=FL^||BTF!=L_TCyf_q(*4>;cU+__;X}@;|20*qMM&6GN;#)?m7p&j1qD&J90a; zV8cI^!-qd>yHWyb;-%mAg5YZL+$hhYf;;=(Ob(X?DJ`}?l#B0avolV|Z!n9>ZYLfl zY6h|`~I?9xNEX#0 zN*7WJhMyz5iqllz0W3%_{iL?-5gzpeI_66s6vL|p9WT>kMvopPDBsiV4)0LsnNC~e z<(J=1BIHNK>l3gKAAZGB*N^%xO?I& zXnI(e&u>drRSC^+wN9Jk`g)*N{Q~1P)e~-lPq;RkEtupwiRGoD zVJZt~l7!)0oQzuOUzJ;&W%M}F?=;eMkutbm-=h+JnqXjtSu%CcYg_1$MUyQ&hj9&h zjbd3(@;)V~uSzAonuUG}wS9LG6(;&MrBO+^5$4^sNH2(H8`h1D=nF6*U30v;gZp9xk|I5@UuUgOYBk`W(*%_=+#K~(g z&`+W5py_AvWU+(Isb+~I^N<}sb7}jR<8fb;|L6oMh6;w{`yMGc#)TQ3C$8{R^{KYD z_FxQ8^Z9vlQul+-R`W*yem@<{(s)l7sSyFK(u~4Fb>S|wkcMbZ=`OX1QwY{r%R%tg z(!L&*V1Tda>cxse*Q)ul9hvrOe8jlN48kiBib;0RsWA$h$Xwid25kt;aTM`t9Dbyj z9TCtl5mgdl=UUTOK7}pSBnPjQ+eSPTk_c>OA9` zzvPuEIf8Fk{z!1q44mg0G$7Ys6H9MO_$1a-*PV3YUQLuR*txDr6%>m6YY8byfZ(us z+6`Z$_PNt(63BT>PUrcpO zGx*~gsttp3JLZaGwNg!4d&!5VHi45(fABKcv2^fE!2PrdC^$qoM{tuf9UNjzWX#eg zx^)^$k+@dvF-9;Cu*5}89?ui<_l&gHT%stA!i`g4Gj{uso{=;x?q0Tqa|*y3!-jAe z>JlG)CuY?C(Iz*ba1NiuRNaif}a&Z3^NCJt? zKk2ZcKKv`=_8(2pe~(BBVH+oXOQZiDgjNwU=DqxI{+arevNM|^2zoYLQ|Bo&D%4;g z+I@~+oc)rSZvBGs>>KFpzY~-wTf3Nn?_7~}PH`bhT^j(jac7Z(`{^jCb| z6SLy2kP~RwoCy|aNvw|3U2F2rju>O-pH9IYXttdIPE*=H(-amI4DEk9CGn4*{&%k+ z{2K-8JK7r={RbujQSU!s;-3rt0~Q69f!F_Q0;{b%psJv~uTxZ=Icrvd=Xr|L!bqf| zWcdqfYG#86Hwc)~V~DKhS>TGhj5^OEqQCTANtjGgie)At4{m=nrb55m?YF~wCmU1% zxNx+}NEXX@F170(WZZCf0vyhlTLCX#?%{vm zqNS-R1TXGyh11A(QtP_2DT=EZzy*XfGBJ3d+B1*_Vxno4>8GkFL`uYS5AKgDe;eMX zM%T2HSgA*srnN=e17nvxc95_bOX6cbz-lNq0T&j`Dwt#`#9wz51^lv{fI_~x0Iz|b zO)Mvz)S%||7?>KISiv5|s+z$BPl04O{N&DaU|4fiXH?+OV3j1xGunUT8f}@#@A?B3 z6s2r?kd4y#fU)ReG8Q&VI)y}?RXUlHHe8Sb$B%0=)LeF%9X>5Myc_EI1P;~ zo9!Bk8k>6At(?V*vexO#ZgrIyg?go;R~LDkxx&c7;?cEVipSpI+iJ1ivsl6iy7dX9g5*yJ<~d;_*=}7wBHI)$K;H zzGTlil#ubxSbgDSh1(%5J}`hcVIs55TWjgQn7QUbUQ*M@sVaLJq&C^0Q^(GzDp#-E z@F%(E(b3s{h`-^bHHoat42!G%R6XJ{$og|g(shL07>vZ?H4XOkPVS)u+(BkrU6N0= zBkd?Y_YF`3t355|(Sg>p#!Ei>C1zFWeb)cSs+^d*U1UU7FlKK$lIxGg5&R{8(YsXF z(bBG1ybA`L#bKO5?{qrVw8*9#1->$#?#}_Yr;YlA&vp}@qjun{yW^oqgeVu84eH@$?xY8?%Gy-M7A>$Gpgreg=_4htr__@@78croe652I zJ2e293O5N7CqLjl%m(g$VDDhNm`Fm_;bq9o!~G`B)`*p#2m( zCy|JfAr(pQQp~XYgxkQ3cguojUh28tT_9p8#yQYSi)k8DhlWHtJ)m4(yNJXiw%@FDY`^y&Wr8vnHxvNneQ zV78%~r7p86}Z@BtIX7+D|AKuc`cu+;V?u+Bp0$c(X5C zaHk7#ON`j~Yybp@7#v|EJ=yE#=eX28CPiV}C@*YZQnw-twoPojz7)O>vRA_S@kJ;( zeA0OL?QxCcWf8iu%fmavxyJM>i}+`T44(>(SE4OOag(TXI7y~y<&=(v z_eYEK41K|P|^Yvi1w z?4y)K)f=lf1#9Myt?q4hug_F!(iFV8AKAnur!I}$pJo0=W-(yp7?(f*QUKgi{Xu5` z#vlX`(6j)D|HdDTzXyzjHE?@it!MccT-2+s0>K5|n{2GijAOPu4pMUn5xE(3c^wEn zF#Ulk=!^zEREw`dlv7+p?!*JxuG3UEq%oaF(MVY2;lo7iZY|)u4P4OX z^19_L>;&1dwEdly_W>a7;@LZKUHARzit+*vW8iWdTHmPN!<15lBwo~bw>^Sx z^f)mmkCu;`9obk0#ew^NL7t_ez@hz95#BFEaN#}EWHf#x4W2w)38HA)K#h1fvx1%@dgRCjYFN6R%(r#EJw-Cq~D!HfT z-*dpmx!`zVkK?d9`Gk|>P0!5FuZ=jofDWh=5U+rN(2EXKX@CcqhwxN2n?CQZP>)xy zCY(*06&M?h|72V5s|`N^i9Zt+m4myxKQ>NWNc}0T;=x{=-^w-{W#BSnga4^$KR_RR z=xa+dP_9fUPVfmZQ|#{J)Nem@VTA-m$%_JVV?sfRNx_UYvafE*fb6{y7%p-?2~2-X zI+MWV@@QYJx0R;3QUKs5=_OI1!{C-#A;34`&Br_Zmi{;gQl%mmE?q+ZDJwN0di-=0 z2m9w^1&)cP#^|s9`#XZgSk;@98GBtabv{|MBLQyuf^^_1GtvktGpb!;@*uTb^z?2y zLZizc2hAy`GZB!!SWsI*rR94AkgIHVzwTEcSW$IaJp+hB z(8z=)uwNZ}D)2+-%;24)*Og&Ld3xnDtKYHy>nV8H=ZZ*ao3k_ZW?ZQYC?YT}#*pKA9kWsXmezVs8nhSk%t10V z8GY0~0Xx1j=RD>5&L(lK8aSI|k?az7V2()x<;a z;Z&;38Mzx2Y>d6G#{O|N?O$>6?w!19M{iRiZX$O=APErt(ra3p0i9GPfA`(FlyKNs zvF7SoxLUVy%>wT9a8g|e?iY@_($e+a_k_`-nu57`X~dT!vz?Yow~+S4)yNtXEY)jG zL@J?pK$sL1U9H&Yqe+k;M0ln^bw^1VAqJLC0vd*p3(H`t8bGb}+DB%@9qif6x9&>p@zhGr0v zFos7gAB&%>WjRR5xNmXAHw(CC1{O=T(X3Dx{xt#zgM?A z!E6OVe%=9bF1!9Ngmj;cQU&XXdtvzM1o$4}kcYYTG6W{id<>GFUgQwKf_|JQcB%{| z;xT}9NvP*H9qiNmuwPOa<3+x{*QiGN7FS#5IOGN>kqD+D)N43f(A(dx4+Lz88b#uX zzObMTFjFefy1K6W^`eF;ZP8t&=W zY+kHxg|<*s&kVx=t)6GC$1c@|`2}|bXBH~7;ni8!zFF6$`OiHryEkq9gRzZFm#RpM z!V?j$Es?G*h^;N$+$YUS?%zJ54T9b?a|bo3AXZ5Sc{s-GMy>Ni*nHx!KW`bHQI4Xc zH4#scFuP7YscuHyDbL#VkrTgG0>Ip3Lwij$GrA6{pghDJJ*fbX)7MF0b^E~@9TrP| zJ6n@COv+fU!BgrvrP7l)8DpeRBEe-kgvq2Saf`?yfuN>R&uw{;KrR-MzphssXo>Q3Zvi+mU9~U151x^b=q!MjsvNwk><0diR zA%0u8O$^l1Frd$259$Bfx{0{{@768$ud-Il=$upC*K||_M@zKH|ksry&M5B^V z#mLXkFB)QBPw;1jPKC!X@!VAzP(#9x5A3-7{)rynXaLg$Z_q^dmDW=z&)dYM^;f{B z@K3`wfJB=~&P`6h<|BD$=gX1Fhd}uf0Vpp$OMwCi`!ici!}TUHwE5y=&Mp zXHt6Bh>*-AlcU680e0Yo5LXIV@DE=DN{Ky5CM{S@iNpM(;PthLne>v#rLyT`>811O zl}1N6v>U>-xJ<(G>vN(R(50ma8IM%m$5@XpS;#f1j`9%(uqYrj2Zno-Ir&bJF89N%&WKT%6iig|Saz z(Tk8HN(L>QdExs_vvJgh^=nneI5o~3uM1AOdA64L0&r8_lXdy2ZuDY`+hqs3db2Bb z!`&hDQ7VUtPj;k z`Bc=8&l?hGSBrJQ6`+hq3tW|;_HWHblb=krv#qCQYB-+jbJEG!KZ2v~1}~N8FZu{h zIx38IPCUnpe5OS1d9LRUefVUty{HZXQFKj4Y0j4iuASk@A#b9e&; zH00^M2BeRj--j;@=E| zh%sWU`o%(gla;|L0Cb1+2kS<}H%bZ;S5QsTr3idv=@Co9J4icT5Ks;@gz_(h`nSel zoYnIofH*-9BNaB}rZe9_8GC=~(w(ubL>4+6j_b>0icw72js?i6-UAD!@9< zy&z-(e}t351wxEK)uKO#!9|p&L1UvdwVPgR5CGG*I8@ndzyX+Oyd)GrPZ4gEUox)D zc~R_1Q|o2y*>>kpyWiTySxOS05X}Fn5HCK_oftIH{SJEXwbWZZJkY1$3&u_qYz%WR zb554#a)ogRayNEG{pm{8XmW|E4t3WPGZTUDd^?xHo3Q+G%{2CovNM z4&eD(UJC`6hjm&hZlB9VeM5-+D|>x&2hdfe`GTmx>)R6No38$l%XSNwJ#9$)IIoo~ zFjV_moP!Awd~v@I`eNJTK_qt@Q4CP0EF%=^t~^_YTudwZAQttv3m{ekMripM74 zozTzsbI&BKu*xOp$J9YD0`;NL@7b2aiWOjvicf#Y|yG@cabPZ$}X$dk79?M$5g+(5HDvqYw<`Vd4QZD z!I0B3dT>N7iQ>V><@k_|!GO59s|tXg@ph$2P*_2Z;=S7(@6}f_tS_$l5U$7zq;eP{ z4c+t^$z?jh%e0@E$sM?k-_Mi)N%_AMJ_8j&WQ~bor2|b&n6gemR=&u z)N9%(k3Ztht4%uUia9?P@CZ`F%tz~v#p_yP)0}4`cu*myPZbHw!-9$HSB`F>5x?AS zQH4`;qx1>S28787nlH4!^2t(U=DdAm|02|T$b-{tZf^eb5)Jp0)ZLIIuLvOYbOr3< z>FbWJ7XK^-$ted1w@pV*N2Je3M51EoZQ{~bwv$_Uk6)Ui5BL*ym4uOPv?URoiITAU z&>YNshSR>aWXL`9Npq`8hn0=hoClLpCTI|%EgLcusNrL6G#x8>`nvpo}jwE!hUliU$O}On_`iqrmXSQ9dsQg z+Yj80-!|UfK)(^5!&$a57@%7iN$af#!IN$*0bDH%V)fX=0K*;Dp-k~`q2o$dN|*A& z+TMqmBm4k8D#BuGGRG8o!Rq-Oaq7PP{k04hAGAca=Bt zy?Z3$ytnFn60=JIyG~`%IvB{mlP>TDj{GI(D&HDkGg7=U&aeFbdSAW>1dJF!I2LO6 zx-#mr?Rb^NJO}#1yDPG{H<2X3Nlh`h0l>#%t_ zT0h>HFH*tD|1t;ty3zVkQmq$5PJ$s<*gmj}fo&;Ykf7hZ?)$9*^b<|j6pjK@f|*X& z4W#-!odw@$iQ+=gj-+vW*J!r4(;tP5Muo<&KH!E|_rEv1ApbApKv>UF?|&NyisRNR z{77#YaIA9_tTLu6zYvSR49L_f#!2Dykt^hhQ}KRps!UgDiLQ=TCv`te{Y7riAMbt$ zx|f|ZmI4+$BUL_s`f|$gX*1&I&%1lV56`Bm3Im~Raa9Hti9g*{x=#Qw8%%K&B+}%) zqW6;0V2C1)!<|oREK_1|7Q9Z_(CHLlRCmv4)uO>^On8pIu0u{FAf!C=G<(_C%~;fY z5vcTx1Bwp1l#17=+5O20LL+=*(F+9~__LfbO5^g@rQ$~jy@)RHb_A+!_8Oo&4tTm# zUl&?wVh)a`5+!sW^wR4>*MYgB zMkYG%vR6W9$)XBJ(^;V0zjhEg5`f#_M+_Trsppi`j#p7Z~j^8ZKY#_!MhzgeerC2gBoCNy5zR%7@L%KR<} zOK=FQ5Gb6W%J>d>VwsYG-ja6ibQPX_nRqVr7uu+RDAz5>%Uq;I3AJ)saZR-4#g2~q zG!MDcO@Q|sNL45$f~6&<0)Kwcv+K74K%ntT_0`aT0j^3w+f2>mWzIVMb4G;lA{^ra z%_Ydbhkj9;0+yK@Pt3}V6IuQuipHtZ0s$~d!7YT~V6>rY?8ltxqf<8M!YO&s{8YHg z5_Onjd+MhCsrw2)bVnaVulGFU+vSOl}sgm9_teifn z?}{$8fgY{XA&!(=AUSFJ(zW*F@^#ZRv6p6i)X;}MWRoHcX2DcVHn%$Bgug~|TI!Pt zM}djwh*rV8%v#E21CZNfXeq3cjeh1bq9=Px_X{s%Ms1i08??Av z2xz5t^e&*JES}CEd*X!P8|x#us8)zNTqJWJY;ZcT-JIM< zW&(NjXkQ5DMfpK(fW^7GLuYWH%qjHT0n12S*TALLL_Xp0!x-QTpIWvXeHF%G?bj4K z;%qJ)X2n!;b6FS5IpHqe2!nV$5f&L08r#osg{YfkEOoP!VG%FlkzSm$-dr+aTr%-j zzIfmxVD2oTb3XL_BQ}zh_r;dq>ids#kxhR&m5uXnN!?$k14d}+G9MrORw4=tEps^L zs@)T1257K{?c(mea7GS=DOM`pAuN9S8h-%-Ouq5AGr;-vh}!8Q`s`VL%j@*^J_h33 zeN5W76!L8M((;!H|C&jwAE8dCt)@FEp(m!zrn&QdK3%0Xg5WEd@t4>x<=2Vi<~twX zWqiMsj7TJJ0p5v9y0Y_k^v|Xn)E0gi;N$jjU_VOg=3e~j5c-utCx67Nowef8z<;Kx z(<#(CJo}xVrBVF=;pB|T1G5q9jQ(cW>?5bfarF>Q@!pVm(as&rChdc4_dQI9*qh>w zs!xMk!!36cFp>1}sUPg$fU2~fxuap=TJsHPSK$9^SS0=l3z`xFMzy59K*jvg8UiDN z6#vFEeS{W_AAy(K&}`V?({7c8n+P|7W!|{O`{Dw^w?i<;ztc&bP;f{ zA|_g(LKFXaJP-$!{ZT#s&p<{@Y(s@cYV^Q@M*1sU{G+7&(+F6Bk+>`l)O7A)L;p+G z2t1(lU$+F-A~rJNLQDM>R{rh)BnKlqM>8uUL!d!*)C2PS|K_q2fhTMHZ3p=8D;SlG z?395`e%oN>-%UhZ4UB9Z&1|gyPNDK>Ik_Gttbxp)B}WpNATd;%pkSF_LE9wu8f{^= z7oxsw&!C#b-t=;01TP=FsdiUuOmnV&rnIl${5q?YTey#0(~o*8u7*nc{G>>dg84QppDIs3g1*vhc)ny9FzTwe>&RFdHgGNTnY4( ziQ=tzxcH)}Gi0J>6=rLP*nH!8Hgtg36W6*^G~gRGLrUEUU8zd7br}SXFTME}1uKV9 zjQKS-6{;4=Rks)D-}bOvgt+dv+5iK%di~cB6$Z)>fM2mmOqhSh#6LfXf1n^`DLGUH zv^Q&$6>1|5^L=7Q`jF37J_1PKlE^UnL?WnC;(qgW=~LZ&wn-_#)LLC5>A2FY(fX{+ zEq2>-ECa6RR%YVFeMtpC)0naOIAH~NdwfCf`b_YQ)b|w@q-it~VCOAO)pX#CYoH@- z@0Ll3*p{?ozY>bgunS}CCH_seTL|JJD<4X-o{fLRu1eX7AlyuI!aAVerRlPF&C*_e4^s>O=y2$*a=6iLF@AJ=Atir!dXgvb1h|szOAOdw^Dm5i8%S-8jF6pu`M~Dp!bm*E?mIxmK2^ zhT0YvAzH!tx?tRd8?%2)j$8DuKc$-1nB2YCVPk1@5$ZZ0FpAD?!JwH2@qQ$o+ZGYR zFjm_1BPyiSRBhTvHlV^dZ8$__=;~ZrsrozF7mOqJv3PPt5*i4*RK1-qLn@cz{YVMe zKgM&R_047p3|toJ_OV3kZtyY+>k$%zj%HOETq<>bR%|0H(JeJpcMj@qs>bMSQa~Km zLms&h5%(RD0_gj&(PG5OioYlIv2nE?spg4u3`ZP7f33U6sxW=PQmoVD3wZItWB4_- z3>4f;Yry717V&eVACg@FvQ++S(`V_V5KG^rQ?%j?&$JfoUHT7m*q!{CxPC6!19Zd29%0YNW+EG`6?*wRKSJ{m z=ff}(W``t1RTfC8)CDNdlwNWyMy3^4vK=FC%eKD}Pm@JU_j!M#xWo4WmG0{xt{U)w zo4tpJaD`ROUkt4KoZ{p7h`NH1HDp69B{mc73c{;@=E>H`k;3WiA)Oh zsxK5g!25;*#w__wKku3Nb)fFYw%eW57T#myt&Fz#PEpXz-yLNRb${0@a3+=hKXAam zl!%C!|5(rdY%Q&ftp2_9{d=MP=Y;#8EB_E~|03F=CuM=+#Q_s!>MWACZZW3)9SR-R z?j%z<2^n7sJRX}>AG!w3bp*aNNW!kxN`o};K3_?Y0qYdT&GQKxV-uatOx+J#SO`2z z_gUmwIWj&8U~7+hX(lmcA?u8Q!91npWAPG5dRR}gbiJG)wYZ!%eR!x^Sr37^@|gzG ztc69PTvJP+4)5KNQ!@Z3T4anVHk@-ckhhduav0wI_HH(oI%Tfbim5g!6z zMYrV8jaI86WprV_VXHa)AvEQNTa15RL2#d_)!`dsp%mVpwQAwpZOI|vCuNEqCa557 zmVjGk8GCORnd#?)>@R0xC1dikkz8jNnjpuoo|Xou$8n)0x7T2iYxE4Z@zu?)441T% zGORsGG$3GcU0yosT{v;em9?}2{_T+S&6U-ay$~Nxeo8lHF;AZ*v+D(3*c(tHdl3F! z30i4wTF24fCAONY)f7mO=e}5*@X4V$}*%Rg~Dp<_QqrJ@(3}eh% ztE#P+SUa@TC%cMx&M1Hhm=@pa){}|4*;(<|e0tDH+%wsO9lUWqYOm~g1}7Cdk6lxuSWrH*tQC9+?nw-4VU`oIY>w-ytdX#bCSQVCqStEXvZ^KP@aI5wo#F@n%~*}}vi(dMO?GtNa@We~ zwy@TTLqd|;>1iG5vw6(}zlk^7G_dRZos-1L%FD|>z(woV9|!s$Lm9NC)Ci~Oly1tcP~GjhwxIOQ)-i(7eiyK zIQI(No~NX8#YIJHPvP`AD%U`b>oB*C-@%Fguo$$*rBF82g=54~1c+_vLXC4pyH)*= zUX3PXLYI^EDM9Zz3|ym5Hy2xp^M@**?sZ4@rJ1Oukx)`!I>C@QT#k-Sx)1Cp+p8Nq zS6VS2s`P zdGxo9Q)~hch)ns*j2Ei_6QcfZy{CruXyb^9_JF!Wi#^4nX1{nRGS9StZdyy2p9D$>T)x!N%2N zuiP0V6XH8mE;0wCofHeGQbdzFC-GWnKX#IH|aaQ zV8h7Tp#QU9RI6bVeVE*iHTK3Qgo}Z8JZHlqMr2RY zj6P7NmzwIel(~BLr!2cLxfP5SuuL&_eg) z8Z%Oc_tRm&d3BRt8cx_Z*Jbkt&e-a2XVOVd>C1IycS*cX%(TrN{t1{IGoo4Dk{ZjR z@Qb2@1(uerrK)C^Y=tdYX3YM^n!0j@$vV`fY{L4*P0?SC^60U-1$CX%MNV1U44CQ1#u2mc26OwR+|G z8Z1zMOz592h-M4S8=Wc6h^OI}%26=@U!d$VyZlONN;U3~F>6MeD(N6mG$`pDM3GUi zij9MtzWs1lhOFD!bEpyoafSB`ZvohlG#+zQ?LBr;zA?GBz&v4gPB;0wqxg9?wW
#Rzl$kStVb<=nl3zcud9(u>(bP-4Aag+ZY5D~H8vLNYPoiC=T zp-~dScY<0}FeyDS9#OB#)r*360fQ0{7Mka}TiikBX0t-Z>p@4k04^K#-U0~tO57=0 z8oeP|H+m|L(NSBQp>{w#jiR(IrsZ!{e5&YRC3)T0FBmB<3 zO2-$CCd|uJW>;eQ!v}gD%-VyKhjiB~owDgGkKX~&{k31VC%N88$N<4{5%5bz2C-4J zcJUFVcIctR3(g8(d+kWKbyrNtTgK`lI(OhHs%jJm!ykyAZG3|^@`|EY>x#nOM2!*1 zRkR%8E681-58CGFxN!}w^2?sN7qxqOxb;ugCPv3TTNq56%9|=-O6(n;$o2j*a1mC0 zx}4=3E6y(YB{~mFw*nv`teno*_-dJV6I#kJC!g`|XDUmMl2)ZCaf<|{UZH6;UDDI& zWSbss_KCwdUg7wLeE!=*v+e1XmSgHNx~hFX*TCA;aZp%zM3JUD*{W6dI(b@nK6}!U zbKxirKK(V5nVA*7N~7AOD7dO<`}sj+4)c{pp@EnujJ=^rlO7-rk>Y^)#KjujMpQl- z--!Rj$O?W@d;^gD%ECrI+Q(y5+O1`dc(JdGYEZQ2^o-h!s4cnYFWrEgOz9$U&HP^Z zWcOfhe_4rP9KjSx1AY*d&@IQ6u4$457Ee_OI(TCfHY2HtubL(E%9ZKwU`G%ReNSLd zVD#qc-R}wMK}QO+PUW0sS}S%! z$dg53X{Tw0V8rHOmj-#;VZZ56HD|~<-$omFJs$x;z?K`29-v;DGgCglHvT3&Fg z;H>+XJ76m;#43qI&?LK(HW0g%!vo?OQnzvqss&m(@B`Fw7jn)^>n6n0j z!a&^l2Yc2Bl&MvwU}f2UMT)HCc?S$VzEny<*)GnqQ1q{(ATqT?HiqEqBQPz2o9Sf< z>tL<`LLaY$t_~>QNOUFg!sxB_A$QhdZAJU8@*Ys>B@Mx|?G5)5 zKP7=#Q7VYo97C*rryR(PIvBfQYbKVbfoPa4z zepq6_C^07Ctsj<=LEWfK35Kt_M-sFI5Z#;&*wvprSPxzqV+IYFxfu9_6)Z>LtzOVP zwJ&pjUc03(kv#vF+KH-E?T=oM9?EImqh0Z=q zW@EPDW(DUQS?ZZ|p7t)=c${ENud^sTO*%$M8y7J~^MTvW>rEi~rXF6w1oNT?u%fZ- z1{2I$D30JjdSl5(E6R{^K}R#OJvU@FktGZPnw%>N_^~whUMFN~T=-Pzu6fxQu0(2MA4?@8CL4L>ZTFFv z;~hkAFf%^W*BzH0d{3Wy?(0U|8V4t9kg~QY;=u)Pue$E1>k==&Ty0P0Q{!92s_z8* zt%uAV(9YFQncrwf_R}NW!9x?f%AC14NNvAe)r_xaeM5#Kx$FS%#0Rv8Py%R7`0_Nm zWd81Ozt0=a$-sR0(1Z04f6@ORSpDrU`VUyGSF=!7UPOOma5(7x3?++&B;qD~`awV= zye|j^ydO@M2_uw^J!RCk8}_CvnHwUuVY-iJrC@StL5yG_Z=X@S)W4x(K7RA);^j!g z>iy0J;6$GY|9S^V=3YcDFiS7%+1c=#SUy9#7F92+pc8JxH55p;nCe}{;)(l{xp zdG{ScuFgRV;L8`6oEN>)6wfYpQw;sdL-5WNYVZ>@DW|m|5LCvtcunN2f+8At^-QD~ zk4RHM@nPI8USm8(uoU1o`49)#%NtLJNSa*~$5mY_-Yn28(8 z{4dVlDLS)2TNkW~omA`!Dz;N`#kOtR{$u~KZQC{~wr$(K*?XUJ`kvkQboc0|^|Id9 z8gtBVe(GZ#J}VzXFkvb|3F`!%o(O}YCYqMhGM%YzBYI?31gNbt&n5MbJeTX4)M~=! zm*1%^55M8XKn+k1s;$uu=C)$f46IUQB z)v1lNvEqAbjE+>=RkR%(cQ&46VnXp(3%_rP7 z|6L(SnpIdn4yabE1;qE2whNXH^lkgyli}0GNFdV_klXFsFM;EZFZmxR3a4&l9*GuA zxf80LhM|YJ+lC!WJ)DgtXEe;r*4*V&z*)JBaGT{9dB94%XIK;|Mq)Szj8;H|HJ6u| zF25`NUX8jmgNih|Sq}JZDYjZl!P<{kOB|r>nUYZ>38+gdXc9OYG$8Wg4fwegT;7S~ z9q=>w3Z*?yPX$~v&=!q%zWOr!mlYIm-=siu(CN+!5Xn01j&`FAsYz4}>MLT`_Y1xx zsxAe;d%qC{tUk#K?3#x`-YVPAj0NfH5(PwENf-lUfv%Hy*xcjW28^FnFq2Gd7ni)O z3!aypO<1V{moZt=lzcC$IAs;#Zu{@pxf%Pfb)V@m zI4p-&PX4?iOZC`X$!>wwf*vdNGR1z`tg9`-2J<>2=2g2V$+L2Ek;!YyH8C+^9BHj* z^CH61UI=ax!(!0XjSKar&koPfVlJTtmXEhafN{kG5Z^{`Z;Yazcm|1%?IU#wBg@Z6 z*uN%Y<0&)A9v?-K+xZ5clwOM)fEf`SUIxzY!G5|aV^3(gk&HLPa(;~7nQ!bDa=Un{ zA}KWflU23{NRHxs%@$bdi+G9|rMEPFw;D%X$}gPHnuyPDmf6O{th&U`cBqtRy}L`} zfF^cv}5O#&(44A~0JNG`74*giHA=^EgoQ8I&b~Eix?w#=1rL{4C_~ zRp{NHP~DR*x%i!+_fT-o?+`D@ejXiC8=D3hL8nU;@}hl1#mlD_H#B-)qw_9u75Shi z%g*;+CS1+cH^X>ynaHS<$S2Jv&^YQV08Dw;X?O$9(q~IO%nIgB;f>Yfu{_5*P{4tICMB$ixm1?EP2`f(0TqnDGdv5rqL2&!S6F8n-Qo@xnB6`)~KaBq#KrP4K zwQLuTzzeJCA9Zx}rHIWnEB*(mmhFT5hkoFOO%5 zTSqhX$#dKCEjy(y9PJ+$LpBb9u37_;A#Axa))W_4t(>X6dkAzcp4i)WG*1oSg8BF` ze{;o)xt80*4Y3Km5Y*nEGjzUjCyKpX{> z&sU9v+pE&W>&~~S7Gb)7Kg9-(AzI#^yETnc7}xMv(_ct)R2pO3wwS@dQ2tPkwS+f+ zMD-YBhP}a^H3{k;YlWU41AHEnxH+P)vns{nC!`CNy3ngr4EK#lOmc&%22}*!yat`_ zbFR^eG_3-ibw{NHjp_@M5gf0$=Mp7pw6CkhKiUVEZ!M$Loa1m2kGSwUveT^!q@yo> zDiuj8RED=nXu^es|EYMVjbT=v#Tx3#b5yO(zeZrqQa3K7_0_h72Q*#P1T4%K-PntX zSUbYA)c)z{c9QCh!rkI6i3gnsSlub;D9ZB6F)i1iZor@(kT?jFGJ%d72{V`cnRKyy zaCY1&BgZ)j-P}KIw1Hd2XrfI_SXC#bX^i3)XL6G$k(5-A>ol?(ff5*ONTP_!uL95Y z>9-gqrdJ*qS4gCPg8$F*ll?kC(=g<>Z(LukivNvMO8Ux~{Xf%s|8+?HcYg1mo>i&; z6kSy$SpCDcI`qjVRwq?!W)c0ur$Gs0$!~-PQX%VCuPQ1Ankrx3d1Tqxh9DYpnaX;==0leSwx=B1U2cXxc zWQU1-aj;NrnSv4RB>MQ#!<#F%i8GUGMRyS{*Y#*McmmJ_Y&F023}ry$2} zLr#8Sd^6;gKkQ@rLGoCJ67H?rDBhKLNM(!N8t2vPJc9wjy^2?Xvp;~guQItq%UFMn<&|X|LKx7T-!Uw2sPdrdT$we_nH6 zFi*CaF3m?K9I}>BNyFBN+a=KSt27`qfF|51rSbkg=-f$5n=RdH5vG-AKhGKIPE{;~ zHnd~1x=g~0%^0XZHi~a7F7>Px;^Rc88m)oP$!$VCBvj-YEMHGe-)I5|{M*zBavZna zZ2{J~YYp3O9-wBXcG0+;H1kZ@MnW?)#@b$KGfEO1ZhlkUWcSAcd!vjKCi#gg;^pa_wpnvTZKPT}c(wsv zmRw55SfrrJAY>@3p z1zb%1yFr0g$c&9>`6baalRR%MwJ)359hK@CJt1a!;^f;Q8Kij{-^@_n!O{VHk4- z5fw#WL8rbUyG$$q)|@vAOPyryL`$8-PVR@~N`VUQL9Qf|g~_raVk>5~!NsUC>&QVt zAJp?7{%5{kBd1tswqZ7Dok-{+4@@xx0v_2AJ2Yup7}j8_=F+iAd>9AX zQjdL+E;C=q;=fOf|MLl4?BB=4e`qpe#-%|RkcK{mbiecRq2B#S43}yb1!0&b7)}M^ z6Ksq!M#maqIJXU&+-`wB$qrHo>vzM}r$`=d9S~x@WJNok~`TFMrR$F zQcu}(Pg*=P-Iscsc1lZ$WOGf;il$H#e*f%ZTkoK3X)sM|sfvroX|>bvumclM+@XRk zsq&SnX(LMqI_ScfmeZ=l&nz3+lqU8Wp*X8Y%t_-(7q{Y8Pj{%?8!@c+`p(%}3L z^rfMhgT;SQnEx+O@E`B~ZzSfpvV;SU5YmURa=9rDumA260}|QyyKhx)fx(t2y!uN7 zynaAJTUnrr0aHzVS-Eh@-ZztpwUQl3TY9AcR9PXr12#F-bz(KSa>Y{16iNiny7%OZ zDi46W`@z>xe+BKvX+F>XNhNl-Er8Fn)3dv6{F6tR&{qi(x}2o?f?)VINT62(+LMUB zof@bKD`Bp12|hDNOhSMT{*u(2ZEUF9Xw*{a{&$j*0>iQTGk_8HtDaHlHlY#?6d8@U zL4uUa*RTdT@vc&tB)1F6R#ow?&t3Pgw;bT5vvfY}ajBEIkW_a7KHoZQ(h9hD z6RyMab&z3@jyXT|xmoNo%kS! zRrfaBPcP7p#2j)EUDnnd0ky~bvg)3?@k;7@4mWd$ea@a?_y+HHktYMcAn2hu90dpH zCGSLWyds^0AA&!2+pU~Cr}Br+*JN5znnibi$;B@8o8nLM`2Bt05{wAO$KP2FN3=U2 zvv_dBCj0x_HV_Ut4I85;8Pir~_(9Un7kdjY;QAx!+bf$8LP@%93P5?Fyv@dpg%^^k zMQ56dp6z%28|Vu_WamNf;>QHOBmx?BMt!{l0fJ~t(YRL-eu-Kbp$LIC@dvYZPlOYo zL~n^B@V!mE8|+wTa5O##)6B1UX%iF0UvxDdN*<;CcHD7{ZFbubAj~DAb3CN{){zgY zX%RkeHgK|~^nzmd2asJT_OR4yu{asM=j0yfBEIo!Si?1qod(*j(#dwCr%ijAi{q8l%2s;&o`}OBh0%P!ILun9@?oB&4K+Us&O@}d?iM$8?#qqEGnVecnkcO zwO#NTW0U%eGWGiRR*Zismy(+JK`{T1%BBC@HUDc4N$J`ChX7MaN(M&-m3xw@v4>q+ z9M*Tqx^gr*VO@S=NF!bXcY6 zbRN6uxSpy}=30U0(IIDtfjXA>=wFr%15+bZb@40#Bkb)NNA~C{uzgbsP;ps&G$(0# z1`5g@=~%^QoD|O98RA)|eh^3bD7W_39h@P{UxHR?Q_N9aFzJtAzy#X)F*&b+_BuW# zJk6uhqIngy3nV#!J#Xz?WL8}nbWUY3?ykeED!4rMQKmRm?8!F~CW!n=wXV31pH>!a zI`wfnxu3dUc4<9{uNtxdpy5buRvZB@QIQ7*wd>Bjk3$ECtHl+fGK!>;lk9}_YH*Om z?g&%*N%CW#6odX~CJ^Ebc_PEA8b-M+%fYCCRcJY#Af5wf44TG1M6JZSti|*iQ!o5s zUBVz~7Jyzy%xM#UT53iI>u$o#$Y&rsJcdC$n2kK!J(F10A-f&D>OHPp^y56q56O4+=W~ z*T|23wkVmkum2|l;FtzZ4|q2f-WKdBb^3Q8pq zpnOz>7z2e)X8-HpR3PHy_e)4%%A&_$bxF-zB)-K`ynbjD0DsKt_h02b(FGKtQw{6n z)$&3Pv$EepuDND#izVIU6$}^a-CQ^1y0|#ap)jP?{$gj>dz#B`u{e6$QiRimt(yz3 zSyD-PNDN$hhSRF?Kw|r#^wg~cIdtl&9U1LddK~rewiLebNpL69Iek{ua!6FlB3~k` z@#!gq6o=eefb76<975!uAxS2nqFd0Z8e^)#z0I!0S`YRz6kRyI{T}C_T&ic@xCQYZ zS{~`5)1{$bZkC=Dj7Of-0oG){%Na<@wlw3g?)4i#9&)Q$BBq0y0Ty&_{f~%-9}$iO zq9K`iCw1&)1D`(^sQC0wok4SBKh9}87O#!-^g!bF05(Aryn#R-GwKol{0)&WWoYul|$(6jX`QKy(tOu(02Hw4AkvS^9L3Cd~lW`ug7 zPm~G-Iyyfm@L-uWnk?VObT%mi;8Q&M``qR|2O#_zk(rt4?cz+bCxT0zzPp<;y_Vw9 z^}5dKV*GjE)%68~o!4M;L9fm04fs>xSlw$N zJy;v7Y`-?~m1F;|ICppg$kFDGljaOAZ-$`Zw`@C)KXB5D;L-V?yaO9!%Wa$lv^JY{CMUWCfVwU=Td^9*G~xB!qb_-}8n!woS+w zQO6SLi!^E>KOu@taO6gwVRlRc%8?T#il<>8pRF9(-R2)tE~O0vl8f@37v%mlY2yO% z>QHvSitlpVpgks28UtocGAK*yX1D)#&lc$~#2XYTpuP;G2MvwhXb9}C*=A%?23Q(0 zJhhFI2D>qqNIK6RHB?=>fsd8LO{M-BijaCpQ3ylDOHwss1`B^<8hZ)Y$CXd1Bb#%s zYT1oiEwLNqKn)fAYC{IuDv&l>n=mvR&Y~|UlTIxXHaON4AM}3^9C5VA3ZiXYdN8t6 zmqa4WQRlmFP0Uiwj3Nm|N1~AnY|AOG<~_ety{`A!_`xAtY2GEOdWv9LOsAT2&|xQ1 zYThk|$brSllA|Z}*MLN?khJ%rdP0cm-o(OP?{?U#_iz zFql|iKFum;vn5yU2DSJ_c~&u_u0h49K3}0R7w5e zX}m?wW1z+XuvY|yd;1PxzVdn-?#{BC=fh|;Q)SCK$i#)s49F{0IkIBC!jP0;>MJpB zfvkh*43@*o!g&H=QzesdW3feXgBQ>ldhaQ3@psleYeqAOywDCR`0IH-y4snIL`|2j z<1S-S3^byim21}QAkGn@E4VpT{=*_8cV4V{lXoHph>K|wA&7~TYIS}LB6`%ssj8eh zEog`ARoR^B(JYP*tbJqWitq`I;J= zm#zMHn`YU>s4k+L%TyZ;J$RqeDF;_AN0rVSn5k9bVw>_f--1@F^AM<}(Tq%7Vke=E z7@EKV(16`-T?%Z||4H=2O-1&^SSsN+i;=0^|aTvyeGhNtCCGXFgK%FwYsi{yH%V>+FpL6gxTe_EVi;%BM@w{O}OFR=g zpy`h_RTf>~73(f2oez;|#mRCwoh6O0%RGOb0aT^eNaok{nOl;1ZSb5|G2t>&T}4{Bb}u|F6D z0L9&Ingotr_6-R#g2-D9PM~>;%CPya{@tH#kb2q$1P ze1QKI$3rp8IH8K)7A`pWhQ%o=_LJm&Y~wrl9>WN0w(vD^=#u(gQG(6(IgD}H4q&iI z*dMp0{pd>RF?@v65{^QrtM8f~!K1u=V!zhu6ol@^G(9p563+oowG3A^??oOm$n1um zggBmSvW0lSVde;2wcGk0mz~Cx}iVYLK&~#aa-^1dx?ET2G@dZQ@lK|tSxy* z9wd`O>?;A#1+*9{JwpWtl#>g&5aY{)0ZCyE^^Rtxqg$uxqb{aqY0OzU5_}K%$LVDu zzwBY^>ug?v{_jVy|IBDb{D0gc{!N1{_pkJ>Gxb2<`r9Rb)!jl{E|nt>xlmYO~q!Xyf(venr^gSwUk5 zm?r8OMD@`iH2pr~Y~J0f>{e&@u8Qpu?8qo(mj%fsu11a$F^?`*i5KR;&*#|5piR2b zms^Q)ET0wlUNnE~%z57Fb02RDa>|WYmS@JSI2?)_G*|L4=nMDCIIT&Ss{EoZ1D1K` zXSY`r3itF_RD$p>i|F+%{Q4_=SeUT@5XJXFFBKoA$UGD0kYzf-SrUd1W0_uBKO`N= zL_DCJR<3Be+Y%`gVQB7zLF2|?V`nE=s#Ci>vRBooTxP*FzMfhE=g5^w>g!&(Ja}S* z1!;1V;aC%t7>*XNrRjI7pCABkr@W-6%embAb>1cokpqP?c zOX)D9u8U@@#Zm_9?oh6WqQi7y;K9-7=ln z>39pUZqvUglfs;oEKXaQuwEpUsU+-7;T=`abc!;@dox&w>^b|FX)#jqg$euX%=hPO zk^S#S*Z=ST+yABL7+3phV)~LFUyh}@F~vc~!sEvBAq9#-gyi}_g!o|Mq!yr+fj8)h zYVX9U?HJ}x32BryR5XOompYl%G}c=WC`0}zTWD-FFSDvxfBc%=R6;YKYkD;$J%+yt5A8oqxhr{UfN z%vxYu;7h(NXJBg)NLFuAL5s@5I+>K5u?w_Q7jv6HQba3&-zMl)#F>Wx`LX|sfJebM z_^R6UIz%=LxGm=?o#?4u44{L$Pp1CJ9rG4zjgq(xaLhtI8D*I87DDGGdCNpE(MdS4 zsNFm5ea15DBpk1s@l;5)5xTNM*$mZ#?xY%jBzo%f1R(Vw?!V?idE@rT1iYDn@rG|5 z)}CVgc$4q!Lf+_Ny~aW5QiA#o&dF3RyXxkB(yKTs+9TFfK5?k93gF1t?zIm*ayOw* zqm0yIFzB>F8ZLP12Q`SSR^0P zl}0p+E{4Ivnva6BE#YULbS)HujM9xN$?~gCGg4OftufT&N8{mm3jL)UpzqPd?Wm<> zNPGYU&Z@%?ic{EGc&8M6`5G=cWEj09rY}^ut30D)SIlCAW^uf;hLzW21ob6z9uyxtCm3a4eAS zyg90)KUS>UcbE?u1;Bhwrv07tqkL}d^U(5*_ZlgO7vcy_mpV$NHCq`fsibfLq#Jy+2katM!pl%PeB7;g}9; z<;Ui-cd`348gi2JqLfYQFK`|7t)w0~LC52{AEh_*ClhE3OQ63?a%eMuy)WRE`UPo# zOHv(+tt%sU^k#R1?oHvnF%)|x4a7-O5I#&;%Rkt5+5qhz;+Lw{0!iPv0 zV29r`0#E=(Ju^v-3W^;DwYd|$w9MUV__s?hYoeW#)-L(QY4WAu7@_9q+M!))?C@2( z=LXxCu8}OWU@TdIfUPW;G6W1S!Hq6zuBZ;u4=cSasLKHdbZsPaUSYLP)qw9tB|b3f zBU|Y(+gXV)RI~Cwa&j5b70?oEt23u;N+?_-0Zl#yg{8#Qnm+9Xg(c$Jkcp+pr1PbN z@?Q#<_EhT9+eC-^p5XAmVmkqIxkUA`{p;c8m(HG+vIZo2;1D2v(%i-P1nP z?B~`?0?Co#UQ_RN@HcFjI_)<}m75lI8 zesfPVUxW5=(Xp&q**KxCl4I0d%>7D5NR$DEJm)XsYFhAEWc=){#D`I7YFKd9=16UA%;2_ge`2k#@qN`m1VDM#;g| zur4i%B@{Zm!Zp|OEMAjCYJtl?^>q%k>dzt;4l>H8zCNRO7t+LP%39!!M7X3mEblqW zuXjOjkKw}*u`E|CHqFG$R)rCvS-_}J#&bPaIyQFgz%4Aw=lJ}&Zv!fC0gV1z&9%+- zOU-Yd%s<>RBjMLcWopa=xance-vYk31U;YY()l=5>s~dU{aPQ0;qmr=Q)XT~_cPbF z{R#3|jU&jmTHq>HA%y4t z8@-=2a)E$e92^dX!bqP9;{&G*6GAqOpBG>W=i1|z=mF~wq6Zl zdrYBq*RL3eV@niDjEe8OoS~`O&)gweJhEO4V_v#aWPW!RIZcOqH3Q(X5@R}-&P9i{ zrAE(9=+uxnZCm!BiOlN=Z*WO1S*#J&MLM)|0bSYJ@-&yy@{&pSrxBgal;S`Ed7aWF zs>7ff!W?Z4sla@7fnj~dEBMJHCMZ+T{I{|JxI!~f z%(IsD6^yjolQc3JPnbj{beV8^g=w|hID?!k9Io?M7|z2n{X^c2?h@0hZA55J2fIEU zUSWRq9=1C{G^q?e^gOzy1wE@rb_c1;{oesKt6FI{d%bl~pEIVVq@jl6R|{BouY zPQx_md6@C6C&uqmYS|R$Y!ZjD8)F=t4|<;HwL-cHFLUH|g@8pJRi%ZSngjc#o(1FV zW|f$bqc)-rG60I&s|v%IDueLH=rw$0s;Q;M&F>|F?&9Zja?yW{<%SxyK&Q9{ZNH1i z=;N+pmEj9ZL_YlHx;tdjU&Sm5$Ewo6Y>2vT<{6;&O2m4>8GMn6c;Ia|=WQ&ew=cyO zD8|E-O1qpR24}HWB?@_=>`KivIcb$Da^#TNgmUYmr`nqT-GQ2T?w4zYqv| zKEyDg&$r)Jv$}T3Kgp{O5&U01U~Rf_QcSq_o=8uy9{w!gwFD5S-PJHZ#t6-lHxly) zFkBm>*bq*@r8Lj=Yy1WqTMK_lVH%arft^f>IpFLc7n~>FCkFMqKQK(@mKAh ze`G4z5sm?X*Olg2B#tsRAQTH~$E3C?I~vmPx5o=^8?SgLXeD~c@%3o;`pWlji;ow> zmuG&%k5p3}sWm;*7034(M)=}9VxAMaTIB|;a}@H-`Q;VHUEKE9IjCIgo{_vwkqv07 zd+>R}zfHefYH8VU#hy-4tL~TvT0H;fYw=y2zc?v3G=EAaPAAS_#EzJ7F&Mq-E zz4fellbtsj6Tc#ygHqh!8Pen?HgMU#!ueU)=^1M#5D^I)gku8T?-hwezv&#$=Qp$dj?5-2u<9f#(!VtfYg=#rqyDRcmvnSoBT$ZOH~8&@##yQyq{J ztY^LQAeXhJD9fmJ@@0RDuMqDlnoDt^!TwnD#Wex%hZudoG&U9d*EcE3{Wqm(w0VGw z{D}DI-E11F4w?j^dZmmrr_zEtagM@HOl6O9W^$tuS5x4kYaJ|N^RiI7*4g^xNdy}P zzOGbtF!+?eoR)_J}lJ=>b1NNcBjlGQsc;M&jn8|nIrKp^Ys+CW=i%u?alk8;_x>dvg!2PX7wP&tA^&=d~ zI+OQ!iCU0W{!NLxCl}dGr5nJBeJ2>R@@r$&gC(Nvr!%no;u)shCwWF`8=BN$tWBwJ zTIA!OM$tL)b{4^}tg;b;e+O*;V_e36<2uFuf!3t-to2Nc{-ejGAx`7VK#m#s`6u5e zj6mB(Ry)04B?1~cm6_s(Pk(2({*4v1W6RNeN~!rf+m1i1t=wSOJ`r>~)JlHJ#P5_# zNtvQ7oy(u!Y`>d8;NsZ!W&?I|E<&!Y=7uso|3GLM_Yo~Ckux_2qL&P8ZPY{Vh+PRv zWHrSjPc}w4)#sVfFDLaReMy zUCR1%Xfne;TE9RhZ6?HPUsRA7cV#le$VSX|5%SJ=62BgfyuSmyGn22AuOa5&&f$<5 zHJ;LDvx*tWPW9r@3BbSP0Ci;XcPAYV(O7>jp`+K0_P=Aixr5}#_ZhSzk2a4^CF_DB zaUc1Ul8(2ewS5+&;LGSGe}F(SPf`E@iPe=EnU7~&7S;LBY7dO|@8pJG!W!KFpJ@BP z`zOX#p&b?Xzs$a4X(VwNPz-TMgb>k2vELZT$-yWD1%n}E|4YlSk^5jhSwJz_8T^>&4d(Y;Q;a~|J^03h?1#uUvX^)Br26eAVh&W( z->Dc*C7|Yv#f6y z=#`HFvOtf0z?v81x=O_4RvqJeI@d+gu{Rbsf$Gor<3j;>)f@JlOp)-|%6#?BIE*EJ!(70yJ2M8V9jkYn5~L{*a=aTGw#YNnwX2YTzi; zSgH)pP05ECTT5tgEJu&`uk1~3NUB!A*C3ST%(fF*07%ANsYf>#0+y?zFUi4K$B9po zNm^UJVmF#9i9BdZdw3Vc)t23`ONplaLd;dnrzRza#CU55#8V{NVk?Y|18o;&7h2N$Y9Pc{z7*Y z)4%0l10?Dz$7V=A+3q<-^U(L^BvhGs9?< z?C8Sg!MOOpt@nOPs+Zh1;`j|Ov9cI;63|o4n;An{x)}bo&qel^&@WjMW}0RGWzdxp zi7`+fVH(X?rFboM`PHS`lgYw27cab!G@SPK-CJP@*HS((wIh-?@y zug121-Xz3!_o76$ps96gHctN5S->2ZMB?vPAe2%y7TCa?Va2*SE1$Z_B>9Anh^A|X zK=6mgbaDjzXN(lxq;dJ&pedzwSRG2A6F@8>7^P-z5JH1b;e1$v@_5C56y_Yl;YgK+ z2z=6mqwc$B40t__-e9qeW3#@Gm$a!{E7hzqZEe09`LGk_EO?-Ai|b4`b-I-lF#t0El*W4*ivp=Rmj0H4D56LXQK{*(pTkAQQwv_HZ}bmDGY3=w@#dyeH%)3*d^| zlh?-t{*6zTW_FEiWqcq^lr#+i_EpBI~>H|V^vBCLL2#)!?nWqhg#pP?ZfYH zz1?&|D^*t(%N_&WWluogO_&Jy?0N6tO~@-qP(Z)M84AQ}UCXF=|9Av$$6WWlYnAVp zw^)*oki`I~-s}ipq+s+;hg6&Kx3A(1)}knNKJJvLDcaUL~wz*Od#e+-(wFrpeE8A z({52$?o_utsBFoSqDJc%CTf+l5ZTT;z>$=?YWa;TuV`+szwGGh*ATGq1i;BDF)vPii zH;cmp6lfwtD2iy}GYB^VZ-9yr3?LAGXegf7fOJ-a;@4b)c>ZeR5;6uH_b6p&?x2~? zM$~lH@Dlonx*~xOtm`Fj)A)y~NH9gkeEna5LVCL49COG(|1={Qg9IL z3AwuEH-nc0S1`xIW{2NdL(Ax$9iCC;IJ2-7)+dCnC+^$ae@W;zdH^O^!@iVHwv_ys zeY*w>Sya1c2C6>B_d205>``YdEXY0h)O$;|DqueF`<~#sG4On_ggyj|ixl4!Ub8Kd zRb0S2mF7FHP^9mjZ9b*bEiT7qo)JvhS4}2=!cIU-R>0g59{pJ-zQX-8U0SWZT?TVU zH0QrF`f$(dmdnEw0u3n6;n`)!j+##HUOrCt3pMTjwUaKHQK(Boq#CGRE|f9z$iI`$ z9|l`oF!6|5DL8DzT3Um7xku|{TTwI7FvwA_3+=>W3vFS5B1ipY%Hn!Qt$?(RLGK>; z^#)m>*^8G^lo*#8m_ckejZhg{VY1+c*n>plAv+_#8TPf9?CxuW}=7WH|bDPp5Xz1MpbVzK;?D5~k7L8asxR^J$wj7;`m!VO6r zIz3OV!TSb5Hl69ywJN0(H@V>@4GGFgf~blkNBHW}mVD7;7 z`F?S#P9(#4q{_ilsb{2)@!!ABW4M5Xte5$-r9dK<)G9#cuFKl-SlZ(~uNT;SSwQ$A zD*}xIQ|9fgyPQL}k|$%1Z?uyi0%L#SRV)kg{xaMsINV=Y%rNAL+&i_qJ5+b2_=nl1 zx&#>#h!wm7r}`T<@eDu3P9QNSJ5_dB}yDW?S3r# zQ#cmW(gC~@HiAPSl{zUu_2%%WAku4-e&^@W$kv7L!`Pu%ei0l>GY|S@txr5R z>ih<5)A9Pba{5G7taA9XLt}{9qcx+-wSacy)pQA7on>@qYL3M_pInq;Z;}rP@T%TI zhixnbE}*Ra8tpIK9Lk@lA%V>s=|c@``|20=9Wa;9%^MOdBs|y-&|*o=O3)?3!qWWu zsroBeO}hwBWuB0(oVS+Ni=i5-s5V9;YkxG>2hHu;(iF^9fBu^C921IDlK%0Nr4>+( zt);{KrM{74W!>VABr=YNb|!6E32YBO6xy*|gsQu%_J%xB)IDn1B3xM~AJ^fm*1FtW z{9F^8a=FetGiK?E`{vBRa$#q|$B>{_#_zkM*Lts{}m&YxE$M*SXRZIOvqVk&l3YUkUW%p_gN+_hB)({c$jrWq_4)`19I|} zznVn{Cy(GeH(b@6z|m%EJJD4+gqc9Me+JO`oMUTg^$k4kf=24sJtg|%d(nRqFyT#v z+eTTCM&WG#RIJ1K0RIOudxa8xCjZqf8jkbtxNEHYe~2mkZ@BCK4!cxgJvA29K7VT5 zB<$mmIcGX+JEyWn{;b(h5yxYjNr-P=l3vNDkZx?P^|UIBAyH|a$UEzKt#L8}WnHNL zP7~Oz-dG6CFwC)VbE1F1$DzjrzzuH5E)5DdH9~(nkVw?#8Ea^Ie>|_m zY;>!F@YaCxvuBh2N{(#kr^1yT*}@jqvL`=lmf{r!8SB<8f@k4(K=|Yc0ELI_@goBi zaM{nhb!&g{6YWibd*vFO?B#kg_DW~(Dm?bMi}Hz$%y~-!=0mIYrPi`DcdHD+Qwsmu ziLCV+9(>cWH}KQeeibmX0c$@A3-ck-1K|A8os@pHt6~4VTKni{_>Nbyb7*+W3)4w8 z`H`9ap#x6G@ni7uboq@Bz^W5JE+Jo;gx0>(&(qQf0a}m?{oIf z_Yh!d?iyKKaMgQ&I7G|p0FYepw=xjgp z7upelv~IS;m^LQk!O$!M?1@ETn-5SmCHAHB~-IHl~yku-|L+4V1J7wfGog`MwCu*4Zcb~A9~N??jL zW_3wL0q<38WRu2`2$v$01{%Wzhkb{srNm&F_FhIaCLSc`-_+OBAzrxigHXGswo~B9>?lH zDHN(d%I{1bBZlh7m%%*tK3L*61Y3TNxuiye1m>lcA&twHzt$IO1~a2bL}Or_CblQ} zeF%gZ4vBvZ0FA|lWw9D2U`*teZf$ei2Ui@TBpBHf$Av$6uv2$@C(Dr9+$I2`NNiwv znp+oCXPkZ&IV4R2#EQ!yM@$MvI_GMTl7y|NU9r<$NKauwRI~V1IjX)ZUP?Z#&TllT zT6nCJiNmDD5XdCJqz20_!8jWc>By=_0`FVh@1S-FfLc<(k|;?O5{EY&;TTS*OEra9 z@mgZ1cw9GIE-@hUYLBTC2vx9uh~D1=id`_m4;K*Zi;4&(2RgL$n^C=4YudJ+sS5RszH_am=}GhLXY*PQmR59oGbaZ%6V5ocM2Jd>^*%Nj)5t6P)QxXc@*wouvt62Y4O6(T0Fc3 zJbQi5I)HuQ@2sZVne~#>8N+yNd$aBuy1ajV^9FQRJ^Oz|^PWXLIRWN=-VT9Q`fzSR zyMlK5@FGE(`sxy(LO~LJ=SG6>2eBJ3Xkm#unDy8lErh&}#$Ovy(lI-0lwsZm=cr<`A^FNm~nxE z_omF);e!U1$(#qFZU&vnyyzijHM-QOutP))+~~jo{cCn9%p$Cv07hx{9YqT1njJ&D zLq-XBCFbz?_9YjoG%++@|131suDMa-x{|x*7=;1s8fCOo+t3<0HSwvs0rv&LzObe2 zFm$fMFy$J#0nAvCa~C7S=7to&a$`C}2De`QFm|pLQBsDr!ClRQVn3;pCAx-UjZ!`I z9SvEK(zNs=`f9&a=@PBA@&Ju~a9Gw59h9YO!-+`PCSmn+>wP?>bC|S}VXmErvzF@Zvjk`B+{u)fno_jH3nI^TofH& z$u&@BVD&1R@{OoE+LRL3OUbkz-WxFr`b_KModOZQl<3#?22GlV&?e&&?Jswwvz+BC z?YBnCmjSENC91UZT4)K3P*Kn`_H<6Eu6Q1jS*M;n>B6y+#+lo^3NIO)AM*p~`Z_UT zEkly|6?kr=d~%)4t>Kvf)${|2ytb|K#6wz1*5Tpa)iUOyE{G}wrGCM54B=&M$ia*M zMc8w|Q~8CR?Y`IEJCbA*71Da7=mlt3l?6 zW8>|Nqjqk8F==zG<9u1)u_rppK8hP2T>N=pvd`c*2TQk|^xS>q+1}+vu5rt@e-t~V z7Q%H4^^;Jv(~ncRzGn4QoxQW})czP)XJM7Fb|KP8O&l!IQeqZwr*fw6i}h1-CSF(1 zH#)HWTFRY%M-A^hbP#I}*tI9Ca-_l}z52@;YbCWh)J}XYs9bsN$ky-oG{@e+G?dvUi`?hdkcEWPsT z(A13vXGd+*{?-yH*?-HnkWiIAXP;Tx4m~$ldc=WsWokoH973!jFR80EY4xqVJb87P zJ-QsWI?MuHwp$%$dE}|-)QRur-2Qq0%9{028_M?mh`8z~+p@c{dj9s>!0XB_$8U1w zN>QNo%p9R(DdiPYCsj_Dp-#)By?fQt{yb>mw7WMC+LgV2Y_s(8&AODa88?T$UNz0W zb;6vLzP~c=I_(?0Y{QIYpY&f}de?H{oqRusKWdjYm}b69uTpoG9_zSwNqJ^afyREr zki79_RSm!KtP0v%~ z+f^F|O$gj2a{S1c%@#pJD%VUouxbP9c-<;)6o0_rg}F_6=@#?CU4u`l^to{##-2jth5Y%`F*tKmV2J+{LNK zo^L$*xw6%4NBI2pCJ6yup<=QtoD>c##F^-2R7qZykX-&=VQfwLo$r!5ts_cmcP+j3 zU1`YBq>|eEN7BnJAAZ$pk}$}AkTLD_vJK`jbEG?7w!DOwjTsqQv=nc>-e{F`J6lNm z!X){}iSIUzcs2d>qNDGmjz@ZayCR&XU}1MQcT0pz=aHS+s8vI6ZpV}32UQ+9I2``4 zrfK%qw-K-QeABk|?raLX_*L@$;cOANH>*CM9~z@M<5bDtXWRZn_=B{yi<$|S?RVBqH z9o&*@GRd~e+o#Dpmzv8>m)D$fWxV)tm!bX>l*d`>Ir*3D;2gWTN@2EZ%WwCgx`B7i zb#1G1Dqi&)emc>y-c2v-^!RE19mkRQ*GXoz53e;8wLVPD=H^d-=Fp{9^3Q3`qza)N zuT$Sz2Q0XA^-%89nE_J`YVWRC9VRhbUPq$vTxZMdt7C*+3M01oGg_%m=!xx=4Z5+ z^PGYx`@i$L?v2~9dC`+KTQ?u>e3GKxTHqIv%5Iq zXkzT1FX6+hRAzrG%1ODV(k7aM!PWY4`rEB_4 zKYI94wDh_UOHMhjocAJG>d4lu9UhdlMZj!Tr8Udf7A zXNqam!4Y-W8|woeg>TKWzln?xXZ~0{FJ5TfZ`#BFBRu?Tk($BqI zd3;6C(XD<*<51%g$5{K=wqnZlnStvw+@7zQXUI7+J>%+cI_Hwmz5ne@Qt1K^Z4o=!xZgJ20cp-93=7Sg`0=b z@i3ElDT6ysF1xk6BJ8cH@yz|cRmkjM&PROO+V0;#=NPyZxlg~IlX}ZTx zOioM|yrkg$GPq5FzWSi&v1|sh*Ifm{n4Wo{>p;l(U@Z?d$GRJbk*{P~SpRT^nXan+ z!VEZaNNz~}x=Yd=Q`d7`nd|cw&e1!4MCHT7&CzS))h!l>e$enSC=LyC`qi=h=gX)r zgLsE0Io7^I%8g8ljt$QK@$+4$ebnYVXU_ip^sK&*c-JV=z?GsitQLr`Ty$$epxr4Y zSIM}UMO542_S$%By$vRZPa11Sa$3iUPWJV_zTb7if)hFyi(G2b%7%OB7Kd3s9jX{o zd($xJ#o-LYxG@`dxY$lTq~f=G+;P!CU$Q(0oea`zEvn1e`1|ptsxw8F58k_9I`zI$ zZ0f_*@8hqHU(WeDd4TWgL8;G{MI0>+6f$eJH{6nkic6jcTs9vUeDD04kWFjnh;Azh zx_P6%B!2u&$L$?eQx(5Hzcb-bAC<95 z_|-RM7njE0w}mB+tj;N!(|PJ-b#TXui0}bEA5w=s8Sl2RHb^lp^g!&*ap4u$l|z#o zhCcVRlU2R3>QeaLanHw=1-ef-5?S`=hXP%uuL`yz>*Ud9l$!b<2LzC~+9Gx~XU)lC_-g&7T zZN>BLss`LSGd)^|E8bkQq3o#rvP!9{RURU${nt$W_h42l$7$FWO`oZutFu-)czpic zabm~r*qrFSujUPHsoD2pf}{MM%Oz`aa?C8IkNdc-zD}ZQnh1E@-U$*tu1xzTXY27nhHh76&G13DsqP)B8Dh_=Vru z2~XYr<#=YyE?=7?7QXbRSE-izLFZG+MQ3WPQs-1u$D6MDvofvXNT>blN0QUpUcX7S zxfb?n$bz%F7au;5%xTm;uJgD0*%z^-UucZk zwS2_3cWLtJub;oR{UiD(sjdI0iq5TrrTz$MxYt+6p-mTZKG|Fs{_E+o_-TXqmvtRz zOZ4T`zG1ZkxD8=7eXX)B<+$HhJ65>(Ms+kjl<%6II`@*cP{(&O&re8lh1tdDF{cZ5 zPI~Sn^Z3*Cl^;*I*33AcJzrAQ_U-6YqpZ0ZnnyPWEt4M7=4Hu0be$E-OYhifWv0L>nlk&S7H}3KfpFaD#wNv1p4bOd3xN}4+12eup zc)k6}!hwq7w;F8JFXo1)UzK(4j#Nt-dTCv3 zkvC}Psx-$@2@hoKCGxUU(dUF6uj)rQnGJRt-*9RA^d;#I7m6N<9WE@(DTuuGYQW+Y z+e15MkNmL3sA!4FnZm?4^SiEn-`48f-4XRK;lb`X6?4rioE~R>S5^JTeXf?2v|?z> zAOojBkwI@3`ey3t@0h51W!dX#A6#TAMyo!z^H-KHv1|G)BJ<=^6oodWN;}H`TYIAA ztnI?~7vrv&9m)9SW9`u|>F!0zA6EL2p_{HUPv!R;HfQZMIO6cG*PC46o*5HF6>+4{ z=8;^9$|@bsdJ5lDBO^l@R?aixI7;H5a5Ic)WxEx}n7NdO{)skubLA>ShH{F;F-mJX z1m1ZWlF|sS(BW40bK=x8ta$g^phzE(VLFmi&Rmak?iIvzOmV=6OI0fQa<1ZgXbM|D z;&Pc{UoID^25_{nfGzzPbf!4Um#cyP)`&`0<^^z6m}_t59~I^kRz3~ncroR4Wl;H9 zNpbGvq?wabGiJ<+pT)n|rtsD>9n?@SDVrkzwFsEW{0gLT$(sm5!;&;Zl>T0x+UC0< zb;kM=GO35+3}+bmc_-FBxqazUM4|cQt?w_YubKS5v9a*BlH7)i#>U4DDdo1E77j`r z1y28qCJvu2o^6ret5j`my~%d6@z$5;zHOTG^2Y4yPft>EI=_Eg>ikDI+VJ3_autb% zYSg)f3+u)%bc@xNi4j}9Y}V){;mc%KFYnB#_}FHzbCDCav8c5uebn`2&uvQS|Zi zf|rzpw5*!7Ik?4stWM4=lLHxX-p*DL8h7^VH;paWbnnMZSCdV5_AM#X-d(@Z#w&Jq zb%41+-M-NL&82%1RvH{zQBXE|)y#XAlOi|W-~Hve(WF`9LT@xz%gx#qG~%X%`h+va zNq$3>+w)#*l6v{PaqyYwT^@2ThUM7(oo=iauA&iEP`V)6v^K2#Ow;!h3v#k6;tQoN zYF&(u3EHr}Vfm@p#=F#mys63~8eBpw)+RLsOl*vLd}_tPBS+6vbi}K?$eT4j{)h34 z*aE4QOLQ7=1Yeu65?=jPt7F=(5) zd~EpJ_1_xa&N}({rRkShmKg~R-sYj7HEphL^H}%Ybin(rl*i{jsJ}f@cw>`6Y|cVY zZSxH>OIL0FWj&k}+@ADy>5ETIc2F#yd<`-#XMN%gNhbvVBWZ zmde{hV`ubxYg>KkTKdu1tK&LN$GzI$xMbq$utAS}ruH?lt0~plc53O>XOd~H1HZPc z3ki9A^KIzbnfu?)-q++?ZU6eyxGxWlcRc`#A|cqzW)vGU8)*M8P&oYH?*K6>xs1J6Q7 zIQlQlR9|{hTRCO>bJOLSRTh6;S6r*h%`trOp?cQAe7&3fTnjJ!KJ9TnB1!Jx8~N7t z8$YkeGA;I$GWWFkr@C!~6C4)(ed*f^%bo9+ROeRjNqQW0!+U}GoDo6Q?uLa2su$Kc z9=*K1)M>@j!Reh(o!bI8U0?f2`^gBuc^gDG#*BCxJM^sgzo~cM&)WA@eD*Ux?=LFx znH!Xvb1JTM#HN}gt+m;{qp19t?CtlHD<9pNUMXv{u~m9{){Rw9UtfOUpQ2eM+wy8u zV$ccyoWhth^^PAe9g7b*e34om8kM6Ye{nRaM<{>&59|EwaC!>>fYtK&bRD zb(fNk*i~n=>dfzUzPt4ENm$v_KK~AibRND5Z$vp$f9I!A&b#k&OS|N6w#9w@l$h{c zwD7yqgD!RN?^=ViV&1GX_*YjoSlsMOY?aU9(btj&dqj-5bFZTP^&W+(hjwN~1ZrLg zy_>2$SX1f6Y!{{MWrf@J+&&x-{HST^pZi%Y&h(aH@Z0NI6>tgAlmC7>O&`>_N<{Vl z843Jjr^LsllJQ_o9URnDT3lZgBpN6O*V;YS<8C;i)IU%`QR;)C+*h~Kmi*hXlUG$2 zYAUW;&h1;XbWro&we!lY>u$iU9XB4N7=HTk<*!hS+-alNjb8nh)QKKfXdD&2RL?x8 zRP#ekjfRwG$gtaotS);0(iT6{(JZ;dH+WcEKeg1w4_-!WCD z?)*pgMAP&!Q`{{+FLDl<{yWuQI{4q>Tfg^3PT7<5%j@`r`qsHE1sgJtW#4W8G-rC# z<2NM*f11i{w%&bmE`QFx4N>-ci#5}WZ439E`(jrud-sivO1|#}mjzot=J`Zl`FpH3 zfAcHV+SC0(faI%w_s^gB3s?I|NX}h;s(C#e?~LhnuP84ksf-Km zPDl7<{-GSiq$qREn2bUimB(=;kyaSVMR48(MV!i{FwQlB+x{?Gax6yzsm5@n(ZX=f zZJfO58Fyz#fFV;^5W!Jn?%cx{0`jOTUq*9AGf4{%Pzmf5n6d(js8b0P9awB7t4!OP7_;)g;QAitG~#lsM^Ggv7+W5> zdPtfe`!KF-W!q^^0z-Oeh_D%Qy3CPAZ@a{qY*%t9QzT#c`~t_A(MJ96xC1HG*+a*>PZ@W*$)rDxy1#-#E&ZYF{|5j1#b-l>pUY zW{U+H+y13W3%OJQ_66GCuM{Q_^m(#D*?ESnVbCw6`CE( zk*=K4pZd)x(cNtsyB8Z$9CTm+#h!UFFqqMZF!q{n-EC`H9?-z(IROMmno!=jPgO$I z&N41;^+6Q7I0wbMNb2Fb?qau}hx%i;Lfhby7Tm{Jt6E>iIozgEE zpMOCNyj(((idpQO3LZbZm)GaHNopXAVbmNf?!YkW8vY~Go+`(G_`6V+$jOb8L46ar zs%YXcN&{_lpm4W9bq>^NjO9C8L<9Yr#8pA39jT?bXyR}noaIC%<6p0xsI&MF88;~Y z<3LHH$x{IRk2B?u%R$ag{0FE20;h6KP>?GX*qcTceRQQ};BpYb&;ZjNxa61{Wh)IG z*8QLL$|55e);!QpH)?PoT?AF{18)t$ADHM1eCT_%;eA)|Mpq$Wr9Mi1B$=DFSrqX2 ziooYFn&(a$ezQBJDXcDBcuQCdHM&zw(68}^!Z^B67v8(Y;)?#<16TzO8;H_9C@Ec9 zxi1$J)1$@v^ihU)XxIq|>eEL;gvKoN7g0n{0UK8n*5);ZZ*njTYfnlA7y5cqnnK@E zf+wYee+PHqL2?p5ByA zp=@6Y<>U%BereyrR()Oc$D0&x3IHwM(S-(Rln*J4@PWEH>Ego8K2%>^;d6AMg&tj4 zD6P*a%+`}Zo#BMS$QOWD`VpWi-z5rYz&U*YIHGsH1p6mlIC%gR4lb-7AdZF|BLH|w zou=>@qrzK8rf8oZDLhFRR?vlRNPQG(>W^2f%Kk#VcS0X=W*O+4F<_b%x|=AWaoSud z>_!usLlTYy*3Vv+ibWNpD4blNf1^Oo0yEIc8u|NE3Yc<@!ufFeCi2tZgzOG556$zZ zn2{M9@$;a<3NHgB9YEC93IKK^tfA1TFyC4UEpMV2Wois|oqGy%nNH}twk*}n(AEHI z1h)1&ntG-yO^swjDM@4(NNM6$zkAbTuo+~<3eYXtX1jP;od5X=Est(Huk>V$9B*bx-Iv zkOz&dS(wJ*7;{uGrG~Yc91Pl6Mxt|LDPvULU3#4^-4jQb4naDjDFw{+;@|i1{CuB2 zjqv&&8y44bQy2!BqbW7qKnLDGSh4|3VJg$7&mb1+K~yoCa>YpUmJMhWv|t6NPajVf z(q1GVLb8O(A)w0COwih(usl;1oeCi>d6fnnn~SRhZJ~!%{QFZ<-WODi?9~i1iviOD z<%CkIxZ)+DKwh&9lRpimbTRpZ?^`@>0_iSLdl*Zj39D$#!UL-maid`tQ^o*`^+lLP z`53AnuGFmhJ_g>vVka~byXGg*hcTq)`#I8&_e0HZ!1}Q(OHM2b97_dYPTp%q&EbHP z7Xu>M0Y$fxAIA?b0fi_SA(q_StKW`x=Bp1RGn~;ns~mukme(c zW`>itTuDRUT;}Q25Kb{73nFs#j#FDOCPbhS8 zEXCM1T2&v-pj~HxuI$iO*MQZMi=@o3t}c-vbkYM{Na_s3Lw8KxOGAe=Vd%?9qGxG4 zjAg0FGK#XsP5(!3aqbCFi30W5(c}t^WLP-ri4r;!McS#N6j;7|LJuw`NGY0$e66rjlR4^;rIKT;i*aK*7 zYhmCErC#ntkK@9eHh`)1g5ea%pN46nLouWZ)iFR&LKkYI?=h4X7F)l&F~kE*-Uuci zNW?S$eRvL-#waqDkSE3h`O0@Rc_F8ri_*t%85cfB17^H0y!GA`QE>!YJq`-0(?zbs?7P)jm{)ex)Xr;q_4QJZwBy) zoB8ec&=~8zMtQ@M*5Ryfi6Nyb*Xi$R@lFy{HiziUxNOuyYj!7*1nloI_mbU~YDom$Anb$YP<8BxEH* z98^4+#G>-aAZM~81MhGYk|~}-^pc$d5SQT$BybARYZ_hpbtF@Ia0(f~Kjk|vd=H{! zAog3aOp@lyp#Jg|l0hR+kys`Zwc~^V;CI279m~#-V(`0FRYYO`h(b883l5;+rl@@? zDg8AS6gq%QZIJIYN*VXXDeK0}eFz@&62dimWb6#4889i=jcGt@Ifmg_H3nKG_a)JA z#^U4LHem1r7_g1GG@Qn_LTV90a>xuC7z@mN=);i!AAbv{SJ-32{U`>1QZ!g#Yy?qT zZaRqen#70>n@*`?F01E$xMl*)_YT4cdkk$%Vz^9B2DfUMPPja!xg@4ApuZ_XT;vr< z6gt04`La5&S_!P!IhD#R8s7kIm<1K!<-&8vRGOWI3OhrE*&!o8oxy*bM2ssx|ENwA zpiTnTmaNv^JBL9v&jKxNj}tAUlR?YYxeTbgVI|SwWRj@eJYwf64xX?BzU;EULcUsHoNwUyB0jXeMYyB&{P`*L638Vl#$bNXpzW5 zVlZp*w2N&}`M)qr^kmh_KUaV$P->#O6vFx4OyIn;Kmfiu6@sWk3T1)yot^^l4MhyR zJcY~!AJe6?*Y~6{OQj5O%sZ*pdh8IW@BzMTRd%BpsU*|9nhH#=ZR9b5^@~#Q;Ou#1 zhLB8IWi*3Do52ZOSoK+pVrEhP*x?+MwQZUJSPO`3Nv&IXoF!3rrDRZ83Td>+^Y*HP zzyYp;!?WYE#dbPsX`{03VBVxOVyU@lpzaCCl)EEtbs7mN4{6AeeN1e$*e@jCE3y?R zJTxhUO`8<0K7EW?RXKKuA)7jz3?eIL1G6nvOf=O#BP834+4 z*Q;x^R2`(3K}I0Q43Idxo*4%-GbknONdl%FQG#vpy3_ou1RB{gKlg7!0pGhwj_a4gW*KD_-#6%uFn7 z4wRmJ$tx8{Tjr1kJxzl;U(q0Al<`_f9%U>boor}g;O`0GkO952yl<~*e0`MpMo5t# z#V))Xxp@cJR22M^omrk~r|~_pZQf=m&=n}x6qZIqvnfRk@yiBP27O>yugWI* z>uI|5{3oVVRv!o?=TaKjj&FDDmJNp3CIgXxJu-ixXiobdCq|k`xlmcGt`Kd6l@CsD02r9^4qe&DMMq4T2eyB@SSP6$I zEc;Xrr75ybOc=!s5H?5R^N4$B&x2w~Nm4v{9;J)1SJK5U6y`JpwLomZ#htiV28v~b zZIIt{QXDWJu-jBH_CPdMRagq`Ur1mTG|WsB!z|F>`IH{ke{`bho;{#{JAByeNflMKyrF@TgUc~p|9#1eMj@)i@Wn`pR+ z6%Vd~hM%GoaYeUE`VHIz6;*;b;=wly{N*-?hd&gzU*;0v5;;T|xqcAUMvsSp-fFOZ ziD9>BSYTo(8>e<5B4$pA}?vmY!z{DWIn-{$puW zu|(zJVDLhZUTZ^1G=UqQXXcqDOvUp|A#voI3tJj6`9$#PeHSRSIHESOqco6K<2b!C0pH_wfP4yO5L{GYzwXXzb-$&6R zj8T%Lh&ZxaMQLDD)7FK`;14_fo)X01>xKz)dTB5^db2?dO$Wi)myk36UFVZ&BlXoJ zm|Cm`4a7ou9JNtlH)t;nDjv&#x>i$`Sg$HuZL1Iv5C~nDE#UTe0WK{UMI_MbJYuxc zJYcmoQUHDzQHq$Kw4>*!?Z8h1_yw>Oc8d|fpOa6i;$XgNM8}hh(4%Bv(Dh>BPe2v< z)EL~L+yaodDS=1NLE{T3LrlLXJGtsP7-ySCPyId7)&gogrvFFN@0i7-*Ft~FNRDKP zibY(Ad_R`_DXSR#hG`HS9z{S}1cR~XAK1HN~V~T)Pzy<-3a|1NZ-wnc&$h3nD;m^26#X1Z~k0Og3aglWWDU>diR0lnWTtc-3{6IRW~-J`RC)d}bZ?7?>5J_cX5 zn5*DpwOvQDiMq3DTscsv z4q_mibUK=`jv9l*%=7mdS5d<{SkC%f$7pF%LTu++0$i^&FkJIWC?=TD+?g&dDo%O`b3onW8c?$UJ!L8W%$L}%ttJV{bF|GA}a~JHY3F(MC%go$| zj28J#(1P@49aiObS@yoyfLj6fVY3cwVpz}GKm!-xi%BA4yNApYBd;0`Vwjt*`op1Uy1_C*5N;SzL;5v;mdo6-!J zLmYNHfL6|eE*Os1?I5mskS^V0#gt0#Bpj26Z2Y+r76W8qKE<{cHJC{{sk?|e^XXE3Kc=(=N^t^5*9q!}KkS)> zi@ykx_lEwkc{fENuia!H*=Nh%@w7$qpfR9#34FE-5NKoOXIsu3$pVad@O2fyggJK* zpScX0l}o0@^s=}}WUvS-2ocdV>uG`sRzYojA+iny(QIA!FeEnoypNugiVPeJQpSn& z!?RMX(J=WH))L^Wr{K#@`!l+6Q4X2q;B@#|mrI7w|v^sPAGC88i^qV{v8Wm!|#x4V9eX&~%R!nBfzmpK zJf#)UkaDsB9#9S-;bIh`Km%iSS;&H|zm}ntz<+P|u#r~CX0ZL4Q9FSeJ6Hgg*Xbn=coJvXwt7EYvtRx(WOh9wC zmws#G5vd@!c18kq6%eQ5o&a-PdBKBYrp$1dMT16mFd*oaiYU1Y@)del1xKGQ8h-f$ z18@2SXY=Su0#;?8&m^NHP^GSy5p}-sL}?@cZx9)IyELd!_R@T?5q?GdPO;1{76a^-MS&-g`y9_X%?L-lKI(J0c^M&hQZ9HiU>JC7b7K7T&*H1f#l8* zTlGByuxK?b#RP3$4#`-+A=1lM)36^}f}(hxla9?IiMp815Y-roilRuE&S1?Gs{vNa zf;pY1ShkaJlTHK0O>vmoqe4W*8m*}a<+kqfWcUhF zfk~MheDZl(XMaH$s33_>ohRn5rxA7q3nGABl~H{Nj6ElkNvqKl01@>FQ4}~^)Euq8 zK#EH*KyeyVY>hfDkSTriwbI|0!LkWpS$0xo7KL;#l8wcw7lC;E1j4hrNK_8hUZnbA zFKMS?yQT}m>XJp3(36Wq^3+R!kT44)IHR&TBv0A=Zl=n6DM;tEdp5(cbiz#kGT9~@ zc^O!gEGEU}OTo^=E)(`AY1s69LHWwHBo>?20&MphK1>CzuI)Am4J+8eShKLWLIHiQ zC1dDQdsr&f+{1vZ<6`0x;w^fngiA9fe769n*a+DNdvu>o2$dK@-U_XxzjUPSzs^L#av-%Wf<*yi#5-jV*MOan!T`j z#ZHJk>lsp6hLnDpxEpOd)EZ7{V6iA=f_5{Un!NxjvuQUxWN1|wTDYusR{Zq1|EP8N z1+PisV771Iuy6~-+a538CG05^5rGkkUq@a|=9>U76C~3@_#XtPE-$$Yo9-(}#+MNB2SJ zI$*CZ-vd_MVYVfPs;|J_1Mg7TmH1CNV-Wf$V-pgpb`FPlK+|iJ~I?3zXAp^ zscVFp<~3mEEJlk^r$HS4VbsYqD31@K(B5n0l;~9&R0U^u2$dRAy-uRA>2(0rO7K8^ z`D-uvG^jSb%VydrB|xd1jWBgKX1@_^S5 z_zr{9>ok0bjsTo{i{vV1w*Vxe%LD1~j|k<{pmF*<5IEE=axkEi2Bq~E0D-NT5gE70 zJp%@WdxP^GN?g0=2=?xxy?F~%Eeon>gBn|4-rkKYfpqUd9C&w|QpMgI-GqZ~lOZCAuuRd5ZgQv=pvZ2Y=M#>C5kK?iD%r_k^!{1fg%pBT^@yM}VZ~@gGRc9!kLZ@4p&wJViKjxI;Lo1|%9(fT1;COWwm+JRAuiOC3slsx;G>JZvYbG;fvt}s09K?v_r`iz8dzZm4&3MX$$pA!Ab zo&s(vDSZz=aINJ7P^3m_QDK+ZKj!9cw9Irz*|fKAE?2)3|MyW6rKO>h1AoPrLp!N z9+4Oddq$Q~-q%+zE(82lXpiA6{EU49_*b5hbGKG4H{0DIvG-BynQyKwoc;S`Jf{P@C3uOUWe>YF!;~}VrXxim^3;Br-Cripx-INk`SvyV60#d?PHr6qJPiH zA`SM3Q(cUg%)2cnj>29LE{5}TElQzUPB4pNZwW|!;Nurx6%$1VUXbDB>I-NI>N5+> zL5-WmRQTJ`?IuCy=Fr@0zzpo?1}eYu36bL~7@z08B>uJjB`_ZGyC=M@8~z*MBHJXy zQQ&LnML^38Z(*;XxSs+pHbq@$pnmk|jAttrQLAR!L;adS7e7|hE!H3e!I#DQVZ||l zQqs|*x-d^jgDELH7T9ZJ0>)8GUW3eY@O&FqrRgz$qYVSkq^Ty1+ZvjEbf=_IKhf3UuR@1OP zR(u#2U3&}DIXV~-#mN^TYFiw72L zyoIrW<-T;s0*kXdmVyIcni^7XC-(9e`fWP}dZ86`VfL6==g22u4mHuku*hatIP~*g z%mB5tlgf0`#h<#1=XSt)ufuyX+yuS{n3y*q0hlb>4lu0i$A&viHlU{*czy`WGu-_7 z1agSmK~7rPcL1}WfqWQq_L<&X5>bZ-B;e4OA$rq6$z%5ZA!nX01@^WuX<$di)NWkI ze_~STUp$xbR9UM()1m+ONrEr?X~glNggFp!c+Y+2^n554Ci4Ox^D*Lml4efowctae zfLiziZZ7bNB@|OqlBV|}E8mj6e?C}>zEcl^4t$VH7bPjYVGQ!_B7;U^7Z@l!QXKu| zz%${zbLQnxEWB_6s_P=*QMD-G$xYC9377_YnZQRi0V#c;n1}F=TZ9C-f~Ir8Jv0Px z`t%u)h(bP))y8=rK<1$&=v$iN>S*tK((RQmY<%eij>rWDu)rhm>6|By3QWbd`O_`A zkD%Y|1<34U_cqpucWc%INiBTYbMwU2DET8fExwT^3&^7jR8T$qgQN0WZu6{tupX=q zE3E1QV&VK?0gbARG(HjMwD<%J#43>QCt{Z^t3H$kfYS5<7`EJt6{zqNk$V=%L?Vue zqkoa&n#k`9sj;#DQRT1DKvmE{Y{B(speIch*FesnNtYS*8CXbN5=RG<#d|MJD195> z^Z`br_Yg1H&0~KBReYxOqG`|j`7&Q4RSGavyB(stwuv2#=V3n@>*xF5+wo@}h9~QT z0}h0(e73%NztEsBf%+WVCZlH&}=ZMp;1n+}Yl^H%Nq-|U(qMz6k?Lh} zdAwwTnfeFCwO$8iV}Pj}i)mkN9;E^Q#8*qHtz<)PZ|0gSIYU7It^<1<3io9#&{wG zOe0KrAi~tN5n>Dv$yXSV7Mrn=7_Y|-eskK#;5i_)1itq0<$qq96Cyg$ftL&P0xJ=& zrqDGMEzi|MI1c7Lf{~YwYd8m?c|c3y4|@t+WF>(=|k$6z&StQ{d}NM zg}}^Ce!s8?%3DAc?WL3~lj{6x-EvKoftG}| z(BGmY#BkviA|ij7;@+?Cpl9A99z!sZHjer@q~O%Bpu70K7?*iC=RJ-6qPuvYIG0Hz zar%3CC}EN>PBxQ@>n%*^SCTD-p@ZE5nznpg4m$K&oI)MX#HH{F~8-e0+HnA;pzIqeDonb7(MX zr6crJAR5k6W~mQPh&EatM8_k z;JzDaxB*i1l2GF(m-Hz};nBMpkZvTbUhr1r8fdBmf}qS`2_1fAKF6PrJPQ7r5B-eY zfnpB`@UsY$kVBIj{k{QV<13c_753Y$4l?iDU4e~tB14*Du z%3Nla`;G=ZKgk1$BWo2dvnM!oMQ4E_Xt)k~3cEWxoe{vtOIUJ~!Fy;&#EtdlR#%$_ z)XxDr2YdE^|GWSt=+C_L##|MY7<-8UC986o9NIRc@gTiQ8U^!Gc0fCKjfX0Mo~m-0 zw){zRakfL%r%MgK z3J^qVaXH0WWCvbd3&5J+*}QAH+pYx?E-kF+2g2@ zE{!?@xwa{S$Za-}Ig>(GR z(|cx1z)tM+_{m@%KC^Y~H~_dP4W&zmp`rn#hI{GKFegT$j3JR|VF{&qt_+8NT9Q(| z;*a0F19Br_)SL_>VF0V99|XwISp!KG^v;mWoO5;e&r6aA)96w?59*xoU}QNP!K#X{ z8Bs24AgJNy%~ylr7iC1U>7O**(2qyT9PV{fc$ifKD!GA8f>^5g2k=<+M^AwZwqaDo zGCy%H_%1B^c(U;G0(tlve66M!gH%og13F~PWu*G4TKDM(?KuTJ%ZtV3MU(&+p6=l$ zz$H7D$AzgjR>AwROk)w$xxt%nf5&r~$>x#WE@d-7wKVv$Rh(HQCGF=xCg+Guc2z&z9 zqWM-N2f#0!DqM@sS#g=yy5hG?JzR?_hH|ygo9^XD%Ryj3{bHtcnunw;k{U_^6+Wi5 zY#jrt8N_8?d6&NB@t{*6Qw;iuseo(3J^UHkd)(V?P3E=k8Kx#=h$$W7u?|&wLREH zgACCV8&b`7n!t27T{;BS*g+2AKA22iVg>`~`d%7jk4{1_k zYddhY(YzsWZ-~(lF7qD5<60ZP=K<qzuZkC8o=@1<{<_G~5lnvt?p$u^kjvKcoxo(O5eYkQ3=r zug5%AOeWM!m)>pWm15mBk?jzYRIl+`9FPxs3xQT_TjaN*`NO#69ZSQ2%d*!r7cC@a z&t*2EJ*70HrD6FX5N1zCtQzUQXHX;QGsSd5mIZ$ZFM_>XngP5%Tmn_ulWs(xI~Kn8 zg8_9Ng8*afKx%L203fTM0-!|>B&o4_S-9c~#Nh}C2W%HP`BwnH%Ymf*p+S?ZeE~lQ z^kuI)eH20Sy+JDwZGhTch+T|bYO*&0{#UblMH`(7-F|=z`J-DS zp+jKGs7GKtUJXW7g>H<88U#ZP*cKXY&hQLy;SR>GSyHNO9R*iUoN2DTN?rR&zBHHdGhl$?S_2&+g5;R+*{~sJR;$nCDZ8C*O!E@Kb)nM zlN*mn5&auZCLT_qd)+?)o*D@9WZ@5h*QXJ;Q9vMsc~d1m{I$v(Sp{*m2Pe%;jh~Pb zpW17CRQ^ZM$9K>)%X^JwtqiFuk2Dzn6&#<6ueiep>2Z_2)b|!{1%>E0L$gmzSO;L~ z@;nwJ2@f zf*vNI2YXmH7>!ve@K}v>Pns1wEuK&_i3>c4BRnnyBii#2V^e3w_kQ+3Dt!E_MrgXt zvOS~xQHBBEXbgjH)ltPTqU;*c11j{}>UM$c*+zr)F}lh4$}%iQB~9R){HcoKe)>gh z&*ggV+;~S~YS|t&Ku>K+v|2K0xbkVxy#U;fz3Jl;|KFrjx=F_!9QXMm=o|$)vu6?U zQ!%Mx52>JYJTf1`Wp3BREc)OKL<3%SNB2!#PcFCD?8wy5X3qs+G9I*Ik9hC|Z8w<|>T-wME08Cbd7f>* zI|RG+8EiQ7{@!%&n|&XFiRib6vQ2b8lZVemvLYN+G~I*5?raa>CjoEM>sFJ7OQ7-M za7iVNg!|UVRqb`BLDNXkl)Y3HGml5fJVxc?3H;{4i}naVMZW5X03>u06@Yse@8`&% zubw2cml^>yV;5r@fJmdD2y)f{pG`E(rQ1ixDrRO}Lazw+A!}FRNN7?&m@Ts1DxD!i zRf-%fB<)4&tK|i<4D&HZ8U))GY8(ar0kB!e#U{{k_B2%0gVn;b3wT5-=%E)GZhz7I zo~+@4WRayNM;!UE-Xl1v7{l#*TsGSCNcaH{y`yyaQh`SFX30L!FzDNZ0X(NeKbu1L zl+iH|(UANM@<#4$Mwe>-0Gs{LUD(kuejV0Du?H)5Z=;0+HU6;U)71@_+`R`mj#7#7 zy)OnB?vEaNL!Ibe!|rxw8!;!%|BVwXp=r(8;#HwA=5zNZ`f2&0qTz(eDN}oOTK6BSPw6WZvSAgKtyKfV?SE?p%#CF_ ze{qh5Hn7Trd`-~>1_p9s17?B8hsth)BV5XvHkq+z^cLYnYU}YJ$bd& zVP5+GA1{`bRjY&sP5?L30@c`&Y5gP2PVfI>HFFQ* zv>IzNZX}E=RezarwEId`W^XZVr^94~u-`^1jUcydg#;6k1p&Vs&(3=dLXJ{_Q@M2L~fLxfQD885#xdxK= z+swYCW>XB_`vpcEINB}nF*ag|bPGT$xU`k2MYGYJ4Tm7+`9V$k3t;!@qirf61nzQW zo|wctA3ByikQiL&%B=Nh_=0AsYzP;@jjqft!K=cPGnK)2U&EI@J$q@z;>JaA$E!L@ z@+URMJ5CAK3=VzYt1atBSN5w^+y$=#WbX{s2(VCMu^^YiqJOdEi4MUl=3H4uX|m(% zq4r-)=$mCxz7m&t_46pR^4odP8Y0lb?ACZ@&oIKR;owJF{dkjp62T;oOo=!?4mRrdvV%(3`t8ZPfC z0RK0NEHG;L11Nr^00@)D`g0l5xitK%Hyf^mw#1P61{)K42C0Q4=R$0V0qm zj;iAz5v5-zi8J6mk32hPfw$5xsb=?^*nhclScFm{Y++zQZW4j7QVmb@;DFrZ@!L^?>utBBQo-iI{{gH~(cYsjif z);s|sSjLlPKP4c1XFi4(i$J5`YXNgd6Z5Vg4eAYCzyc5CAF$+x0QlEJhPz7$(LUS# z-q~0%%@i2_j9H4bE@4oiJ4vH`A!J5VLvz`%oB`orDZQ1Wgrq}>5?Y}E_gE%K4IAx&S_1FnaUi(x0J7<1;o-!qqC36 z89%{04B0AQJBV4>{`Y1Jq$UENZHH)0w9$car2X*8>-VG-u0CXzim{PKsMQrWfWPhp zSMd|z0f|#N!v*8RMTolmhoZJtZQZ5{GKavt-IGPM_Ar}B5iJZNlVg1JL-!^FcL^sk za(p41uYZdIKAI2M}re}bg8-iK%aO35IO6aud);(7#;`X2`@W!J?) z4cr?C)Rl~of-HH|i3f^?zkzEIoDRHW5mMIA21yTvc0gkRW^BYfu0V_RwV344;4lIAdm4Yu{gjB5iHY9Jd0!D8XMnU=K zACh|YWT;zaCIZvt90uS5_n*LZ!@%GW0bR=oS?mp7rsxV>9RijE-ml0x-9L$&8yxJh zNkDxIkmN*x0}H^b0O1FP|k3UkA=LY3vwdu1{p)s{S6{xQPCJemI5ox;-^7V|U71b9- zs5w9li0wvThudwQyb)Av>jAL?io?$-af9x_)0=)Ejawe9*4xRyF%+og6cBr%s7+G_ zt5pEr?!pZ@I6EpG=ul%7s4)1d#_iTz@)*0)_}A1pfb2E`Viy!MpFuPSf~|hGf`bJ# zMgj_3)M0=&RfHYJ7{+fF>TmJ}>S+LCM<7N37d5Cp;9ZkS82ZGl&K(0a4na*J)S}=Y zOds%8O7-b8GocH!MeT0I_y84m1F;7XBfvs+MA!feLwn%eo%~tQ)F_t)45efZgb%=mW`dXd*Q8PVaHxl7MTTmJO8O0Fl?5?vu@}2@a_!f!E!j3C^uR?1XRykQJ|m zW)J`LOWE9zGcWU0bL&B)S)k@Qs(1.0.0-SNAPSHOT com.tractrac.clientmodule - 3.0.1 + 3.0.2 eclipse-plugin diff --git a/java/com.tractrac.clientmodule/src/com/tractrac/subscription/app/tracapi/EventListener.java b/java/com.tractrac.clientmodule/src/com/tractrac/subscription/app/tracapi/EventListener.java old mode 100755 new mode 100644 diff --git a/java/com.tractrac.clientmodule/src/com/tractrac/subscription/app/tracapi/Main.java b/java/com.tractrac.clientmodule/src/com/tractrac/subscription/app/tracapi/Main.java old mode 100755 new mode 100644 From b74572ea728bdff34faf595abe498f7e9cf794b7 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Thu, 24 Jul 2014 18:08:06 +0200 Subject: [PATCH 14/20] adjusted manifest for new TracAPI packages --- .../META-INF/MANIFEST.MF | 51 ++++++++++++++++--- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/java/com.tractrac.clientmodule/META-INF/MANIFEST.MF b/java/com.tractrac.clientmodule/META-INF/MANIFEST.MF index aedba8362ad..9719eabfc57 100755 --- a/java/com.tractrac.clientmodule/META-INF/MANIFEST.MF +++ b/java/com.tractrac.clientmodule/META-INF/MANIFEST.MF @@ -12,6 +12,9 @@ Export-Package: com.tractrac.asio.lib.api, com.tractrac.asio.lib.api.protocol.session, com.tractrac.asio.lib.api.record, com.tractrac.asio.lib.api.session, + com.tractrac.asio.lib.spi, + com.tractrac.asio.lib.spi.record, + com.tractrac.common.lib.api.messenger, com.tractrac.common.lib.api.observer, com.tractrac.common.lib.api.service, com.tractrac.dataflow.lib.api, @@ -21,6 +24,20 @@ Export-Package: com.tractrac.asio.lib.api, com.tractrac.dataflow.lib.api.live.server, com.tractrac.dataflow.lib.api.protocol, com.tractrac.dataflow.lib.api.stored, + com.tractrac.dataflow.lib.impl, + com.tractrac.dataflow.lib.prov.live.ascii, + com.tractrac.dataflow.lib.prov.live.provider, + com.tractrac.dataflow.lib.prov.live.server, + com.tractrac.dataflow.lib.prov.live.tcp, + com.tractrac.dataflow.lib.prov.live.tcp.provider, + com.tractrac.dataflow.lib.prov.live.tcp.server, + com.tractrac.dataflow.lib.prov.stored, + com.tractrac.dataflow.lib.prov.stored.jdbc, + com.tractrac.dataflow.lib.prov.stored.stream, + com.tractrac.dataflow.lib.prov.stored.tcp, + com.tractrac.dataflow.lib.prov.stored.url, + com.tractrac.dataflow.lib.spi, + com.tractrac.dataflow.lib.spi.authentication, com.tractrac.model.lib.api, com.tractrac.model.lib.api.attachment, com.tractrac.model.lib.api.data, @@ -28,11 +45,30 @@ Export-Package: com.tractrac.asio.lib.api, com.tractrac.model.lib.api.metadata, com.tractrac.model.lib.api.route, com.tractrac.model.lib.api.spatial, + com.tractrac.model.lib.impl, + com.tractrac.model.lib.impl.attachment, + com.tractrac.model.lib.impl.data, + com.tractrac.model.lib.impl.event, + com.tractrac.model.lib.impl.event.json, + com.tractrac.model.lib.impl.metadata, + com.tractrac.model.lib.impl.route, + com.tractrac.model.lib.impl.spatial, + com.tractrac.osgi, + com.tractrac.subscription.app.tracapi, com.tractrac.subscription.lib.api, com.tractrac.subscription.lib.api.competitor, com.tractrac.subscription.lib.api.control, com.tractrac.subscription.lib.api.event, com.tractrac.subscription.lib.api.race, + com.tractrac.subscription.lib.impl, + com.tractrac.subscription.lib.impl.competitor, + com.tractrac.subscription.lib.impl.control, + com.tractrac.subscription.lib.impl.event, + com.tractrac.subscription.lib.impl.gms, + com.tractrac.subscription.lib.impl.io, + com.tractrac.subscription.lib.impl.io.controllers, + com.tractrac.subscription.lib.impl.io.receivers, + com.tractrac.subscription.lib.impl.race, com.tractrac.util.lib.api, com.tractrac.util.lib.api.autolog, com.tractrac.util.lib.api.classloader, @@ -40,17 +76,18 @@ Export-Package: com.tractrac.asio.lib.api, com.tractrac.util.lib.api.enumerable, com.tractrac.util.lib.api.exceptions, com.tractrac.util.lib.api.image, - com.tractrac.util.lib.api.priorityqueue, + com.tractrac.util.lib.api.map, com.tractrac.util.lib.api.programparameters, com.tractrac.util.lib.api.search, com.tractrac.util.lib.api.selector, - com.tractrac.util.lib.api.seqtimedatatable, - com.tractrac.util.lib.api.seqtimedatatable.btreemem, - com.tractrac.util.lib.api.seqtimedatatable.btreememdisk, - com.tractrac.util.lib.api.seqtimedatatable.simple, com.tractrac.util.lib.api.serialize, - com.tractrac.util.lib.api.sortedset, - com.tractrac.util.lib.api.sortedset.btree + com.tractrac.util.lib.impl.autolog, + com.tractrac.util.lib.impl.autolog.file, + com.tractrac.util.lib.impl.autolog.log4j, + com.tractrac.util.lib.impl.autolog.multiple, + com.tractrac.util.lib.impl.autolog.println, + com.tractrac.util.lib.impl.classloader, + com.tractrac.util.lib.impl.programparameters Bundle-ActivationPolicy: lazy Bundle-Vendor: TracTrac Import-Package: org.osgi.framework From 194edc0552b2dea11a6645fe7b555ce58918dcb4 Mon Sep 17 00:00:00 2001 From: "Steffen Wagner (d059727)" Date: Thu, 24 Jul 2014 19:33:58 +0200 Subject: [PATCH 15/20] fixed bug #2089. works fine in the RaceCommitteeApp. Added a new method to sort the TreeMap by Fleets. --- .../app/domain/impl/BoatClassSeriesFleet.java | 15 ++++++++++++++- .../BoatClassSeriesDataFleetComparator.java | 2 +- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/domain/impl/BoatClassSeriesFleet.java b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/domain/impl/BoatClassSeriesFleet.java index 60c6d5bb08c..da318d32b67 100644 --- a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/domain/impl/BoatClassSeriesFleet.java +++ b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/domain/impl/BoatClassSeriesFleet.java @@ -12,6 +12,7 @@ public class BoatClassSeriesFleet { private BoatClass boatClass; private SeriesBase series; private int seriesOrder; + private int fleetOrder; private Fleet fleet; private static BoatClass getBoatClassForRace(ManagedRace managedRace) { @@ -29,10 +30,15 @@ public class BoatClassSeriesFleet { this.boatClass = getBoatClassForRace(race); this.series = race.getSeries(); this.seriesOrder = getSeriesIndex(race, series); + this.fleetOrder = getFleetIndex(series.getFleets(), race.getFleet()); this.fleet = race.getFleet(); } - public String getBoatClassName() { + private int getFleetIndex(Iterable fleets, Fleet fleet) { + return Util.indexOf(fleets, fleet); + } + + public String getBoatClassName() { return boatClass.getName(); } @@ -86,6 +92,7 @@ public class BoatClassSeriesFleet { result = prime * result + ((fleet == null) ? 0 : fleet.hashCode()); result = prime * result + ((series == null) ? 0 : series.hashCode()); result = prime * result + seriesOrder; + result = prime * result + fleetOrder; return result; } @@ -115,7 +122,13 @@ public class BoatClassSeriesFleet { return false; if (seriesOrder != other.seriesOrder) return false; + if (fleetOrder != other.fleetOrder) + return false; return true; } + + public int getFleetOrder() { + return fleetOrder; + } } diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/comparators/BoatClassSeriesDataFleetComparator.java b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/comparators/BoatClassSeriesDataFleetComparator.java index d67ee6655c2..d1d03872efd 100644 --- a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/comparators/BoatClassSeriesDataFleetComparator.java +++ b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/comparators/BoatClassSeriesDataFleetComparator.java @@ -20,7 +20,7 @@ public class BoatClassSeriesDataFleetComparator implements Comparator Date: Thu, 24 Jul 2014 22:03:53 +0200 Subject: [PATCH 16/20] fix tree-grow by using wind of next step for height comparison (ensure comparability of path candidates); cleanup tree-grow variety; --- .../impl/WindPatternDisplayManagerImpl.java | 2 +- .../sailing/simulator/test/TreeGrowTest.java | 4 +- .../impl/PathGeneratorTreeGrowTarget.java | 421 ---------- .../impl/PathGeneratorTreeGrowWind.java | 584 ++++++++++---- .../impl/PathGeneratorTreeGrowWind2.java | 566 ------------- .../impl/PathGeneratorTreeGrowWind3.java | 751 ------------------ .../simulator/impl/SailingSimulatorImpl.java | 2 +- .../simulator/impl/SimulatorUtils.java | 2 +- .../windfield/WindControlParameters.java | 2 +- 9 files changed, 426 insertions(+), 1908 deletions(-) delete mode 100644 java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PathGeneratorTreeGrowTarget.java delete mode 100644 java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PathGeneratorTreeGrowWind2.java delete mode 100644 java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PathGeneratorTreeGrowWind3.java diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/windpattern/impl/WindPatternDisplayManagerImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/windpattern/impl/WindPatternDisplayManagerImpl.java index ec2296132f3..82450651445 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/windpattern/impl/WindPatternDisplayManagerImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/windpattern/impl/WindPatternDisplayManagerImpl.java @@ -79,7 +79,7 @@ public class WindPatternDisplayManagerImpl implements WindPatternDisplayManager 1, 1, 10); display.addSetting(maxBlastSize); WindPatternSetting blastWindSpeed = new WindPatternSettingSliderBar("blastWindSpeed", - "Average Speed (%)", 0, 200, 5, 100, 10); + "Average Speed (%)", 0, 200, 5, 120, 10); display.addSetting(blastWindSpeed); WindPatternSetting blastWindSpeedVar = new WindPatternSettingSliderBar("blastWindSpeedVar", "Speed Variance (%)", 1e-4, 100, 5, 10, 10); diff --git a/java/com.sap.sailing.simulator.test/src/com/sap/sailing/simulator/test/TreeGrowTest.java b/java/com.sap.sailing.simulator.test/src/com/sap/sailing/simulator/test/TreeGrowTest.java index 4f50acfdbd8..6d102285d06 100644 --- a/java/com.sap.sailing.simulator.test/src/com/sap/sailing/simulator/test/TreeGrowTest.java +++ b/java/com.sap.sailing.simulator.test/src/com/sap/sailing/simulator/test/TreeGrowTest.java @@ -20,7 +20,7 @@ import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; import com.sap.sailing.simulator.Path; import com.sap.sailing.simulator.PolarDiagram; import com.sap.sailing.simulator.SimulationParameters; -import com.sap.sailing.simulator.impl.PathGeneratorTreeGrowWind3; +import com.sap.sailing.simulator.impl.PathGeneratorTreeGrowWind; import com.sap.sailing.simulator.impl.PolarDiagram49STG; import com.sap.sailing.simulator.impl.RectangularGrid; import com.sap.sailing.simulator.impl.SimulationParametersImpl; @@ -62,7 +62,7 @@ public class TreeGrowTest { param.setProperty("Djikstra.gridv[int]", 10.0); param.setProperty("Djikstra.gridh[int]", 100.0);*/ - PathGeneratorTreeGrowWind3 treeGrow = new PathGeneratorTreeGrowWind3(param); + PathGeneratorTreeGrowWind treeGrow = new PathGeneratorTreeGrowWind(param); Path path = treeGrow.getPath(); diff --git a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PathGeneratorTreeGrowTarget.java b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PathGeneratorTreeGrowTarget.java deleted file mode 100644 index 33a01c330cc..00000000000 --- a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PathGeneratorTreeGrowTarget.java +++ /dev/null @@ -1,421 +0,0 @@ -package com.sap.sailing.simulator.impl; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.logging.Logger; - -import com.sap.sailing.domain.common.Bearing; -import com.sap.sailing.domain.common.Distance; -import com.sap.sailing.domain.common.Position; -import com.sap.sailing.domain.common.SpeedWithBearing; -import com.sap.sailing.domain.common.TimePoint; -import com.sap.sailing.domain.common.impl.DegreeBearingImpl; -import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; -import com.sap.sailing.domain.tracking.Wind; -import com.sap.sailing.simulator.Path; -import com.sap.sailing.simulator.PolarDiagram; -import com.sap.sailing.simulator.SimulationParameters; -import com.sap.sailing.simulator.TimedPosition; -import com.sap.sailing.simulator.TimedPositionWithSpeed; -import com.sap.sailing.simulator.windfield.WindFieldGenerator; - -public class PathGeneratorTreeGrowTarget extends PathGeneratorBase { - - private static Logger logger = Logger.getLogger("com.sap.sailing"); - - int maxLeft; - int maxRight; - boolean startLeft; - - public PathGeneratorTreeGrowTarget(SimulationParameters params) { - this.parameters = params; - } - - public void setEvaluationParameters(int maxLeftVal, int maxRightVal, boolean startLeftVal) { - this.maxLeft = maxLeftVal; - this.maxRight = maxRightVal; - this.startLeft = startLeftVal; - } - - class PathCand implements Comparable { - public PathCand(TimedPosition pos, double vrt, double hrz, String path) { - this.pos = pos; - this.vrt = vrt; - this.hrz = hrz; - this.path = path; - } - - TimedPosition pos; - double vrt; - double hrz; - String path; - - /*@Override - // sort ascending by horizontal distance - public int compareTo(PathCand other) { - if (Math.abs(this.hrz) == Math.abs(other.hrz)) - return 0; - return (Math.abs(this.hrz) < Math.abs(other.hrz) ? -1 : +1); - }*/ - @Override - // sort descending by height - public int compareTo(PathCand other) { - if (this.vrt == other.vrt) { - if (Math.abs(this.hrz) == Math.abs(other.hrz)) { - return 0; - } else { - return (Math.abs(this.hrz) < Math.abs(other.hrz) ? -1 : +1); - } - } else { - return (this.vrt > other.vrt ? -1 : +1); - } - } - - } - - // generate step - TimedPosition getStep(TimedPosition pos, long timeStep, long turnLoss, char prevDirection, char nextDirection) { - - double offDeg = 5.0; - WindFieldGenerator wf = this.parameters.getWindField(); - TimePoint curTime = pos.getTimePoint(); - Position curPosition = pos.getPosition(); - Wind posWind = wf.getWind(new TimedPositionWithSpeedImpl(curTime, curPosition, null)); - - PolarDiagram pd = this.parameters.getBoatPolarDiagram(); - pd.setWind(posWind); - - // get beat-angle left and right - Bearing travelBearing = null; - Bearing tmpBearing = null; - if (nextDirection == 'L') { - travelBearing = pd.optimalDirectionsUpwind()[0]; - } else if (nextDirection == 'R') { - travelBearing = pd.optimalDirectionsUpwind()[1]; - } else if (nextDirection == 'M') { - tmpBearing = pd.optimalDirectionsUpwind()[0]; - travelBearing = tmpBearing.add(new DegreeBearingImpl(+offDeg)); - } else if (nextDirection == 'S') { - tmpBearing = pd.optimalDirectionsUpwind()[1]; - travelBearing = tmpBearing.add(new DegreeBearingImpl(-offDeg)); - } - - // determine beat-speed left and right - SpeedWithBearing travelSpeed = pd.getSpeedAtBearing(travelBearing); - - TimePoint travelTime; - TimePoint nextTime = new MillisecondsTimePoint(curTime.asMillis()+timeStep); - char prevBaseDirection = prevDirection; - if (prevBaseDirection == 'M') { - prevBaseDirection = 'L'; - } - if (prevBaseDirection == 'S') { - prevBaseDirection = 'R'; - } - char nextBaseDirection = nextDirection; - if (nextBaseDirection == 'M') { - nextBaseDirection = 'L'; - } - if (nextBaseDirection == 'S') { - nextBaseDirection = 'R'; - } - boolean sameBaseDirection = (nextBaseDirection == prevBaseDirection)||(prevBaseDirection == '0'); - if (sameBaseDirection) { - travelTime = nextTime; - } else { - travelTime = new MillisecondsTimePoint(nextTime.asMillis() - turnLoss); - } - - return new TimedPositionImpl(nextTime, travelSpeed.travelTo(curPosition, curTime, travelTime)); - } - - PathCand getPathCand(PathCand path, char nextDirection, long timeStep, long turnLoss, Position posStart, Bearing bearVrt, double tgtHeight) { - - // calculate next path position (taking turn-loss into account) - TimedPosition pathPos = this.getStep(path.pos, timeStep, turnLoss, path.path.charAt(path.path.length()-1), nextDirection); - - // calculate height-position with reference to race course - Position posHeight = pathPos.getPosition().projectToLineThrough(posStart, bearVrt); - - // calculate vertical distance as distance of height-position to start - double vrtDist = Math.round(posHeight.getDistance(posStart).getMeters()*100.0)/100.0; - /*if (vrtDist > tgtHeight) { - // scale last step so that vrtDist ~ tgtHeight - Position prevPos = path.pos.getPosition(); - TimePoint prevTime = path.pos.getTimePoint(); - double heightFrac = (tgtHeight - path.vrt) / (vrtDist - path.vrt); - Position newPos = prevPos.translateGreatCircle(prevPos.getBearingGreatCircle(pathPos.getPosition()), prevPos.getDistance(pathPos.getPosition()).scale(heightFrac)); - TimePoint newTime = new MillisecondsTimePoint(Math.round(prevTime.asMillis() + (pathPos.getTimePoint().asMillis()-prevTime.asMillis())*heightFrac)); - pathPos = new TimedPositionImpl(newTime, newPos); - posHeight = pathPos.getPosition().projectToLineThrough(posStart, bearVrt); - }*/ - - // calculate horizontal side: left or right in reference to race course - double posSide = 1; - double posBear = posStart.getBearingGreatCircle(pathPos.getPosition()).getDegrees(); - if ((posBear < 0.0)||(posBear > 180.0)) { - posSide = -1; - } else if ((posBear == 0.0)||(posBear == 180.0)) { - posSide = 0; - } - // calculate horizontal distance as distance of height-position to current position - double hrzDist = Math.round(posSide*posHeight.getDistance(pathPos.getPosition()).getMeters()*100.0)/100.0; - - //System.out.println(""+hrzDist+", "+vrtDist+", "+pathPos.getPosition().getLatDeg()+", "+pathPos.getPosition().getLngDeg()+", "+posHeight.getLatDeg()+", "+posHeight.getLngDeg()); - - // extend path-string by step-direction - String pathStr = path.path + nextDirection; - - return (new PathCand(pathPos, vrtDist, hrzDist, pathStr)); - } - - // generate path candidates based on beat angles - List getPathCandsBeat(PathCand path, long timeStep, long turnLoss, Position posStart, Bearing bearVrt, double tgtHeight) { - - List result = new ArrayList(); - PathCand newPathCand; - - // step left - newPathCand = getPathCand(path, 'L', timeStep, turnLoss, posStart, bearVrt, tgtHeight); - result.add(newPathCand); - - // step wide left - newPathCand = getPathCand(path, 'M', timeStep, turnLoss, posStart, bearVrt, tgtHeight); - result.add(newPathCand); - - // step right - newPathCand = getPathCand(path, 'R', timeStep, turnLoss, posStart, bearVrt, tgtHeight); - result.add(newPathCand); - - // step wide right - newPathCand = getPathCand(path, 'S', timeStep, turnLoss, posStart, bearVrt, tgtHeight); - result.add(newPathCand); - - return result; - } - - @Override - public Path getPath() { - - WindFieldGenerator wf = this.parameters.getWindField(); - PolarDiagram pd = this.parameters.getBoatPolarDiagram(); - Position startPos = this.parameters.getCourse().get(0); - Position endPos = this.parameters.getCourse().get(1); - TimePoint startTime = wf.getStartTime();// new MillisecondsTimePoint(0); - List path = new ArrayList(); - - Position currentPosition = startPos; - TimePoint currentTime = startTime; - - Distance distStartEnd = startPos.getDistance(endPos); - double distStartEndMeters = distStartEnd.getMeters(); - - long timeStep = wf.getTimeStep().asMillis()/3; - logger.info("Time step :" + timeStep); - long turnLoss = pd.getTurnLoss(); // 4000; // time lost when doing a turn - - Wind wndStart = wf.getWind(new TimedPositionWithSpeedImpl(startTime, startPos, null)); - logger.fine("wndStart speed:" + wndStart.getKnots() + " angle:" + wndStart.getBearing().getDegrees()); - pd.setWind(wndStart); - Bearing bearVrt = startPos.getBearingGreatCircle(endPos); - //Bearing bearHrz = bearVrt.add(new DegreeBearingImpl(90.0)); - Position middlePos = startPos.translateGreatCircle(bearVrt, distStartEnd.scale(0.5)); - - System.out.println("start : "+startPos.getLatDeg()+", "+startPos.getLngDeg()); - System.out.println("middle: "+middlePos.getLatDeg()+", "+middlePos.getLngDeg()); - System.out.println("end : "+endPos.getLatDeg()+", "+endPos.getLngDeg()); - - // initialize list of paths - PathCand initPath = new PathCand(new TimedPositionImpl(currentTime, currentPosition), 0.0, 0.0, "0"); - String initPathStr = "0"; - //String initPathStr = "0RRRRRRRRRRRRRRRLLL"; - //String initPathStr = "0RRRRRRRRRRRRRRRRRR"; - //PathCand initPath = new PathCand(new TimedPositionImpl(currentTime, currentPosition), 0.0, 0.0, ); //"0RRRRRRRRRRRRRRR"); - if (initPathStr.length()>1) { - //char prevDirection = '0'; - char nextDirection = '0'; - for(int idx=1; idx allPaths = new ArrayList(); - List trgPaths = new ArrayList(); - allPaths.add(initPath); - - - TimedPosition tstPosition = this.getStep(new TimedPositionImpl(startTime, startPos), timeStep, turnLoss, '0', 'L'); - double tstDist1 = startPos.getDistance(tstPosition.getPosition()).getMeters(); - tstPosition = this.getStep(new TimedPositionImpl(startTime, startPos), timeStep, turnLoss, '0', 'R'); - double tstDist2 = startPos.getDistance(tstPosition.getPosition()).getMeters(); - - double hrzBinSize = (tstDist1 + tstDist2)/3; // horizontal bin size in meters - System.out.println("Horizontal Bin Size: "+hrzBinSize); - - double oobFact = 0.75; // out-of-bounds factor - //int hrzLeft = (int)Math.round(Math.floor( -distStartEndMeters / 2.0 / hrzBinSize / oobFact )); - //int hrzRight = (int)Math.round(Math.floor( distStartEndMeters / 2.0 / hrzBinSize / oobFact )); - - boolean reachedEnd = false; - int addSteps = 0; - int finalSteps = 0; // maximum number of additional steps after first target-path found - //for(int count=0; count<10; count++) { - while ((!reachedEnd)||(addSteps newPathCands; - List newPaths = new ArrayList(); - for(PathCand curPath : allPaths) { - - if ((curPath.vrt > distStartEndMeters)) { - continue; - } else { - newPathCands = this.getPathCandsBeat(curPath, timeStep, turnLoss, startPos, bearVrt, distStartEndMeters); - } - - for(PathCand newPath : newPathCands) { - - newPaths.add(newPath); - if (newPath.hrz < hrzMin) { - hrzMin = newPath.hrz; - } - if (newPath.hrz > hrzMax) { - hrzMax = newPath.hrz; - } - } - } - - int hrzLeft = (int)Math.round(Math.floor( (hrzMin + hrzBinSize/2.0) / hrzBinSize )); - int hrzRight = (int)Math.round(Math.floor( (hrzMax + hrzBinSize/2.0) / hrzBinSize )); - //System.out.println("hrzLeft: "+hrzLeft+", hrzRight: "+hrzRight); - - // build map of best path per hrz-bin - int countPath = 0; - int mapSize = hrzRight-hrzLeft+1; - //System.out.println("MapSize: "+mapSize); - - int[] binIdx = new int[mapSize]; // TODO: init with zero? - double[] binVal = new double[mapSize]; // TODO: init with zero? - for(int curIdx=0; curIdx oobFact*distStartEndMeters) { - continue; // ignore curPath - } - - // increase path-counter - countPath++; - - // determine map-key - int curKey = (int)Math.round(Math.floor( (curPath.hrz + hrzBinSize/2.0) / hrzBinSize )) - hrzLeft; - - /*String allR = "0" + (new String(new char[curPath.path.length()-1]).replace('\0', 'R')); - if (curPath.path.equals(allR)) { - System.out.println(""+curPath.path+": "+curPath.hrz+", "+curPath.vrt+", "+curPath.pos.getPosition().getLatDeg()+", "+curPath.pos.getPosition().getLngDeg()); - }*/ - - // check whether curPath is better then the ones looked at - if ((curKey>=0)&&(curKey < mapSize)) { - if (curPath.vrt > binVal[curKey]) { - binVal[curKey] = curPath.vrt; - binIdx[curKey] = curIdx+1; - } - } - } - - // check if there are still paths inside regatta-area - if (countPath > 0) { - // take best from each horizontal-bin and check if target is reached - allPaths = new ArrayList(); - for (int curKey = 0; curKey < mapSize; curKey++) { - - if (binIdx[curKey] > 0) { - PathCand curPath = newPaths.get(binIdx[curKey] - 1); - allPaths.add(curPath); - - // debug output - /*if ((Math.abs(curKey + hrzLeft) <= 3)) { - System.out.println("" + curPath.path + ": " + (curKey + hrzLeft) + ", vrt:" + curPath.vrt + ", hrz:" + curPath.hrz + ", bin:" + (Math.floor((curPath.hrz + hrzBinSize / 2.0) / hrzBinSize))); - }*/ - - // terminate path-search if paths close enough to target are found - if ((curPath.vrt > distStartEndMeters)) { - if ((Math.abs(curKey + hrzLeft) <= 2)) { - reachedEnd = true; - trgPaths.add(curPath); // add path to list of target-paths - } - } - } - } - } else { - // terminate path-search as no path inside regatta-area are left - reachedEnd = true; - } - } - - - // if no target-paths were found, return empty path - if (trgPaths.size() == 0) { - //trgPaths = allPaths; // TODO: only for testing; remove lateron - return new PathImpl(path, wf); // return empty path - } - - // sort target-paths ascending by distance-to-target - Collections.sort(trgPaths); - - // debug output - for(PathCand curPath : trgPaths) { - System.out.print(""+curPath.path+": "+curPath.pos.getTimePoint().asMillis()+", "+curPath.pos.getPosition().getLatDeg()+", "+curPath.pos.getPosition().getLngDeg()+", "); - System.out.println(" height:"+curPath.vrt+" of "+startPos.getDistance(endPos).getMeters()+", dist:"+curPath.hrz+" ~ "+curPath.pos.getPosition().getDistance(endPos)); - } - - // - // fill gwt-path - // - - // generate intermediate steps - PathCand bstPath = trgPaths.get(0); // target-path ending closest to target - TimedPositionWithSpeed curPosition = null; - char nextDirection = '0'; - char prevDirection = '0'; - for(int step=0; step<(bstPath.path.length()-1); step++) { - - nextDirection = bstPath.path.charAt(step); - - if (nextDirection == '0') { - - curPosition = new TimedPositionWithSpeedImpl(startTime, startPos, null); - path.add(curPosition); - - } else { - - TimedPosition newPosition = this.getStep(curPosition, timeStep, turnLoss, prevDirection, nextDirection); - curPosition = new TimedPositionWithSpeedImpl(newPosition.getTimePoint(), newPosition.getPosition(), null); - path.add(curPosition); - - } - - prevDirection = nextDirection; - } - - // add final position (rescaled before to end on height of target) - path.add(new TimedPositionWithSpeedImpl(bstPath.pos.getTimePoint(), bstPath.pos.getPosition(), null)); - - return new PathImpl(path, wf); - - } - -} diff --git a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PathGeneratorTreeGrowWind.java b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PathGeneratorTreeGrowWind.java index 6e843497337..f398ffe6eb1 100644 --- a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PathGeneratorTreeGrowWind.java +++ b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PathGeneratorTreeGrowWind.java @@ -1,7 +1,11 @@ package com.sap.sailing.simulator.impl; +import java.io.BufferedWriter; +import java.io.FileWriter; +import java.io.IOException; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.logging.Logger; @@ -27,57 +31,62 @@ public class PathGeneratorTreeGrowWind extends PathGeneratorBase { private static Logger logger = Logger.getLogger("com.sap.sailing"); private boolean debugMsgOn = false; + double oobFact = 0.75; // out-of-bounds factor int maxTurns = 0; + boolean upwindLeg = false; String initPathStr = "0"; PathCandidate bestCand = null; + long usedTimeStep = 0; + boolean gridStore = false; + ArrayList> gridPositions = null; + ArrayList> isocPositions = null; + String gridFile = null; public PathGeneratorTreeGrowWind(SimulationParameters params) { this.parameters = params; } - public void setEvaluationParameters(String startDirection, int maxTurns) { + public void setEvaluationParameters(String startDirection, int maxTurns, String gridFile) { if (startDirection != null) { this.initPathStr = "0" + startDirection; } else { this.initPathStr = "0"; } this.maxTurns = maxTurns; + this.gridFile = gridFile; + if (this.gridFile != null) { + this.gridStore = true; + this.gridPositions = new ArrayList>(); + this.isocPositions = new ArrayList>(); + } else { + this.gridStore = false; + this.gridPositions = null; + this.isocPositions = null; + } } - class PathCandidate implements Comparable { - public PathCandidate(TimedPosition pos, double vrt, double hrz, int trn, String path) { - this.pos = pos; - this.vrt = vrt; - this.hrz = hrz; - this.trn = trn; - this.path = path; + class SortPathCandsAbsHorizontally implements Comparator { + + @Override + public int compare(PathCandidate p1, PathCandidate p2) { + if (Math.abs(p1.hrz) == Math.abs(p2.hrz)) { + return 0; + } else { + return (Math.abs(p1.hrz) < Math.abs(p2.hrz) ? -1 : +1); + } } - TimedPosition pos; - double vrt; - double hrz; - int trn; - String path; + } + + class SortPathCandsHorizontally implements Comparator { - /*@Override - // sort ascending by horizontal distance - public int compareTo(PathCand other) { - if (Math.abs(this.hrz) == Math.abs(other.hrz)) - return 0; - return (Math.abs(this.hrz) < Math.abs(other.hrz) ? -1 : +1); - }*/ @Override - // sort descending by height - public int compareTo(PathCandidate other) { - if (this.vrt == other.vrt) { - if (Math.abs(this.hrz) == Math.abs(other.hrz)) { - return 0; - } else { - return (Math.abs(this.hrz) < Math.abs(other.hrz) ? -1 : +1); - } + public int compare(PathCandidate p1, PathCandidate p2) { + if (p1.hrz == p2.hrz) { + return 0; } else { - return (this.vrt > other.vrt ? -1 : +1); - } + return (p1.hrz < p2.hrz ? -1 : +1); + } } } @@ -86,36 +95,57 @@ public class PathGeneratorTreeGrowWind extends PathGeneratorBase { PathCandidate getBestCand() { return this.bestCand; } + + long getUsedTimeStep() { + return this.usedTimeStep; + } // generate step in one of the possible directions // default: L - left, R - right // extended: M - wide left, S - wide right - Util.Pair getStep(TimedPosition pos, long timeStep, long turnLoss, boolean sameBaseDirection, char nextDirection) { + TimedPosition getStep(TimedPosition pos, long timeStep, long turnLoss, boolean sameBaseDirection, char nextDirection) { - double offDeg = 5.0; + double offDeg = 3.0; WindFieldGenerator wf = this.parameters.getWindField(); TimePoint curTime = pos.getTimePoint(); Position curPosition = pos.getPosition(); - Wind posWind = wf.getWind(new TimedPositionWithSpeedImpl(curTime, curPosition, null)); + Wind posWind = wf.getWind(new TimedPositionImpl(curTime, curPosition)); PolarDiagram pd = this.parameters.getBoatPolarDiagram(); pd.setWind(posWind); - Wind appWind = new WindImpl(posWind.getPosition(), posWind.getTimePoint(), pd.getWind());; // get beat-angle left and right Bearing travelBearing = null; Bearing tmpBearing = null; if (nextDirection == 'L') { - travelBearing = pd.optimalDirectionsUpwind()[0]; + if (this.upwindLeg) { + travelBearing = pd.optimalDirectionsUpwind()[0]; + } else { + travelBearing = pd.optimalDirectionsDownwind()[0]; + } } else if (nextDirection == 'R') { - travelBearing = pd.optimalDirectionsUpwind()[1]; + if (this.upwindLeg) { + travelBearing = pd.optimalDirectionsUpwind()[1]; + } else { + travelBearing = pd.optimalDirectionsDownwind()[1]; + } } else if (nextDirection == 'M') { - tmpBearing = pd.optimalDirectionsUpwind()[0]; - travelBearing = tmpBearing.add(new DegreeBearingImpl(+offDeg)); + if (this.upwindLeg) { + tmpBearing = pd.optimalDirectionsUpwind()[0]; + travelBearing = tmpBearing.add(new DegreeBearingImpl(-offDeg)); + } else { + tmpBearing = pd.optimalDirectionsDownwind()[0]; + travelBearing = tmpBearing.add(new DegreeBearingImpl(-offDeg)); + } } else if (nextDirection == 'S') { - tmpBearing = pd.optimalDirectionsUpwind()[1]; - travelBearing = tmpBearing.add(new DegreeBearingImpl(-offDeg)); + if (this.upwindLeg) { + tmpBearing = pd.optimalDirectionsUpwind()[1]; + travelBearing = tmpBearing.add(new DegreeBearingImpl(+offDeg)); + } else { + tmpBearing = pd.optimalDirectionsDownwind()[1]; + travelBearing = tmpBearing.add(new DegreeBearingImpl(+offDeg)); + } } // determine beat-speed left and right @@ -129,7 +159,8 @@ public class PathGeneratorTreeGrowWind extends PathGeneratorBase { travelTime = new MillisecondsTimePoint(nextTime.asMillis() - turnLoss); } - return new Util.Pair(new TimedPositionImpl(nextTime, travelSpeed.travelTo(curPosition, curTime, travelTime)), appWind); + Position nextPosition = travelSpeed.travelTo(curPosition, curTime, travelTime); + return new TimedPositionImpl(nextTime, nextPosition); } // use base direction to distinguish direction changes that do or don't require a turn @@ -167,20 +198,29 @@ public class PathGeneratorTreeGrowWind extends PathGeneratorBase { } // calculate next path position (taking turn-loss into account) - Util.Pair nextStep = this.getStep(path.pos, timeStep, turnLoss, sameBaseDirection, nextDirection); - TimedPosition pathPos = nextStep.getA(); - Wind posWind = nextStep.getB(); + TimedPosition pathPos = this.getStep(path.pos, timeStep, turnLoss, sameBaseDirection, nextDirection); + + // determine apparent wind at next path position & time + Wind posWind = this.parameters.getWindField().getWind(pathPos); + PolarDiagram pd = this.parameters.getBoatPolarDiagram(); + pd.setWind(posWind); + Wind appWind = new WindImpl(posWind.getPosition(), posWind.getTimePoint(), pd.getWind()); - // calculate height-position with reference to race course - Position posHeight = pathPos.getPosition().projectToLineThrough(posEnd, posWind.getBearing()); - Bearing bearVrt = posStart.getBearingGreatCircle(posEnd); - Position posHeightTrgt = pathPos.getPosition().projectToLineThrough(posStart, bearVrt); - //Position posHeightWind = pathPos.getPosition().projectToLineThrough(posRef, posWind.getBearing()); + // calculate height-position with reference to apparent wind + Position posHeight = pathPos.getPosition().projectToLineThrough(posEnd, appWind.getBearing()); - // calculate vertical distance as distance of height-position to start - //double vrtDist = tgtHeight - Math.round(posHeight.getDistance(posEnd).getMeters()*100.0)/100.0; - double vrtDist = Math.round(posHeight.getDistance(posStart).getMeters()*100.0)/100.0; + // calculate vertical distance as distance of height-position to end + Bearing bearHeight = posEnd.getBearingGreatCircle(posHeight); + double bearHeightSide = appWind.getBearing().getDifferenceTo(bearHeight).getDegrees(); + double vrtSide = (this.upwindLeg ? -1.0 : +1.0); + if (Math.abs(bearHeightSide) > 170.0) { + vrtSide = (this.upwindLeg ? +1.0 : -1.0); + } + double vrtDist = vrtSide*Math.round(posHeight.getDistance(posEnd).getMeters()*1000.0)/1000.0; + // + // TODO: scale last step to exactly reach height of posEnd, and adjust time correspondingly + // /*if (vrtDist > tgtHeight) { // scale last step so that vrtDist ~ tgtHeight Position prevPos = path.pos.getPosition(); @@ -194,23 +234,23 @@ public class PathGeneratorTreeGrowWind extends PathGeneratorBase { // calculate horizontal side: left or right in reference to race course double posSide = 1; - //double posBear = posWind.getBearing().getDegrees() - posEnd.getBearingGreatCircle(pathPos.getPosition()).getDegrees(); - double posBear = posStart.getBearingGreatCircle(pathPos.getPosition()).getDegrees(); - if ((posBear < 0.0)||(posBear > 180.0)) { + Bearing posBear = posStart.getBearingGreatCircle(pathPos.getPosition()); + Bearing bearVrt = posStart.getBearingGreatCircle(posEnd); + double posBearDiff = bearVrt.getDifferenceTo(posBear).getDegrees(); + if ((posBearDiff < 0.0)||(posBearDiff > 180.0)) { posSide = -1; - } else if ((posBear == 0.0)||(posBear == 180.0)) { + } else if ((posBearDiff == 0.0)||(posBearDiff == 180.0)) { posSide = 0; } // calculate horizontal distance as distance of height-position to current position - //double hrzDist = Math.round(posSide*posHeight.getDistance(pathPos.getPosition()).getMeters()*100.0)/100.0; - double hrzDist = Math.round(posSide*posHeightTrgt.getDistance(pathPos.getPosition()).getMeters()*100.0)/100.0; - - //System.out.println(""+hrzDist+", "+vrtDist+", "+pathPos.getPosition().getLatDeg()+", "+pathPos.getPosition().getLngDeg()+", "+posHeight.getLatDeg()+", "+posHeight.getLngDeg()); + Position posHeightTrgt = pathPos.getPosition().projectToLineThrough(posStart, bearVrt); + double hrzDist = Math.round(posSide*posHeightTrgt.getDistance(pathPos.getPosition()).getMeters()*1000.0)/1000.0; // extend path-string by step-direction String pathStr = path.path + nextDirection; + char nextBaseDirection = this.getBaseDirection(nextDirection); - return (new PathCandidate(pathPos, vrtDist, hrzDist, turnCount, pathStr)); + return (new PathCandidate(pathPos, vrtDist, hrzDist, turnCount, pathStr, nextBaseDirection, appWind)); } @@ -256,13 +296,200 @@ public class PathGeneratorTreeGrowWind extends PathGeneratorBase { return result; } + Util.Pair,List> generateCandidate(List oldPaths, long timeStep, long turnLoss, Position posStart, Position posMiddle, Position posEnd, double tgtHeight) { + + List newPathCands; + List leftPaths = new ArrayList(); + List rightPaths = new ArrayList(); + for(PathCandidate curPath : oldPaths) { + + newPathCands = this.getPathCandsBeatWind(curPath, timeStep, turnLoss, posStart, posEnd, tgtHeight); + for (PathCandidate curNewPath : newPathCands) { + // check whether path is *outside* regatta-area + double distFromMiddleMeters = posMiddle.getDistance(curPath.pos.getPosition()).getMeters(); + if (distFromMiddleMeters > oobFact * tgtHeight) { + continue; // ignore curPath + } + + if (curNewPath.sid == 'L') { + leftPaths.add(curNewPath); + } else if (curNewPath.sid == 'R') { + rightPaths.add(curNewPath); + } + + } + + } + + Util.Pair,List> newPaths = new Util.Pair,List>(leftPaths, rightPaths); + return newPaths; + } + + + List filterCandidates(List allCands, double hrzBinWidth) { + + boolean[] filterMap = new boolean[allCands.size()]; + + // sort candidates by horizontal distance + Comparator sortHorizontal = new SortPathCandsHorizontally(); + Collections.sort(allCands, sortHorizontal); + + // start scan with index 0 + int idxL = 0; + int idxR = 0; + + // for each candidate, check the neighborhoods and identify bad candidates + for(int idx = 0; idx < allCands.size(); idx++) { + + // current horizontal distance + double hrzDist = allCands.get(idx).hrz; + + // align left index + while(Math.abs(hrzDist - allCands.get(idxL).hrz) > hrzBinWidth) { + idxL++; + } + + // align right index + boolean finished = false; + while(!finished && (idxR < (allCands.size()-1))) { + if (Math.abs(hrzDist - allCands.get(idxR+1).hrz) <= hrzBinWidth) { + idxR++; + } else { + finished = true; + } + } + + // search maximum height + // in neighborhood idxL, ..., idxR + + // init max for search + int vrtIdx = idxL; + double vrtMax = allCands.get(vrtIdx).vrt; + filterMap[vrtIdx] = false; + + // evaluate remainder of neighborhood + if (idxL < idxR) { + for(int jdx = (idxL+1); jdx <= idxR; jdx++) { + if (allCands.get(jdx).vrt > vrtMax) { + // reset previous max candidate + filterMap[vrtIdx] = true; + // keep max height + vrtMax = allCands.get(jdx).vrt; + // keep max index + vrtIdx = jdx; + // set current max candidate + filterMap[vrtIdx] = false; + } else { + filterMap[jdx] = true; + } + } + } + + } // endfor each candidate + + // collect all good candidates (i.e. filterMap == false) + List filterCands = new ArrayList(); + for(int idx=0; idx < allCands.size(); idx++) { + if (!filterMap[idx]) { + filterCands.add(allCands.get(idx)); + } + } + + // return remaining good candidates + return filterCands; + } + + + List filterIsochrone(List allCands, double hrzBinWidth) { + + boolean[] filterMap = new boolean[allCands.size()]; + for(int idx = 0; idx < allCands.size(); idx++) { + filterMap[idx] = true; + } + + // sort candidates by horizontal distance + Comparator sortHorizontal = new SortPathCandsHorizontally(); + Collections.sort(allCands, sortHorizontal); + + // start scan with index 0 + int idxL = 0; + int idxR = 0; + + // for each candidate, check the neighborhoods and identify bad candidates + for(int idx = 0; idx < allCands.size(); idx++) { + + // current horizontal distance + double hrzDist = allCands.get(idx).hrz; + + // align left index + while(Math.abs(hrzDist - allCands.get(idxL).hrz) > hrzBinWidth) { + idxL++; + } + + // align right index + boolean finished = false; + while(!finished && (idxR < (allCands.size()-1))) { + if (Math.abs(hrzDist - allCands.get(idxR+1).hrz) <= hrzBinWidth) { + idxR++; + } else { + finished = true; + } + } + + // search maximum height + // in neighborhood idxL, ..., idxR + + // init max for search + ArrayList vrtIdx = new ArrayList(); + vrtIdx.add(idxL); + double vrtMax = allCands.get(idxL).vrt; + + // evaluate remainder of neighborhood + if (idxL < idxR) { + for(int jdx = (idxL+1); jdx <= idxR; jdx++) { + if (allCands.get(jdx).vrt > vrtMax) { + // keep max height + vrtMax = allCands.get(jdx).vrt; + // keep max index + vrtIdx = new ArrayList(); + vrtIdx.add(jdx); + } else if (allCands.get(jdx).vrt == vrtMax) { + // add further max indexes + vrtIdx.add(jdx); + } + } + } + + for(Integer jdx : vrtIdx) { + filterMap[jdx] = false; + } + + } // endfor each candidate + + // collect all good candidates (i.e. filterMap == false) + List filterCands = new ArrayList(); + for(int idx=0; idx < allCands.size(); idx++) { + if (!filterMap[idx]) { + filterCands.add(allCands.get(idx)); + } + } + + // return remaining good candidates + return filterCands; + } + @Override public Path getPath() { - WindFieldGenerator wf = this.parameters.getWindField(); PolarDiagram pd = this.parameters.getBoatPolarDiagram(); + Position startPos = this.parameters.getCourse().get(0); Position endPos = this.parameters.getCourse().get(1); + + // test downwind: exchange start and end + //Position startPos = this.parameters.getCourse().get(1); + //Position endPos = this.parameters.getCourse().get(0); + TimePoint startTime = wf.getStartTime();// new MillisecondsTimePoint(0); List path = new ArrayList(); @@ -272,25 +499,42 @@ public class PathGeneratorTreeGrowWind extends PathGeneratorBase { Distance distStartEnd = startPos.getDistance(endPos); double distStartEndMeters = distStartEnd.getMeters(); - long timeStep = wf.getTimeStep().asMillis()/3; - logger.info("Time step :" + timeStep); - long turnLoss = pd.getTurnLoss(); // 4000; // time lost when doing a turn - Wind wndStart = wf.getWind(new TimedPositionWithSpeedImpl(startTime, startPos, null)); logger.fine("wndStart speed:" + wndStart.getKnots() + " angle:" + wndStart.getBearing().getDegrees()); pd.setWind(wndStart); Bearing bearVrt = startPos.getBearingGreatCircle(endPos); //Bearing bearHrz = bearVrt.add(new DegreeBearingImpl(90.0)); Position middlePos = startPos.translateGreatCircle(bearVrt, distStartEnd.scale(0.5)); - + + Bearing bearRCWind = wndStart.getBearing().getDifferenceTo(bearVrt); + String legType = "downwind"; + this.upwindLeg = false; + + if ((Math.abs(bearRCWind.getDegrees()) > 90.0)&&(Math.abs(bearRCWind.getDegrees()) < 270.0)) { + legType = "upwind"; + this.upwindLeg = true; + } + if (debugMsgOn) { System.out.println("start : "+startPos.getLatDeg()+", "+startPos.getLngDeg()); System.out.println("middle: "+middlePos.getLatDeg()+", "+middlePos.getLngDeg()); System.out.println("end : "+endPos.getLatDeg()+", "+endPos.getLngDeg()); } + logger.info("Leg Direction: "+legType); + long timeStep = wf.getTimeStep().asMillis()/2; + if (!this.upwindLeg) { + timeStep = timeStep / 2; + } + this.usedTimeStep = timeStep; + logger.info("Time step :" + timeStep); + long turnLoss = pd.getTurnLoss(); // 4000; // time lost when doing a turn + if (!this.upwindLeg) { + turnLoss = turnLoss / 2; + } + // calculate initial position according to initPathStr - PathCandidate initPath = new PathCandidate(new TimedPositionImpl(currentTime, currentPosition), 0.0, 0.0, 0, "0"); + PathCandidate initPath = new PathCandidate(new TimedPositionImpl(currentTime, currentPosition), 0.0, 0.0, 0, "0", '0', wndStart); if (initPathStr.length()>1) { char nextDirection = '0'; for(int idx=1; idx newPathCands; - List newPaths = new ArrayList(); - for(PathCandidate curPath : allPaths) { + // generate new candidates (inside regatta-area) + Util.Pair,List> newPaths = this.generateCandidate(allPaths, timeStep, turnLoss, startPos, middlePos, endPos, distStartEndMeters); - if ((curPath.vrt > distStartEndMeters)) { - continue; - } else { - //newPathCands = this.getPathCandsBeat(curPath, timeStep, turnLoss, startPos, bearVrt, distStartEndMeters); - newPathCands = this.getPathCandsBeatWind(curPath, timeStep, turnLoss, startPos, endPos, distStartEndMeters); + + // select good candidates + List leftPaths = this.filterCandidates(newPaths.getA(), hrzBinSize/2.0); + List rightPaths = this.filterCandidates(newPaths.getB(), hrzBinSize/2.0); + + List nextPaths = new ArrayList (); + nextPaths.addAll(leftPaths); + nextPaths.addAll(rightPaths); + + allPaths = nextPaths; + + if (this.gridStore) { + + /*ArrayList isoChrone = new ArrayList(); + for(PathCand curCand : allPaths) { + isoChrone.add(curCand.pos); } + this.gridPositions.add(isoChrone);*/ - for(PathCandidate newPath : newPathCands) { + this.gridPositions.add(allPaths); + + List isocPaths = this.filterIsochrone(allPaths, hrzBinSize); + this.isocPositions.add(isocPaths); - newPaths.add(newPath); - if (newPath.hrz < hrzMin) { - hrzMin = newPath.hrz; - } - if (newPath.hrz > hrzMax) { - hrzMax = newPath.hrz; - } - } } - int hrzLeft = (int)Math.round(Math.floor( (hrzMin + hrzBinSize/2.0) / hrzBinSize )); - int hrzRight = (int)Math.round(Math.floor( (hrzMax + hrzBinSize/2.0) / hrzBinSize )); - //System.out.println("hrzLeft: "+hrzLeft+", hrzRight: "+hrzRight); + // check if there are still paths in the regatta-area + if (allPaths.size() > 0) { - // build map of best path per hrz-bin - int countPath = 0; - int mapSize = hrzRight-hrzLeft+1; - //System.out.println("MapSize: "+mapSize); - - int[] binIdx = new int[mapSize]; // TODO: init with zero? - double[] binVal = new double[mapSize]; - // TODO: init of binVal - /*for(int idx = 0; idx < mapSize; idx++) { - binVal[idx] = 2*distStartEndMeters; - }*/ - for(int curIdx=0; curIdx 3900) { - if (debugMsgOn) { - System.out.println(""+curPath.path+": "+curPath.hrz+", "+curPath.vrt+", "+curPath.pos.getPosition().getLatDeg()+", "+curPath.pos.getPosition().getLngDeg()); - } - //} - - // check whether path is *outside* regatta-area - double distFromMiddleMeters = middlePos.getDistance(curPath.pos.getPosition()).getMeters(); - if (distFromMiddleMeters > oobFact*distStartEndMeters) { - continue; // ignore curPath - } - - // increase path-counter - countPath++; - - // determine map-key - int curKey = (int)Math.round(Math.floor( (curPath.hrz + hrzBinSize/2.0) / hrzBinSize )) - hrzLeft; - - /*String allR = "0" + (new String(new char[curPath.path.length()-1]).replace('\0', 'R')); - if (curPath.path.equals(allR)) { - System.out.println(""+curPath.path+": "+curPath.hrz+", "+curPath.vrt+", "+curPath.pos.getPosition().getLatDeg()+", "+curPath.pos.getPosition().getLngDeg()); - }*/ - - // check whether curPath is better then the ones looked at - if ((curKey>=0)&&(curKey < mapSize)) { - if (curPath.vrt > binVal[curKey]) { - binVal[curKey] = curPath.vrt; - binIdx[curKey] = curIdx+1; - } - } - } - - // check if there are still paths inside regatta-area - if (countPath > 0) { - // take best from each horizontal-bin and check if target is reached - allPaths = new ArrayList(); - for (int curKey = 0; curKey < mapSize; curKey++) { - - if (binIdx[curKey] > 0) { - PathCandidate curPath = newPaths.get(binIdx[curKey] - 1); - allPaths.add(curPath); - - // debug output - /*if ((Math.abs(curKey + hrzLeft) <= 10)) { - System.out.println("" + curPath.path + ": " + (curKey + hrzLeft) + ", vrt:" + curPath.vrt + ", hrz:" + curPath.hrz + ", bin:" + (Math.floor((curPath.hrz + hrzBinSize / 2.0) / hrzBinSize))); - }*/ - - // terminate path-search if paths close enough to target are found - if ((curPath.vrt > distStartEndMeters)) { - if ((Math.abs(curKey + hrzLeft) <= 3)) { - reachedEnd = true; - trgPaths.add(curPath); // add path to list of target-paths - } + for(PathCandidate curPath : allPaths) { + // terminate path-search if paths are found that are close enough to target + //if ((curPath.vrt > distStartEndMeters)) { + if ((curPath.vrt > 0.0)) { + //logger.info("\ntPath: " + curPath.path + "\n Time: " + (Math.round((curPath.pos.getTimePoint().asMillis()-startTime.asMillis())/1000.0/60.0*10.0)/10.0)+", Height: "+curPath.vrt+" of "+(Math.round(startPos.getDistance(endPos).getMeters()*100.0)/100.0)+", Dist: "+curPath.hrz+"m ~ "+(Math.round(curPath.pos.getPosition().getDistance(endPos).getMeters()*100.0)/100.0)+"m"); + int curBin = (int)Math.round(Math.floor( (curPath.hrz + hrzBinSize/2.0) / hrzBinSize )); + if ((Math.abs(curBin) <= 2)) { + reachedEnd = true; + trgPaths.add(curPath); // add path to list of target-paths } } } + } else { // terminate path-search as no path inside regatta-area are left reachedEnd = true; } + + } + + if (this.gridStore) { + + double distResolution = distStartEndMeters*0.01; + BufferedWriter outputCSV; + try { + outputCSV = new BufferedWriter(new FileWriter(this.gridFile+"-grid.csv")); + outputCSV.write("step; lat; lng; time; side; path; vrt\n"); + outputCSV.write("0; "+startPos.getLatDeg()+"; "+startPos.getLngDeg()+"; "+(startTime.asMillis()/1000)+"; 0; 0; "+(-distStartEndMeters)+"\n"); + outputCSV.write("0; "+endPos.getLatDeg()+"; "+endPos.getLngDeg()+"; "+(startTime.asMillis()/1000)+"; 0; 0; 0\n"); + int stepCount = 0; + for(List isoChrone : this.gridPositions) { + stepCount++; + PathCandidate prevPos = null; + for(PathCandidate isoPos : isoChrone) { + + if (prevPos != null) { + if (prevPos.pos.getPosition().getDistance(isoPos.pos.getPosition()).getMeters() < distResolution) { + continue; + } + } + + String outStr = ""+stepCount+"; "+isoPos.pos.getPosition().getLatDeg()+"; "+isoPos.pos.getPosition().getLngDeg()+"; "+(isoPos.pos.getTimePoint().asMillis()/1000)+"; "+isoPos.sid; + outStr += "; "+isoPos.path+"; "+isoPos.vrt; + outStr += "\n"; + outputCSV.write(outStr); + + prevPos = isoPos; + } + } + outputCSV.close(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + try { + outputCSV = new BufferedWriter(new FileWriter(this.gridFile+"-isoc.csv")); + outputCSV.write("step; lat; lng; time; side; path; vrt\n"); + outputCSV.write("0; "+startPos.getLatDeg()+"; "+startPos.getLngDeg()+"; "+(startTime.asMillis()/1000)+"; 0; 0; "+(-distStartEndMeters)+"\n"); + outputCSV.write("0; "+endPos.getLatDeg()+"; "+endPos.getLngDeg()+"; "+(startTime.asMillis()/1000)+"; 0; 0; 0\n"); + int stepCount = 0; + for(List isoChrone : this.isocPositions) { + stepCount++; + PathCandidate prevPos = null; + for(PathCandidate isoPos : isoChrone) { + + if (prevPos != null) { + if (prevPos.pos.getPosition().getDistance(isoPos.pos.getPosition()).getMeters() < distResolution) { + continue; + } + } + + String outStr = ""+stepCount+"; "+isoPos.pos.getPosition().getLatDeg()+"; "+isoPos.pos.getPosition().getLngDeg()+"; "+(isoPos.pos.getTimePoint().asMillis()/1000)+"; "+isoPos.sid; + outStr += "; "+isoPos.path+"; "+isoPos.vrt; + outStr += "\n"; + outputCSV.write(outStr); + + prevPos = isoPos; + } + } + outputCSV.close(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } @@ -443,13 +701,11 @@ public class PathGeneratorTreeGrowWind extends PathGeneratorBase { Collections.sort(trgPaths); // debug output - //if (debugMsgOn) { for(PathCandidate curPath : trgPaths) { logger.info("\nPath: " + curPath.path + "\n Time: " + (Math.round((curPath.pos.getTimePoint().asMillis()-startTime.asMillis())/1000.0/60.0*10.0)/10.0)+", Height: "+curPath.vrt+" of "+(Math.round(startPos.getDistance(endPos).getMeters()*100.0)/100.0)+", Dist: "+curPath.hrz+"m ~ "+(Math.round(curPath.pos.getPosition().getDistance(endPos).getMeters()*100.0)/100.0)+"m"); //System.out.print(""+curPath.path+": "+curPath.pos.getTimePoint().asMillis()+", "+curPath.pos.getPosition().getLatDeg()+", "+curPath.pos.getPosition().getLngDeg()+", "); //System.out.println(" height:"+curPath.vrt+" of "+startPos.getDistance(endPos).getMeters()+", dist:"+curPath.hrz+" ~ "+curPath.pos.getPosition().getDistance(endPos)); } - //} // // fill gwt-path @@ -472,7 +728,7 @@ public class PathGeneratorTreeGrowWind extends PathGeneratorBase { } else { boolean sameBaseDirection = this.isSameDirection(prevDirection, nextDirection); - TimedPosition newPosition = this.getStep(curPosition, timeStep, turnLoss, sameBaseDirection, nextDirection).getA(); + TimedPosition newPosition = this.getStep(curPosition, timeStep, turnLoss, sameBaseDirection, nextDirection); curPosition = new TimedPositionWithSpeedImpl(newPosition.getTimePoint(), newPosition.getPosition(), null); path.add(curPosition); diff --git a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PathGeneratorTreeGrowWind2.java b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PathGeneratorTreeGrowWind2.java deleted file mode 100644 index 8cdac6d5ca6..00000000000 --- a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PathGeneratorTreeGrowWind2.java +++ /dev/null @@ -1,566 +0,0 @@ -package com.sap.sailing.simulator.impl; - -import java.io.BufferedWriter; -import java.io.FileWriter; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.logging.Logger; - -import com.sap.sailing.domain.common.Bearing; -import com.sap.sailing.domain.common.Distance; -import com.sap.sailing.domain.common.Position; -import com.sap.sailing.domain.common.SpeedWithBearing; -import com.sap.sailing.domain.common.TimePoint; -import com.sap.sailing.domain.common.impl.DegreeBearingImpl; -import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; -import com.sap.sailing.domain.tracking.Wind; -import com.sap.sailing.domain.tracking.impl.WindImpl; -import com.sap.sailing.simulator.Path; -import com.sap.sailing.simulator.PolarDiagram; -import com.sap.sailing.simulator.SimulationParameters; -import com.sap.sailing.simulator.TimedPosition; -import com.sap.sailing.simulator.TimedPositionWithSpeed; -import com.sap.sailing.simulator.windfield.WindFieldGenerator; -import com.sap.sse.common.Util; - -public class PathGeneratorTreeGrowWind2 extends PathGeneratorBase { - - private static Logger logger = Logger.getLogger("com.sap.sailing"); - private boolean debugMsgOn = false; - - double oobFact = 0.75; // out-of-bounds factor - int maxTurns = 0; - String initPathStr = "0"; - PathCandidate bestCand = null; - boolean gridStore = false; - ArrayList> gridPositions = null; - String gridFile = null; - - public PathGeneratorTreeGrowWind2(SimulationParameters params) { - this.parameters = params; - } - - public void setEvaluationParameters(String startDirection, int maxTurns, String gridFile) { - if (startDirection != null) { - this.initPathStr = "0" + startDirection; - } else { - this.initPathStr = "0"; - } - this.maxTurns = maxTurns; - this.gridFile = gridFile; - if (this.gridFile != null) { - this.gridStore = true; - this.gridPositions = new ArrayList>(); - } else { - this.gridStore = false; - this.gridPositions = null; - } - } - - class SortPathCandsAbsHorizontally implements Comparator { - - @Override - public int compare(PathCandidate p1, PathCandidate p2) { - if (Math.abs(p1.hrz) == Math.abs(p2.hrz)) { - return 0; - } else { - return (Math.abs(p1.hrz) < Math.abs(p2.hrz) ? -1 : +1); - } - } - - } - - class SortPathCandsHorizontally implements Comparator { - - @Override - public int compare(PathCandidate p1, PathCandidate p2) { - if (p1.hrz == p2.hrz) { - return 0; - } else { - return (p1.hrz < p2.hrz ? -1 : +1); - } - } - - } - - // getter for evaluating best path cand propoerties further - PathCandidate getBestCand() { - return this.bestCand; - } - - - // generate step in one of the possible directions - // default: L - left, R - right - // extended: M - wide left, S - wide right - Util.Pair getStep(TimedPosition pos, long timeStep, long turnLoss, boolean sameBaseDirection, char nextDirection) { - - double offDeg = 3.0; - WindFieldGenerator wf = this.parameters.getWindField(); - TimePoint curTime = pos.getTimePoint(); - Position curPosition = pos.getPosition(); - Wind posWind = wf.getWind(new TimedPositionWithSpeedImpl(curTime, curPosition, null)); - - PolarDiagram pd = this.parameters.getBoatPolarDiagram(); - pd.setWind(posWind); - Wind appWind = new WindImpl(posWind.getPosition(), posWind.getTimePoint(), pd.getWind());; - - // get beat-angle left and right - Bearing travelBearing = null; - Bearing tmpBearing = null; - if (nextDirection == 'L') { - travelBearing = pd.optimalDirectionsUpwind()[0]; - } else if (nextDirection == 'R') { - travelBearing = pd.optimalDirectionsUpwind()[1]; - } else if (nextDirection == 'M') { - tmpBearing = pd.optimalDirectionsUpwind()[0]; - travelBearing = tmpBearing.add(new DegreeBearingImpl(-offDeg)); - } else if (nextDirection == 'S') { - tmpBearing = pd.optimalDirectionsUpwind()[1]; - travelBearing = tmpBearing.add(new DegreeBearingImpl(+offDeg)); - } - - // determine beat-speed left and right - SpeedWithBearing travelSpeed = pd.getSpeedAtBearing(travelBearing); - - TimePoint travelTime; - TimePoint nextTime = new MillisecondsTimePoint(curTime.asMillis()+timeStep); - if (sameBaseDirection) { - travelTime = nextTime; - } else { - travelTime = new MillisecondsTimePoint(nextTime.asMillis() - turnLoss); - } - - return new Util.Pair(new TimedPositionImpl(nextTime, travelSpeed.travelTo(curPosition, curTime, travelTime)), appWind); - } - - // use base direction to distinguish direction changes that do or don't require a turn - char getBaseDirection(char direction) { - char baseDirection = direction; - - if (direction == 'M') { - baseDirection = 'L'; - } - if (direction == 'S') { - baseDirection = 'R'; - } - - return baseDirection; - } - - // check whether nextDirection is same base direction as previous direction, i.e. no turn - boolean isSameDirection(char prevDirection, char nextDirection) { - - char prevBaseDirection = this.getBaseDirection(prevDirection); - char nextBaseDirection = this.getBaseDirection(nextDirection); - - return ((nextBaseDirection == prevBaseDirection)||(prevBaseDirection == '0')); - } - - // get path candidate measuring height towards (local, current-apparent) wind - PathCandidate getPathCandWind(PathCandidate path, char nextDirection, long timeStep, long turnLoss, Position posStart, Position posEnd, double tgtHeight) { - - char prevDirection = path.path.charAt(path.path.length()-1); - boolean sameBaseDirection = this.isSameDirection(prevDirection, nextDirection); - - int turnCount = path.trn; - if (!sameBaseDirection) { - turnCount++; - } - - // calculate next path position (taking turn-loss into account) - Util.Pair nextStep = this.getStep(path.pos, timeStep, turnLoss, sameBaseDirection, nextDirection); - TimedPosition pathPos = nextStep.getA(); - Wind posWind = nextStep.getB(); - - // calculate height-position with reference to race course - Position posHeight = pathPos.getPosition().projectToLineThrough(posEnd, posWind.getBearing()); - Bearing bearVrt = posStart.getBearingGreatCircle(posEnd); - Position posHeightTrgt = pathPos.getPosition().projectToLineThrough(posStart, bearVrt); - //Position posHeightWind = pathPos.getPosition().projectToLineThrough(posRef, posWind.getBearing()); - - // calculate vertical distance as distance of height-position to start - //double vrtDist = Math.round(posHeightTrgt.getDistance(posStart).getMeters()*100.0)/100.0; - double vrtDist = Math.round(posHeight.getDistance(posStart).getMeters()*100.0)/100.0; - - /*if (vrtDist > tgtHeight) { - // scale last step so that vrtDist ~ tgtHeight - Position prevPos = path.pos.getPosition(); - TimePoint prevTime = path.pos.getTimePoint(); - double heightFrac = (tgtHeight - path.vrt) / (vrtDist - path.vrt); - Position newPos = prevPos.translateGreatCircle(prevPos.getBearingGreatCircle(pathPos.getPosition()), prevPos.getDistance(pathPos.getPosition()).scale(heightFrac)); - TimePoint newTime = new MillisecondsTimePoint(Math.round(prevTime.asMillis() + (pathPos.getTimePoint().asMillis()-prevTime.asMillis())*heightFrac)); - pathPos = new TimedPositionImpl(newTime, newPos); - posHeight = pathPos.getPosition().projectToLineThrough(posStart, bearVrt); - }*/ - - // calculate horizontal side: left or right in reference to race course - double posSide = 1; - //double posBear = posWind.getBearing().getDegrees() - posEnd.getBearingGreatCircle(pathPos.getPosition()).getDegrees(); - Bearing posBear = posStart.getBearingGreatCircle(pathPos.getPosition()); - double posBearDiff = bearVrt.getDifferenceTo(posBear).getDegrees(); - if ((posBearDiff < 0.0)||(posBearDiff > 180.0)) { - posSide = -1; - } else if ((posBearDiff == 0.0)||(posBearDiff == 180.0)) { - posSide = 0; - } - - // calculate horizontal distance as distance of height-position to current position - //double hrzDist = Math.round(posSide*posHeight.getDistance(pathPos.getPosition()).getMeters()*100.0)/100.0; - double hrzDist = Math.round(posSide*posHeightTrgt.getDistance(pathPos.getPosition()).getMeters()*100.0)/100.0; - - //System.out.println(""+hrzDist+", "+vrtDist+", "+pathPos.getPosition().getLatDeg()+", "+pathPos.getPosition().getLngDeg()+", "+posHeight.getLatDeg()+", "+posHeight.getLngDeg()); - - // extend path-string by step-direction - String pathStr = path.path + nextDirection; - char nextBaseDirection = this.getBaseDirection(nextDirection); - - return (new PathCandidate(pathPos, vrtDist, hrzDist, turnCount, pathStr, nextBaseDirection, posWind)); - } - - - // generate path candidates based on beat angles - List getPathCandsBeatWind(PathCandidate path, long timeStep, long turnLoss, Position posStart, Position posEnd, double tgtHeight) { - - List result = new ArrayList(); - PathCandidate newPathCand; - - if (this.maxTurns > 0) { - - char prevDirection = path.path.charAt(path.path.length()-1); - if ((path.trn < this.maxTurns)||(this.isSameDirection(prevDirection, 'L'))) { - newPathCand = getPathCandWind(path, 'L', timeStep, turnLoss, posStart, posEnd, tgtHeight); - result.add(newPathCand); - } - - if ((path.trn < this.maxTurns)||(this.isSameDirection(prevDirection, 'R'))) { - newPathCand = getPathCandWind(path, 'R', timeStep, turnLoss, posStart, posEnd, tgtHeight); - result.add(newPathCand); - } - - } else { - - // step left - newPathCand = getPathCandWind(path, 'L', timeStep, turnLoss, posStart, posEnd, tgtHeight); - result.add(newPathCand); - - // step wide left - //newPathCand = getPathCandWind(path, 'M', timeStep, turnLoss, posStart, posEnd, tgtHeight); - //result.add(newPathCand); - - // step right - newPathCand = getPathCandWind(path, 'R', timeStep, turnLoss, posStart, posEnd, tgtHeight); - result.add(newPathCand); - - // step wide right - //newPathCand = getPathCandWind(path, 'S', timeStep, turnLoss, posStart, posEnd, tgtHeight); - //result.add(newPathCand); - - } - - return result; - } - - Util.Pair,List> generateCandidate(List oldPaths, long timeStep, long turnLoss, Position posStart, Position posMiddle, Position posEnd, double tgtHeight) { - - List newPathCands; - List leftPaths = new ArrayList(); - List rightPaths = new ArrayList(); - for(PathCandidate curPath : oldPaths) { - - newPathCands = this.getPathCandsBeatWind(curPath, timeStep, turnLoss, posStart, posEnd, tgtHeight); - for (PathCandidate curNewPath : newPathCands) { - // check whether path is *outside* regatta-area - double distFromMiddleMeters = posMiddle.getDistance(curPath.pos.getPosition()).getMeters(); - if (distFromMiddleMeters > oobFact * tgtHeight) { - continue; // ignore curPath - } - - if (curNewPath.sid == 'L') { - leftPaths.add(curNewPath); - } else if (curNewPath.sid == 'R') { - rightPaths.add(curNewPath); - } - - } - - } - - Util.Pair,List> newPaths = new Util.Pair,List>(leftPaths, rightPaths); - return newPaths; - } - - - List filterCandidates(List allCands, double hrzBinWidth) { - - boolean[] filterMap = new boolean[allCands.size()]; - - // sort candidates by horizontal distance - Comparator sortHorizontal = new SortPathCandsHorizontally(); - Collections.sort(allCands, sortHorizontal); - - // start scan with index 0 - int idxL = 0; - int idxR = 0; - - // for each candidate, check the neighborhoos and identify bad candidates - for(int idx = 0; idx < allCands.size(); idx++) { - - // current horizontal distance - double hrzDist = allCands.get(idx).hrz; - - // align left index - while(Math.abs(hrzDist - allCands.get(idxL).hrz) > hrzBinWidth) { - idxL++; - } - - // align right index - boolean finished = false; - while(!finished && (idxR < (allCands.size()-1))) { - if (Math.abs(hrzDist - allCands.get(idxR+1).hrz) <= hrzBinWidth) { - idxR++; - } else { - finished = true; - } - } - - // search maximum height - // in neighborhood idxL, ..., idxR - - // init max for search - int vrtIdx = idxL; - double vrtMax = allCands.get(vrtIdx).vrt; - filterMap[vrtIdx] = false; - - // evaluate remainder of neighborhood - if (idxL < idxR) { - for(int jdx = (idxL+1); jdx <= idxR; jdx++) { - if (allCands.get(jdx).vrt > vrtMax) { - // reset previous max candidate - filterMap[vrtIdx] = true; - // keep max height - vrtMax = allCands.get(jdx).vrt; - // keep max index - vrtIdx = jdx; - // set current max candidate - filterMap[vrtIdx] = false; - } else { - filterMap[jdx] = true; - } - } - } - - } // endfor each candidate - - // collect all good candidates (i.e. filterMap == false) - List filterCands = new ArrayList(); - for(int idx=0; idx < allCands.size(); idx++) { - if (!filterMap[idx]) { - filterCands.add(allCands.get(idx)); - } - } - - // return remaining good candidates - return filterCands; - } - - @Override - public Path getPath() { - - WindFieldGenerator wf = this.parameters.getWindField(); - PolarDiagram pd = this.parameters.getBoatPolarDiagram(); - Position startPos = this.parameters.getCourse().get(0); - Position endPos = this.parameters.getCourse().get(1); - TimePoint startTime = wf.getStartTime();// new MillisecondsTimePoint(0); - List path = new ArrayList(); - - Position currentPosition = startPos; - TimePoint currentTime = startTime; - - Distance distStartEnd = startPos.getDistance(endPos); - double distStartEndMeters = distStartEnd.getMeters(); - - long timeStep = wf.getTimeStep().asMillis()/3; - logger.info("Time step :" + timeStep); - long turnLoss = pd.getTurnLoss(); // 4000; // time lost when doing a turn - - Wind wndStart = wf.getWind(new TimedPositionWithSpeedImpl(startTime, startPos, null)); - logger.fine("wndStart speed:" + wndStart.getKnots() + " angle:" + wndStart.getBearing().getDegrees()); - pd.setWind(wndStart); - Bearing bearVrt = startPos.getBearingGreatCircle(endPos); - //Bearing bearHrz = bearVrt.add(new DegreeBearingImpl(90.0)); - Position middlePos = startPos.translateGreatCircle(bearVrt, distStartEnd.scale(0.5)); - - if (debugMsgOn) { - System.out.println("start : "+startPos.getLatDeg()+", "+startPos.getLngDeg()); - System.out.println("middle: "+middlePos.getLatDeg()+", "+middlePos.getLngDeg()); - System.out.println("end : "+endPos.getLatDeg()+", "+endPos.getLngDeg()); - } - - // calculate initial position according to initPathStr - PathCandidate initPath = new PathCandidate(new TimedPositionImpl(currentTime, currentPosition), 0.0, 0.0, 0, "0", '0', wndStart); - if (initPathStr.length()>1) { - char nextDirection = '0'; - for(int idx=1; idx allPaths = new ArrayList(); - List trgPaths = new ArrayList(); - allPaths.add(initPath); - - - TimedPosition tstPosition = this.getStep(new TimedPositionImpl(startTime, startPos), timeStep, turnLoss, true, 'L').getA(); - double tstDist1 = startPos.getDistance(tstPosition.getPosition()).getMeters(); - tstPosition = this.getStep(new TimedPositionImpl(startTime, startPos), timeStep, turnLoss, true, 'R').getA(); - double tstDist2 = startPos.getDistance(tstPosition.getPosition()).getMeters(); - - double hrzBinSize = (tstDist1 + tstDist2)/2.0; // horizontal bin size in meters - if (debugMsgOn) { - System.out.println("Horizontal Bin Size: "+hrzBinSize); - } - - //double oobFact = 0.75; // out-of-bounds factor - boolean reachedEnd = false; - int addSteps = 0; - int finalSteps = 0; // maximum number of additional steps after first target-path found - - while ((!reachedEnd)||(addSteps,List> newPaths = this.generateCandidate(allPaths, timeStep, turnLoss, startPos, middlePos, endPos, distStartEndMeters); - - - // select good candidates - List leftPaths = this.filterCandidates(newPaths.getA(), hrzBinSize/2.0); - List rightPaths = this.filterCandidates(newPaths.getB(), hrzBinSize/2.0); - - List nextPaths = new ArrayList (); - nextPaths.addAll(leftPaths); - nextPaths.addAll(rightPaths); - - allPaths = nextPaths; - - if (this.gridStore) { - - /*ArrayList isoChrone = new ArrayList(); - for(PathCand curCand : allPaths) { - isoChrone.add(curCand.pos); - } - this.gridPositions.add(isoChrone);*/ - - this.gridPositions.add(allPaths); - - } - - // check if there are still paths in the regatta-area - if (allPaths.size() > 0) { - - for(PathCandidate curPath : allPaths) { - // terminate path-search if paths are found that are close enough to target - if ((curPath.vrt > distStartEndMeters)) { - int curBin = (int)Math.round(Math.floor( (curPath.hrz + hrzBinSize/2.0) / hrzBinSize )); - if ((Math.abs(curBin) <= 2)) { - reachedEnd = true; - trgPaths.add(curPath); // add path to list of target-paths - } - } - } - - } else { - // terminate path-search as no path inside regatta-area are left - reachedEnd = true; - } - - } - - if (this.gridStore) { - - BufferedWriter outputCSV; - try { - outputCSV = new BufferedWriter(new FileWriter(this.gridFile)); - outputCSV.write("step; lat; lng; time; side\n"); - int stepCount = 0; - for(List isoChrone : this.gridPositions) { - stepCount++; - for(PathCandidate isoPos : isoChrone) { - - outputCSV.write(""+stepCount+"; "+isoPos.pos.getPosition().getLatDeg()+"; "+isoPos.pos.getPosition().getLngDeg()+"; "+(isoPos.pos.getTimePoint().asMillis()/1000)+"; "+isoPos.sid+"\n"); - - } - } - outputCSV.close(); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - - } - - - // if no target-paths were found, return empty path - if (trgPaths.size() == 0) { - //trgPaths = allPaths; // TODO: only for testing; remove lateron - TimedPositionWithSpeed curPosition = new TimedPositionWithSpeedImpl(startTime, startPos, null); - path.add(curPosition); - return new PathImpl(path, wf); // return empty path - } - - // sort target-paths ascending by distance-to-target - Collections.sort(trgPaths); - - // debug output - //if (debugMsgOn) { - for(PathCandidate curPath : trgPaths) { - logger.info("\nPath: " + curPath.path + "\n Time: " + (Math.round((curPath.pos.getTimePoint().asMillis()-startTime.asMillis())/1000.0/60.0*10.0)/10.0)+", Height: "+curPath.vrt+" of "+(Math.round(startPos.getDistance(endPos).getMeters()*100.0)/100.0)+", Dist: "+curPath.hrz+"m ~ "+(Math.round(curPath.pos.getPosition().getDistance(endPos).getMeters()*100.0)/100.0)+"m"); - //System.out.print(""+curPath.path+": "+curPath.pos.getTimePoint().asMillis()+", "+curPath.pos.getPosition().getLatDeg()+", "+curPath.pos.getPosition().getLngDeg()+", "); - //System.out.println(" height:"+curPath.vrt+" of "+startPos.getDistance(endPos).getMeters()+", dist:"+curPath.hrz+" ~ "+curPath.pos.getPosition().getDistance(endPos)); - } - //} - - // - // fill gwt-path - // - - // generate intermediate steps - bestCand = trgPaths.get(0); // target-path ending closest to target - TimedPositionWithSpeed curPosition = null; - char nextDirection = '0'; - char prevDirection = '0'; - for(int step=0; step<(bestCand.path.length()-1); step++) { - - nextDirection = bestCand.path.charAt(step); - - if (nextDirection == '0') { - - curPosition = new TimedPositionWithSpeedImpl(startTime, startPos, null); - path.add(curPosition); - - } else { - - boolean sameBaseDirection = this.isSameDirection(prevDirection, nextDirection); - TimedPosition newPosition = this.getStep(curPosition, timeStep, turnLoss, sameBaseDirection, nextDirection).getA(); - curPosition = new TimedPositionWithSpeedImpl(newPosition.getTimePoint(), newPosition.getPosition(), null); - path.add(curPosition); - - } - - prevDirection = nextDirection; - } - - // add final position (rescaled before to end on height of target) - path.add(new TimedPositionWithSpeedImpl(bestCand.pos.getTimePoint(), bestCand.pos.getPosition(), null)); - - return new PathImpl(path, wf); - - } - -} diff --git a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PathGeneratorTreeGrowWind3.java b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PathGeneratorTreeGrowWind3.java deleted file mode 100644 index 6e80083d4d2..00000000000 --- a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/PathGeneratorTreeGrowWind3.java +++ /dev/null @@ -1,751 +0,0 @@ -package com.sap.sailing.simulator.impl; - -import java.io.BufferedWriter; -import java.io.FileWriter; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.logging.Logger; - -import com.sap.sailing.domain.common.Bearing; -import com.sap.sailing.domain.common.Distance; -import com.sap.sailing.domain.common.Position; -import com.sap.sailing.domain.common.SpeedWithBearing; -import com.sap.sailing.domain.common.TimePoint; -import com.sap.sailing.domain.common.impl.DegreeBearingImpl; -import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; -import com.sap.sailing.domain.tracking.Wind; -import com.sap.sailing.domain.tracking.impl.WindImpl; -import com.sap.sailing.simulator.Path; -import com.sap.sailing.simulator.PolarDiagram; -import com.sap.sailing.simulator.SimulationParameters; -import com.sap.sailing.simulator.TimedPosition; -import com.sap.sailing.simulator.TimedPositionWithSpeed; -import com.sap.sailing.simulator.windfield.WindFieldGenerator; -import com.sap.sse.common.Util; - -public class PathGeneratorTreeGrowWind3 extends PathGeneratorBase { - - private static Logger logger = Logger.getLogger("com.sap.sailing"); - private boolean debugMsgOn = false; - - double oobFact = 0.75; // out-of-bounds factor - int maxTurns = 0; - boolean upwindLeg = false; - String initPathStr = "0"; - PathCandidate bestCand = null; - long usedTimeStep = 0; - boolean gridStore = false; - ArrayList> gridPositions = null; - ArrayList> isocPositions = null; - String gridFile = null; - - public PathGeneratorTreeGrowWind3(SimulationParameters params) { - this.parameters = params; - } - - public void setEvaluationParameters(String startDirection, int maxTurns, String gridFile) { - if (startDirection != null) { - this.initPathStr = "0" + startDirection; - } else { - this.initPathStr = "0"; - } - this.maxTurns = maxTurns; - this.gridFile = gridFile; - if (this.gridFile != null) { - this.gridStore = true; - this.gridPositions = new ArrayList>(); - this.isocPositions = new ArrayList>(); - } else { - this.gridStore = false; - this.gridPositions = null; - this.isocPositions = null; - } - } - - class SortPathCandsAbsHorizontally implements Comparator { - - @Override - public int compare(PathCandidate p1, PathCandidate p2) { - if (Math.abs(p1.hrz) == Math.abs(p2.hrz)) { - return 0; - } else { - return (Math.abs(p1.hrz) < Math.abs(p2.hrz) ? -1 : +1); - } - } - - } - - class SortPathCandsHorizontally implements Comparator { - - @Override - public int compare(PathCandidate p1, PathCandidate p2) { - if (p1.hrz == p2.hrz) { - return 0; - } else { - return (p1.hrz < p2.hrz ? -1 : +1); - } - } - - } - - // getter for evaluating best path cand propoerties further - PathCandidate getBestCand() { - return this.bestCand; - } - - long getUsedTimeStep() { - return this.usedTimeStep; - } - - - // generate step in one of the possible directions - // default: L - left, R - right - // extended: M - wide left, S - wide right - Util.Pair getStep(TimedPosition pos, long timeStep, long turnLoss, boolean sameBaseDirection, char nextDirection) { - - double offDeg = 3.0; - WindFieldGenerator wf = this.parameters.getWindField(); - TimePoint curTime = pos.getTimePoint(); - Position curPosition = pos.getPosition(); - Wind posWind = wf.getWind(new TimedPositionWithSpeedImpl(curTime, curPosition, null)); - - PolarDiagram pd = this.parameters.getBoatPolarDiagram(); - pd.setWind(posWind); - Wind appWind = new WindImpl(posWind.getPosition(), posWind.getTimePoint(), pd.getWind());; - - // get beat-angle left and right - Bearing travelBearing = null; - Bearing tmpBearing = null; - if (nextDirection == 'L') { - if (this.upwindLeg) { - travelBearing = pd.optimalDirectionsUpwind()[0]; - } else { - travelBearing = pd.optimalDirectionsDownwind()[0]; - } - } else if (nextDirection == 'R') { - if (this.upwindLeg) { - travelBearing = pd.optimalDirectionsUpwind()[1]; - } else { - travelBearing = pd.optimalDirectionsDownwind()[1]; - } - } else if (nextDirection == 'M') { - if (this.upwindLeg) { - tmpBearing = pd.optimalDirectionsUpwind()[0]; - travelBearing = tmpBearing.add(new DegreeBearingImpl(-offDeg)); - } else { - tmpBearing = pd.optimalDirectionsDownwind()[0]; - travelBearing = tmpBearing.add(new DegreeBearingImpl(-offDeg)); - } - } else if (nextDirection == 'S') { - if (this.upwindLeg) { - tmpBearing = pd.optimalDirectionsUpwind()[1]; - travelBearing = tmpBearing.add(new DegreeBearingImpl(+offDeg)); - } else { - tmpBearing = pd.optimalDirectionsDownwind()[1]; - travelBearing = tmpBearing.add(new DegreeBearingImpl(+offDeg)); - } - } - - // determine beat-speed left and right - SpeedWithBearing travelSpeed = pd.getSpeedAtBearing(travelBearing); - - TimePoint travelTime; - TimePoint nextTime = new MillisecondsTimePoint(curTime.asMillis()+timeStep); - if (sameBaseDirection) { - travelTime = nextTime; - } else { - travelTime = new MillisecondsTimePoint(nextTime.asMillis() - turnLoss); - } - - return new Util.Pair(new TimedPositionImpl(nextTime, travelSpeed.travelTo(curPosition, curTime, travelTime)), appWind); - } - - // use base direction to distinguish direction changes that do or don't require a turn - char getBaseDirection(char direction) { - char baseDirection = direction; - - if (direction == 'M') { - baseDirection = 'L'; - } - if (direction == 'S') { - baseDirection = 'R'; - } - - return baseDirection; - } - - // check whether nextDirection is same base direction as previous direction, i.e. no turn - boolean isSameDirection(char prevDirection, char nextDirection) { - - char prevBaseDirection = this.getBaseDirection(prevDirection); - char nextBaseDirection = this.getBaseDirection(nextDirection); - - return ((nextBaseDirection == prevBaseDirection)||(prevBaseDirection == '0')); - } - - // get path candidate measuring height towards (local, current-apparent) wind - PathCandidate getPathCandWind(PathCandidate path, char nextDirection, long timeStep, long turnLoss, Position posStart, Position posEnd, double tgtHeight) { - - char prevDirection = path.path.charAt(path.path.length()-1); - boolean sameBaseDirection = this.isSameDirection(prevDirection, nextDirection); - - int turnCount = path.trn; - if (!sameBaseDirection) { - turnCount++; - } - - // calculate next path position (taking turn-loss into account) - Util.Pair nextStep = this.getStep(path.pos, timeStep, turnLoss, sameBaseDirection, nextDirection); - TimedPosition pathPos = nextStep.getA(); - Wind posWind = nextStep.getB(); - - // calculate height-position with reference to race course - Position posHeight = pathPos.getPosition().projectToLineThrough(posEnd, posWind.getBearing()); - Bearing bearVrt = posStart.getBearingGreatCircle(posEnd); - Position posHeightTrgt = pathPos.getPosition().projectToLineThrough(posStart, bearVrt); - - // calculate vertical distance as distance of height-position to start - //double vrtDist = Math.round(posHeightTrgt.getDistance(posStart).getMeters()*100.0)/100.0; - - Bearing bearHeight = posEnd.getBearingGreatCircle(posHeight); - double bearHeightSide = posWind.getBearing().getDifferenceTo(bearHeight).getDegrees(); - /*if (Math.abs(bearHeightSide) > 5.0) { - System.out.println("bearHeightSide: "+bearHeightSide); - }*/ - double vrtSide = (this.upwindLeg ? -1.0 : +1.0); - if (Math.abs(bearHeightSide) > 170.0) { - vrtSide = (this.upwindLeg ? +1.0 : -1.0); - } - double vrtDist = vrtSide*Math.round(posHeight.getDistance(posEnd).getMeters()*1000.0)/1000.0; - - /*if (vrtDist > tgtHeight) { - // scale last step so that vrtDist ~ tgtHeight - Position prevPos = path.pos.getPosition(); - TimePoint prevTime = path.pos.getTimePoint(); - double heightFrac = (tgtHeight - path.vrt) / (vrtDist - path.vrt); - Position newPos = prevPos.translateGreatCircle(prevPos.getBearingGreatCircle(pathPos.getPosition()), prevPos.getDistance(pathPos.getPosition()).scale(heightFrac)); - TimePoint newTime = new MillisecondsTimePoint(Math.round(prevTime.asMillis() + (pathPos.getTimePoint().asMillis()-prevTime.asMillis())*heightFrac)); - pathPos = new TimedPositionImpl(newTime, newPos); - posHeight = pathPos.getPosition().projectToLineThrough(posStart, bearVrt); - }*/ - - // calculate horizontal side: left or right in reference to race course - double posSide = 1; - //double posBear = posWind.getBearing().getDegrees() - posEnd.getBearingGreatCircle(pathPos.getPosition()).getDegrees(); - Bearing posBear = posStart.getBearingGreatCircle(pathPos.getPosition()); - double posBearDiff = bearVrt.getDifferenceTo(posBear).getDegrees(); - if ((posBearDiff < 0.0)||(posBearDiff > 180.0)) { - posSide = -1; - } else if ((posBearDiff == 0.0)||(posBearDiff == 180.0)) { - posSide = 0; - } - // calculate horizontal distance as distance of height-position to current position - //double hrzDist = Math.round(posSide*posHeight.getDistance(pathPos.getPosition()).getMeters()*100.0)/100.0; - double hrzDist = Math.round(posSide*posHeightTrgt.getDistance(pathPos.getPosition()).getMeters()*1000.0)/1000.0; - - //System.out.println(""+hrzDist+", "+vrtDist+", "+pathPos.getPosition().getLatDeg()+", "+pathPos.getPosition().getLngDeg()+", "+posHeight.getLatDeg()+", "+posHeight.getLngDeg()); - - // extend path-string by step-direction - String pathStr = path.path + nextDirection; - char nextBaseDirection = this.getBaseDirection(nextDirection); - - return (new PathCandidate(pathPos, vrtDist, hrzDist, turnCount, pathStr, nextBaseDirection, posWind)); - } - - - // generate path candidates based on beat angles - List getPathCandsBeatWind(PathCandidate path, long timeStep, long turnLoss, Position posStart, Position posEnd, double tgtHeight) { - - List result = new ArrayList(); - PathCandidate newPathCand; - - if (this.maxTurns > 0) { - - char prevDirection = path.path.charAt(path.path.length()-1); - if ((path.trn < this.maxTurns)||(this.isSameDirection(prevDirection, 'L'))) { - newPathCand = getPathCandWind(path, 'L', timeStep, turnLoss, posStart, posEnd, tgtHeight); - result.add(newPathCand); - } - - if ((path.trn < this.maxTurns)||(this.isSameDirection(prevDirection, 'R'))) { - newPathCand = getPathCandWind(path, 'R', timeStep, turnLoss, posStart, posEnd, tgtHeight); - result.add(newPathCand); - } - - } else { - - // step left - newPathCand = getPathCandWind(path, 'L', timeStep, turnLoss, posStart, posEnd, tgtHeight); - result.add(newPathCand); - - // step wide left - //newPathCand = getPathCandWind(path, 'M', timeStep, turnLoss, posStart, posEnd, tgtHeight); - //result.add(newPathCand); - - // step right - newPathCand = getPathCandWind(path, 'R', timeStep, turnLoss, posStart, posEnd, tgtHeight); - result.add(newPathCand); - - // step wide right - //newPathCand = getPathCandWind(path, 'S', timeStep, turnLoss, posStart, posEnd, tgtHeight); - //result.add(newPathCand); - - } - - return result; - } - - Util.Pair,List> generateCandidate(List oldPaths, long timeStep, long turnLoss, Position posStart, Position posMiddle, Position posEnd, double tgtHeight) { - - List newPathCands; - List leftPaths = new ArrayList(); - List rightPaths = new ArrayList(); - for(PathCandidate curPath : oldPaths) { - - newPathCands = this.getPathCandsBeatWind(curPath, timeStep, turnLoss, posStart, posEnd, tgtHeight); - for (PathCandidate curNewPath : newPathCands) { - // check whether path is *outside* regatta-area - double distFromMiddleMeters = posMiddle.getDistance(curPath.pos.getPosition()).getMeters(); - if (distFromMiddleMeters > oobFact * tgtHeight) { - continue; // ignore curPath - } - - if (curNewPath.sid == 'L') { - leftPaths.add(curNewPath); - } else if (curNewPath.sid == 'R') { - rightPaths.add(curNewPath); - } - - } - - } - - Util.Pair,List> newPaths = new Util.Pair,List>(leftPaths, rightPaths); - return newPaths; - } - - - List filterCandidates(List allCands, double hrzBinWidth) { - - boolean[] filterMap = new boolean[allCands.size()]; - - // sort candidates by horizontal distance - Comparator sortHorizontal = new SortPathCandsHorizontally(); - Collections.sort(allCands, sortHorizontal); - - // start scan with index 0 - int idxL = 0; - int idxR = 0; - - // for each candidate, check the neighborhoods and identify bad candidates - for(int idx = 0; idx < allCands.size(); idx++) { - - // current horizontal distance - double hrzDist = allCands.get(idx).hrz; - - // align left index - while(Math.abs(hrzDist - allCands.get(idxL).hrz) > hrzBinWidth) { - idxL++; - } - - // align right index - boolean finished = false; - while(!finished && (idxR < (allCands.size()-1))) { - if (Math.abs(hrzDist - allCands.get(idxR+1).hrz) <= hrzBinWidth) { - idxR++; - } else { - finished = true; - } - } - - // search maximum height - // in neighborhood idxL, ..., idxR - - // init max for search - int vrtIdx = idxL; - double vrtMax = allCands.get(vrtIdx).vrt; - filterMap[vrtIdx] = false; - - // evaluate remainder of neighborhood - if (idxL < idxR) { - for(int jdx = (idxL+1); jdx <= idxR; jdx++) { - if (allCands.get(jdx).vrt > vrtMax) { - // reset previous max candidate - filterMap[vrtIdx] = true; - // keep max height - vrtMax = allCands.get(jdx).vrt; - // keep max index - vrtIdx = jdx; - // set current max candidate - filterMap[vrtIdx] = false; - } else { - filterMap[jdx] = true; - } - } - } - - } // endfor each candidate - - // collect all good candidates (i.e. filterMap == false) - List filterCands = new ArrayList(); - for(int idx=0; idx < allCands.size(); idx++) { - if (!filterMap[idx]) { - filterCands.add(allCands.get(idx)); - } - } - - // return remaining good candidates - return filterCands; - } - - - List filterIsochrone(List allCands, double hrzBinWidth) { - - boolean[] filterMap = new boolean[allCands.size()]; - for(int idx = 0; idx < allCands.size(); idx++) { - filterMap[idx] = true; - } - - // sort candidates by horizontal distance - Comparator sortHorizontal = new SortPathCandsHorizontally(); - Collections.sort(allCands, sortHorizontal); - - // start scan with index 0 - int idxL = 0; - int idxR = 0; - - // for each candidate, check the neighborhoods and identify bad candidates - for(int idx = 0; idx < allCands.size(); idx++) { - - // current horizontal distance - double hrzDist = allCands.get(idx).hrz; - - // align left index - while(Math.abs(hrzDist - allCands.get(idxL).hrz) > hrzBinWidth) { - idxL++; - } - - // align right index - boolean finished = false; - while(!finished && (idxR < (allCands.size()-1))) { - if (Math.abs(hrzDist - allCands.get(idxR+1).hrz) <= hrzBinWidth) { - idxR++; - } else { - finished = true; - } - } - - // search maximum height - // in neighborhood idxL, ..., idxR - - // init max for search - ArrayList vrtIdx = new ArrayList(); - vrtIdx.add(idxL); - double vrtMax = allCands.get(idxL).vrt; - - // evaluate remainder of neighborhood - if (idxL < idxR) { - for(int jdx = (idxL+1); jdx <= idxR; jdx++) { - if (allCands.get(jdx).vrt > vrtMax) { - // keep max height - vrtMax = allCands.get(jdx).vrt; - // keep max index - vrtIdx = new ArrayList(); - vrtIdx.add(jdx); - } else if (allCands.get(jdx).vrt == vrtMax) { - // add further max indexes - vrtIdx.add(jdx); - } - } - } - - for(Integer jdx : vrtIdx) { - filterMap[jdx] = false; - } - - } // endfor each candidate - - // collect all good candidates (i.e. filterMap == false) - List filterCands = new ArrayList(); - for(int idx=0; idx < allCands.size(); idx++) { - if (!filterMap[idx]) { - filterCands.add(allCands.get(idx)); - } - } - - // return remaining good candidates - return filterCands; - } - - @Override - public Path getPath() { - WindFieldGenerator wf = this.parameters.getWindField(); - PolarDiagram pd = this.parameters.getBoatPolarDiagram(); - - Position startPos = this.parameters.getCourse().get(0); - Position endPos = this.parameters.getCourse().get(1); - - // test downwind: exchange start and end - //Position startPos = this.parameters.getCourse().get(1); - //Position endPos = this.parameters.getCourse().get(0); - - TimePoint startTime = wf.getStartTime();// new MillisecondsTimePoint(0); - List path = new ArrayList(); - - Position currentPosition = startPos; - TimePoint currentTime = startTime; - - Distance distStartEnd = startPos.getDistance(endPos); - double distStartEndMeters = distStartEnd.getMeters(); - - Wind wndStart = wf.getWind(new TimedPositionWithSpeedImpl(startTime, startPos, null)); - logger.fine("wndStart speed:" + wndStart.getKnots() + " angle:" + wndStart.getBearing().getDegrees()); - pd.setWind(wndStart); - Bearing bearVrt = startPos.getBearingGreatCircle(endPos); - //Bearing bearHrz = bearVrt.add(new DegreeBearingImpl(90.0)); - Position middlePos = startPos.translateGreatCircle(bearVrt, distStartEnd.scale(0.5)); - - Bearing bearRCWind = wndStart.getBearing().getDifferenceTo(bearVrt); - String legType = "downwind"; - this.upwindLeg = false; - - if ((Math.abs(bearRCWind.getDegrees()) > 90.0)&&(Math.abs(bearRCWind.getDegrees()) < 270.0)) { - legType = "upwind"; - this.upwindLeg = true; - } - - if (debugMsgOn) { - System.out.println("start : "+startPos.getLatDeg()+", "+startPos.getLngDeg()); - System.out.println("middle: "+middlePos.getLatDeg()+", "+middlePos.getLngDeg()); - System.out.println("end : "+endPos.getLatDeg()+", "+endPos.getLngDeg()); - } - logger.info("Leg Direction: "+legType); - - long timeStep = wf.getTimeStep().asMillis()/2; - if (!this.upwindLeg) { - timeStep = timeStep / 2; - } - this.usedTimeStep = timeStep; - logger.info("Time step :" + timeStep); - long turnLoss = pd.getTurnLoss(); // 4000; // time lost when doing a turn - if (!this.upwindLeg) { - turnLoss = turnLoss / 2; - } - - // calculate initial position according to initPathStr - PathCandidate initPath = new PathCandidate(new TimedPositionImpl(currentTime, currentPosition), 0.0, 0.0, 0, "0", '0', wndStart); - if (initPathStr.length()>1) { - char nextDirection = '0'; - for(int idx=1; idx allPaths = new ArrayList(); - List trgPaths = new ArrayList(); - allPaths.add(initPath); - - - TimedPosition tstPosition = this.getStep(new TimedPositionImpl(startTime, startPos), timeStep, turnLoss, true, 'L').getA(); - double tstDist1 = startPos.getDistance(tstPosition.getPosition()).getMeters(); - tstPosition = this.getStep(new TimedPositionImpl(startTime, startPos), timeStep, turnLoss, true, 'R').getA(); - double tstDist2 = startPos.getDistance(tstPosition.getPosition()).getMeters(); - - double hrzBinSize = (tstDist1 + tstDist2)/6.0; // horizontal bin size in meters - if (debugMsgOn) { - System.out.println("Horizontal Bin Size: "+hrzBinSize); - } - - //double oobFact = 0.75; // out-of-bounds factor - boolean reachedEnd = false; - int addSteps = 0; - int finalSteps = 0; // maximum number of additional steps after first target-path found - - while ((!reachedEnd)||(addSteps,List> newPaths = this.generateCandidate(allPaths, timeStep, turnLoss, startPos, middlePos, endPos, distStartEndMeters); - - - // select good candidates - List leftPaths = this.filterCandidates(newPaths.getA(), hrzBinSize/2.0); - List rightPaths = this.filterCandidates(newPaths.getB(), hrzBinSize/2.0); - - List nextPaths = new ArrayList (); - nextPaths.addAll(leftPaths); - nextPaths.addAll(rightPaths); - - allPaths = nextPaths; - - if (this.gridStore) { - - /*ArrayList isoChrone = new ArrayList(); - for(PathCand curCand : allPaths) { - isoChrone.add(curCand.pos); - } - this.gridPositions.add(isoChrone);*/ - - this.gridPositions.add(allPaths); - - List isocPaths = this.filterIsochrone(allPaths, hrzBinSize); - this.isocPositions.add(isocPaths); - - } - - // check if there are still paths in the regatta-area - if (allPaths.size() > 0) { - - for(PathCandidate curPath : allPaths) { - // terminate path-search if paths are found that are close enough to target - //if ((curPath.vrt > distStartEndMeters)) { - if ((curPath.vrt > 0.0)) { - //logger.info("\ntPath: " + curPath.path + "\n Time: " + (Math.round((curPath.pos.getTimePoint().asMillis()-startTime.asMillis())/1000.0/60.0*10.0)/10.0)+", Height: "+curPath.vrt+" of "+(Math.round(startPos.getDistance(endPos).getMeters()*100.0)/100.0)+", Dist: "+curPath.hrz+"m ~ "+(Math.round(curPath.pos.getPosition().getDistance(endPos).getMeters()*100.0)/100.0)+"m"); - int curBin = (int)Math.round(Math.floor( (curPath.hrz + hrzBinSize/2.0) / hrzBinSize )); - if ((Math.abs(curBin) <= 2)) { - reachedEnd = true; - trgPaths.add(curPath); // add path to list of target-paths - } - } - } - - } else { - // terminate path-search as no path inside regatta-area are left - reachedEnd = true; - } - - } - - if (this.gridStore) { - - double distResolution = distStartEndMeters*0.01; - BufferedWriter outputCSV; - try { - outputCSV = new BufferedWriter(new FileWriter(this.gridFile+"-grid.csv")); - outputCSV.write("step; lat; lng; time; side; path; vrt\n"); - outputCSV.write("0; "+startPos.getLatDeg()+"; "+startPos.getLngDeg()+"; "+(startTime.asMillis()/1000)+"; 0; 0; "+(-distStartEndMeters)+"\n"); - outputCSV.write("0; "+endPos.getLatDeg()+"; "+endPos.getLngDeg()+"; "+(startTime.asMillis()/1000)+"; 0; 0; 0\n"); - int stepCount = 0; - for(List isoChrone : this.gridPositions) { - stepCount++; - PathCandidate prevPos = null; - for(PathCandidate isoPos : isoChrone) { - - if (prevPos != null) { - if (prevPos.pos.getPosition().getDistance(isoPos.pos.getPosition()).getMeters() < distResolution) { - continue; - } - } - - String outStr = ""+stepCount+"; "+isoPos.pos.getPosition().getLatDeg()+"; "+isoPos.pos.getPosition().getLngDeg()+"; "+(isoPos.pos.getTimePoint().asMillis()/1000)+"; "+isoPos.sid; - outStr += "; "+isoPos.path+"; "+isoPos.vrt; - outStr += "\n"; - outputCSV.write(outStr); - - prevPos = isoPos; - } - } - outputCSV.close(); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - try { - outputCSV = new BufferedWriter(new FileWriter(this.gridFile+"-isoc.csv")); - outputCSV.write("step; lat; lng; time; side; path; vrt\n"); - outputCSV.write("0; "+startPos.getLatDeg()+"; "+startPos.getLngDeg()+"; "+(startTime.asMillis()/1000)+"; 0; 0; "+(-distStartEndMeters)+"\n"); - outputCSV.write("0; "+endPos.getLatDeg()+"; "+endPos.getLngDeg()+"; "+(startTime.asMillis()/1000)+"; 0; 0; 0\n"); - int stepCount = 0; - for(List isoChrone : this.isocPositions) { - stepCount++; - PathCandidate prevPos = null; - for(PathCandidate isoPos : isoChrone) { - - if (prevPos != null) { - if (prevPos.pos.getPosition().getDistance(isoPos.pos.getPosition()).getMeters() < distResolution) { - continue; - } - } - - String outStr = ""+stepCount+"; "+isoPos.pos.getPosition().getLatDeg()+"; "+isoPos.pos.getPosition().getLngDeg()+"; "+(isoPos.pos.getTimePoint().asMillis()/1000)+"; "+isoPos.sid; - outStr += "; "+isoPos.path+"; "+isoPos.vrt; - outStr += "\n"; - outputCSV.write(outStr); - - prevPos = isoPos; - } - } - outputCSV.close(); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - - } - - - // if no target-paths were found, return empty path - if (trgPaths.size() == 0) { - //trgPaths = allPaths; // TODO: only for testing; remove lateron - TimedPositionWithSpeed curPosition = new TimedPositionWithSpeedImpl(startTime, startPos, null); - path.add(curPosition); - return new PathImpl(path, wf); // return empty path - } - - // sort target-paths ascending by distance-to-target - Collections.sort(trgPaths); - - // debug output - //if (debugMsgOn) { - for(PathCandidate curPath : trgPaths) { - logger.info("\nPath: " + curPath.path + "\n Time: " + (Math.round((curPath.pos.getTimePoint().asMillis()-startTime.asMillis())/1000.0/60.0*10.0)/10.0)+", Height: "+curPath.vrt+" of "+(Math.round(startPos.getDistance(endPos).getMeters()*100.0)/100.0)+", Dist: "+curPath.hrz+"m ~ "+(Math.round(curPath.pos.getPosition().getDistance(endPos).getMeters()*100.0)/100.0)+"m"); - //System.out.print(""+curPath.path+": "+curPath.pos.getTimePoint().asMillis()+", "+curPath.pos.getPosition().getLatDeg()+", "+curPath.pos.getPosition().getLngDeg()+", "); - //System.out.println(" height:"+curPath.vrt+" of "+startPos.getDistance(endPos).getMeters()+", dist:"+curPath.hrz+" ~ "+curPath.pos.getPosition().getDistance(endPos)); - } - //} - - // - // fill gwt-path - // - - // generate intermediate steps - bestCand = trgPaths.get(0); // target-path ending closest to target - TimedPositionWithSpeed curPosition = null; - char nextDirection = '0'; - char prevDirection = '0'; - for(int step=0; step<(bestCand.path.length()-1); step++) { - - nextDirection = bestCand.path.charAt(step); - - if (nextDirection == '0') { - - curPosition = new TimedPositionWithSpeedImpl(startTime, startPos, null); - path.add(curPosition); - - } else { - - boolean sameBaseDirection = this.isSameDirection(prevDirection, nextDirection); - TimedPosition newPosition = this.getStep(curPosition, timeStep, turnLoss, sameBaseDirection, nextDirection).getA(); - curPosition = new TimedPositionWithSpeedImpl(newPosition.getTimePoint(), newPosition.getPosition(), null); - path.add(curPosition); - - } - - prevDirection = nextDirection; - } - - // add final position (rescaled before to end on height of target) - path.add(new TimedPositionWithSpeedImpl(bestCand.pos.getTimePoint(), bestCand.pos.getPosition(), null)); - - return new PathImpl(path, wf); - - } - -} diff --git a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/SailingSimulatorImpl.java b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/SailingSimulatorImpl.java index 17288df9988..e5b97963a33 100644 --- a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/SailingSimulatorImpl.java +++ b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/SailingSimulatorImpl.java @@ -113,7 +113,7 @@ public class SailingSimulatorImpl implements SailingSimulator { // // get instance of heuristic searcher - PathGeneratorTreeGrowWind3 genTreeGrow = new PathGeneratorTreeGrowWind3(this.simulationParameters); + PathGeneratorTreeGrowWind genTreeGrow = new PathGeneratorTreeGrowWind(this.simulationParameters); // search best left-starting 1-turner genTreeGrow.setEvaluationParameters("L", 1, null); diff --git a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/SimulatorUtils.java b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/SimulatorUtils.java index ddd8b3a47ca..c4b41214098 100644 --- a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/SimulatorUtils.java +++ b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/SimulatorUtils.java @@ -373,7 +373,7 @@ public class SimulatorUtils { Map paths = new HashMap(); // get instance of heuristic searcher - PathGeneratorTreeGrowWind3 genTreeGrow = new PathGeneratorTreeGrowWind3(parameters); + PathGeneratorTreeGrowWind genTreeGrow = new PathGeneratorTreeGrowWind(parameters); // search best left-starting 1-turner genTreeGrow.setEvaluationParameters("L", 1, null); diff --git a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/WindControlParameters.java b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/WindControlParameters.java index 26e15352a5c..e5204649103 100644 --- a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/WindControlParameters.java +++ b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/windfield/WindControlParameters.java @@ -140,7 +140,7 @@ public class WindControlParameters implements Serializable { blastProbability = 25.0; maxBlastSize = 1.0; - blastWindSpeed = 100.0; + blastWindSpeed = 120.0; blastWindSpeedVar = 10.0; if (blastRandomStreamManager == null || !resetBlastRandomStream) { From 5ad019f00055edf801e9e9bc8352b8ade67566bd Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Thu, 24 Jul 2014 23:43:43 +0200 Subject: [PATCH 17/20] formatting changes only --- .../app/domain/impl/BoatClassSeriesFleet.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/domain/impl/BoatClassSeriesFleet.java b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/domain/impl/BoatClassSeriesFleet.java index da318d32b67..6c69f4a6b32 100644 --- a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/domain/impl/BoatClassSeriesFleet.java +++ b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/domain/impl/BoatClassSeriesFleet.java @@ -35,10 +35,10 @@ public class BoatClassSeriesFleet { } private int getFleetIndex(Iterable fleets, Fleet fleet) { - return Util.indexOf(fleets, fleet); - } + return Util.indexOf(fleets, fleet); + } - public String getBoatClassName() { + public String getBoatClassName() { return boatClass.getName(); } @@ -127,8 +127,8 @@ public class BoatClassSeriesFleet { return true; } - public int getFleetOrder() { - return fleetOrder; - } - + public int getFleetOrder() { + return fleetOrder; + } + } From 1463ce7675f529e81d621f41ca677c00b0ed60f7 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Fri, 25 Jul 2014 00:18:47 +0200 Subject: [PATCH 18/20] fixed another NPE regarding null speedWithBearing --- .../com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java index 86a2bb40188..8d27a4c3d9a 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java @@ -1587,7 +1587,8 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe multiply(factorForAfter)).divide(1).getDegrees(); } SpeedWithBearingDTO betweenSpeed = new SpeedWithBearingDTO( - factorForBefore*fixBefore.speedWithBearing.speedInKnots + factorForAfter*fixAfter.speedWithBearing.speedInKnots, + factorForBefore*(fixBefore.speedWithBearing==null?0:fixBefore.speedWithBearing.speedInKnots) + + factorForAfter*(fixAfter.speedWithBearing==null?0:fixAfter.speedWithBearing.speedInKnots), betweenBearing); result = new GPSFixDTO(date, betweenPosition, betweenSpeed, closer.degreesBoatToTheWind, closer.tack, closer.legType, fixBefore.extrapolated || fixAfter.extrapolated); From c18b307de06223e4b0a65510444133d7625bc750 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Fri, 25 Jul 2014 00:23:51 +0200 Subject: [PATCH 19/20] reduced artifical wind speed for course-based wind fix from 1.0kts to 0.0kts to not get people confused --- .../src/com/sap/sailing/domain/tracking/TrackedRace.java | 2 +- .../com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRace.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRace.java index 0c04c02c7ce..7ca16436693 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRace.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRace.java @@ -397,7 +397,7 @@ public interface TrackedRace extends Serializable { /** * Computes a default wind direction based on the direction of the first leg at time at, with a default - * speed of one knot. Note that this wind direction can only be used if {@link #raceIsKnownToStartUpwind()} returns + * speed of zero knots. Note that this wind direction can only be used if {@link #raceIsKnownToStartUpwind()} returns * true. * * @param at diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java index c7372728b4c..975bfe7b6d1 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java @@ -1378,7 +1378,7 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl Position firstLegEnd = getApproximatePosition(firstLeg.getTo(), at); Position firstLegStart = getApproximatePosition(firstLeg.getFrom(), at); if (firstLegStart != null && firstLegEnd != null) { - result = new WindImpl(firstLegStart, at, new KnotSpeedWithBearingImpl(1.0, + result = new WindImpl(firstLegStart, at, new KnotSpeedWithBearingImpl(0.0, firstLegEnd.getBearingGreatCircle(firstLegStart))); } else { result = null; From 686e8f3ce771501816d451a9e1ef1efae7c345c5 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Fri, 25 Jul 2014 00:35:16 +0200 Subject: [PATCH 20/20] added null check for access to legDetails, addressing bug 2085, as a follow-up to commit a437b2b7ef4aeddbeef6af33bce44adc6b63e2f6 --- .../java/com/sap/sailing/gwt/ui/leaderboard/LegColumn.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LegColumn.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LegColumn.java index 10a30c6386f..6ef18e570fe 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LegColumn.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LegColumn.java @@ -348,11 +348,10 @@ public class LegColumn extends ExpandableSortableColumn { @Override public String getValue(LeaderboardRowDTO row) { LeaderboardEntryDTO leaderboardEntryDTO = row.fieldsByRaceColumnName.get(raceColumnName); - int legDetailsCount = leaderboardEntryDTO.legDetails.size(); LegEntryDTO legEntry = getLegEntry(row); if (legEntry != null && legEntry.rank != 0) { return ""+legEntry.rank; - } else if (legIndex+1 > legDetailsCount) { + } else if (leaderboardEntryDTO.legDetails != null && legIndex+1 > leaderboardEntryDTO.legDetails.size()) { return "n/a"; } else { return "";