package org.opentripplanner.analyst;
import com.beust.jcommander.internal.Maps;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.geotools.feature.FeatureCollection;
import org.geotools.geojson.feature.FeatureJSON;
import org.opentripplanner.analyst.core.IsochroneData;
import org.opentripplanner.api.resource.LIsochrone;
import org.opentripplanner.api.resource.SurfaceResource;
import org.opentripplanner.common.geometry.ZSampleGrid;
import org.opentripplanner.profile.IsochroneGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Serializable;
import java.io.StringWriter;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
* This holds the results of a one-to-many search from a single origin point to a whole set of destination points.
* This may be either a profile search (capturing variation over a time window of several hours) or a "classic" search
* at a single departure time. Those results are expressed as histograms: bins representing how many destinations you
* can reach after M minutes of travel. For large point sets (large numbers of origins and destinations), this is
* significantly more compact than a full origin-destination travel time matrix. It makes the total size of the results
* linear in the number of origins, rather than quadratic.
*
* Optionally, this can also carry travel times to every point in the target pointset and/or a series vector
* isochrones around the origin point.
*/
public class ResultSet implements Serializable{
private static final long serialVersionUID = -6723127825189535112L;
private static final Logger LOG = LoggerFactory.getLogger(ResultSet.class);
/** An identifier consisting of the ids for the pointset and time surface that were combined. */
public String id;
/** One histogram for each category of destination points in the target pointset. */
public Map<String,Histogram> histograms = Maps.newHashMap();
// FIXME aren't the histogram.counts identical for all these histograms?
/** Times to reach every feature, may be null */
public int[] times;
/** Isochrone geometries around the origin, may be null. */
public IsochroneData[] isochrones;
public ResultSet() {
}
/** Build a new ResultSet by evaluating the given TimeSurface at all the given sample points, not including times. */
public ResultSet(SampleSet samples, TimeSurface surface){
this(samples, surface, false, false);
}
/** Build a new ResultSet by evaluating the given TimeSurface at all the given sample points, optionally including times. */
public ResultSet(SampleSet samples, TimeSurface surface, boolean includeTimes, boolean includeIsochrones){
id = samples.pset.id + "_" + surface.id;
PointSet targets = samples.pset;
// Evaluate the surface at all points in the pointset
int[] times = samples.eval(surface);
buildHistograms(times, targets);
if (includeTimes)
this.times = times;
if (includeIsochrones)
buildIsochrones(surface);
}
private void buildIsochrones(TimeSurface surface) {
List<IsochroneData> id = SurfaceResource.getIsochronesAccumulative(surface, 5, 24);
this.isochrones = new IsochroneData[id.size()];
id.toArray(this.isochrones);
}
private void buildIsochrones(int[] times, PointSet targets) {
ZSampleGrid zs = IsochroneGenerator.makeGrid(targets, times, 1.3);
List<IsochroneData> id = IsochroneGenerator.getIsochronesAccumulative(zs, 5, 120, 24);
this.isochrones = new IsochroneData[id.size()];
id.toArray(this.isochrones);
}
/**
* Build a new ResultSet that contains only isochrones, built by accumulating the times at all street vertices
* into a regular grid without an intermediate pointSet.
*/
public ResultSet (TimeSurface surface) {
buildIsochrones(surface);
}
/** Build a new ResultSet directly from times at point features, optionally including histograms or interpolating isochrones */
public ResultSet(int[] times, PointSet targets, boolean includeTimes, boolean includeHistograms, boolean includeIsochrones) {
if (includeTimes)
this.times = times;
if (includeHistograms)
buildHistograms(times, targets);
if (includeIsochrones)
buildIsochrones(times, targets);
}
/**
* Given an array of travel times to reach each point in the supplied pointset, make a histogram of
* travel times to reach each separate category of points (i.e. "property") within the pointset.
* Each new histogram object will be stored as a part of this result set keyed on its property/category.
*/
protected void buildHistograms(int[] times, PointSet targets) {
this.histograms = Histogram.buildAll(times, targets);
}
/**
* Sum the values of specified categories at all time limits within the
* bounds of the search. If no categories are specified, sum all categories.
*/
public long sum (String... categories) {
return sum((Integer) null, categories);
}
/**
* Sum the values of the specified categories up to the time limit specified
* (in seconds). If no categories are specified, sum all categories.
*/
public long sum(Integer timeLimit, String... categories) {
if (categories.length == 0)
categories = histograms.keySet().toArray(new String[histograms.keySet().size()]);
long value = 0l;
int maxMinutes;
if(timeLimit != null)
maxMinutes = timeLimit / 60;
else
maxMinutes = Integer.MAX_VALUE;
for(String k : categories) {
int minute = 0;
for(int v : histograms.get(k).sums) {
if(minute < maxMinutes)
value += v;
minute++;
}
}
return value;
}
/**
* Serialize this ResultSet to the given output stream as a JSON document, when the pointset is not available.
* TODO: explain why and when that would happen
*/
public void writeJson(OutputStream output) {
writeJson(output, null);
}
/**
* Serialize this ResultSet to the given output stream as a JSON document.
* properties: a list of the names of all the pointSet properties for which we have histograms.
* data: for each property, a histogram of arrival times.
*/
public void writeJson(OutputStream output, PointSet ps) {
try {
JsonFactory jsonFactory = new JsonFactory();
JsonGenerator jgen = jsonFactory.createGenerator(output);
jgen.setCodec(new ObjectMapper());
jgen.writeStartObject(); {
if(ps == null) {
jgen.writeObjectFieldStart("properties"); {
if (id != null)
jgen.writeStringField("id", id);
}
jgen.writeEndObject();
}
else {
ps.writeJsonProperties(jgen);
}
jgen.writeObjectFieldStart("data"); {
for(String propertyId : histograms.keySet()) {
jgen.writeObjectFieldStart(propertyId); {
histograms.get(propertyId).writeJson(jgen);
}
jgen.writeEndObject();
}
}
jgen.writeEndObject();
if (times != null) {
jgen.writeArrayFieldStart("times");
for (int t : times) jgen.writeNumber(t);
jgen.writeEndArray();
}
}
jgen.writeEndObject();
jgen.close();
} catch (IOException ioex) {
LOG.info("IOException, connection may have been closed while streaming JSON.");
}
}
/** Write the isochrones as GeoJSON */
public void writeIsochrones(JsonGenerator jgen) throws IOException {
if (this.isochrones == null)
return;
FeatureJSON fj = new FeatureJSON();
FeatureCollection fc = LIsochrone.makeContourFeatures(Arrays.asList(isochrones));
StringWriter sw = new StringWriter();
fj.writeFeatureCollection(fc, sw);
// TODO cludge
String json = sw.toString();
jgen.writeRaw(json.substring(1, json.length() - 1));
}
/** A set of result sets from profile routing: min, avg, max */;
public static class RangeSet implements Serializable {
public static final long serialVersionUID = 1L;
public ResultSet min;
public ResultSet avg;
public ResultSet max;
}
}