Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
au.gov.amsa.geo.distance.DistanceTravelledCalculator Maven / Gradle / Ivy
package au.gov.amsa.geo.distance;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicLong;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.github.davidmoten.guavamini.Preconditions;
import com.github.davidmoten.rx.slf4j.Logging;
import com.google.common.annotations.VisibleForTesting;
import au.gov.amsa.geo.model.Bounds;
import au.gov.amsa.geo.model.Cell;
import au.gov.amsa.geo.model.CellValue;
import au.gov.amsa.geo.model.GridTraversor;
import au.gov.amsa.geo.model.Options;
import au.gov.amsa.geo.model.SegmentOptions;
import au.gov.amsa.geo.model.Util;
import au.gov.amsa.risky.format.BinaryFixes;
import au.gov.amsa.risky.format.Fix;
import au.gov.amsa.risky.format.HasPosition;
import au.gov.amsa.util.navigation.Position;
import rx.Observable;
import rx.Observable.OnSubscribe;
import rx.Observer;
import rx.Subscriber;
import rx.functions.Action1;
import rx.functions.Action2;
import rx.functions.Func0;
import rx.functions.Func1;
import rx.schedulers.Schedulers;
import ucar.ma2.Array;
import ucar.ma2.ArrayDouble;
import ucar.ma2.DataType;
import ucar.ma2.Index2D;
import ucar.ma2.InvalidRangeException;
import ucar.nc2.Attribute;
import ucar.nc2.Dimension;
import ucar.nc2.NetcdfFileWriter;
import ucar.nc2.Variable;
public class DistanceTravelledCalculator {
private static Logger log = LoggerFactory.getLogger(DistanceTravelledCalculator.class);
private final Options options;
private final DistanceCalculationMetrics metrics;
public DistanceTravelledCalculator(Options options, DistanceCalculationMetrics metrics) {
this.options = options;
this.metrics = metrics;
}
/**
* Returns distinct cells and the the total nautical miles travelled in that
* cell. Uses RxJava {@link Observable}s to maximize throughput and will
* scale to use all available processors and to handle a large number of
* files (number of open file handles should be limited by number of
* available processors).
*
* @param files
* @return
*/
public Observable calculateDistanceByCellFromFiles(Observable files) {
// use a map-reduce approach where the parallel method shares out
// ('maps') the fixes by craft to multiple threads (number determined by
// available processors) and is passed the 'reduce'
// calculateDistanceByCellFromFile() method to combine the results.
int numFiles = files.count().toBlocking().single();
log.info("numFiles=" + numFiles);
AtomicLong fileCount = new AtomicLong();
AtomicLong cellCount = new AtomicLong(1);
return files
// buffer for parallel processing of groups of files
.buffer(Math.max(1,
(int) Math.round(
Math.ceil(numFiles / Runtime.getRuntime().availableProcessors()))))
.flatMap(fileList -> extractCellDistances(fileCount, cellCount, fileList))
// sum distances into global map
.collect(bigMapFactory(), collectCellDistances())
// report the cell distances for the grid
.flatMap(listCellDistances())
// record total nm in metrics
.doOnNext(sumNauticalMiles());
}
private Func1 super HashMap, Observable> listCellDistances() {
return map -> Observable.from(map.entrySet())
.map(entry -> new CellAndDistance(entry.getKey(), entry.getValue()));
}
private Func0> bigMapFactory() {
return () -> new HashMap(20_000_000, 1.0f);
}
private Action2, Map> collectCellDistances() {
return (a, b) -> {
// put all entries in b into a
long t = System.currentTimeMillis();
log.info("reducing");
for (Entry entry : b.entrySet()) {
Double val = a.putIfAbsent(entry.getKey(), entry.getValue());
if (val != null) {
a.put(entry.getKey(), val + entry.getValue());
}
}
log.info("reduced in " + (System.currentTimeMillis() - t) + "ms");
};
}
private Observable> extractCellDistances(AtomicLong fileCount,
AtomicLong cellCount, List fileList) {
return // extract fixes from each file
Observable.from(fileList)
.lift(Logging. logger().showCount(fileCount).every(1000).log())
.map(file -> BinaryFixes.from(file))
// for one craft aggregate distance (not a
// problem with SerializedObserver buffering
// because each file relatively small), also
// subscribes on computation() to get
// concurrency
.flatMap(toCraftCellAndDistances)
// log
.lift(Logging. logger().showCount("cellsReceived", cellCount)
.every(1_000_000).showMemory().log())
// sum cell distances and emit maps of up to
// 100K entries
.lift(OperatorSumCellDistances.create(1_000_000))
.subscribeOn(Schedulers.computation());
}
private final Func1, Observable> toCraftCellAndDistances = new Func1, Observable>() {
@Override
public Observable call(Observable allFixesForASingleCraft) {
return allFixesForASingleCraft
// count fixes
.doOnNext(incrementFixesCount)
// filter on time between startTime and finishTime if exist
.filter(inTimeRange)
// restrict to fixes in filter bounds
.filter(inRegion)
// sort fixes by position time
// .toSortedList(au.gov.amsa.geo.Util.COMPARE_FIXES_BY_POSITION_TIME)
// convert list to Observable and flatten
// .concatMap(au.gov.amsa.geo.Util.TO_OBSERVABLE)
// keep only positions that pass effective speed
.lift(filterOnEffectiveSpeedOk())
// filter on passed effective speed check
.filter(check -> check.isOk())
// map back to the fix again
.map(check -> check.fix())
// update metrics with fixes passing effective speed check
.doOnNext(countFixesPassedEffectiveSpeedCheck)
// pair them up again
.buffer(2, 1)
// segments only
.filter(PAIRS_ONLY)
// count segments
.doOnNext(segment -> metrics.segments.incrementAndGet())
// remove segments with invalid time separation
.filter(timeDifferenceOk)
// remove segments with invalid distance separation
.filter(distanceOk)
// calculate distances
.flatMap(toCellAndDistance)
// update counts of cells in each segment
.doOnNext(countSegmentCells)
// use memory to buffer if producing fast
.onBackpressureBuffer();
}
};
private OperatorEffectiveSpeedChecker filterOnEffectiveSpeedOk() {
return new OperatorEffectiveSpeedChecker(options.getSegmentOptions());
}
private final Func1 inRegion = new Func1() {
@Override
public Boolean call(Fix fix) {
boolean in = options.getFilterBounds().contains(fix);
if (in)
metrics.fixesWithinRegion.incrementAndGet();
return in;
}
};
private final Func1 inTimeRange = new Func1() {
@Override
public Boolean call(Fix fix) {
boolean lowerBoundOk = !options.getStartTime().isPresent()
|| fix.time() >= options.getStartTime().get();
boolean upperBoundOk = !options.getFinishTime().isPresent()
|| fix.time() < options.getFinishTime().get();
boolean result = lowerBoundOk && upperBoundOk;
if (result)
metrics.fixesInTimeRange.incrementAndGet();
return result;
}
};
private final Func1, Boolean> timeDifferenceOk = new Func1, Boolean>() {
@Override
public Boolean call(List pair) {
Preconditions.checkArgument(pair.size() == 2);
Fix a = pair.get(0);
Fix b = pair.get(1);
boolean ok = timeDifferenceOk(a, b, options.getSegmentOptions());
if (ok)
metrics.segmentsTimeDifferenceOk.incrementAndGet();
return ok;
}
};
private final Func1, Boolean> distanceOk = new Func1, Boolean>() {
@Override
public Boolean call(List pair) {
Preconditions.checkArgument(pair.size() == 2);
Fix a = pair.get(0);
Fix b = pair.get(1);
boolean ok = distanceOk(a, b, options.getSegmentOptions());
if (ok)
metrics.segmentsDistanceOk.incrementAndGet();
return ok;
}
};
private static boolean timeDifferenceOk(Fix a, Fix b, SegmentOptions o) {
long timeDiffMs = Math.abs(a.time() - b.time());
return o.maxTimePerSegmentMs() == null || timeDiffMs <= o.maxTimePerSegmentMs();
}
private static boolean distanceOk(Fix a, Fix b, SegmentOptions o) {
return o.maxDistancePerSegmentNm() > Util.greatCircleDistanceNm(a, b);
}
private static final Func1, Boolean> PAIRS_ONLY = new Func1, Boolean>() {
@Override
public Boolean call(List list) {
return list.size() == 2;
}
};
private final Func1, Observable> toCellAndDistance = new Func1, Observable>() {
@Override
public Observable call(List pair) {
Preconditions.checkArgument(pair.size() == 2);
Fix fix1 = pair.get(0);
Fix fix2 = pair.get(1);
return getCellDistances(fix1, fix2, options);
}
};
private final Action1 countSegmentCells = new Action1() {
@Override
public void call(CellAndDistance cd) {
metrics.segmentCells.incrementAndGet();
}
};
@VisibleForTesting
static final Observable getCellDistances(HasPosition a, HasPosition b,
Options options) {
return getCellDistances(Util.toPos(a), Util.toPos(b), options);
}
@VisibleForTesting
static final Observable getCellDistances(final Position a, final Position b,
final Options options) {
return Observable.create(new OnSubscribe() {
@Override
public void call(Subscriber super CellAndDistance> subscriber) {
try {
GridTraversor grid = new GridTraversor(options);
boolean keepGoing = true;
Position p1 = a;
Position destination = b;
int count = 0;
while (keepGoing) {
Position p2 = grid.nextPoint(p1, destination);
double distanceNm = p1.getDistanceToKm(p2) / 1.852;
// report cell and distance
Optional cell = Cell.cellAt(p1.getLat(), p1.getLon(), options);
if (cell.isPresent())
subscriber.onNext(new CellAndDistance(cell.get(), distanceNm));
keepGoing = p2.getLat() != destination.getLat()
|| p2.getLon() != destination.getLon();
keepGoing = keepGoing && !subscriber.isUnsubscribed();
p1 = p2;
count++;
checkCount(p1, destination, count, options);
}
subscriber.onCompleted();
} catch (Throwable t) {
// TODO resolve all problems so that this will revert to a
// call to onError
log.warn(t.getMessage(), t);
subscriber.onCompleted();
// subscriber.onError(t);
}
}
});
}
private static void checkCount(Position p1, Position destination, int count, Options options) {
if (count > 100000)
throw new RuntimeException("unexpectedly stuck in loop p1=" + p1 + ",destination="
+ destination + ",options=" + options);
}
public DistanceCalculationMetrics getMetrics() {
return metrics;
}
public static class CalculationResult {
private final Observable cells;
private final DistanceCalculationMetrics metrics;
public CalculationResult(Observable cells, DistanceCalculationMetrics metrics) {
this.cells = cells;
this.metrics = metrics;
}
public Observable getCells() {
return cells;
}
public DistanceCalculationMetrics getMetrics() {
return metrics;
}
}
public static Observable calculateDensityByCellFromFiles(Options options,
Observable files, int horizontal, int vertical,
DistanceCalculationMetrics metrics) {
Observable cells = partition(options, horizontal, vertical)
// get results (blocks to limit memory use)
.concatMap(calculateDistanceTravelled(files, metrics));
if (horizontal > 1 || vertical > 1)
// aggregate using a file backed map
return cells.lift(new OperatorSumCellValues(true));
else
return cells;
}
private static Func1> calculateDistanceTravelled(
final Observable files, final DistanceCalculationMetrics metrics) {
return new Func1>() {
@Override
public Observable call(Options options) {
log.info("running distance calculation on " + options);
DistanceTravelledCalculator c = new DistanceTravelledCalculator(options, metrics);
// blocks to return answer, this is desirable because we need to
// back the results with a file because they can get so large
return Observable.from(c.calculateDistanceByCellFromFiles(files)
// as cell density values
.map(toCellDensityValue(options))
// as list
.toList()
// block and get
.toBlocking().single());
}
};
}
public static CalculationResult calculateTrafficDensity(Options options,
Observable files) {
return calculateTrafficDensity(options, files, 1, 1);
}
public static CalculationResult calculateTrafficDensity(Options options, Observable files,
int horizontal, int vertical) {
int maxNumCells = (int) Math.round(options.getBounds().getWidthDegrees()
* options.getBounds().getHeightDegrees() / options.getCellSizeDegreesAsDouble()
/ options.getCellSizeDegreesAsDouble());
log.info("maxNumCells=" + maxNumCells);
DistanceCalculationMetrics metrics = new DistanceCalculationMetrics();
final Observable cells = DistanceTravelledCalculator
.calculateDensityByCellFromFiles(options, files, horizontal, vertical, metrics);
return new CalculationResult(cells, metrics);
}
private static Func1 toCellDensityValue(final Options options) {
return new Func1() {
@Override
public CellValue call(CellAndDistance cd) {
return new CellValue(cd.getCell().getCentreLat(options),
cd.getCell().getCentreLon(options), cd.getTrafficDensity(options));
}
};
}
public static void saveCalculationResultAsText(Options options,
CalculationResult calculationResult, String filename) {
try {
final PrintWriter out = new PrintWriter(filename);
Bounds b = options.getBounds();
out.println(
"#originLat, originLon, cellSizeDegrees, topLefLat, topLeftLon, bottomRightLat, bottomRightLon");
out.format("%s\t%s\t%s\t%s\t%s\t%s\t%s\n", options.getOriginLat(),
options.getOriginLon(), options.getCellSizeDegrees(), b.getTopLeftLat(),
b.getTopLeftLon(), b.getBottomRightLat(), b.getBottomRightLon());
out.println("#centreLat, centreLon, distanceNmPerNm2");
calculationResult.getCells().subscribe(new Observer() {
@Override
public void onCompleted() {
out.close();
}
@Override
public void onError(Throwable e) {
out.close();
}
@Override
public void onNext(CellValue cell) {
out.format("%s\t%s\t%s\n", cell.getCentreLat(), cell.getCentreLon(),
cell.getValue());
}
});
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
}
public static void saveCalculationResultAsNetcdf(Options options,
CalculationResult calculationResult, String filename) {
List list = calculationResult.getCells().toList().toBlocking().single();
int maxLonIndex = list.stream()
.map(cell -> options.getGrid().cellAt(cell.getCentreLat(), cell.getCentreLon()))
.filter(x -> x.isPresent()) //
.map(x -> x.get().getLonIndex())
.max(Comparator. naturalOrder()) //
.get().intValue();
int maxLatIndex = list.stream()
.map(cell -> options.getGrid().cellAt(cell.getCentreLat(), cell.getCentreLon()))
.filter(x -> x.isPresent()).map(x -> x.get().getLatIndex())
.max(Comparator. naturalOrder()).get().intValue();
File file = new File(filename);
// Create the file.
NetcdfFileWriter f = null;
try {
// Create new netcdf-3 file with the given filename
f = NetcdfFileWriter.createNew(NetcdfFileWriter.Version.netcdf3, file.getPath());
// In addition to the latitude and longitude dimensions, we will
// also create latitude and longitude netCDF variables which will
// hold the actual latitudes and longitudes. Since they hold data
// about the coordinate system, the netCDF term for these is:
// "coordinate variables."
Dimension dimLat = f.addDimension(null, "latitude", maxLatIndex + 1);
Dimension dimLon = f.addDimension(null, "longitude", maxLonIndex + 1);
List dims = new ArrayList();
dims.add(dimLat);
dims.add(dimLon);
// coordinate variables
Variable vLat = f.addVariable(null, "latitude", DataType.DOUBLE, "latitude");
Variable vLon = f.addVariable(null, "longitude", DataType.DOUBLE, "longitude");
// value variables
Variable vDensity = f.addVariable(null, "traffic_density", DataType.DOUBLE, dims);
// Define units attributes for coordinate vars. This attaches a
// text attribute to each of the coordinate variables, containing
// the units.
vLon.addAttribute(new Attribute("units", "degrees_east"));
vLat.addAttribute(new Attribute("units", "degrees_north"));
// Define units attributes for variables.
vDensity.addAttribute(new Attribute("units", "nm-1"));
vDensity.addAttribute(new Attribute("long_name", ""));
// Write the coordinate variable data. This will put the latitudes
// and longitudes of our data grid into the netCDF file.
f.create();
{
Array dataLat = Array.factory(DataType.DOUBLE, new int[] { dimLat.getLength() });
Array dataLon = Array.factory(DataType.DOUBLE, new int[] { dimLon.getLength() });
// set latitudes
for (int i = 0; i <= maxLatIndex; i++) {
dataLat.setDouble(i, options.getGrid().centreLat(i));
}
// set longitudes
for (int i = 0; i <= maxLonIndex; i++) {
dataLon.setDouble(i, options.getGrid().centreLon(i));
}
f.write(vLat, dataLat);
f.write(vLon, dataLon);
}
// write the value variable data
{
int[] iDim = new int[] { dimLat.getLength(), dimLon.getLength() };
Array dataDensity = ArrayDouble.D2.factory(DataType.DOUBLE, iDim);
Index2D idx = new Index2D(iDim);
for (CellValue point : list) {
Optional cell = options.getGrid().cellAt(point.getCentreLat(),
point.getCentreLon());
if (cell.isPresent()) {
idx.set((int) cell.get().getLatIndex(), (int) cell.get().getLonIndex());
dataDensity.setDouble(idx, point.getValue());
}
}
f.write(vDensity, dataDensity);
}
} catch (IOException | InvalidRangeException e) {
throw new RuntimeException(e);
} finally {
if (f != null) {
try {
f.close();
} catch (IOException ioe) {
throw new RuntimeException(ioe);
}
}
}
}
static List makeConstantDifference(List list) {
List result = new ArrayList<>();
double diff = list.get(1) - list.get(0);
Double previous = null;
for (int i = 0; i < list.size(); i++) {
double next;
if (previous == null) {
next = list.get(0);
} else {
next = previous + diff;
}
result.add(next);
previous = next;
}
return result;
}
// public static void saveCalculationResultAsBinary(Options options,
// CalculationResult calculationResult, String filename) {
// try {
// List cells = calculationResult.getCells();
// DataOutputStream out = new DataOutputStream(
// new BufferedOutputStream(new FileOutputStream(filename)));
//
// Bounds b = options.getBounds();
// out.writeDouble(options.getCellSizeDegreesAsDouble());
// out.writeDouble(b.getTopLeftLat());
// out.writeDouble(b.getTopLeftLon());
// out.writeDouble(b.getBottomRightLat());
// out.writeDouble(b.getBottomRightLon());
//
// for (CellValue cell : cells) {
// out.writeDouble(cell.getCentreLat());
// out.writeDouble(cell.getCentreLon());
// out.writeDouble(cell.getValue());
// }
// out.close();
// } catch (FileNotFoundException e) {
// throw new RuntimeException(e);
// } catch (IOException e) {
// throw new RuntimeException(e);
// }
// }
private Action1 sumNauticalMiles() {
return new Action1() {
@Override
public void call(CellAndDistance cell) {
metrics.totalNauticalMiles.addAndGet(cell.getDistanceNm());
}
};
}
private final Action1 super Fix> incrementFixesCount = new Action1() {
@Override
public void call(Fix fix) {
metrics.fixes.incrementAndGet();
}
};
private final Action1 super Fix> countFixesPassedEffectiveSpeedCheck = new Action1() {
@Override
public void call(Fix fix) {
metrics.fixesPassedEffectiveSpeedCheck.incrementAndGet();
}
};
/**
* Returns a sequence of {@link Options} that are same as the source apart
* from the {@link Bounds} which are partitioned according to horizontal and
* vertical parameters. For map-reduce purposes we need to be able to
* partition the bounds of Options. Passing horizontal=1 and vertical=1 will
* return one item only being a copy of the source {@link Options}.
*
* @param options
* @param horizontal
* number of regions (with longitude)
* @param vertical
* number of regions (with latitude)
* @return
*/
public static Observable partition(final Options options, final int horizontal,
final int vertical) {
List list = new ArrayList<>();
Bounds bounds = options.getBounds();
double h = bounds.getWidthDegrees() / horizontal;
double v = bounds.getHeightDegrees() / vertical;
for (int i = 0; i < horizontal; i++) {
for (int j = 0; j < vertical; j++) {
double lat = bounds.getTopLeftLat() - j * v;
double lon = bounds.getTopLeftLon() + i * h;
Bounds b = new Bounds(lat, lon, lat - v, lon + h);
list.add(options.buildFrom().bounds(b).filterBounds(b.expand(7, 7)).build());
}
}
return Observable.from(list);
}
}
| | | | | |