/* Copyright (c) 2011 Danish Maritime Authority. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.maritimecloud.util.geometry; import static java.util.Objects.requireNonNull; import java.util.Arrays; import java.util.LinkedList; import java.util.Random; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicLong; import java.util.function.DoubleSupplier; import java.util.function.LongSupplier; import java.util.function.Supplier; import net.maritimecloud.util.units.SpeedUnit; /** * A simple builder for creating position readers that simulate simple sailing patterns. * * @author Kasper Nielsen */ public final class PositionReaderSimulator { final Random random; DoubleSupplier speedSource; LongSupplier timeSource = new LongSupplier() { public long getAsLong() { return System.currentTimeMillis(); } }; /** Creates a new PositionReaderSimulator. With a non-deterministic random source */ public PositionReaderSimulator() { this(new Random()); setSpeedVariable(1, 40, SpeedUnit.KNOTS); } /** * Creates a new PositionReaderSimulator. * * @param random * the random source of data */ public PositionReaderSimulator(Random random) { this.random = requireNonNull(random); setSpeedVariable(1, 40, SpeedUnit.KNOTS); } /** * Creates a new simulated position reader. The simulated vessel will start at random position within the specified * area. And travel to another random position within the area with a random speed. When it arrives at the position * it will choose another random position within the area to travel to and so on. * * @param supplier * a supplier of positions * @return the simulated position reader * @throws NullPointerException * if the specified area is null */ PositionReader forA(Supplier<Position> supplier) { return new AbtractSimulatedReader(this, supplier); } /** * Creates a new simulated position reader. The simulated vessel will start at random position within the specified * area. And then travel to another random position within the area. When it arrives at the position it will choose * another random position within the area to travel to and so on. * * @param area * the area to travel within * @return a new position reader * @throws NullPointerException * if the specified area is null */ public PositionReader forArea(final Area area) { final Random r = random; return forA(new Supplier<Position>() { public Position get() { return r == null ? area.getRandomPosition() : area.getRandomPosition(r); } }); } /** * Creates a new simulated reader with the specified route. When the vessel reaches the last of the specified * positions. It will sail the same route back. Continuing indefinitely. * <p> * If the first position and the last position is equivalent the positions will be delivered as if the vessel is * sailing in a circle. When the ship reaches the final position it will sail to position number 2 instead of * sailing the same route back. * * @param positions * each position for route * @return a new simulated reader */ public PositionReader forRoute(Position... positions) { final LinkedList<Position> l = new LinkedList<>(Arrays.asList(positions)); new ConcurrentLinkedQueue<>(l);// im lazy, this checks for null positions if (l.size() < 2) { throw new IllegalArgumentException("Must specified at least 2 positions"); } // if the first position is equal to the last position we are sailing in circles, remove the last one then if (l.getFirst().equals(l.getLast())) { l.removeLast(); } else if (l.size() > 2) { // dont do it for A->B->A->B.... // but A->B->C->D should become A->B->C->D->C->B LinkedList<Position> l2 = new LinkedList<>(l); l2.removeFirst(); l2.removeLast(); l.addAll(l2); } final Position[] p = l.toArray(new Position[l.size()]); return forA(new Supplier<Position>() { int counter = p.length - 1; public Position get() { return p[counter = (counter + 1) % p.length]; } }); } /** * Sets a fixed speed for the vessel. * <p> * If no speed is set the simulated position reader will use a variable speed between 1 and 40 knots. * * @param speed * the speed of the vessel * @param speedUnit * the unit of speed * @return this builder * @throws NullPointerException * if the speed unit is null * @throws IllegalArgumentException * if the specified speed is non positive * @see #setSpeedVariable(double, double, SpeedUnit) */ public PositionReaderSimulator setSpeedFixed(double speed, SpeedUnit speedUnit) { if (speed <= 0) { throw new IllegalArgumentException("Speed must be positive (>0)"); } final double metersPerSecond = speedUnit.toMetersPerSecond(speed); speedSource = new DoubleSupplier() { public double getAsDouble() { return metersPerSecond; } }; return this; } /** * Sets a variable speed for the vessel. Every time the vessel reaches a target position. It will change the speed * to a random number between minimum speed and maximum speed. * <p> * If no speed is set the simulated position reader will use a variable speed between 1 and 40 knots. * * @param minSpeed * the minimum speed of the vessel * @param maxSpeed * the maximum speed of the vessel * @param speedUnit * the unit of speed * @return this builder * @throws NullPointerException * if the speed unit is null * @throws IllegalArgumentException * if the specified minimum speed is non positive or the max speed is than or equal to minimum speed * @see #setSpeedFixed(double, SpeedUnit) */ public PositionReaderSimulator setSpeedVariable(double minSpeed, double maxSpeed, SpeedUnit speedUnit) { if (minSpeed <= 0) { throw new IllegalArgumentException("Minimum Speed must be positive (>0), was " + minSpeed); } else if (maxSpeed <= minSpeed) { throw new IllegalArgumentException("Maximum Speed must greater than minimum speed, minSpeed= " + minSpeed + ", maxSpeed=" + maxSpeed); } final double metersPerSecondMin = speedUnit.toMetersPerSecond(minSpeed); final double metersPerSecondMax = speedUnit.toMetersPerSecond(maxSpeed); speedSource = new DoubleSupplier() { public double getAsDouble() { return Area.nextDouble(random, metersPerSecondMin, metersPerSecondMax); } }; return this; } /** * Sets the time source that is used to determine how long duration has passed between succint invocations of * {@link PositionReader#getCurrentPosition()}. If no time source is set, {@link System#currentTimeMillis()} is * used. * * @param timeSource * the time source * @return this builder * @throws NullPointerException * if the specified time source is null */ public PositionReaderSimulator setTimeSource(LongSupplier timeSource) { this.timeSource = requireNonNull(timeSource); return this; } /** * Sets a deterministic time source that increment the time with the specified amount of milliseconds every time. * * @param milliesIncrement * the number of milliseconds that the ship will sail every time * {@link PositionReader#getCurrentPosition()} is invoked * @return this builder */ public PositionReaderSimulator setTimeSourceFixedSlice(final long milliesIncrement) { if (milliesIncrement <= 0) { throw new IllegalArgumentException(); } return setTimeSource(new LongSupplier() { final AtomicLong al = new AtomicLong(); public long getAsLong() { return al.incrementAndGet() * milliesIncrement; } }); } static class AbtractSimulatedReader extends PositionReader { PositionTime currentPosition; /** The current speed of the vessel in meters per second. */ double currentSpeed; /** A supplier that can calculate the next speed */ DoubleSupplier distanceSupplier; final Supplier<Position> positionSupplier; /** The current target of the vessel. */ Position target; /** The time source. */ final LongSupplier timeSource; AbtractSimulatedReader(PositionReaderSimulator prs, Supplier<Position> positionSupplier) { this.timeSource = requireNonNull(prs.timeSource); this.positionSupplier = requireNonNull(positionSupplier); this.currentPosition = positionSupplier.get().withTime(timeSource.getAsLong()); this.target = positionSupplier.get(); this.distanceSupplier = prs.speedSource; this.currentSpeed = distanceSupplier.getAsDouble(); } /** {@inheritDoc} */ @Override public final PositionTime getCurrentPosition() { long now = timeSource.getAsLong(); // no time elapsed, return last position if (now <= currentPosition.getTime()) { return currentPosition; } double distanceSailed = currentSpeed * (now - currentPosition.getTime()) / 1000; for (;;) { double distanceToTarget = currentPosition.rhumbLineDistanceTo(target); if (distanceSailed <= distanceToTarget) { return currentPosition = CoordinateSystem.CARTESIAN.pointOnBearing(currentPosition, distanceSailed, currentPosition.rhumbLineBearingTo(target)).withTime(now); } else {// okay we need to travel long than to target. Find the next point to travel to distanceSailed -= distanceToTarget; currentPosition = target.withTime(0);// not going to use the time parameter target = positionSupplier.get(); this.currentSpeed = distanceSupplier.getAsDouble(); } } } } }