package org.opentripplanner.profile;
import com.vividsolutions.jts.geom.Coordinate;
import org.apache.commons.math3.util.FastMath;
import org.opentripplanner.analyst.PointSet;
import org.opentripplanner.analyst.core.IsochroneData;
import org.opentripplanner.analyst.request.SampleGridRenderer;
import org.opentripplanner.common.geometry.AccumulativeGridSampler;
import org.opentripplanner.common.geometry.DelaunayIsolineBuilder;
import org.opentripplanner.common.geometry.SparseMatrixZSampleGrid;
import org.opentripplanner.common.geometry.SphericalDistanceLibrary;
import org.opentripplanner.common.geometry.ZSampleGrid;
import org.opentripplanner.analyst.request.SampleGridRenderer.WTWD;
import java.util.ArrayList;
import java.util.List;
import static org.apache.commons.math3.util.FastMath.toRadians;
/**
* This class contains a couple of utility methods for making Isochrones when all you have is a PointSet and times to
* reach that PointSet. Eventually we may want to just define a standard grid PointSet for each study area in such a way
* that all streets within each grid cell contribute to its travel time value, then use that grid directly when making
* isolines. This would allow us to drop the separate grid accumulation step.
*
* The reason why we need this functionality is that we are now producing travel time stats directly for pointsets as targets
* of a repeated RAPTOR search, without saving accompanying travel time values for the street vertices along the way.
*/
public abstract class IsochroneGenerator {
public static final double GRID_SIZE_METERS = 300;
// Off-road max distance MUST be APPROX EQUALS to the grid precision
// TODO: Loosen this restriction (by adding more closing samples).
// Change the 0.8 magic factor here with caution.
public static final double WALK_DISTANCE_GRID_SIZE_RATIO = 0.8;
/**
* Make a ZSampleGrid from a PointSet and a parallel array of travel times for that PointSet.
* Those times could come from a ResultSetWithTimes or directly from a PropagatedTimesStore, which has one
* such array for each of min, avg, and max travel time over the departure time window it represents.
* If your PointSet is dense enough (e.g. every block in a city) then this should yield a decent surface and
* isochrones.
* FIXME code duplication, this is ripped off from TimeSurface and should probably replace the version there as it is more generic.
* @param walkSpeed the walk speed in meters per second
* @return a grid suitable for making isochrones, based on an arbitrary PointSet and times to reach all those points.
*/
public static ZSampleGrid makeGrid (PointSet pointSet, int[] times, double walkSpeed) {
final double D0 = WALK_DISTANCE_GRID_SIZE_RATIO * GRID_SIZE_METERS; // offroad walk distance roughly grid size
final double V0 = walkSpeed; // off-road walk speed in m/sec
Coordinate coordinateOrigin = pointSet.getCoordinate(0); // Use the first feature as the center of the projection
final double cosLat = FastMath.cos(toRadians(coordinateOrigin.y));
double dY = Math.toDegrees(GRID_SIZE_METERS / SphericalDistanceLibrary.RADIUS_OF_EARTH_IN_M);
double dX = dY / cosLat;
ZSampleGrid grid = new SparseMatrixZSampleGrid<WTWD>(16, times.length, dX, dY, coordinateOrigin);
AccumulativeGridSampler.AccumulativeMetric<WTWD> metric =
new SampleGridRenderer.WTWDAccumulativeMetric(cosLat, D0, V0, GRID_SIZE_METERS);
AccumulativeGridSampler<WTWD> sampler = new AccumulativeGridSampler<>(grid, metric);
// Iterate over every point in this PointSet, adding it to the ZSampleGrid
for (int p = 0; p < times.length; p++) {
int time = times[p];
WTWD z = new WTWD();
z.w = 1.0;
z.d = 0.0;
z.wTime = time;
z.wBoardings = 0; // unused
z.wWalkDist = 0; // unused
sampler.addSamplingPoint(pointSet.getCoordinate(p), z, V0);
}
sampler.close();
return grid;
}
/**
* Make isochrones from a grid. This more general function should probably be reused by SurfaceResource.
* FIXME code duplication: function ripped off from SurfaceResource
* @param spacingMinutes the number of minutes between isochrones
* @return a list of evenly-spaced isochrones
*/
public static List<IsochroneData> getIsochronesAccumulative(ZSampleGrid<WTWD> grid,
int spacingMinutes, int cutoffMinutes, int nMax) {
DelaunayIsolineBuilder<WTWD> isolineBuilder = new DelaunayIsolineBuilder<>(
grid.delaunayTriangulate(), new WTWD.IsolineMetric());
List<IsochroneData> isochrones = new ArrayList<>();
for (int minutes = spacingMinutes, n = 0; minutes <= cutoffMinutes && n < nMax; minutes += spacingMinutes, n++) {
int seconds = minutes * 60;
SampleGridRenderer.WTWD z0 = new SampleGridRenderer.WTWD();
z0.w = 1.0;
z0.wTime = seconds;
z0.d = GRID_SIZE_METERS;
IsochroneData isochrone = new IsochroneData(seconds, isolineBuilder.computeIsoline(z0));
isochrones.add(isochrone);
if (n + 1 >= nMax) {
break;
}
}
return isochrones;
}
}