package mil.nga.giat.geowave.types.stanag4676.service.rest; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.util.Arrays; import java.util.Calendar; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; import java.util.Set; import java.util.TimeZone; import java.util.TreeMap; import javax.imageio.ImageIO; import javax.servlet.ServletContext; import javax.ws.rs.DefaultValue; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; import mil.nga.giat.geowave.core.index.ByteArrayId; import mil.nga.giat.geowave.core.index.ByteArrayUtils; import mil.nga.giat.geowave.core.store.CloseableIterator; import mil.nga.giat.geowave.core.store.DataStore; import mil.nga.giat.geowave.core.store.GeoWaveStoreFinder; import mil.nga.giat.geowave.core.store.query.PrefixIdQuery; import mil.nga.giat.geowave.core.store.query.QueryOptions; import mil.nga.giat.geowave.format.stanag4676.Stanag4676IngestPlugin; import mil.nga.giat.geowave.format.stanag4676.image.ImageChip; import mil.nga.giat.geowave.format.stanag4676.image.ImageChipDataAdapter; import mil.nga.giat.geowave.format.stanag4676.image.ImageChipUtils; import mil.nga.giat.geowave.service.ServiceUtils; import org.apache.commons.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.jcodec.codecs.vpx.NopRateControl; import org.jcodec.codecs.vpx.RateControl; import org.jcodec.codecs.vpx.VP8Encoder; import org.jcodec.common.FileChannelWrapper; import org.jcodec.common.NIOUtils; import org.jcodec.common.model.ColorSpace; import org.jcodec.common.model.Picture; import org.jcodec.common.model.Size; import org.jcodec.containers.mkv.muxer.MKVMuxer; import org.jcodec.containers.mkv.muxer.MKVMuxerTrack; import org.jcodec.scale.AWTUtil; import org.jcodec.scale.RgbToYuv420p; import com.google.common.io.Files; @Path("stanag4676") public class Stanag4676ImageryChipService { private static Logger LOGGER = LoggerFactory.getLogger(Stanag4676ImageryChipService.class); @Context ServletContext context; private static DataStore dataStore; // private Map<String, Object> configOptions; @GET @Path("image/{mission}/{track}/{year}-{month}-{day}T{hour}:{minute}:{second}.{millis}.jpg") @Produces("image/jpeg") public Response getImage( final @PathParam("mission") String mission, final @PathParam("track") String track, @PathParam("year") final int year, @PathParam("month") final int month, @PathParam("day") final int day, @PathParam("hour") final int hour, @PathParam("minute") final int minute, @PathParam("second") final int second, @PathParam("millis") final int millis, @QueryParam("size") @DefaultValue("-1") final int targetPixelSize ) { final Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT")); cal.set( year, month - 1, day, hour, minute, second); cal.set( Calendar.MILLISECOND, millis); final DataStore dataStore = getSingletonInstance(); if (null == dataStore) { return Response.serverError().entity( "Error accessing datastore!!").build(); } String chipNameStr = "mission = '" + mission + "', track = '" + track + "'"; Object imageChip = null; // ImageChipUtils.getDataId(mission,track,cal.getTimeInMillis()).getBytes() try (CloseableIterator<Object> imageChipIt = dataStore.query( new QueryOptions( ImageChipDataAdapter.ADAPTER_ID, Stanag4676IngestPlugin.IMAGE_CHIP_INDEX.getId()), new PrefixIdQuery( new ByteArrayId( ByteArrayUtils.combineArrays( ImageChipDataAdapter.ADAPTER_ID.getBytes(), ImageChipUtils.getDataId( mission, track, cal.getTimeInMillis()).getBytes()))))) { imageChip = (imageChipIt.hasNext()) ? imageChipIt.next() : null; } catch (IOException e1) { LOGGER.error( "Unablable to find image chip for " + chipNameStr + " at " + cal, e1); return Response.serverError().entity( "Error generating JPEG from image chip").build(); } if ((imageChip != null) && (imageChip instanceof ImageChip)) { if (targetPixelSize <= 0) { LOGGER.info("Sending ImageChip for " + chipNameStr); final byte[] imageData = ((ImageChip) imageChip).getImageBinary(); return Response.ok().entity( imageData).type( "image/jpeg").build(); } else { LOGGER.info("Sending BufferedImage for " + chipNameStr); final BufferedImage image = ((ImageChip) imageChip).getImage(targetPixelSize); final ByteArrayOutputStream baos = new ByteArrayOutputStream(); try { ImageIO.write( image, "jpeg", baos); return Response.ok().entity( baos.toByteArray()).type( "image/jpeg").build(); } catch (final IOException e) { LOGGER.error( "Unable to write image chip content to JPEG", e); return Response.serverError().entity( "Error generating JPEG from image chip for mission/track.").build(); } } } return Response.serverError().entity( "Cannot find image chip with mission/track/time.").build(); } // ------------------------------------------------------------------------------ // ------------------------------------------------------------------------------ @GET @Path("video/{mission}/{track}.webm") @Produces("video/webm") public Response getVideo( final @PathParam("mission") String mission, final @PathParam("track") String track, @QueryParam("size") @DefaultValue("-1") final int targetPixelSize, @QueryParam("speed") @DefaultValue("1") final double speed, @QueryParam("source") @DefaultValue("0") final double source ) { String videoNameStr = "mission = '" + mission + "', track = '" + track + "'" + "', source = '" + source + "'"; final DataStore dataStore = getSingletonInstance(); final TreeMap<Long, BufferedImage> imageChips = new TreeMap<Long, BufferedImage>(); int width = -1; int height = -1; try (CloseableIterator<Object> imageChipIt = dataStore.query( new QueryOptions( ImageChipDataAdapter.ADAPTER_ID, Stanag4676IngestPlugin.IMAGE_CHIP_INDEX.getId()), new PrefixIdQuery( new ByteArrayId( ByteArrayUtils.combineArrays( ImageChipDataAdapter.ADAPTER_ID.getBytes(), ImageChipUtils.getTrackDataIdPrefix( mission, track).getBytes()))))) { while (imageChipIt.hasNext()) { final Object imageChipObj = imageChipIt.next(); if ((imageChipObj != null) && (imageChipObj instanceof ImageChip)) { final ImageChip imageChip = (ImageChip) imageChipObj; final BufferedImage image = imageChip.getImage(targetPixelSize); if ((width < 0) || (image.getWidth() > width)) { width = image.getWidth(); } if ((height < 0) || (image.getHeight() > height)) { height = image.getHeight(); } imageChips.put( imageChip.getTimeMillis(), image); } } } catch (Exception e1) { LOGGER.error( "Unable to read data to compose video file", e1); return Response .serverError() .entity( "Video generation failed \nException: " + e1.getLocalizedMessage() + "\n stack trace: " + Arrays.toString(e1.getStackTrace())) .build(); } // ---------------------------------------------------- if (imageChips.isEmpty()) { return Response.serverError().entity( "Unable to retrieve image chips").build(); } else { LOGGER.info("Sending Video for " + videoNameStr); try { final File responseBody; LOGGER.debug("Attempting to build the video the new way ..."); responseBody = buildVideo2( mission, track, imageChips, width, height, speed); LOGGER.debug("Got a response body (path): " + responseBody.getAbsolutePath()); try (FileInputStream fis = new FileInputStream( responseBody) { @Override public void close() throws IOException { // super.close(); // try to delete the file immediately after it is // returned if (!responseBody.delete()) { LOGGER.warn("Cannot delete response body"); } if (!responseBody.getParentFile().delete()) { LOGGER.warn("Cannot delete response body's parent file"); } } }) { LOGGER.info("Returning video object: " + fis); return Response.ok().entity( fis).type( "video/webm").build(); } catch (final FileNotFoundException fnfe) { LOGGER.error( "Unable to find video file", fnfe); return Response.serverError().entity( "Video generation failed.").build(); } catch (final IOException e) { LOGGER.error( "Unable to write video file", e); return Response.serverError().entity( "Video generation failed.").build(); } } catch (final IOException e) { LOGGER.error( "Unable to write video file", e); return Response.serverError().entity( "Video generation failed.").build(); } } } // ------------------------------------------------------------------------------ // ------------------------------------------------------------------------------ // private static File buildVideo( // final String mission, // final String track, // final TreeMap<Long, BufferedImage> data, // final int width, // final int height, // final double timeScaleFactor ) // throws IOException { // final File videoFileDir = Files.createTempDir(); // LOGGER.info("Write to tempfile: " + videoFileDir.getAbsolutePath()); // videoFileDir.deleteOnExit(); // final File videoFile = new File( // videoFileDir, // mission + "_" + track + ".webm"); // videoFile.deleteOnExit(); // final IMediaWriter writer = // ToolFactory.makeWriter(videoFile.getAbsolutePath()); // writer.addVideoStream( // 0, // 0, // ICodec.ID.CODEC_ID_VP8, // width, // height); // final Long startTime = data.firstKey(); // // final double timeNormalizationFactor = 1.0 / timeScaleFactor; // // int i = 0; // int y = 0; // for (final Entry<Long, BufferedImage> e : data.entrySet()) { // if ((e.getValue().getWidth() == width) && (e.getValue().getHeight() == // height)) { // writer.encodeVideo( // 0, // e.getValue(), // (long) ((e.getKey() - startTime) * timeNormalizationFactor), // TimeUnit.MILLISECONDS); // ++y; // } // ++i; // } // writer.close(); // LOGGER.error("Found " + y + " of " + i + " old fashioned frames"); // // return videoFile; // } // ------------------------------------------------------------------------------ // ------------------------------------------------------------------------------ private static final int MAX_FRAMES = 2000; private static File buildVideo2( final String mission, final String track, final TreeMap<Long, BufferedImage> data, final int width, final int height, final double timeScaleFactor ) throws IOException { final File videoFileDir = Files.createTempDir(); final File videoFile = new File( videoFileDir, mission + "_" + track + ".webm"); FileChannelWrapper sink = null; try { sink = NIOUtils.writableFileChannel(videoFile.getAbsolutePath()); /* * Version 0.1.9 */ RateControl rc = new NopRateControl( 10); // (int) timeScaleFactor); VP8Encoder encoder = new VP8Encoder( rc); // qp RgbToYuv420p transform = new RgbToYuv420p( 0, 0); MKVMuxer muxer = new MKVMuxer(); MKVMuxerTrack videoTrack = null; int i = 0; int y = 0; for (final Entry<Long, BufferedImage> e : data.entrySet()) { BufferedImage rgb = e.getValue(); if (videoTrack == null) { videoTrack = muxer.createVideoTrack( new Size( rgb.getWidth(), rgb.getHeight()), "V_VP8"); } Picture yuv = Picture.create( rgb.getWidth(), rgb.getHeight(), ColorSpace.YUV420); transform.transform( AWTUtil.fromBufferedImage(rgb), yuv); ByteBuffer buf = ByteBuffer.allocate(rgb.getWidth() * rgb.getHeight() * 3); ByteBuffer ff = encoder.encodeFrame( yuv, buf); // Frame number must be from 1 to ... videoTrack.addSampleEntry( ff, (int) (i * timeScaleFactor) + 1); ++y; if ((++i) > MAX_FRAMES) { break; } } if (i == 1) { LOGGER.error("Image sequence not found"); return null; } if (videoTrack != null) { LOGGER.debug("Found " + y + " of " + i + " new frames." + " videoTrack timescale is " + videoTrack.getTimescale()); } muxer.mux(sink); // ------------------------------------------------------------------ // Version 0.2.0 // ------------------------------------------------------------------ // VP8Encoder encoder = VP8Encoder.createVP8Encoder((int) // timeScaleFactor); // qp // RgbToYuv420p8Bit transform = new RgbToYuv420p8Bit(); // final Long startTime = data.firstKey(); // final double timeNormalizationFactor = 1.0 / timeScaleFactor; // // MKVMuxer muxer = new MKVMuxer(); // MKVMuxerTrack videoTrack = null; // // /* // * writer.encodeVideo( 0, frame_data, (long) ((e.getKey() - // * startTime) * timeNormalizationFactor), TimeUnit.MILLISECONDS); // */ // // int i = 0; // for (final Entry<Long, BufferedImage> e : data.entrySet()) { // BufferedImage rgb = e.getValue(); // if (videoTrack == null) { // videoTrack = // muxer.createVideoTrack( // new Size(rgb.getWidth(), rgb.getHeight()), "V_VP8"); } // Picture8Bit yuv = // Picture8Bit.create(rgb.getWidth(), rgb.getHeight(), // ColorSpace.YUV420); // // transform.transform(AWTUtil.fromBufferedImageRGB8Bit(rgb), yuv); // ByteBuffer buf = ByteBuffer.allocate(rgb.getWidth() * // rgb.getHeight() * 3); // ByteBuffer ff = encoder.encodeFrame8Bit(yuv, buf); // // videoTrack.addSampleEntry(ff, i - 1); // if ((++i) > MAX_FRAMES) { // break; // } // } // if (i == 1) { // System.out.println("Image sequence not found"); return null; // } // muxer.mux(sink); // ------------------------------------------------------------------ } finally { if (sink != null) { sink.close(); IOUtils.closeQuietly(sink); } } return videoFile; } // ------------------------------------------------------------------------------ // ------------------------------------------------------------------------------ private synchronized DataStore getSingletonInstance() { if (dataStore != null) { return dataStore; } String confPropFilename = context.getInitParameter("config.properties"); // HP Fortify "Log Forging" false positive // What Fortify considers "user input" comes only // from users with OS-level access anyway LOGGER.info("Creating datastore singleton for 4676 service. conf prop filename: " + confPropFilename); Properties props = null; try (InputStream is = context.getResourceAsStream(confPropFilename)) { props = ServiceUtils.loadProperties(is); } catch (IOException e) { LOGGER.error( e.getLocalizedMessage(), e); } LOGGER.info( "Found {} props", (props != null ? props.size() : 0)); if (props != null) { final Map<String, String> strMap = new HashMap<String, String>(); final Set<Object> keySet = props.keySet(); final Iterator<Object> it = keySet.iterator(); while (it.hasNext()) { final String key = it.next().toString(); final String value = ServiceUtils.getProperty( props, key); strMap.put( key, value); // HP Fortify "Log Forging" false positive // What Fortify considers "user input" comes only // from users with OS-level access anyway LOGGER.info(" Key/Value: " + key + "/" + value); } // Can be removed when factory is fixed GeoWaveStoreFinder.getRegisteredStoreFactoryFamilies(); dataStore = GeoWaveStoreFinder.createDataStore(strMap); } if (dataStore == null) { LOGGER.error("Unable to create datastore for 4676 service"); } return dataStore; } }