All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.opentripplanner.api.resource.SurfaceResource Maven / Gradle / Ivy

package org.opentripplanner.api.resource;

import com.google.common.collect.Maps;
import org.geotools.feature.FeatureCollection;
import org.geotools.geojson.feature.FeatureJSON;
import org.geotools.geometry.Envelope2D;
import org.opentripplanner.analyst.PointSet;
import org.opentripplanner.analyst.ResultSet;
import org.opentripplanner.analyst.SampleSet;
import org.opentripplanner.analyst.TimeSurface;
import org.opentripplanner.analyst.core.IsochroneData;
import org.opentripplanner.analyst.core.SlippyTile;
import org.opentripplanner.analyst.request.RenderRequest;
import org.opentripplanner.analyst.request.SampleGridRenderer.WTWD;
import org.opentripplanner.analyst.request.TileRequest;
import org.opentripplanner.api.common.ParameterException;
import org.opentripplanner.api.common.RoutingResource;
import org.opentripplanner.api.model.TimeSurfaceShort;
import org.opentripplanner.api.parameter.CRSParameter;
import org.opentripplanner.api.parameter.IsoTimeParameter;
import org.opentripplanner.api.parameter.Layer;
import org.opentripplanner.api.parameter.MIMEImageFormat;
import org.opentripplanner.api.parameter.Style;
import org.opentripplanner.common.geometry.DelaunayIsolineBuilder;
import org.opentripplanner.routing.algorithm.EarliestArrivalSearch;
import org.opentripplanner.routing.core.RoutingRequest;
import org.opentripplanner.routing.spt.ShortestPathTree;
import org.opentripplanner.standalone.Router;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.StreamingOutput;
import javax.ws.rs.core.UriInfo;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * Surfaces cannot be isolated per-router because sometimes you want to compare two surfaces from different router IDs.
 * Though one could question whether that really makes sense (perhaps alternative scenarios should be "within" the same router)
 */
@Path("/surfaces")
@Produces({ MediaType.APPLICATION_JSON })
public class SurfaceResource extends RoutingResource {

    private static final Logger LOG = LoggerFactory.getLogger(TimeSurface.class);

    @Context
    UriInfo uriInfo;

    @POST
    public Response createSurface(@QueryParam("cutoffMinutes") 
    @DefaultValue("90") int cutoffMinutes,
    @QueryParam("routerId") String routerId) {

        // Build the request
        try {
            RoutingRequest req = buildRequest(); // batch must be true
           
            // routerId is optional -- select default graph if not set
            Router router = otpServer.getRouter(routerId);
            req.setRoutingContext(router.graph);
        	
            EarliestArrivalSearch sptService = new EarliestArrivalSearch();
            sptService.maxDuration = (60 * cutoffMinutes);
            ShortestPathTree spt = sptService.getShortestPathTree(req);
            req.cleanup();
            if (spt != null) {
                TimeSurface surface = new TimeSurface(spt);
                surface.params = Maps.newHashMap();
                for (Map.Entry> e : uriInfo.getQueryParameters().entrySet()) {
                    // include only the first instance of each query parameter
                    surface.params.put(e.getKey(), e.getValue().get(0));
                }
                surface.cutoffMinutes = cutoffMinutes;
                otpServer.surfaceCache.add(surface);
                return Response.ok().entity(new TimeSurfaceShort(surface)).build(); // .created(URI)
            } else {
                return Response.noContent().entity("NO SPT").build();
            }
        } catch (ParameterException pex) {
            return Response.status(Response.Status.BAD_REQUEST).entity("BAD USER").build();
        }

    }

    /** List all the available surfaces. */
    @GET
    public Response getTimeSurfaceList () {
        return Response.ok().entity(TimeSurfaceShort.list(otpServer.surfaceCache.cache.asMap().values())).build();
    }

    /** Describe a specific surface. */
    @GET @Path("/{surfaceId}")
    public Response getTimeSurfaceList (@PathParam("surfaceId") Integer surfaceId) {
        TimeSurface surface = otpServer.surfaceCache.get(surfaceId);
        if (surface == null) return Response.status(Response.Status.NOT_FOUND).entity("Invalid surface ID.").build();
        return Response.ok().entity(new TimeSurfaceShort(surface)).build();
        // DEBUG return Response.ok().entity(surface).build();
    }

    /**
     * Evaluate a surface at all the points in a PointSet.
     * This sends back a ResultSet serialized as JSON.
     * Normally we return historgrams with the number of points reached (in field 'counts') and the number of
     * opportunities reached (i.e. the sum of the magnitudes of all points reached) in each one-minute bin of travel
     * time.
     * @param detail if true, include the travel time to every point in the pointset (which is in fact an ordered list)
     */
    @GET @Path("/{surfaceId}/indicator")
    public Response getIndicator (@PathParam("surfaceId") Integer surfaceId,
                                  @QueryParam("targets")  String  targetPointSetId,
                                  @QueryParam("origins")  String  originPointSetId,
                                  @QueryParam("detail")   boolean detail) {

        final TimeSurface surf = otpServer.surfaceCache.get(surfaceId);
        if (surf == null) return badRequest("Invalid TimeSurface ID.");
        final PointSet pset = otpServer.pointSetCache.get(targetPointSetId);
        if (pset == null) return badRequest("Missing or invalid target PointSet ID.");

        Router router = otpServer.getRouter(surf.routerId);
        // TODO cache this sampleset
        SampleSet samples = pset.getSampleSet(router.graph);
        final ResultSet indicator = new ResultSet(samples, surf, detail, detail);
        if (indicator == null) return badServer("Could not compute indicator as requested.");

        return Response.ok().entity(new StreamingOutput() {
            @Override
            public void write(OutputStream output) throws IOException, WebApplicationException {
                indicator.writeJson(output);
            }
        }).build();

    }

    /** Create vector isochrones for a surface. */
    @GET @Path("/{surfaceId}/isochrone")
    public Response getIsochrone (
            @PathParam("surfaceId") Integer surfaceId,
            @QueryParam("spacing") int spacing,
            @QueryParam("nMax") @DefaultValue("1") int nMax) {
        final TimeSurface surf = otpServer.surfaceCache.get(surfaceId);
        if (surf == null) return badRequest("Invalid TimeSurface ID.");
        if (spacing < 1) spacing = 30;
        List isochrones = getIsochronesAccumulative(surf, spacing, nMax);
        // NOTE that cutoffMinutes in the surface must be properly set for the following call to work
        final FeatureCollection fc = LIsochrone.makeContourFeatures(isochrones);
        return Response.ok().entity(new StreamingOutput() {
            @Override
            public void write(OutputStream output) throws IOException {
                FeatureJSON fj = new FeatureJSON();
                fj.writeFeatureCollection(fc, output);
            }
        }).build();
    }

    @Path("/{surfaceId}/isotiles/{z}/{x}/{y}.png")
    @GET @Produces("image/png")
    public Response tileGet(@PathParam("surfaceId") Integer surfaceId,
                            @PathParam("x") int x,
                            @PathParam("y") int y,
                            @PathParam("z") int z) throws Exception {

        Envelope2D env = SlippyTile.tile2Envelope(x, y, z);
        TimeSurface surfA = otpServer.surfaceCache.get(surfaceId);
        if (surfA == null) return badRequest("Unrecognized surface ID.");
        	
        TileRequest tileRequest = new TileRequest(env, 256, 256);
       
        MIMEImageFormat imageFormat = new MIMEImageFormat("image/png");
        RenderRequest renderRequest =
                new RenderRequest(imageFormat, Layer.TRAVELTIME, Style.COLOR30, true, false);
        // TODO why can't the renderer be static?
        Router router = otpServer.getRouter(surfA.routerId);
        return router.renderer.getResponse(tileRequest, surfA, null, renderRequest);
    }
    /**
     * Renders a raster tile for showing the difference between two TimeSurfaces.
     * This service is included as a way to provide difference tiles using existing mechanisms in OTP.
     * TODO However, there is some room for debate around how differences are expressed in URLs.
     * We may want a more general purpose mechanism for combining time surfaces.
     * For example you could make a web service request to create a time surface A-B or A+B, and the server would give
     * you an ID for that surface, and then you could use that ID anywhere a surface ID is required. Perhaps internally
     * there would be some sort of DifferenceTimeSurface subclass that could just drop in anywhere TimeSurface is used.
     * This approach would be more stateful but more flexible.
     *
     * @author hannesj
     * 
     * @param surfaceId The id of the first surface
     * @param compareToSurfaceId The id of of the surface, which is compared to the first surface
    */
    @Path("/{surfaceId}/differencetiles/{compareToSurfaceId}/{z}/{x}/{y}.png")
    @GET @Produces("image/png")
    public Response differenceTileGet(@PathParam("surfaceId") Integer surfaceId,
                            @PathParam("compareToSurfaceId") Integer compareToSurfaceId,
                            @PathParam("x") int x,
                            @PathParam("y") int y,
                            @PathParam("z") int z) throws Exception {

        Envelope2D env = SlippyTile.tile2Envelope(x, y, z);
        TimeSurface surfA = otpServer.surfaceCache.get(surfaceId);
        if (surfA == null) return badRequest("Unrecognized surface ID.");

        TimeSurface surfB = otpServer.surfaceCache.get(compareToSurfaceId);
        if (surfB == null) return badRequest("Unrecognized surface ID.");

        if ( ! surfA.routerId.equals(surfB.routerId)) {
            return badRequest("Both surfaces must be from the same router to perform subtraction.");
        }

        TileRequest tileRequest = new TileRequest(env, 256, 256);
        MIMEImageFormat imageFormat = new MIMEImageFormat("image/png");
        RenderRequest renderRequest = new RenderRequest(imageFormat, Layer.DIFFERENCE, Style.DIFFERENCE, true, false);
        // TODO why can't the renderer be static?
        Router router = otpServer.getRouter(surfA.routerId);
        return router.renderer.getResponse(tileRequest, surfA, surfB, renderRequest);
    }

    private Response badRequest(String message) {
        return Response.status(Response.Status.BAD_REQUEST).entity("Bad request: " + message).build();
    }

    private Response badServer(String message) {
        return Response.status(Response.Status.BAD_REQUEST).entity("Server fail: " + message).build();
    }

    /**
     * Use Laurent's accumulative grid sampler. Cutoffs in minutes.
     * The grid and Delaunay triangulation are cached, so subsequent requests are very fast.
     *
     * @param spacing the number of minutes between isochrones
     * @return a list of evenly-spaced isochrones up to the timesurface's cutoff point
     */
    public static List getIsochronesAccumulative(TimeSurface surf, int spacing, int nMax) {

        long t0 = System.currentTimeMillis();
        if (surf.sampleGrid == null) {
            // The sample grid was not built from the SPT; make a minimal one including only time from the vertices in this timesurface
            surf.makeSampleGridWithoutSPT();
        }
        DelaunayIsolineBuilder isolineBuilder = new DelaunayIsolineBuilder(
                surf.sampleGrid.delaunayTriangulate(), new WTWD.IsolineMetric());

        List isochrones = new ArrayList();
        for (int minutes = spacing, n = 0; minutes <= surf.cutoffMinutes && n < nMax; minutes += spacing, n++) {
            int seconds = minutes * 60;
            WTWD z0 = new WTWD();
            z0.w = 1.0;
            z0.wTime = seconds;
            z0.d = 300; // meters. TODO set dynamically / properly, make sure it matches grid cell size?
            IsochroneData isochrone = new IsochroneData(seconds, isolineBuilder.computeIsoline(z0));
            isochrones.add(isochrone);
         }

        long t1 = System.currentTimeMillis();
        LOG.debug("Computed {} isochrones in {} msec", isochrones.size(), (int) (t1 - t0));

        return isochrones;
    }

    /**
     * Produce a single grayscale raster of travel time, like travel time tiles but not broken into tiles.
     */
    @Path("/{surfaceId}/raster")
    @GET @Produces("image/*")
    public Response getRaster(
            @PathParam("surfaceId") Integer surfaceId,
            @QueryParam("width") @DefaultValue("1024") Integer width,
            @QueryParam("height") @DefaultValue("768") Integer height,
            @QueryParam("resolution") Double resolution,
            @QueryParam("time") IsoTimeParameter time,
            @QueryParam("format") @DefaultValue("image/geotiff") MIMEImageFormat format,
            @QueryParam("crs") @DefaultValue("EPSG:4326") CRSParameter crs) throws Exception {

        TimeSurface surface = otpServer.surfaceCache.get(surfaceId);
        Router router = otpServer.getRouter(surface.routerId);
        // BoundingBox is a subclass of Envelope, an Envelope2D constructor parameter
        Envelope2D bbox = new Envelope2D(router.graph.getGeomIndex().getBoundingBox(crs.crs));
        if (resolution != null) {
            width  = (int) Math.ceil(bbox.width  / resolution);
            height = (int) Math.ceil(bbox.height / resolution);
        }

        TileRequest tileRequest = new TileRequest(bbox, width, height);
        RenderRequest renderRequest = new RenderRequest(format, Layer.TRAVELTIME, Style.GRAY, false, false);
        return router.renderer.getResponse(tileRequest, surface, null, renderRequest);
    }


}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy