/* * 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.server; import gnu.trove.TIntArrayList; import java.awt.Color; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Rectangle; import java.awt.geom.Point2D; import java.net.URI; import java.util.ArrayList; import java.util.List; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jdesktop.swingx.mapviewer.AbstractTileProviderDecorator; import org.jdesktop.swingx.mapviewer.GeoPosition; import org.jdesktop.swingx.mapviewer.IllegalGeoPositionException; import org.jdesktop.swingx.mapviewer.JXMapViewer; import org.jdesktop.swingx.mapviewer.PixelConverter; import org.jdesktop.swingx.mapviewer.TileProvider; import org.jdesktop.swingx.mapviewer.TileProviderUtils; import org.jdesktop.swingx.painter.AbstractPainter; import org.jdesktop.swingx.painter.Painter; /** * ClippingTileProviderDecorator * * @author <a href="mailto:simon.templer@igd.fhg.de">Simon Templer</a> */ public class ClippingTileProviderDecorator extends AbstractTileProviderDecorator { /** * ClippingPainter */ public class ClippingPainter extends AbstractPainter<JXMapViewer> { private Color customOverlayColor; /** * Default constructor * * @param customOverlayColor a custom overlay color, may be * <code>null</code> */ public ClippingPainter(Color customOverlayColor) { setAntialiasing(true); setCacheable(false); this.customOverlayColor = customOverlayColor; } /** * @see AbstractPainter#doPaint(Graphics2D, Object, int, int) */ @Override protected void doPaint(Graphics2D g, JXMapViewer map, int width, int height) { Rectangle viewport = map.getViewportBounds(); final int zoom = map.getZoom(); final int mapWidth = getMapWidthInTiles(zoom) * getTileWidth(zoom); final int mapHeight = getMapHeightInTiles(zoom) * getTileHeight(zoom); Point topLeft = getTopLeft(zoom); Point bottomRight = getBottomRight(zoom); Rectangle view = new Rectangle(topLeft); view.add(bottomRight); g.translate(-viewport.x, -viewport.y); Color back = map.getBackground(); Color trans = (customOverlayColor != null) ? (customOverlayColor) : (new Color(back.getRed(), back.getGreen(), back.getBlue(), 90)); g.setColor(map.getBackground()); // draw view border if (viewport.intersects(view)) { g.draw(view); } // generate other rects List<Rectangle> rects = new ArrayList<Rectangle>(); rects.add(new Rectangle(0, 0, topLeft.x, topLeft.y)); rects.add(new Rectangle(topLeft.x, 0, bottomRight.x - topLeft.x, topLeft.y)); rects.add(new Rectangle(bottomRight.x, 0, mapWidth - bottomRight.x, topLeft.y)); rects.add(new Rectangle(0, topLeft.y, topLeft.x, bottomRight.y - topLeft.y)); rects.add(new Rectangle(bottomRight.x, topLeft.y, mapWidth - bottomRight.x, bottomRight.y - topLeft.y)); rects.add(new Rectangle(0, bottomRight.y, topLeft.x, mapHeight - bottomRight.y)); rects.add(new Rectangle(topLeft.x, bottomRight.y, bottomRight.x - topLeft.x, mapHeight - bottomRight.y)); rects.add(new Rectangle(bottomRight.x, bottomRight.y, mapWidth - bottomRight.x, mapHeight - bottomRight.y)); g.setPaint(trans); for (Rectangle rect : rects) { if (viewport.intersects(rect)) g.fill(rect); } g.translate(viewport.x, viewport.y); } } /** * PixelConverter decorator that converts pixels conforming to the * {@link ClippingTileProviderDecorator}'s map clipping */ public class PixelConverterDecorator implements PixelConverter { private final PixelConverter converter; /** * Constructor * * @param converter the internal pixel converter */ public PixelConverterDecorator(PixelConverter converter) { this.converter = converter; } /** * @see PixelConverter#geoToPixel(GeoPosition, int) */ @Override public Point2D geoToPixel(final GeoPosition pos, final int zoom) throws IllegalGeoPositionException { Point2D result = converter.geoToPixel(pos, zoom); // move point (remove offset pixels) return new Point2D.Double(result.getX() - getXTileOffset(zoom) * getTileWidth(zoom), result.getY() - getYTileOffset(zoom) * getTileHeight(zoom)); } /** * @see PixelConverter#pixelToGeo(Point2D, int) */ @Override public GeoPosition pixelToGeo(final Point2D pixelCoordinate, final int zoom) { // move point (add offset pixels) Point2D moved = new Point2D.Double( pixelCoordinate.getX() + getXTileOffset(zoom) * getTileWidth(zoom), pixelCoordinate.getY() + getYTileOffset(zoom) * getTileHeight(zoom)); return converter.pixelToGeo(moved, zoom); } /** * @see PixelConverter#getMapEpsg() */ @Override public int getMapEpsg() { return converter.getMapEpsg(); } /** * @see PixelConverter#supportsBoundingBoxes() */ @Override public boolean supportsBoundingBoxes() { return converter.supportsBoundingBoxes(); } } private static final Log log = LogFactory.getLog(ClippingTileProviderDecorator.class); private final TIntArrayList xTileOffset = new TIntArrayList(); private final TIntArrayList xTileRange = new TIntArrayList(); private final TIntArrayList yTileOffset = new TIntArrayList(); private final TIntArrayList yTileRange = new TIntArrayList(); private final ArrayList<Point> topLeft = new ArrayList<Point>(); private final ArrayList<Point> bottomRight = new ArrayList<Point>(); private final int maxZoom; private final Painter<JXMapViewer> painter; private PixelConverter lastConverter; private PixelConverterDecorator lastConverterDecorator; /** * Constructor * * @param tileProvider the tile provider * @param topLeft the top left constraint * @param bottomRight the bottom right constraint */ public ClippingTileProviderDecorator(final TileProvider tileProvider, final GeoPosition topLeft, final GeoPosition bottomRight) { this(tileProvider, topLeft, bottomRight, 1); } /** * Constructor * * @param tileProvider the tile provider * @param topLeft the top left constraint * @param bottomRight the bottom right constraint * @param minRange the minimum visible range */ public ClippingTileProviderDecorator(final TileProvider tileProvider, final GeoPosition topLeft, final GeoPosition bottomRight, int minRange) { this(tileProvider, topLeft, bottomRight, minRange, null); } /** * Constructor * * @param tileProvider the tile provider * @param topLeft the top left constraint * @param bottomRight the bottom right constraint * @param minRange the minimum visible range * @param customOverlayColor custom overlay color to use, may be * <code>null</code> */ public ClippingTileProviderDecorator(final TileProvider tileProvider, final GeoPosition topLeft, final GeoPosition bottomRight, int minRange, Color customOverlayColor) { super(tileProvider); if (minRange <= 0) minRange = 1; int zoom = tileProvider.getMinimumZoom(); boolean tryNextZoom = true; // determine valid zoom levels and their tile offsets/ranges while (tryNextZoom && zoom <= tileProvider.getMaximumZoom()) { try { Point2D topLeftPixel = tileProvider.getConverter().geoToPixel(topLeft, zoom); Point2D bottomRightPixel = tileProvider.getConverter().geoToPixel(bottomRight, zoom); int xMin = ((int) topLeftPixel.getX()) / tileProvider.getTileWidth(zoom); int yMin = ((int) topLeftPixel.getY()) / tileProvider.getTileHeight(zoom); int xMax = ((int) bottomRightPixel.getX()) / tileProvider.getTileWidth(zoom); int yMax = ((int) bottomRightPixel.getY()) / tileProvider.getTileHeight(zoom); // check for validity if (xMin <= xMax && yMin <= yMax && TileProviderUtils.isValidTile(tileProvider, xMin, yMin, zoom) && TileProviderUtils.isValidTile(tileProvider, xMax, yMax, zoom)) { // valid tiles, enter offset and ranges xTileOffset.add(xMin); xTileRange.add(xMax - xMin + 1); yTileOffset.add(yMin); yTileRange.add(yMax - yMin + 1); this.topLeft.add(new Point( (int) topLeftPixel.getX() - xMin * tileProvider.getTileWidth(zoom), (int) topLeftPixel.getY() - yMin * tileProvider.getTileHeight(zoom))); this.bottomRight.add(new Point( (int) bottomRightPixel.getX() - xMin * tileProvider.getTileWidth(zoom), (int) bottomRightPixel.getY() - yMin * tileProvider.getTileHeight(zoom))); if (xMax - xMin + 1 <= minRange || yMax - yMin + 1 <= minRange) tryNextZoom = false; // we reached the max zoom else zoom++; // prepare next zoom } else { // invalid tiles tryNextZoom = false; zoom--; // previous zoom } } catch (IllegalGeoPositionException e) { // invalid positions or conversion failed tryNextZoom = false; zoom--; // previous zoom } } if (zoom < getMinimumZoom()) { throw new IllegalArgumentException("No zoom levels are valid for clipping"); //$NON-NLS-1$ } else { maxZoom = zoom; painter = new ClippingPainter(customOverlayColor); log.info("Initialized ClippingTileProviderDecorator with minZoom = " //$NON-NLS-1$ + tileProvider.getMinimumZoom() + ", maxZoom = " + maxZoom); //$NON-NLS-1$ } } private int getXTileOffset(final int zoom) { int index = zoom - tileProvider.getMinimumZoom(); if (index < 0 || index >= xTileOffset.size()) throw new IllegalArgumentException("Illegal zoom value: " + zoom); //$NON-NLS-1$ else return xTileOffset.get(index); } private int getXTileRange(final int zoom) { int index = zoom - tileProvider.getMinimumZoom(); if (index < 0 || index >= xTileRange.size()) throw new IllegalArgumentException("Illegal zoom value: " + zoom); //$NON-NLS-1$ else return xTileRange.get(index); } private int getYTileOffset(final int zoom) { int index = zoom - tileProvider.getMinimumZoom(); if (index < 0 || index >= yTileOffset.size()) throw new IllegalArgumentException("Illegal zoom value: " + zoom); //$NON-NLS-1$ else return yTileOffset.get(index); } private int getYTileRange(final int zoom) { int index = zoom - tileProvider.getMinimumZoom(); if (index < 0 || index >= yTileRange.size()) throw new IllegalArgumentException("Illegal zoom value: " + zoom); //$NON-NLS-1$ else return yTileRange.get(index); } private Point getTopLeft(final int zoom) { int index = zoom - tileProvider.getMinimumZoom(); if (index < 0 || index >= topLeft.size()) throw new IllegalArgumentException("Illegal zoom value: " + zoom); //$NON-NLS-1$ else return topLeft.get(index); } private Point getBottomRight(final int zoom) { int index = zoom - tileProvider.getMinimumZoom(); if (index < 0 || index >= bottomRight.size()) throw new IllegalArgumentException("Illegal zoom value: " + zoom); //$NON-NLS-1$ else return bottomRight.get(index); } /** * @see AbstractTileProviderDecorator#getConverter() */ @Override public PixelConverter getConverter() { PixelConverter converter = super.getConverter(); if (converter == null) { return converter; } else if (lastConverterDecorator != null && converter == lastConverter) { return lastConverterDecorator; } else { lastConverter = converter; lastConverterDecorator = new PixelConverterDecorator(converter); return lastConverterDecorator; } } /** * @see AbstractTileProviderDecorator#getDefaultZoom() */ @Override public int getDefaultZoom() { int zoom = super.getDefaultZoom(); if (zoom > maxZoom) return maxZoom; else return zoom; } /** * @see AbstractTileProviderDecorator#getMapHeightInTiles(int) */ @Override public int getMapHeightInTiles(int zoom) { return getYTileRange(zoom); } /** * @see AbstractTileProviderDecorator#getMapWidthInTiles(int) */ @Override public int getMapWidthInTiles(int zoom) { return getXTileRange(zoom); } /** * @see AbstractTileProviderDecorator#getMaximumZoom() */ @Override public int getMaximumZoom() { return maxZoom; } /** * @see AbstractTileProviderDecorator#getTileUris(int, int, int) */ @Override public URI[] getTileUris(int x, int y, int zoom) { try { return super.getTileUris(x + getXTileOffset(zoom), y + getYTileOffset(zoom), zoom); } catch (Exception e) { log.error("Error getting tile uris", e); //$NON-NLS-1$ return null; } } /** * @see AbstractTileProviderDecorator#getTotalMapZoom() */ @Override public int getTotalMapZoom() { return maxZoom; } /** * @see TileProvider#getMapOverlayPainter() */ @Override public Painter<JXMapViewer> getMapOverlayPainter() { return painter; } }