package org.opentripplanner.analyst.request; import java.awt.AlphaComposite; import java.awt.Color; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics2D; import java.awt.image.BufferedImage; import java.io.OutputStream; import java.text.DateFormat; import java.util.Date; import java.util.TimeZone; import javax.imageio.ImageIO; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.CacheControl; import javax.ws.rs.core.Response; import javax.ws.rs.core.StreamingOutput; import org.geotools.coverage.grid.GridCoverage2D; import org.geotools.coverage.grid.io.AbstractGridFormat; import org.geotools.gce.geotiff.GeoTiffFormat; import org.geotools.gce.geotiff.GeoTiffWriteParams; import org.geotools.gce.geotiff.GeoTiffWriter; import org.opengis.parameter.GeneralParameterValue; import org.opengis.parameter.ParameterValueGroup; import org.opentripplanner.analyst.core.Tile; import org.opentripplanner.analyst.parameter.MIMEImageFormat; import org.opentripplanner.routing.core.RoutingRequest; import org.opentripplanner.routing.spt.ShortestPathTree; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class Renderer { private static final Logger LOG = LoggerFactory.getLogger(Renderer.class); @Autowired private TileCache tileCache; @Autowired private SPTCache sptCache; //Called by TileService.java in otp-rest-api public Response getResponse (TileRequest tileRequest, RoutingRequest[] sptRequest, RenderRequest renderRequest) throws Exception { Tile tile = tileCache.get(tileRequest); ShortestPathTree[] spt = new ShortestPathTree[sptRequest.length]; for(int i=0;i<spt.length;i++) spt[i] = sptCache.get(sptRequest[i]); BufferedImage image; switch (renderRequest.layer) { case DIFFERENCE : image = tile.linearCombination(1, spt[0], -1, spt[1], 0, renderRequest); break; case HAGERSTRAND : long elapsed = Math.abs(sptRequest[1].dateTime - sptRequest[0].dateTime); image = tile.linearCombination(-1, spt[0], -1, spt[1], elapsed/60, renderRequest); break; case AVGTRAVELTIME : image = tile.sptAverage(1, spt, renderRequest); break; case CLOSESTTRAVELTIME : image = tile.sptMin(1, spt, renderRequest); break; case CLOSECOMPSINGLE : ShortestPathTree[] sptA = new ShortestPathTree[spt.length-1]; for(int i=0;i<sptA.length;i++) sptA[i] = spt[i]; image = tile.sptMinDiff(sptA, spt, renderRequest); break; case TRAVELTIME : default : image = tile.generateImage(spt[0], renderRequest); break; } // add a timestamp to the image if requested. // of course this will make it useless as a raster for analysis, but it's good for animations. if (renderRequest.timestamp) { DateFormat df = DateFormat.getDateTimeInstance(); df.setTimeZone(TimeZone.getTimeZone("America/New_York")); String ds = df.format(new Date(sptRequest[0].dateTime * 1000)); shadowWrite(image, ds, sptRequest[0].from.toString()); Graphics2D g2d = image.createGraphics(); g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC)); BufferedImage legend = Tile.getLegend(renderRequest.style, 300, 50); g2d.drawImage(legend, 0, image.getHeight()-50, null); g2d.dispose(); } // geotiff kludge if (renderRequest.format.toString().equals("image/geotiff")) { GridCoverage2D gc = tile.getGridCoverage2D(image); return generateStreamingGeotiffResponse(gc); } else { return generateStreamingImageResponse(image, renderRequest.format); } } private void shadowWrite(BufferedImage image, String... strings) { Graphics2D g2d = image.createGraphics(); g2d.setFont(new Font("Sans", Font.PLAIN, 25)); FontMetrics fm = g2d.getFontMetrics(); int dy = fm.getHeight(); int xsize = 0; for (String s : strings) { int w = fm.stringWidth(s); if (w > xsize) xsize = w; } int y = 5; int x = 5; //g2d.fillRect(x, y, xsize, dy * strings.length + fm.getDescent()); y += dy; for (String s : strings) { g2d.setPaint(Color.black); g2d.drawString(s, x+1, y+1); g2d.setPaint(Color.white); g2d.drawString(s, x, y); y += dy; } g2d.dispose(); } public static Response generateStreamingImageResponse( final BufferedImage image, final MIMEImageFormat format) { if (image == null) { LOG.warn("response image is null"); } StreamingOutput streamingOutput = new StreamingOutput() { public void write(OutputStream outStream) { try { long t0 = System.currentTimeMillis(); ImageIO.write(image, format.type, outStream); long t1 = System.currentTimeMillis(); LOG.debug("wrote image in {}msec", (int)(t1-t0)); } catch (Exception e) { LOG.error("exception while preparing image : {}", e.getMessage()); throw new WebApplicationException(e); } } }; CacheControl cc = new CacheControl(); cc.setMaxAge(3600); cc.setNoCache(false); return Response.ok(streamingOutput) .type(format.toString()) .cacheControl(cc) .build(); } private static Response generateStreamingGeotiffResponse(final GridCoverage2D coverage) { StreamingOutput streamingOutput = new StreamingOutput() { public void write(OutputStream outStream) { try { long t0 = System.currentTimeMillis(); GeoTiffWriteParams wp = new GeoTiffWriteParams(); wp.setCompressionMode(GeoTiffWriteParams.MODE_EXPLICIT); wp.setCompressionType("LZW"); ParameterValueGroup params = new GeoTiffFormat().getWriteParameters(); params.parameter(AbstractGridFormat.GEOTOOLS_WRITE_PARAMS.getName().toString()).setValue(wp); new GeoTiffWriter(outStream).write(coverage, (GeneralParameterValue[]) params.values().toArray(new GeneralParameterValue[1])); //new GeoTiffWriter(outStream).write(coverage, null); //wasn't this line writing twice and trashing compressed version? long t1 = System.currentTimeMillis(); LOG.debug("wrote geotiff in {}msec", t1-t0); } catch (Exception e) { LOG.error("exception while preparing geotiff : {}", e.getMessage()); throw new WebApplicationException(e); } } }; CacheControl cc = new CacheControl(); cc.setMaxAge(3600); cc.setNoCache(false); return Response.ok(streamingOutput) .type("image/geotiff") .cacheControl(cc) .build(); } }