Skip to content
Snippets Groups Projects
CellHandler.java 15.1 KiB
Newer Older
  • Learn to ignore specific revisions
  • import java.awt.image.BufferedImage;
    
    Vojtech Moravec's avatar
    Vojtech Moravec committed
    import java.io.*;
    
    Vojtech Moravec's avatar
    Vojtech Moravec committed
    import java.nio.ByteBuffer;
    
    import java.nio.file.Files;
    import java.nio.file.Path;
    import java.nio.file.Paths;
    
    import javax.imageio.ImageIO;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import compression.U16;
    
    Vojtech Moravec's avatar
    Vojtech Moravec committed
    import compression.data.Chunk3D;
    
    Vojtech Moravec's avatar
    Vojtech Moravec committed
    import compression.data.ChunkIO;
    
    Vojtech Moravec's avatar
    Vojtech Moravec committed
    import compression.data.V3i;
    import compression.data.V3l;
    
    import compression.quantization.scalar.ScalarQuantizer;
    
    import compression.utilities.Utils;
    
    HongKee Moon's avatar
    HongKee Moon committed
    import org.eclipse.jetty.server.Request;
    import org.eclipse.jetty.server.handler.ContextHandler;
    
    import org.eclipse.jetty.util.log.Log;
    
    HongKee Moon's avatar
    HongKee Moon committed
    import org.jdom2.Document;
    
    import org.jdom2.JDOMException;
    import org.jdom2.input.SAXBuilder;
    
    HongKee Moon's avatar
    HongKee Moon committed
    import org.jdom2.output.Format;
    import org.jdom2.output.XMLOutputter;
    
    
    import bdv.BigDataViewer;
    
    import bdv.cache.CacheHints;
    import bdv.cache.LoadingStrategy;
    
    import bdv.img.cache.VolatileCell;
    import bdv.img.cache.VolatileGlobalCellCache;
    
    import bdv.img.cache.VolatileGlobalCellCache.Key;
    import bdv.img.cache.VolatileGlobalCellCache.VolatileCellLoader;
    
    import bdv.img.hdf5.Hdf5ImageLoader;
    import bdv.img.hdf5.Hdf5VolatileShortArrayLoader;
    import bdv.img.remote.AffineTransform3DJsonSerializer;
    import bdv.img.remote.RemoteImageLoader;
    import bdv.img.remote.RemoteImageLoaderMetaData;
    import bdv.spimdata.SequenceDescriptionMinimal;
    import bdv.spimdata.SpimDataMinimal;
    import bdv.spimdata.XmlIoSpimDataMinimal;
    import bdv.util.ThumbnailGenerator;
    
    import mpicbg.spim.data.SpimDataException;
    import net.imglib2.img.basictypeaccess.volatiles.array.VolatileShortArray;
    import net.imglib2.realtransform.AffineTransform3D;
    
    public class CellHandler extends ContextHandler {
        private long transferedDataSize = 0;
    
        private static final org.eclipse.jetty.util.log.Logger LOG = Log.getLogger(CellHandler.class);
    
        private int counter = 0;
        private final VolatileGlobalCellCache cache;
    
        private final Hdf5VolatileShortArrayLoader loader;
    
        private final CacheHints cacheHints;
    
        /**
         * Full path of the dataset xml file this {@link CellHandler} is serving.
         */
        private final String xmlFilename;
    
        /**
         * Full path of the dataset xml file this {@link CellHandler} is serving,
         * without the ".xml" suffix.
         */
        private final String baseFilename;
    
        private final String dataSetURL;
    
        /**
         * Cached dataset XML to be send to and opened by {@link BigDataViewer}
         * clients.
         */
        private final String datasetXmlString;
    
        /**
         * Cached JSON representation of the {@link RemoteImageLoaderMetaData} to be
         * send to clients.
         */
        private final String metadataJson;
    
        /**
         * Cached dataset.settings XML to be send to clients. May be null if no
         * settings file exists for the dataset.
         */
        private final String settingsXmlString;
    
        /**
         * Full path to thumbnail png.
         */
        private final String thumbnailFilename;
        final CustomCompressionParameters compressionParams;
    
        private ScalarQuantizer quantizer;
    
    
        public CellHandler(final String baseUrl, final String xmlFilename, final String datasetName, final String thumbnailsDirectory,
                           final CustomCompressionParameters compressionParams,
    
                           final ScalarQuantizer quantizer) throws SpimDataException, IOException {
    
    
            final XmlIoSpimDataMinimal io = new XmlIoSpimDataMinimal();
            final SpimDataMinimal spimData = io.load(xmlFilename);
            final SequenceDescriptionMinimal seq = spimData.getSequenceDescription();
            final Hdf5ImageLoader imgLoader = (Hdf5ImageLoader) seq.getImgLoader();
            this.quantizer = quantizer;
            this.compressionParams = compressionParams;
    
            cache = imgLoader.getCacheControl();
            loader = imgLoader.getShortArrayLoader();
            cacheHints = new CacheHints(LoadingStrategy.BLOCKING, 0, false);
    
            // dataSetURL property is used for providing the XML file by replace
            // SequenceDescription>ImageLoader>baseUrl
            this.xmlFilename = xmlFilename;
            baseFilename = xmlFilename.endsWith(".xml") ? xmlFilename.substring(0, xmlFilename.length() - ".xml".length()) : xmlFilename;
            dataSetURL = baseUrl;
    
            datasetXmlString = buildRemoteDatasetXML(io, spimData, baseUrl);
            metadataJson = buildMetadataJsonString(imgLoader, seq);
            settingsXmlString = buildSettingsXML(baseFilename);
            thumbnailFilename = createThumbnail(spimData, baseFilename, datasetName, thumbnailsDirectory);
        }
    
        @Override
        public void doHandle(final String target, final Request baseRequest, final HttpServletRequest request, final HttpServletResponse response) throws IOException {
            if (target.equals("/settings")) {
                if (settingsXmlString != null)
                    respondWithString(baseRequest, response, "application/xml", settingsXmlString);
                return;
            }
    
            if (target.equals("/png")) {
                provideThumbnail(baseRequest, response);
                return;
            }
    
            final String cellString = request.getParameter("p");
    
            if (cellString == null) {
                respondWithString(baseRequest, response, "application/xml", datasetXmlString);
                return;
            }
    
            final String[] parts = cellString.split("/");
            if (parts[0].equals("cell")) {
                final int index = Integer.parseInt(parts[1]);
                final int timepoint = Integer.parseInt(parts[2]);
                final int setup = Integer.parseInt(parts[3]);
                final int level = Integer.parseInt(parts[4]);
                final Key key = new VolatileGlobalCellCache.Key(timepoint, setup, level, index);
                VolatileCell<?> cell = cache.getLoadingVolatileCache().getIfPresent(key, cacheHints);
    
    Vojtech Moravec's avatar
    Vojtech Moravec committed
    
                final int[] cellDims = new int[]{
                        Integer.parseInt(parts[5]),
                        Integer.parseInt(parts[6]),
                        Integer.parseInt(parts[7])};
                final long[] cellMin = new long[]{
                        Long.parseLong(parts[8]),
                        Long.parseLong(parts[9]),
                        Long.parseLong(parts[10])};
    
                if (cell == null) {
                    cell = cache.getLoadingVolatileCache().get(key, cacheHints, new VolatileCellLoader<>(loader, timepoint, setup, level, cellDims, cellMin));
                }
    
                @SuppressWarnings("unchecked")
                short[] data = ((VolatileCell<VolatileShortArray>) cell).getData().getCurrentStorageArray();
    
    Vojtech Moravec's avatar
    Vojtech Moravec committed
    
    
    Vojtech Moravec's avatar
    Vojtech Moravec committed
    
    //            // Stupidity just testing chunking
    //            {
    //                Chunk3D dataBox = new Chunk3D(new V3i(cellDims[0], cellDims[1], cellDims[2]),
    //                        new V3l(cellMin[0], cellMin[1], cellMin[1]), data);
    //                Chunk3D[] chunks = dataBox.divideIntoChunks(new V3i(3));
    //                dataBox.zeroData();
    //                dataBox.reconstructFromChunks(chunks);
    //                data = dataBox.getDataAsShort();
    //            }
    
    Vojtech Moravec's avatar
    Vojtech Moravec committed
    
    
                if (compressionParams.shouldCompressData()) {
                    assert (quantizer != null) : "Compressor wasn't created";
                    data = quantizer.quantize(data);
                } else if (compressionParams.renderDifference()) {
    
    Vojtech Moravec's avatar
    Vojtech Moravec committed
                    final int diffThreshold = compressionParams.getDiffThreshold();
    
                    assert (quantizer != null) : "Compressor wasn't created";
                    short[] compressedData = quantizer.quantize(data);
    
    Vojtech Moravec's avatar
    Vojtech Moravec committed
                    int maxError = Integer.MIN_VALUE;
    
    
                    for (int i = 0; i < data.length; i++) {
    
                        final int diff = Math.abs(compressedData[i] - data[i]);
    
    Vojtech Moravec's avatar
    Vojtech Moravec committed
                        maxError = Math.max(maxError, diff);
                        // NOTE(Moravec): We know that we are not producing non-existing values.
                        // There was no data but now there is.
    //                    if (data[i] == 0 && compressedData[i] != 0) {
    //                        data[i] = (short) 0xffff;
    //                        ++nonExistingDataCount;
    //                    } else {
    //                        data[i] = (short) 0x0;
    //                    }
    
    //                    if (compressedData[i] > data[i]) {
    //                        data[i] = compressedData[i];
    //                    } else {
    //                        data[i] = 0;
    //                    }
    
    //                    if (diff > diffThreshold) {
    //                        data[i] = (short) diff;
    //                    } else {
    //                        data[i] = 0;
    //                    }
    
    
                        // NOTE(Moravec): Squared error
                        final short squaredError = (short) Math.floor(Math.pow(compressedData[i] - data[i], 2));
                        data[i] = squaredError;
                    }
                    if (maxError > 0) {
                        System.out.println("Max error: " + maxError);
    
                    }
                }
    
                final byte[] buf = new byte[2 * data.length];
                for (int i = 0, j = 0; i < data.length; i++) {
                    final short s = data[i];
                    buf[j++] = (byte) ((s >> 8) & 0xff);
                    buf[j++] = (byte) (s & 0xff);
                }
    
                if (compressionParams.shouldDumpRequestData()) {
    
    Vojtech Moravec's avatar
    Vojtech Moravec committed
                    // Normal data dump
                    /*
    
                    FileOutputStream dumpStream = new FileOutputStream(compressionParams.getDumpFile(), true);
                    dumpStream.write(buf);
                    dumpStream.flush();
                    dumpStream.close();
    
    Vojtech Moravec's avatar
    Vojtech Moravec committed
                    */
                    // Dumping HDF5 3D chunks
                    ChunkIO.saveChunks(cellDims, cellMin, buf, compressionParams.getDumpFile());
    
                }
    
                transferedDataSize += buf.length;
    
    
    //            LOG.info(String.format("I:%d;T:%d;S:%d;L:%d  Total transfered data: [%d KB] [%d MB]",
    //                    index, timepoint, setup, level,
    //                    (transferedDataSize / 1000), ((transferedDataSize / 1000) / 1000)));
    
    
                response.setContentType("application/octet-stream");
                response.setContentLength(buf.length);
                response.setStatus(HttpServletResponse.SC_OK);
                baseRequest.setHandled(true);
                final OutputStream os = response.getOutputStream();
                os.write(buf);
                os.close();
    
            } else if (parts[0].
    
                    equals("init")) {
    
                respondWithString(baseRequest, response, "application/json", metadataJson);
            }
    
        }
    
        private void provideThumbnail(final Request baseRequest, final HttpServletResponse response) throws IOException {
            final Path path = Paths.get(thumbnailFilename);
            if (Files.exists(path)) {
                final byte[] imageData = Files.readAllBytes(path);
                if (imageData != null) {
                    response.setContentType("image/png");
                    response.setContentLength(imageData.length);
                    response.setStatus(HttpServletResponse.SC_OK);
                    baseRequest.setHandled(true);
    
                    final OutputStream os = response.getOutputStream();
                    os.write(imageData);
                    os.close();
                }
            }
        }
    
        public String getXmlFile() {
            return xmlFilename;
        }
    
        public String getDataSetURL() {
            return dataSetURL;
        }
    
        public String getThumbnailUrl() {
            return dataSetURL + "png";
        }
    
        public String getDescription() {
            throw new UnsupportedOperationException();
        }
    
        /**
         * Create a JSON representation of the {@link RemoteImageLoaderMetaData}
         * (image sizes and resolutions) provided by the given
         * {@link Hdf5ImageLoader}.
         */
        private static String buildMetadataJsonString(final Hdf5ImageLoader imgLoader, final SequenceDescriptionMinimal seq) {
            final RemoteImageLoaderMetaData metadata = new RemoteImageLoaderMetaData(imgLoader, seq);
            final GsonBuilder gsonBuilder = new GsonBuilder();
            gsonBuilder.registerTypeAdapter(AffineTransform3D.class, new AffineTransform3DJsonSerializer());
            gsonBuilder.enableComplexMapKeySerialization();
            return gsonBuilder.create().toJson(metadata);
        }
    
        /**
         * Create a modified dataset XML by replacing the ImageLoader with an
         * {@link RemoteImageLoader} pointing to the data we are serving.
         */
        private static String buildRemoteDatasetXML(final XmlIoSpimDataMinimal io, final SpimDataMinimal spimData, final String baseUrl) throws IOException, SpimDataException {
            final SpimDataMinimal s = new SpimDataMinimal(spimData, new RemoteImageLoader(baseUrl, false));
            final Document doc = new Document(io.toXml(s, s.getBasePath()));
            final XMLOutputter xout = new XMLOutputter(Format.getPrettyFormat());
            final StringWriter sw = new StringWriter();
            xout.output(doc, sw);
            return sw.toString();
        }
    
        /**
         * Read {@code baseFilename.settings.xml} into a string if it exists.
         *
         * @return contents of {@code baseFilename.settings.xml} or {@code null} if
         * that file couldn't be read.
         */
        private static String buildSettingsXML(final String baseFilename) {
            final String settings = baseFilename + ".settings.xml";
            if (new File(settings).exists()) {
                try {
                    final SAXBuilder sax = new SAXBuilder();
                    final Document doc = sax.build(settings);
                    final XMLOutputter xout = new XMLOutputter(Format.getPrettyFormat());
                    final StringWriter sw = new StringWriter();
                    xout.output(doc, sw);
                    return sw.toString();
                } catch (JDOMException | IOException e) {
                    LOG.warn("Could not read settings file \"" + settings + "\"");
                    LOG.warn(e.getMessage());
                }
            }
            return null;
        }
    
        /**
         * Create PNG thumbnail file named "{@code <baseFilename>.png}".
         */
        private static String createThumbnail(final SpimDataMinimal spimData, final String baseFilename, final String datasetName, final String thumbnailsDirectory) {
            final String thumbnailFileName = thumbnailsDirectory + "/" + datasetName + ".png";
            final File thumbnailFile = new File(thumbnailFileName);
            if (!thumbnailFile.isFile()) // do not recreate thumbnail if it already exists
            {
                final BufferedImage bi = ThumbnailGenerator.makeThumbnail(spimData, baseFilename, Constants.THUMBNAIL_WIDTH, Constants.THUMBNAIL_HEIGHT);
                try {
                    ImageIO.write(bi, "png", thumbnailFile);
                } catch (final IOException e) {
                    LOG.warn("Could not create thumbnail png for dataset \"" + baseFilename + "\"");
                    LOG.warn(e.getMessage());
                }
            }
            return thumbnailFileName;
        }
    
        /**
         * Handle request by sending a UTF-8 string.
         */
        private static void respondWithString(final Request baseRequest, final HttpServletResponse response, final String contentType, final String string) throws IOException {
            response.setContentType(contentType);
            response.setCharacterEncoding("UTF-8");
            response.setStatus(HttpServletResponse.SC_OK);
            baseRequest.setHandled(true);
    
            final PrintWriter ow = response.getWriter();
            ow.write(string);
            ow.close();
        }
    
    Tobias Pietzsch's avatar
    Tobias Pietzsch committed
    }