Package org.geowebcache.layer

Source Code of org.geowebcache.layer.MetaTile

/**
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*  This program is distributed in the hope that it will be useful,
*  but WITHOUT ANY WARRANTY; without even the implied warranty of
*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
*  GNU General Public License for more details.
*
*  You should have received a copy of the GNU Lesser General Public License
*  along with this program.  If not, see <http://www.gnu.org/licenses/>.
*
* @author Arne Kepp, The Open Planning Project, Copyright 2008
*/
package org.geowebcache.layer;

import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.awt.image.RasterFormatException;
import java.awt.image.RenderedImage;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;

import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.ImageOutputStream;
import javax.imageio.stream.MemoryCacheImageOutputStream;
import javax.media.jai.JAI;
import javax.media.jai.operator.CropDescriptor;
import javax.media.jai.operator.TranslateDescriptor;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.geowebcache.GeoWebCacheException;
import org.geowebcache.grid.BoundingBox;
import org.geowebcache.grid.GridSubset;
import org.geowebcache.grid.SRS;
import org.geowebcache.io.ByteArrayResource;
import org.geowebcache.io.Resource;
import org.geowebcache.mime.FormatModifier;
import org.geowebcache.mime.MimeType;
import org.springframework.util.Assert;

public class MetaTile implements TileResponseReceiver {

    private static Log log = LogFactory.getLog(MetaTile.class);

    private final RenderingHints no_cache = new RenderingHints(JAI.KEY_TILE_CACHE, null);

    private static final boolean NATIVE_JAI_AVAILABLE;
    static {
        // we directly access the Mlib Image class, if in the classpath it will tell us if
        // the native extensions are available, if not, an Error will be thrown
        boolean nativeJAIAvailable;
        try {
            Class image = Class.forName("com.sun.medialib.mlib.Image");
            nativeJAIAvailable = (Boolean) image.getMethod("isAvailable").invoke(null);
        } catch (Throwable e) {
            nativeJAIAvailable = false;
        }
        NATIVE_JAI_AVAILABLE = nativeJAIAvailable;
    }

    // buffer for storing the metatile, if it is an image
    protected RenderedImage metaTiledImage = null;

    protected int[] gutter = new int[4]; // L,B,R,T in pixels

    protected final Rectangle[] tiles;

    // minx,miny,maxx,maxy,zoomlevel
    protected long[] metaGridCov = null;

    // the grid positions of the individual tiles
    protected long[][] tilesGridPositions = null;

    // X metatiling factor, after adjusting to bounds
    protected int metaX;

    // Y metatiling factor, after adjusting to bounds
    protected int metaY;

    protected GridSubset gridSubset;

    protected long status = -1;

    protected boolean error = false;

    protected String errorMessage;

    protected long expiresHeader = -1;

    protected MimeType responseFormat;

    protected FormatModifier formatModifier;

    private int gutterConfig;

    private BoundingBox metaBbox;

    private int metaTileWidth;

    private int metaTileHeight;

    /**
     * The the request format is the format used for the request to the backend.
     *
     * The response format is what the tiles are actually saved as. The primary example is to use
     * image/png or image/tiff for backend requests, and then save the resulting tiles to JPEG to
     * avoid loss of quality.
     *
     * @param srs
     * @param responseFormat
     * @param requestFormat
     * @param tileGridPosition
     * @param metaX
     * @param metaY
     * @param gutter2
     */
    public MetaTile(GridSubset gridSubset, MimeType responseFormat, FormatModifier formatModifier,
            long[] tileGridPosition, int metaX, int metaY, Integer gutter) {
        this.gridSubset = gridSubset;
        this.responseFormat = responseFormat;
        this.formatModifier = formatModifier;
        this.metaX = metaX;
        this.metaY = metaY;
        this.gutterConfig = gutter == null ? 0 : gutter.intValue();

        metaGridCov = calculateMetaTileGridBounds(
                gridSubset.getCoverage((int) tileGridPosition[2]), tileGridPosition);
        tilesGridPositions = calculateTilesGridPositions();
        calculateEdgeGutter();
        int tileHeight = gridSubset.getTileHeight();
        int tileWidth = gridSubset.getTileWidth();
        this.tiles = createTiles(tileHeight, tileWidth);
    }

    /***
     * Calculates final meta tile width, height and bounding box
     * <p>
     * Adding a gutter should be really easy, just add to all sides, right ?
     *
     * But GeoServer / GeoTools, and possibly other WMS servers, can get mad if we exceed 180,90 (or
     * the equivalent for other projections), so we'lll treat those with special care.
     * </p>
     *
     * @param strBuilder
     * @param metaTileGridBounds
     */
    protected void calculateEdgeGutter() {

        Arrays.fill(this.gutter, 0);

        long[] layerCov = gridSubset.getCoverage((int) this.metaGridCov[4]);

        this.metaBbox = gridSubset.boundsFromRectangle(metaGridCov);

        this.metaTileWidth = metaX * gridSubset.getTileWidth();
        this.metaTileHeight = metaY * gridSubset.getTileHeight();

        double widthRelDelta = ((1.0 * metaTileWidth + gutterConfig) / metaTileWidth) - 1.0;
        double heightRelDelta = ((1.0 * metaTileHeight + gutterConfig) / metaTileHeight) - 1.0;

        double coordWidth = metaBbox.getWidth();
        double coordHeight = metaBbox.getHeight();

        double coordWidthDelta = coordWidth * widthRelDelta;
        double coordHeightDelta = coordHeight * heightRelDelta;

        if (layerCov[0] < metaGridCov[0]) {
            metaTileWidth += gutterConfig;
            gutter[0] = gutterConfig;
            metaBbox.setMinX(metaBbox.getMinX() - coordWidthDelta);
        }
        if (layerCov[1] < metaGridCov[1]) {
            metaTileHeight += gutterConfig;
            gutter[1] = gutterConfig;
            metaBbox.setMinY(metaBbox.getMinY() - coordHeightDelta);
        }
        if (layerCov[2] > metaGridCov[2]) {
            metaTileWidth += gutterConfig;
            gutter[2] = gutterConfig;
            metaBbox.setMaxX(metaBbox.getMaxX() + coordWidthDelta);
        }
        if (layerCov[3] > metaGridCov[3]) {
            metaTileHeight += gutterConfig;
            gutter[3] = gutterConfig;
            metaBbox.setMaxY(metaBbox.getMaxY() + coordHeightDelta);
        }
    }

    public BoundingBox getMetaTileBounds() {
        return metaBbox;
    }

    public int getMetaTileWidth() {
        return metaTileWidth;
    }

    public int getMetaTileHeight() {
        return metaTileHeight;
    }

    public int getStatus() {
        return (int) status;
    }

    public void setStatus(int status) {
        this.status = (long) status;
    }

    public boolean getError() {
        return this.error;
    }

    public void setError() {
        this.error = true;
    }

    public String getErrorMessage() {
        return this.errorMessage;
    }

    public void setErrorMessage(String errorMessage) {
        this.errorMessage = errorMessage;
    }

    public long getExpiresHeader() {
        return this.expiresHeader;
    }

    public void setExpiresHeader(long seconds) {
        this.expiresHeader = seconds;
    }

    public void setImageBytes(Resource buffer) throws GeoWebCacheException {
        Assert.notNull(buffer, "WMSMetaTile.setImageBytes() received null");
        Assert.isTrue(buffer.getSize() > 0, "WMSMetaTile.setImageBytes() received empty contents");

        try {
            ImageInputStream imgStream;
            imgStream = new ResourceImageInputStream(((ByteArrayResource) buffer).getInputStream());
            RenderedImage metaTiledImage = ImageIO.read(imgStream);// read closes the stream for us
            setImage(metaTiledImage);
        } catch (IOException ioe) {
            throw new GeoWebCacheException("WMSMetaTile.setImageBytes() "
                    + "failed on ImageIO.read(byte[" + buffer.getSize() + "])", ioe);
        }
        if (metaTiledImage == null) {
            throw new GeoWebCacheException(
                    "ImageIO.read(InputStream) returned null. Unable to read image.");
        }
    }

    public void setImage(RenderedImage metaTiledImage) {
        this.metaTiledImage = metaTiledImage;
    }

    /**
     * Cuts the metaTile into the specified number of tiles, the actual number of tiles is
     * determined by metaX and metaY, not the width and height provided here.
     *
     * @param tileWidth
     *            width of each tile
     * @param tileHeight
     *            height of each tile
     * @return
     */
    private Rectangle[] createTiles(int tileHeight, int tileWidth) {
        int tileCount = metaX * metaY;
        Rectangle[] tiles = new Rectangle[tileCount];

        for (int y = 0; y < metaY; y++) {
            for (int x = 0; x < metaX; x++) {
                int i = x * tileWidth + gutter[0];
                int j = (metaY - 1 - y) * tileHeight + gutter[3];

                // tiles[y * metaX + x] = createTile(i, j, tileWidth, tileHeight, useJAI);
                tiles[y * metaX + x] = new Rectangle(i, j, tileWidth, tileHeight);
            }
        }
        return tiles;
    }

    /**
     * Extracts a single tile from the metatile.
     *
     * @param minX
     *            left pixel index to crop the meta tile at
     * @param minY
     *            top pixel index to crop the meta tile at
     * @param tileWidth
     *            width of the tile
     * @param tileHeight
     *            height of the tile
     * @return a rendered image of the specified meta tile region
     */
    public RenderedImage createTile(int minX, int minY, int tileWidth, int tileHeight) {

        RenderedImage tile = null;
        boolean useJAI = true;
        if (!(metaTiledImage instanceof BufferedImage)) {
            useJAI = true;
        } else if (responseFormat.getMimeType().startsWith("image/jpeg")) {
            useJAI = NATIVE_JAI_AVAILABLE;
        }
        // JAI is messing up for JPEG with non native JAI
        if (useJAI) {
            // Use JAI
            try {
                tile = CropDescriptor.create(metaTiledImage, new Float(minX), new Float(minY),
                        new Float(tileWidth), new Float(tileHeight), no_cache);
                tile = TranslateDescriptor.create(tile, new Float(-1f * minX),
                        new Float(-1f * minY), null, null);
            } catch (IllegalArgumentException iae) {
                log.error("Error cropping, image is " + metaTiledImage.getWidth() + "x"
                        + metaTiledImage.getHeight() + ", requesting a " + tileWidth + "x"
                        + tileHeight + " tile starting at " + minX + "," + minY + ".");
                log.error("Message from JAI: " + iae.getMessage());
                iae.printStackTrace();
            }
        } else {
            // Don't use JAI
            try {
                tile = ((BufferedImage) metaTiledImage).getSubimage(minX, minY, tileWidth,
                        tileHeight);
            } catch (RasterFormatException rfe) {
                log.error("RendereedImage.getSubimage(" + minX + "," + minY + "," + tileWidth + ","
                        + tileHeight + ") threw exception:");
                rfe.printStackTrace();
            }
        }

        if (log.isDebugEnabled()) {
            log.debug("Thread: " + Thread.currentThread().getName() + "\n" + tile.toString() + ", "
                    + "Information from tile (width, height, minx, miny): " + tile.getWidth()
                    + ", " + tile.getHeight() + ", " + tile.getMinX() + ", " + tile.getMinY()
                    + "\n" + "Information set (width, height, minx, miny): " + new Float(tileWidth)
                    + ", " + new Float(tileHeight) + ", " + new Float(minX) + ", "
                    + new Float(minY));
        }

        return tile;
    }

    /**
     * Outputs one tile from the internal array of tiles to a provided stream
     *
     * @param tileIdx
     *            the index of the tile relative to the internal array
     * @param format
     *            the Java name for the format
     * @param resource
     *            the outputstream
     * @return true if no error was encountered
     * @throws IOException
     */
    public boolean writeTileToStream(int tileIdx, Resource target) throws IOException {
        if (tiles == null) {
            return false;
        }
        String format = responseFormat.getInternalName();

        if (log.isDebugEnabled()) {
            log.debug("Thread: " + Thread.currentThread().getName() + " writing: " + tileIdx);
        }

        // TODO should we recycle the writers ?
        // GR: it'd be only a 2% perf gain according to profiler
        ImageWriter writer = javax.imageio.ImageIO.getImageWritersByFormatName(format).next();
        ImageWriteParam param = writer.getDefaultWriteParam();

        if (this.formatModifier != null) {
            param = formatModifier.adjustImageWriteParam(param);
        }

        Rectangle tileRegion = tiles[tileIdx];
        RenderedImage tile = createTile(tileRegion.x, tileRegion.y, tileRegion.width,
                tileRegion.height);

        OutputStream outputStream = target.getOutputStream();
        ImageOutputStream imgOut = new MemoryCacheImageOutputStream(outputStream);
        writer.setOutput(imgOut);
        IIOImage image = new IIOImage(tile, null, null);
        try {
            writer.write(null, image, param);
        } finally {
            imgOut.close();
            writer.dispose();
        }

        return true;
    }

    public String debugString() {
        return " metaX: " + metaX + " metaY: " + metaY + " metaGridCov: "
                + Arrays.toString(metaGridCov);
    }

    /**
     * Figures out the bounds of the metatile, in terms of the gridposition of all contained tiles.
     * To get the BBOX you need to add one tilewidth to the top and right.
     *
     * It also updates metaX and metaY to the actual metatiling factors
     *
     * @param gridBounds
     * @param tileGridPosition
     * @return
     */
    private long[] calculateMetaTileGridBounds(long[] coverage, long[] tileIdx) {
        long[] metaGridCov = new long[5];
        metaGridCov[0] = tileIdx[0] - (tileIdx[0] % metaX);
        metaGridCov[1] = tileIdx[1] - (tileIdx[1] % metaY);
        metaGridCov[2] = Math.min(metaGridCov[0] + metaX - 1, coverage[2]);
        metaGridCov[3] = Math.min(metaGridCov[1] + metaY - 1, coverage[3]);
        metaGridCov[4] = tileIdx[2];

        // Save the actual metatiling factor, important at the boundaries
        metaX = (int) (metaGridCov[2] - metaGridCov[0] + 1);
        metaY = (int) (metaGridCov[3] - metaGridCov[1] + 1);

        return metaGridCov;
    }

    /**
     * Creates an array with all the grid positions, used for cache keys
     */
    private long[][] calculateTilesGridPositions() {
        if (metaX < 0 || metaY < 0) {
            return null;
        }

        long[][] tilesGridPos = new long[metaX * metaY][3];

        for (int y = 0; y < metaY; y++) {
            for (int x = 0; x < metaX; x++) {
                int tile = y * metaX + x;
                tilesGridPos[tile][0] = metaGridCov[0] + x;
                tilesGridPos[tile][1] = metaGridCov[1] + y;
                tilesGridPos[tile][2] = metaGridCov[4];
            }
        }

        return tilesGridPos;
    }

    /**
     * The bottom left grid position and zoomlevel for this metatile, used for locking.
     *
     * @return
     */
    public long[] getMetaGridPos() {
        long[] gridPos = { metaGridCov[0], metaGridCov[1], metaGridCov[4] };
        return gridPos;
    }

    /**
     * The bounds for the metatile
     *
     * @return
     */
    public long[] getMetaTileGridBounds() {
        return metaGridCov;
    }

    public long[][] getTilesGridPositions() {
        return tilesGridPositions;
    }

    public SRS getSRS() {
        return this.gridSubset.getSRS();
    }

    public MimeType getResponseFormat() {
        return this.responseFormat;
    }

    public MimeType getRequestFormat() {
        if (formatModifier == null) {
            return this.responseFormat;
        } else {
            return this.formatModifier.getRequestFormat();
        }
    }
}
TOP

Related Classes of org.geowebcache.layer.MetaTile

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.