org.geomajas.plugin.rasterizing.layer.RasterDirectLayer.java Source code

Java tutorial

Introduction

Here is the source code for org.geomajas.plugin.rasterizing.layer.RasterDirectLayer.java

Source

/*
 * This is part of Geomajas, a GIS framework, http://www.geomajas.org/.
 *
 * Copyright 2008-2013 Geosparc nv, http://www.geosparc.com/, Belgium.
 *
 * The program is available in open source according to the GNU Affero
 * General Public License. All contributions in this program are covered
 * by the Geomajas Contributors License Agreement. For full licensing
 * details, see LICENSE.txt in the project root.
 */
package org.geomajas.plugin.rasterizing.layer;

import java.awt.AlphaComposite;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.awt.image.ColorConvertOp;
import java.awt.image.RenderedImage;
import java.awt.image.WritableRaster;
import java.awt.image.renderable.ParameterBlock;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

import javax.imageio.ImageIO;
import javax.media.jai.ImageLayout;
import javax.media.jai.InterpolationNearest;
import javax.media.jai.JAI;
import javax.media.jai.PlanarImage;
import javax.media.jai.RenderedOp;
import javax.media.jai.operator.MosaicDescriptor;
import javax.media.jai.operator.TranslateDescriptor;

import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.geomajas.geometry.Bbox;
import org.geomajas.layer.tile.RasterTile;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.map.DirectLayer;
import org.geotools.map.MapContent;
import org.geotools.map.MapViewport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.sun.media.jai.codec.ByteArraySeekableStream;
import com.vividsolutions.jts.geom.Envelope;

/**
 * Layer responsible for rendering raster layers. Most of the code is copied from the printing plugin.
 * 
 * @author Jan De Moerloose
 */
public class RasterDirectLayer extends DirectLayer {

    protected static final int DOWNLOAD_MAX_ATTEMPTS = 2;

    protected static final int DOWNLOAD_MAX_THREADS = 5;

    protected static final long DOWNLOAD_TIMEOUT = 120000; // milliseconds

    protected static final long DOWNLOAD_TIMEOUT_ONE_TILE = 100; // milliseconds

    protected static final int RETRY_WAIT = 100; // milliseconds

    private static final String BUNDLE_NAME = "org/geomajas/plugin/rasterizing/rasterizing"; //$NON-NLS-1$

    private static final String MISSING_TILE_IN_MOSAIC = "missing tile in mosaic ";

    private static final String OPACITY = "opacity:";

    private static final int DEFAULT_IMAGE_BUFFER_SIZE = 1024;

    private final List<RasterTile> tiles;

    private final ResourceBundle resourceBundle = ResourceBundle.getBundle(BUNDLE_NAME);

    private final HttpClient httpClient;

    private final Logger log = LoggerFactory.getLogger(RasterDirectLayer.class);

    private final int tileWidth;

    private final int tileHeight;

    private final String style;

    private double tileScale = -1;

    public RasterDirectLayer(List<RasterTile> tiles, int tileWidth, int tileHeight, double tileScale,
            String style) {
        super();
        this.tileScale = tileScale;
        this.tiles = tiles;
        if (tileWidth < 1) {
            tileWidth = 1;
        }
        this.tileWidth = tileWidth;
        if (tileHeight < 1) {
            tileHeight = 1;
        }
        this.tileHeight = tileHeight;
        this.style = style;
        ThreadSafeClientConnManager manager = new ThreadSafeClientConnManager();
        manager.setDefaultMaxPerRoute(10);
        httpClient = new DefaultHttpClient(manager);
    }

    public RasterDirectLayer(List<RasterTile> tiles, int tileWidth, int tileHeight, String style) {
        this(tiles, tileWidth, tileHeight, -1.0, style);
    }

    @Override
    public void draw(Graphics2D graphics, MapContent map, MapViewport viewport) {
        try {
            if (tiles.size() > 0) {
                Collection<Callable<ImageResult>> callables = new ArrayList<Callable<ImageResult>>(tiles.size());
                // Build the image downloading threads
                for (RasterTile tile : tiles) {
                    RasterImageDownloadCallable downloadThread = new RasterImageDownloadCallable(
                            DOWNLOAD_MAX_ATTEMPTS, tile);
                    callables.add(downloadThread);
                }
                // Loop until all images are downloaded or timeout is reached
                long totalTimeout = DOWNLOAD_TIMEOUT + DOWNLOAD_TIMEOUT_ONE_TILE * tiles.size();
                log.debug("=== total timeout (millis): {}", totalTimeout);
                ExecutorService service = Executors.newFixedThreadPool(DOWNLOAD_MAX_THREADS);
                List<Future<ImageResult>> futures = service.invokeAll(callables, totalTimeout,
                        TimeUnit.MILLISECONDS);
                // determine the pixel bounds of the mosaic
                Bbox pixelBounds = getPixelBounds(tiles);
                // create the images for the mosaic
                List<RenderedImage> images = new ArrayList<RenderedImage>();
                for (Future<ImageResult> future : futures) {
                    ImageResult result = null;
                    if (future.isDone()) {
                        try {
                            result = future.get();
                            // create a rendered image
                            if (result.getImage() != null && result.getImage().length > 0) {
                                RenderedImage image = JAI.create("stream",
                                        new ByteArraySeekableStream(result.getImage()));
                                // convert to common direct color model (some images have their own indexed color model)
                                RenderedImage colored = toDirectColorModel(image);

                                // translate to the correct position in the tile grid
                                double xOffset = result.getRasterImage().getCode().getX() * tileWidth
                                        - pixelBounds.getX();
                                double yOffset;
                                // TODO: in some cases, the y-index is up (e.g. WMS), should be down for
                                // all layers !!!!
                                if (isYIndexUp(tiles)) {
                                    yOffset = result.getRasterImage().getCode().getY() * tileHeight
                                            - pixelBounds.getY();
                                } else {
                                    yOffset = (pixelBounds.getMaxY()
                                            - (result.getRasterImage().getCode().getY() + 1) * tileHeight);
                                }
                                log.debug("adding to(" + xOffset + "," + yOffset + "), url = "
                                        + result.getRasterImage().getUrl());
                                RenderedImage translated = TranslateDescriptor.create(colored, (float) xOffset,
                                        (float) yOffset, new InterpolationNearest(), null);
                                images.add(translated);
                            }
                        } catch (ExecutionException e) {
                            addLoadError(graphics, (ImageException) (e.getCause()), viewport);
                            log.warn(MISSING_TILE_IN_MOSAIC + e.getMessage());
                        } catch (Exception e) {
                            log.warn("Missing tile " + result.getRasterImage().getUrl());
                            log.warn(MISSING_TILE_IN_MOSAIC + e.getMessage());
                        }
                    }
                }

                if (images.size() > 0) {
                    ImageLayout imageLayout = new ImageLayout(0, 0, (int) pixelBounds.getWidth(),
                            (int) pixelBounds.getHeight());
                    imageLayout.setTileWidth(tileWidth);
                    imageLayout.setTileHeight(tileHeight);

                    // create the mosaic image
                    ParameterBlock pbMosaic = new ParameterBlock();
                    pbMosaic.add(MosaicDescriptor.MOSAIC_TYPE_OVERLAY);
                    for (RenderedImage renderedImage : images) {
                        pbMosaic.addSource(renderedImage);
                    }
                    RenderedOp mosaic = JAI.create("mosaic", pbMosaic,
                            new RenderingHints(JAI.KEY_IMAGE_LAYOUT, imageLayout));
                    try {
                        ByteArrayOutputStream baos = new ByteArrayOutputStream();
                        log.debug("rendering to buffer...");
                        ImageIO.write(mosaic, "png", baos);
                        log.debug("rendering done, size = " + baos.toByteArray().length);
                        RasterTile mosaicTile = new RasterTile();
                        mosaicTile.setBounds(getWorldBounds(tiles));
                        log.info("application bounds = " + mosaicTile.getBounds());
                        ImageResult mosaicResult = new ImageResult(mosaicTile);
                        mosaicResult.setImage(baos.toByteArray());
                        addImage(graphics, mosaicResult, viewport);
                    } catch (IOException e) {
                        log.warn("could not write mosaic image " + e.getMessage());
                    }
                }
            }
        } catch (InterruptedException e) {
            log.warn("rendering {} to {} failed : ", getTitle(), viewport.getBounds());
        }
    }

    protected void addImage(Graphics2D graphics, ImageResult imageResult, MapViewport viewport) throws IOException {
        Rectangle screenArea = viewport.getScreenArea();
        ReferencedEnvelope worldBounds = viewport.getBounds();
        // convert map bounds to application bounds
        double printScale = screenArea.getWidth() / worldBounds.getWidth();
        if (tileScale < 0) {
            tileScale = printScale;
        }
        Envelope applicationBounds = new Envelope((worldBounds.getMinX()) * printScale,
                (worldBounds.getMaxX()) * printScale, -(worldBounds.getMinY()) * printScale,
                -(worldBounds.getMaxY()) * printScale);
        Bbox imageBounds = imageResult.getRasterImage().getBounds();
        // find transform between image bounds and application bounds
        double tx = (imageBounds.getX() * printScale / tileScale - applicationBounds.getMinX());
        double ty = (imageBounds.getY() * printScale / tileScale - applicationBounds.getMinY());
        BufferedImage image = ImageIO.read(new ByteArrayInputStream(imageResult.getImage()));
        double scaleX = imageBounds.getWidth() / image.getWidth() * printScale / tileScale;
        double scaleY = imageBounds.getHeight() / image.getHeight() * printScale / tileScale;
        AffineTransform transform = new AffineTransform();
        transform.translate(tx, ty);
        transform.scale(scaleX, scaleY);
        if (log.isDebugEnabled()) {
            log.debug("adding image, width=" + image.getWidth() + ",height=" + image.getHeight() + ",x=" + tx
                    + ",y=" + ty);
        }
        // opacity
        log.debug("before drawImage");
        // create a copy to apply transform
        Graphics2D g = (Graphics2D) graphics.create();
        // apply opacity to image off-graphics to avoid interference with whatever opacity model is used by graphics
        BufferedImage opaqueCopy = makeOpaque(image);
        g.drawImage(opaqueCopy, transform, null);
        log.debug("after drawImage");
    }

    private BufferedImage makeOpaque(BufferedImage image) {
        if (image.getType() == BufferedImage.TYPE_CUSTOM) {
            log.warn("makeOpaque {} Unknown Image Type 0: ", getTitle());
            return image;
        }
        BufferedImage opaqueCopy = new BufferedImage(image.getWidth(), image.getHeight(), image.getType());
        Graphics2D g1 = opaqueCopy.createGraphics();
        g1.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, getOpacity()));
        g1.drawImage(image, null, 0, 0);
        g1.dispose();
        return opaqueCopy;
    }

    private float getOpacity() {
        String match = style;
        // could be 'opacity:0.5;' or '0.5'
        if (style.contains(OPACITY)) {
            match = style.substring(style.indexOf(OPACITY) + OPACITY.length());
        }
        int semiColonPosition = match.indexOf(';');
        if (semiColonPosition >= 0) {
            match = match.substring(0, semiColonPosition);
        }
        try {
            return Float.valueOf(match);
        } catch (NumberFormatException nfe) {
            log.warn("Could not parse opacity " + style + "of raster layer " + getTitle());
            return 1f;
        }
    }

    protected void addLoadError(Graphics2D graphics, ImageException imageResult, MapViewport viewport) {
        Bbox imageBounds = imageResult.getRasterImage().getBounds();
        ReferencedEnvelope viewBounds = viewport.getBounds();
        double rasterScale = viewport.getScreenArea().getWidth() / viewport.getBounds().getWidth();
        double width = imageBounds.getWidth();
        double height = imageBounds.getHeight();
        // subtract screen position of lower-left corner
        double x = imageBounds.getX() - rasterScale * viewBounds.getMinX();
        // shift y to lower left corner, flip y to user space and subtract
        // screen position of lower-left
        // corner
        double y = -imageBounds.getY() - imageBounds.getHeight() - rasterScale * viewBounds.getMinY();
        if (log.isDebugEnabled()) {
            log.debug("adding image, width=" + width + ",height=" + height + ",x=" + x + ",y=" + y);
        }
        // opacity
        log.debug("before drawImage");
        graphics.drawString(getNlsString("loaderror.line1"), (int) x, (int) y);
    }

    @Override
    public ReferencedEnvelope getBounds() {
        return null;
    }

    private boolean isYIndexUp(List<RasterTile> tiles) {
        RasterTile first = tiles.iterator().next();
        for (RasterTile tile : tiles) {
            if (tile.getCode().getY() > first.getCode().getY()) {
                return tile.getBounds().getY() > first.getBounds().getY();
            } else if (tile.getCode().getY() < first.getCode().getY()) {
                return tile.getBounds().getY() < first.getBounds().getY();
            }
        }
        return false;
    }

    private Bbox getPixelBounds(List<RasterTile> tiles) {
        Bbox bounds = null;
        for (RasterTile tile : tiles) {
            Bbox tileBounds = new Bbox(tile.getCode().getX() * tileWidth, tile.getCode().getY() * tileHeight,
                    tileWidth, tileHeight);
            if (bounds == null) {
                bounds = new Bbox(tileBounds.getX(), tileBounds.getY(), tileBounds.getWidth(),
                        tileBounds.getHeight());

            } else {
                double minx = Math.min(tileBounds.getX(), bounds.getX());
                double maxx = Math.max(tileBounds.getMaxX(), bounds.getMaxX());
                double miny = Math.min(tileBounds.getY(), bounds.getY());
                double maxy = Math.max(tileBounds.getMaxY(), bounds.getMaxY());
                bounds = new Bbox(minx, miny, maxx - minx, maxy - miny);
            }
        }
        return bounds;
    }

    private Bbox getWorldBounds(List<RasterTile> tiles) {
        Bbox bounds = null;
        for (RasterTile tile : tiles) {
            Bbox tileBounds = new Bbox(tile.getBounds().getX(), tile.getBounds().getY(),
                    tile.getBounds().getWidth(), tile.getBounds().getHeight());
            if (bounds == null) {
                bounds = new Bbox(tileBounds.getX(), tileBounds.getY(), tileBounds.getWidth(),
                        tileBounds.getHeight());
            } else {
                double minx = Math.min(tileBounds.getX(), bounds.getX());
                double maxx = Math.max(tileBounds.getMaxX(), bounds.getMaxX());
                double miny = Math.min(tileBounds.getY(), bounds.getY());
                double maxy = Math.max(tileBounds.getMaxY(), bounds.getMaxY());
                bounds = new Bbox(minx, miny, maxx - minx, maxy - miny);
            }
        }
        return bounds;
    }

    public String getNlsString(String key) {
        try {
            return resourceBundle.getString(key);
        } catch (MissingResourceException e) {
            return '!' + key + '!';
        }
    }

    /**
     * Converts an image to a RGBA direct color model using a workaround via buffered image directly calling the
     * ColorConvert operation fails for unknown reasons ?!
     * 
     * @param img image to convert
     * @return converted image
     */
    public PlanarImage toDirectColorModel(RenderedImage img) {
        BufferedImage dest = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_4BYTE_ABGR);
        BufferedImage source = new BufferedImage(img.getColorModel(), (WritableRaster) img.getData(),
                img.getColorModel().isAlphaPremultiplied(), null);
        ColorConvertOp op = new ColorConvertOp(null);
        op.filter(source, dest);
        return PlanarImage.wrapRenderedImage(dest);
    }

    /**
     * Image result.
     * 
     * @author Jan De Moerloose
     */
    private static class ImageResult {

        private byte[] image;

        private final RasterTile rasterImage;

        public ImageResult(RasterTile rasterImage) {
            this.rasterImage = rasterImage;
        }

        public byte[] getImage() {
            return image;
        }

        public void setImage(byte[] image) {
            this.image = image;
        }

        public RasterTile getRasterImage() {
            return rasterImage;
        }
    }

    /**
     * Image Exception
     * 
     * @author Jan De Moerloose
     */
    private static class ImageException extends Exception {

        private static final long serialVersionUID = 100L;

        private final RasterTile rasterImage;

        public ImageException(RasterTile rasterImage, Throwable cause) {
            super(cause);
            this.rasterImage = rasterImage;
        }

        public RasterTile getRasterImage() {
            return rasterImage;
        }
    }

    /**
     * Download image with a couple of retries.
     * 
     * @author Jan De Moerloose
     */
    private class RasterImageDownloadCallable implements Callable<ImageResult> {

        private final ImageResult result;

        private final int retries;

        public RasterImageDownloadCallable(int retries, RasterTile rasterImage) {
            this.result = new ImageResult(rasterImage);
            this.retries = retries;
        }

        public ImageResult call() throws ImageException {
            log.debug("Fetching image: {}", result.getRasterImage().getUrl());
            int triesLeft = retries;
            while (true) {
                try {
                    HttpGet get = new HttpGet(result.getRasterImage().getUrl());
                    HttpResponse response = httpClient.execute(get);
                    ByteArrayOutputStream outputStream = new ByteArrayOutputStream(DEFAULT_IMAGE_BUFFER_SIZE);
                    response.getEntity().writeTo(outputStream);
                    result.setImage(outputStream.toByteArray());
                    return result;
                } catch (Exception e) { // NOSONAR
                    if (log.isDebugEnabled()) {
                        log.error("Fetching image: error loading " + result.getRasterImage().getUrl(), e);
                    }
                    triesLeft--;
                    if (triesLeft == 0) {
                        throw new ImageException(result.getRasterImage(), e);
                    } else {
                        log.debug("Fetching image: retrying ", result.getRasterImage().getUrl());
                        try {
                            Thread.sleep(RETRY_WAIT); // give server some time to recover
                        } catch (InterruptedException ie) {
                            // NOSONAR just ignore
                        }
                    }
                }
            }
        }

    }

}