de.fhg.igd.mapviewer.AbstractTileOverlayPainter.java Source code

Java tutorial

Introduction

Here is the source code for de.fhg.igd.mapviewer.AbstractTileOverlayPainter.java

Source

/*
 * Copyright (c) 2016 Fraunhofer IGD
 * 
 * All rights reserved. This program and the accompanying materials are made
 * available 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.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with this distribution. If not, see <http://www.gnu.org/licenses/>.
 * 
 * Contributors:
 *     Fraunhofer IGD <http://www.igd.fraunhofer.de/>
 */
package de.fhg.igd.mapviewer;

import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp;
import java.io.IOException;
import java.lang.ref.SoftReference;
import java.util.Deque;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import javax.imageio.ImageIO;
import javax.swing.SwingUtilities;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jdesktop.swingx.graphics.GraphicsUtilities;
import org.jdesktop.swingx.mapviewer.GeoPosition;
import org.jdesktop.swingx.mapviewer.IllegalGeoPositionException;
import org.jdesktop.swingx.mapviewer.PixelConverter;
import org.jdesktop.swingx.mapviewer.TileOverlayPainter;
import org.jdesktop.swingx.mapviewer.TileProvider;

import gnu.trove.TIntHashSet;
import gnu.trove.TIntIterator;
import gnu.trove.TIntObjectHashMap;
import gnu.trove.TIntObjectProcedure;

/**
 * Abstract tile overlay painter base class.
 * 
 * @author Simon Templer
 */
public abstract class AbstractTileOverlayPainter implements TileOverlayPainter {

    /**
     * Delegates tile painting
     */
    private class TilePaintDelegate {

        private final int x;
        private final int y;
        private final int tilePosX;
        private final int tilePosY;
        private final int tileWidth;
        private final int tileHeight;
        private final PixelConverter converter;
        private final int zoom;

        /**
         * Creates a tile paint delegate
         * 
         * @param x the tile x number
         * @param y the tile y number
         * @param tilePosX the tile x position in pixel
         * @param tilePosY the tile y position in pixel
         * @param tileWidth the tile width
         * @param tileHeight the tile height
         * @param converter the pixel converter
         * @param zoom the zoom level
         */
        public TilePaintDelegate(int x, int y, int tilePosX, int tilePosY, int tileWidth, int tileHeight,
                PixelConverter converter, int zoom) {
            this.x = x;
            this.y = y;
            this.tilePosX = tilePosX;
            this.tilePosY = tilePosY;
            this.tileWidth = tileWidth;
            this.tileHeight = tileHeight;
            this.converter = converter;
            this.zoom = zoom;
        }

        /**
         * Paint the tile
         */
        public void paintTile() {
            BufferedImage img = repaintTile(tilePosX, tilePosY, tileWidth, tileHeight, converter, zoom);
            if (img == null) {
                cacheTile(x, y, zoom, emptyImage);
            } else {
                cacheTile(x, y, zoom, img);
            }

            SwingUtilities.invokeLater(new Runnable() {

                @Override
                public void run() {
                    repaint();
                }
            });
        }

    }

    /**
     * Refresher
     */
    public class DefaultRefresher implements Refresher {

        // private TIntObjectHashMap<TIntHashSet> tileMap = new
        // TIntObjectHashMap<TIntHashSet>();
        private final TIntObjectHashMap<TIntObjectHashMap<TIntHashSet>> tileMap = new TIntObjectHashMap<TIntObjectHashMap<TIntHashSet>>();

        private final TileProvider tiles;

        private final int minZoom;
        private final int maxZoom;

        private final int tileWidth;
        private final int tileHeight;

        private final boolean removeCached;

        private boolean valid = true;

        private BufferedImageOp imageOp;

        /**
         * Constructor
         * 
         * @param tiles the tile provider
         * @param removeCached if the cached tiles shall be removed
         */
        public DefaultRefresher(TileProvider tiles, boolean removeCached) {
            this.tiles = tiles;
            this.removeCached = removeCached;

            if (tiles != null) {
                minZoom = tiles.getMinimumZoom();
                maxZoom = tiles.getTotalMapZoom();

                tileWidth = tiles.getTileWidth(minZoom);
                tileHeight = tiles.getTileHeight(minZoom);
            } else {
                minZoom = maxZoom = tileWidth = tileHeight = 0;
            }
        }

        @Override
        public void setImageOp(BufferedImageOp imageOp) {
            this.imageOp = imageOp;
        }

        @Override
        public void addPosition(GeoPosition pos) {
            if (tiles == null)
                return;

            try {
                Point2D point = tiles.getConverter().geoToPixel(pos, minZoom);

                int x = (int) point.getX();
                int y = (int) point.getY();

                for (int zoom = minZoom; zoom <= maxZoom; zoom++) {
                    addOverlappingTiles(zoom, x, y);

                    x = x / 2;
                    y = y / 2;
                }
            } catch (IllegalGeoPositionException e) {
                log.trace("Error adding position to refresher", e); //$NON-NLS-1$
            }
        }

        /**
         * Add tiles for the given pixel coordinates, also regarding a possible
         * overlap
         * 
         * @param zoom the zoom level
         * @param x the pixel x ordinate
         * @param y the pixel y ordinate
         */
        private void addOverlappingTiles(int zoom, int x, int y) {
            int overlap = getMaxOverlap();

            addTileByPixel(zoom, x + overlap, y + overlap);
            addTileByPixel(zoom, x - overlap, y + overlap);
            addTileByPixel(zoom, x - overlap, y - overlap);
            addTileByPixel(zoom, x + overlap, y - overlap);
        }

        @Override
        public void addArea(GeoPosition topLeft, GeoPosition bottomRight) {
            if (tiles == null)
                return;

            // for each zoom level
            for (int zoom = minZoom; zoom <= maxZoom; zoom++) {
                // add tiles that represent the area
                try {
                    int overlap = getMaxOverlap();

                    Point2D p1 = tiles.getConverter().geoToPixel(topLeft, zoom);
                    Point2D p2 = tiles.getConverter().geoToPixel(bottomRight, zoom);

                    int firstTileX = (int) Math.min(p1.getX() - overlap, p2.getX() - overlap) / tileWidth;
                    int firstTileY = (int) Math.min(p1.getY() - overlap, p2.getY() - overlap) / tileHeight;
                    int lastTileX = (int) Math.max(p1.getX() + overlap, p2.getX() + overlap) / tileWidth;
                    int lastTileY = (int) Math.max(p1.getY() + overlap, p2.getY() + overlap) / tileHeight;

                    // add all identified tiles
                    for (int tileX = firstTileX; tileX <= lastTileX; tileX++) {
                        for (int tileY = firstTileY; tileY <= lastTileY; tileY++) {
                            addTile(zoom, tileX, tileY);
                        }
                    }
                } catch (IllegalGeoPositionException e) {
                    log.trace("Error adding area to refresher");
                }
            }
        }

        private void addTileByPixel(int zoom, int pixelX, int pixelY) {
            int tileX = pixelX / tileWidth;
            int tileY = pixelY / tileHeight;

            addTile(zoom, tileX, tileY);
        }

        private void addTile(int zoom, int tileX, int tileY) {
            TIntObjectHashMap<TIntHashSet> zoomMap = tileMap.get(zoom);
            if (zoomMap == null) {
                zoomMap = new TIntObjectHashMap<TIntHashSet>();
                tileMap.put(zoom, zoomMap);
            }

            TIntHashSet ySet = zoomMap.get(tileX);
            if (ySet == null) {
                ySet = new TIntHashSet();
                zoomMap.put(tileX, ySet);
            }

            ySet.add(tileY);
        }

        @Override
        public void execute() {
            if (tiles == null)
                return;

            if (valid && tiles == AbstractTileOverlayPainter.this.tiles) {
                // execution only possible once
                valid = false;
                synchronized (refreshers) {
                    refreshers.remove(this);
                }

                if (tileMap.isEmpty()) {
                    return;
                }

                synchronized (cache) {
                    // for each zoom level
                    for (int zoom = minZoom; zoom <= tiles.getTotalMapZoom(); zoom++) {
                        final TIntObjectHashMap<TIntObjectHashMap<SoftReference<BufferedImage>>> zoomCache = cache
                                .get(zoom);
                        final int fZoom = zoom;
                        final Rectangle viewBounds = getCurrentViewBounds(zoom);

                        if (zoomCache != null) {
                            // if there are any cached tiles at the given zoom
                            // level

                            TIntObjectHashMap<TIntHashSet> zoomMap = tileMap.get(zoom);
                            if (zoomMap != null) {

                                // process tile map
                                zoomMap.forEachEntry(new TIntObjectProcedure<TIntHashSet>() {

                                    @Override
                                    public boolean execute(int x, final TIntHashSet ySet) {
                                        final TIntObjectHashMap<SoftReference<BufferedImage>> xCache = zoomCache
                                                .get(x);

                                        if (xCache != null) {
                                            TIntIterator it = ySet.iterator();
                                            while (it.hasNext()) {
                                                int y = it.next();

                                                if (removeCached || !isCurrentZoom(fZoom)
                                                        || !insideView(viewBounds, x, y)) {
                                                    // remove cached tile
                                                    xCache.remove(y);
                                                } else {
                                                    /*
                                                     * Only schedule repaint
                                                     * where the cache is not
                                                     * removed and at current
                                                     * zoom level.
                                                     */

                                                    // only schedule repaint for
                                                    // tiles that actually have
                                                    // a cached image
                                                    SoftReference<BufferedImage> imgRef = xCache.get(y);
                                                    if (imgRef != null && imgRef.get() != null) {
                                                        if (imageOp != null) {
                                                            // apply image
                                                            // operation to
                                                            // invalidated image
                                                            BufferedImage img = imgRef.get();
                                                            if (img != null) {
                                                                img = imageOp.filter(img, null);
                                                                xCache.put(y,
                                                                        new SoftReference<BufferedImage>(img));
                                                            }
                                                        }

                                                        int tilePosX = tileWidth * x;
                                                        int tilePosY = tileHeight * y;
                                                        TilePaintDelegate paint = new TilePaintDelegate(x, y,
                                                                tilePosX, tilePosY, tileWidth, tileHeight,
                                                                tiles.getConverter(), fZoom);
                                                        scheduleTileRepaint(paint);
                                                    }
                                                }
                                            }
                                        }

                                        return true;
                                    }

                                });

                            }
                        }
                    }
                }
                repaint();
            }
        }

        /**
         * Determines if a tile is visible in the given view bounds.
         * 
         * @param viewBounds the view bounds, may be <code>null</code> if
         *            unknown
         * @param x the tile x ordinate
         * @param y the tile y ordinate
         * @return if the tile is visible in the view bounds
         */
        protected boolean insideView(Rectangle viewBounds, int x, int y) {
            if (viewBounds == null) {
                return false;
            }

            if (x * tileWidth > viewBounds.x + viewBounds.width) {
                // tile lies to the right of the view
                return false;
            }
            if ((x + 1) * tileWidth < viewBounds.x) {
                // tile lies to the left of the view
                return false;
            }
            if (y * tileHeight > viewBounds.y + viewBounds.height) {
                // tile lies below the view
                return false;
            }
            if ((y + 1) * tileHeight < viewBounds.y) {
                // tile lies above the view
                return false;
            }

            return true;
        }

        /**
         * Invalidate the refresher
         */
        protected final void invalidate() {
            valid = false;
        }

    }

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

    /**
     * Default priority
     */
    public static final int DEF_PRIORITY = 5;

    private final BufferedImage emptyImage = GraphicsUtilities.createCompatibleTranslucentImage(1, 1);

    private final BufferedImage loadingImage;

    private final TIntObjectHashMap<TIntObjectHashMap<TIntObjectHashMap<SoftReference<BufferedImage>>>> cache = new TIntObjectHashMap<TIntObjectHashMap<TIntObjectHashMap<SoftReference<BufferedImage>>>>();

    private final Set<DefaultRefresher> refreshers = new HashSet<DefaultRefresher>();

    private TileProvider tiles;

    private boolean antialiasing = true;

    private int priority = DEF_PRIORITY;

    /**
     * The executor for tile painting
     */
    private final ExecutorService executor;

    private final Deque<TilePaintDelegate> repaintQueue = new LinkedList<TilePaintDelegate>();

    /**
     * Constructor
     * 
     * @param numberOfThreads the number of worker threads to use for tile
     *            painting
     */
    public AbstractTileOverlayPainter(int numberOfThreads) {
        try {
            loadingImage = ImageIO.read(AbstractTileOverlayPainter.class.getResource("images/loading.png"));
        } catch (IOException e) {
            throw new IllegalStateException("Could not load loading image");
        }

        if (numberOfThreads > 1) {
            executor = Executors.newFixedThreadPool(4);
        } else {
            executor = Executors.newSingleThreadExecutor();
        }
    }

    /**
     * @param antialiasing if antialiasing should be used for the painter
     */
    public void setAntialiasing(boolean antialiasing) {
        this.antialiasing = antialiasing;
    }

    /**
     * @see TileOverlayPainter#setTileProvider(org.jdesktop.swingx.mapviewer.TileProvider)
     */
    @Override
    public void setTileProvider(TileProvider tiles) {
        this.tiles = tiles;

        clearAll();
    }

    /**
     * Clear all tiles and other temporary objects
     */
    private void clearAll() {
        synchronized (refreshers) {
            // invalidate refreshers (important for tile provider change)
            for (DefaultRefresher refresher : refreshers) {
                refresher.invalidate();
            }

            refreshers.clear();
        }

        synchronized (cache) {
            cache.clear();
        }

        synchronized (repaintQueue) {
            repaintQueue.clear();
        }
    }

    /**
     * Refresh all tiles
     */
    public void refreshAll() {
        clearAll();
        repaint();
    }

    /**
     * Prepare a refresh
     * 
     * @return the refresher
     */
    public Refresher prepareRefresh() {
        return prepareRefresh(true);
    }

    /**
     * Get if the given zoom level is the current zoom level
     * 
     * @param zoom the zoom level
     * @return if it is the current zoom level
     */
    protected abstract boolean isCurrentZoom(int zoom);

    /**
     * Get the bounds of the current view of the map.
     * 
     * @param zoom the current assumed zoom level
     * @return the view bounds (in world pixel coordinates) or <code>null</code>
     *         if it is not known or the zoom level does not match
     */
    protected abstract Rectangle getCurrentViewBounds(int zoom);

    /**
     * Prepare a refresh
     * 
     * @param removeCached if the changed cached tiles shall be removed
     * @return the refresher
     */
    public Refresher prepareRefresh(boolean removeCached) {
        DefaultRefresher defaultRefresher = new DefaultRefresher(tiles, removeCached);
        synchronized (refreshers) {
            refreshers.add(defaultRefresher);
        }
        return defaultRefresher;
    }

    /**
     * Initiate a repaint
     */
    protected abstract void repaint();

    /**
     * Get the maximum overlap for painted way-points in pixel
     * 
     * @return the maximum overlap for painted way-points in pixel
     */
    protected abstract int getMaxOverlap();

    /**
     * @see TileOverlayPainter#paintTile(Graphics2D, int, int, int, int, int,
     *      int, int, PixelConverter, Rectangle)
     */
    @Override
    public void paintTile(final Graphics2D gfx, final int x, final int y, final int zoom, final int tilePosX,
            final int tilePosY, final int tileWidth, final int tileHeight, final PixelConverter converter,
            Rectangle viewportBounds) {
        BufferedImage img;
        synchronized (this) {
            img = getCachedTile(x, y, zoom);

            if (img == null) {
                // start tile creation

                // temporarily set empty image TODO load image?
                img = loadingImage;
                cacheTile(x, y, zoom, img);

                TilePaintDelegate paint = new TilePaintDelegate(x, y, tilePosX, tilePosY, tileWidth, tileHeight,
                        converter, zoom);
                scheduleTileRepaint(paint);
            }
        }

        if (img == loadingImage) {
            configureGraphics(gfx);
            gfx.drawImage(img, (tileWidth - img.getWidth()) / 2, (tileHeight - img.getHeight()) / 2, null);
        } else if (img != null && img != emptyImage) {
            configureGraphics(gfx);
            drawOverlay(gfx, img, zoom, tilePosX, tilePosY, tileWidth, tileHeight, viewportBounds, converter);
        }
    }

    /**
     * Draws the overlay image on a tile.
     * 
     * @param gfx the graphics to draw the overlay on (with the origin being the
     *            at the upper left corner of the tile)
     * @param img the overlay image
     * @param zoom the current zoom level
     * @param tilePosX the tile x position in world pixel coordinates
     * @param tilePosY the tile y position in world pixel coordinates
     * @param tileWidth the tile width
     * @param tileHeight the tile height
     * @param viewportBounds the view-port bounds
     * @param converter the pixel converter
     */
    protected void drawOverlay(Graphics2D gfx, BufferedImage img, int zoom, int tilePosX, int tilePosY,
            int tileWidth, int tileHeight, Rectangle viewportBounds, PixelConverter converter) {
        gfx.drawImage(img, 0, 0, null);
    }

    /**
     * Schedule a tile repaint after the {@link #repaintQueue} has been changed
     * 
     * @param paint the tile paint delegate
     */
    private void scheduleTileRepaint(TilePaintDelegate paint) {
        synchronized (repaintQueue) {
            repaintQueue.addFirst(paint);
        }

        executor.execute(new Runnable() {

            @Override
            public void run() {
                TilePaintDelegate paint = null;
                synchronized (repaintQueue) {
                    if (!repaintQueue.isEmpty()) {
                        paint = repaintQueue.pop();
                    }
                }
                if (paint != null) {
                    paint.paintTile();
                }
            }
        });
    }

    /**
     * Create a translucent image
     * 
     * @param width the width
     * @param height the height
     * @return a translucent image
     */
    protected BufferedImage createImage(final int width, final int height) {
        return GraphicsUtilities.createCompatibleTranslucentImage(width, height);
    }

    private BufferedImage getCachedTile(int x, int y, int zoom) {
        BufferedImage result = null;

        synchronized (cache) {
            // cache per zoom
            final TIntObjectHashMap<TIntObjectHashMap<SoftReference<BufferedImage>>> zoomCache = cache.get(zoom);

            if (zoomCache != null) {
                // cache per x
                final TIntObjectHashMap<SoftReference<BufferedImage>> xCache = zoomCache.get(x);
                if (xCache != null) {
                    // img cache
                    final SoftReference<BufferedImage> imgCache = xCache.get(y);
                    if (imgCache != null) {
                        result = imgCache.get();
                    }
                }
            }
        }

        return result;
    }

    private void cacheTile(int x, int y, int zoom, BufferedImage img) {
        synchronized (cache) {
            // cache per zoom
            TIntObjectHashMap<TIntObjectHashMap<SoftReference<BufferedImage>>> zoomCache = cache.get(zoom);
            if (zoomCache == null) {
                zoomCache = new TIntObjectHashMap<TIntObjectHashMap<SoftReference<BufferedImage>>>();
                cache.put(zoom, zoomCache);
            }

            // cache per x
            TIntObjectHashMap<SoftReference<BufferedImage>> xCache = zoomCache.get(x);
            if (xCache == null) {
                xCache = new TIntObjectHashMap<SoftReference<BufferedImage>>();
                zoomCache.put(x, xCache);
            }

            // image cache
            xCache.put(y, new SoftReference<BufferedImage>(img));
        }
    }

    /**
     * Paint a tile. Use {@link #createImage(int, int)} to create an image to
     * paint on.
     * 
     * @param posX the x position of the tile in world pixels
     * @param posY the y position of the tile in world pixels
     * @param width the tile width
     * @param height the tile height
     * @param converter the converter
     * @param zoom the zoom level
     * 
     * @return the painted image tile or null (represents the empty image)
     */
    public abstract BufferedImage repaintTile(int posX, int posY, int width, int height, PixelConverter converter,
            int zoom);

    /**
     * Configure the given graphics
     * 
     * @param gfx the graphics device
     */
    protected void configureGraphics(Graphics2D gfx) {
        if (antialiasing) {
            gfx.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        } else {
            gfx.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
        }
    }

    /**
     * Get the painter priority, painters with higher priority will be painted
     * later
     * 
     * @return the priority
     */
    public int getPriority() {
        return priority;
    }

    /**
     * @param priority the priority to set
     */
    public void setPriority(int priority) {
        this.priority = priority;
    }

    /**
     * @see java.lang.Comparable#compareTo(java.lang.Object)
     */
    @Override
    public int compareTo(TileOverlayPainter other) {
        if (this == other)
            return 0;

        if (other instanceof AbstractTileOverlayPainter) {
            int priority = getPriority();
            int otherPriority = ((AbstractTileOverlayPainter) other).getPriority();

            if (priority < otherPriority) {
                return -1;
            } else if (priority > otherPriority) {
                return 1;
            }
        }

        int classCompare = getClass().getName().compareTo(other.getClass().getName());

        if (classCompare != 0) {
            return classCompare;
        }

        int hash = System.identityHashCode(this);
        int otherHash = System.identityHashCode(other);

        if (hash < otherHash)
            return -1;
        else if (hash > otherHash)
            return 1;

        return 0;
    }

    /**
     * @see TileOverlayPainter#dispose()
     */
    @Override
    public void dispose() {
        executor.shutdownNow();

        clearAll();
    }

}