/* * 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.waypoints; import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Polygon; import java.awt.Rectangle; import java.awt.geom.Point2D; import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jdesktop.swingx.mapviewer.GeoPosition; import org.jdesktop.swingx.mapviewer.GeotoolsConverter; import org.jdesktop.swingx.mapviewer.IllegalGeoPositionException; import org.jdesktop.swingx.mapviewer.PixelConverter; import org.jdesktop.swingx.mapviewer.TileOverlayPainter; import org.jdesktop.swingx.mapviewer.TileProviderUtils; import de.fhg.igd.geom.BoundingBox; import de.fhg.igd.geom.Verifier; import de.fhg.igd.geom.indices.RTree; import de.fhg.igd.mapviewer.AbstractTileOverlayPainter; import de.fhg.igd.mapviewer.MapKitTileOverlayPainter; import de.fhg.igd.mapviewer.Refresher; import de.fhg.igd.mapviewer.marker.area.Area; /** * CustomWaypointPainter * * Based on the implementation of the WaypointPainter class * * @param <W> the way-point type * @author <a href="mailto:simon.templer@igd.fhg.de">Simon Templer</a> * @version $Id$ */ public abstract class CustomWaypointPainter<W extends SelectableWaypoint<W>> extends MapKitTileOverlayPainter { private static final Log log = LogFactory.getLog(CustomWaypointPainter.class); private WaypointRenderer<W> renderer; private static final int PAGE_SIZE = 32; private final RTree<W> waypoints = new RTree<W>(PAGE_SIZE); private final Verifier<? super W, BoundingBox> matchTileVerifier = new Verifier<SelectableWaypoint<W>, BoundingBox>() { @Override public boolean verify(SelectableWaypoint<W> first, BoundingBox second) { return second.intersectsOrCovers(first.getBoundingBox()); } }; /** * Way-points with a big area (bounding box) are painted first, selected * way-points are painted last */ private final Comparator<? super W> paintFirstComparator = new Comparator<W>() { @Override public int compare(W o1, W o2) { // selected come last if (o1.isSelected() && !o2.isSelected()) { return 1; } else if (o2.isSelected() && !o1.isSelected()) { return -1; } else { double a1 = (o1.isPoint()) ? (0) : (o1.getBoundingBox().getWidth() * o1.getBoundingBox().getHeight()); double a2 = (o2.isPoint()) ? (0) : (o2.getBoundingBox().getWidth() * o2.getBoundingBox().getHeight()); int areaHint = 0; // compare size if (a1 > a2) { areaHint = -1; } else if (a2 > a1) { areaHint = 1; } return areaHint; } } }; /** * Creates a custom way-point painter that uses markers for painting */ public CustomWaypointPainter() { this(new MarkerWaypointRenderer<W>()); } /** * Creates a new instance of CustomWaypointPainter with one worker thread * for painting tiles. * * @param renderer the way-point renderer */ public CustomWaypointPainter(WaypointRenderer<W> renderer) { this(renderer, 1); } /** * Creates a new instance of CustomWaypointPainter. * * @param renderer the way-point renderer * @param numberOfThreads the number of worker threads to use for painting * tiles */ public CustomWaypointPainter(WaypointRenderer<W> renderer, int numberOfThreads) { super(numberOfThreads); setRenderer(renderer); } /** * Sets the way-point renderer to use when painting way-points * * @param renderer the new CustomWaypointRenderer to use */ public void setRenderer(WaypointRenderer<W> renderer) { this.renderer = renderer; } /** * Add a way-point * * @param wp the way-point * @param refresh the refresher */ public void addWaypoint(W wp, Refresher refresh) { BoundingBox bb = wp.getBoundingBox(); if (bb != null) { synchronized (waypoints) { waypoints.insert(wp); } if (refresh != null) { wp.addToRefresher(refresh); } } } /** * Remove a way-point * * @param wp the way-point * @param refresh the refresher */ public void removeWaypoint(W wp, Refresher refresh) { synchronized (waypoints) { waypoints.delete(wp); } if (refresh != null) { wp.addToRefresher(refresh); } } /** * @see AbstractTileOverlayPainter#repaintTile(int, int, int, int, * PixelConverter, int) */ @Override public BufferedImage repaintTile(int posX, int posY, int width, int height, PixelConverter converter, int zoom) { if (renderer == null) { return null; } int overlap = getMaxOverlap(); // overlap pixel coordinates Point topLeftPixel = new Point(Math.max(posX - overlap, 0), Math.max(posY - overlap, 0)); Point bottomRightPixel = new Point(posX + width + overlap, posY + height + overlap); // TODO // check // against // map // size // overlap geo positions GeoPosition topLeft = converter.pixelToGeo(topLeftPixel, zoom); GeoPosition bottomRight = converter.pixelToGeo(bottomRightPixel, zoom); // overlap geo positions in RTree CRS try { BoundingBox tileBounds = createSearchBB(topLeft, bottomRight); synchronized (waypoints) { Set<W> candidates = waypoints.query(tileBounds, matchTileVerifier); if (candidates != null) { // sort way-points List<W> sorted = new ArrayList<W>(candidates); Collections.sort(sorted, paintFirstComparator); BufferedImage image = createImage(width, height); Graphics2D gfx = image.createGraphics(); configureGraphics(gfx); try { // for each way-point within these bounds for (W w : sorted) { processWaypoint(w, posX, posY, width, height, converter, zoom, gfx); } /* * DEBUG String test = getClass().getSimpleName() + * " - x=" + posX + ", y=" + posY + ": " + * candidates.size() + " WPs"; gfx.setColor(Color.BLUE); * gfx.drawString(test, 4, height - 4); * * gfx.drawString("minX: " + tileBounds.getMinX(), 4, * height - 84); gfx.drawString("maxX: " + * tileBounds.getMaxX(), 4, height - 64); * gfx.drawString("minY: " + tileBounds.getMinY(), 4, * height - 44); gfx.drawString("maxY: " + * tileBounds.getMaxY(), 4, height - 24); * * gfx.drawRect(0, 0, width - 1, height - 1); */ } finally { gfx.dispose(); } return image; } else { return null; } } } catch (IllegalGeoPositionException e) { log.warn("Error painting waypoint tile: " + e.getMessage()); //$NON-NLS-1$ return null; } } /** * Create a search bounding box * * @param topLeft the first geo-position * @param bottomRight the second geo-position * @return the bounding box * * @throws IllegalGeoPositionException if a conversion fails */ private BoundingBox createSearchBB(GeoPosition topLeft, GeoPosition bottomRight) throws IllegalGeoPositionException { topLeft = GeotoolsConverter.getInstance().convert(topLeft, SelectableWaypoint.COMMON_EPSG); bottomRight = GeotoolsConverter.getInstance().convert(bottomRight, SelectableWaypoint.COMMON_EPSG); return new BoundingBox(Math.min(bottomRight.getX(), topLeft.getX()), Math.min(bottomRight.getY(), topLeft.getY()), -2.0, Math.max(bottomRight.getX(), topLeft.getX()), Math.max(bottomRight.getY(), topLeft.getY()), 2.0); } private void processWaypoint(W w, int minX, int minY, int width, int height, PixelConverter converter, int zoom, Graphics2D g) { try { Point2D point = converter.geoToPixel(w.getPosition(), zoom); int x = (int) (point.getX() - minX); int y = (int) (point.getY() - minY); PixelConverter converterWrapper = new TranslationPixelConverterDecorator(converter, (int) point.getX(), (int) point.getY()); g.translate(x, y); Rectangle gBounds = new Rectangle(minX - (int) point.getX(), minY - (int) point.getY(), width, height); renderer.paintWaypoint(g, converterWrapper, zoom, w, gBounds); g.translate(-x, -y); } catch (IllegalGeoPositionException e) { // waypoint not in map bounds or position invalid // log.warn("Error painting waypoint", e); } } /** * Find a way-point at a given position * * @param point the position * @return the way-point */ public W findWaypoint(Point point) { Rectangle viewPort = getMapKit().getMainMap().getViewportBounds(); final int overlap = getMaxOverlap(); // the overlap is the reason why // the point is used instead of // a GeoPosition final int x = viewPort.x + point.x; final int y = viewPort.y + point.y; final int zoom = getMapKit().getMainMap().getZoom(); final PixelConverter converter = getMapKit().getMainMap().getTileFactory().getTileProvider() .getConverter(); final Dimension mapSize = TileProviderUtils .getMapSize(getMapKit().getMainMap().getTileFactory().getTileProvider(), zoom); final int width = mapSize.width * getMapKit().getMainMap().getTileFactory().getTileProvider().getTileWidth(zoom); final int height = mapSize.height * getMapKit().getMainMap().getTileFactory().getTileProvider().getTileHeight(zoom); final GeoPosition topLeft = converter .pixelToGeo(new Point(Math.max(x - overlap, 0), Math.max(y - overlap, 0)), zoom); final GeoPosition bottomRight = converter.pixelToGeo( new Point(Math.min(x + overlap, width), Math.min(y + overlap, height)), zoom); BoundingBox searchBox; try { searchBox = createSearchBB(topLeft, bottomRight); Set<W> wps = waypoints.query(searchBox, new Verifier<W, BoundingBox>() { @Override public boolean verify(W wp, BoundingBox box) { try { Point2D wpPixel = converter.geoToPixel(wp.getPosition(), zoom); int relX = x - (int) wpPixel.getX(); int relY = y - (int) wpPixel.getY(); Area area = wp.getMarker().getArea(zoom); if (area != null && area.contains(relX, relY)) { // match return true; } } catch (IllegalGeoPositionException e) { log.debug("Error converting waypoint position", e); //$NON-NLS-1$ } return false; } }); if (wps == null || wps.isEmpty()) { return null; } else { if (wps.size() == 1) { return wps.iterator().next(); } else { List<W> sorted = new ArrayList<W>(wps); Collections.sort(sorted, new Comparator<W>() { @Override public int compare(W o1, W o2) { double a1 = o1.getMarker().getArea(zoom).getArea(); double a2 = o2.getMarker().getArea(zoom).getArea(); // compare size if (a1 < a2) { return -1; } else if (a2 < a1) { return 1; } else { return 0; } } }); return sorted.get(0); } } } catch (IllegalGeoPositionException e) { return null; } } /** * Find the way-points in a given rectangular area * * @param rect the area * @return the way-points in the area */ public Set<W> findWaypoints(Rectangle rect) { Rectangle viewPort = getMapKit().getMainMap().getViewportBounds(); final Rectangle worldRect = new Rectangle(viewPort.x + rect.x, viewPort.y + rect.y, rect.width, rect.height); final int zoom = getMapKit().getMainMap().getZoom(); final PixelConverter converter = getMapKit().getMainMap().getTileFactory().getTileProvider() .getConverter(); final GeoPosition topLeft = converter.pixelToGeo(new Point(worldRect.x, worldRect.y), zoom); final GeoPosition bottomRight = converter.pixelToGeo( (new Point(worldRect.x + worldRect.width, worldRect.y + worldRect.height)), zoom); return findWaypoints(topLeft, bottomRight, worldRect, converter, zoom); } /** * Find way-points in a rectangular area defined by the given * {@link GeoPosition}s * * @param topLeft the top left position * @param bottomRight the bottom right position * @param worldRect the bounding box in world pixel coordinates * @param converter the pixel converter * @param zoom the zoom level * * @return the way-points in the area */ public Set<W> findWaypoints(GeoPosition topLeft, GeoPosition bottomRight, final Rectangle worldRect, final PixelConverter converter, final int zoom) { BoundingBox searchBox; try { searchBox = createSearchBB(topLeft, bottomRight); final BoundingBox verifyBox = searchBox; Set<W> wps = waypoints.query(searchBox, new Verifier<W, BoundingBox>() { @Override public boolean verify(W wp, BoundingBox second) { try { Point2D wpPixel = converter.geoToPixel(wp.getPosition(), zoom); int dx = (int) wpPixel.getX(); int dy = (int) wpPixel.getY(); worldRect.translate(-dx, -dy); try { Area area = wp.getMarker().getArea(zoom); if (area != null) { return area.containedIn(worldRect); } else { // something that has not been painted yet may // not be selected return false; } } finally { worldRect.translate(dx, dy); } } catch (IllegalGeoPositionException e) { log.warn("Could not convert waypoint position to pixel", e); // fall back to simple method return verifyBox.covers(wp.getBoundingBox()); } } }); if (wps == null) { return new HashSet<W>(); } else { return wps; } } catch (IllegalGeoPositionException e) { return new HashSet<W>(); } } /** * Find the way-points in a given polygon * * @param poly the polygon * @return the way-points in the polygon area */ public Set<W> findWaypoints(final Polygon poly) { Rectangle viewPort = getMapKit().getMainMap().getViewportBounds(); final int zoom = getMapKit().getMainMap().getZoom(); final PixelConverter converter = getMapKit().getMainMap().getTileFactory().getTileProvider() .getConverter(); de.fhg.igd.geom.Point2D[] points = new de.fhg.igd.geom.Point2D[poly.npoints]; // create a metamodel polygon for (int i = 0; i < poly.npoints; i++) { int worldX = viewPort.x + poly.xpoints[i]; int worldY = viewPort.y + poly.ypoints[i]; // convert to geo position GeoPosition pos = converter.pixelToGeo(new Point(worldX, worldY), zoom); // convert to common CRS try { pos = GeotoolsConverter.getInstance().convert(pos, SelectableWaypoint.COMMON_EPSG); } catch (IllegalGeoPositionException e) { log.warn("Error converting polygon point for query"); //$NON-NLS-1$ return new HashSet<W>(); } points[i] = new de.fhg.igd.geom.Point2D(pos.getX(), pos.getY()); } final de.fhg.igd.geom.shape.Polygon verifyPolygon = new de.fhg.igd.geom.shape.Polygon( points); // we need a 3D search bounding box for the R-Tree BoundingBox searchBox = verifyPolygon.getBoundingBox(); searchBox.setMinZ(-2.0); searchBox.setMaxZ(2.0); poly.translate(viewPort.x, viewPort.y); try { Set<W> wps = waypoints.query(searchBox, new Verifier<W, BoundingBox>() { @Override public boolean verify(W wp, BoundingBox second) { try { Point2D wpPixel = converter.geoToPixel(wp.getPosition(), zoom); int dx = (int) wpPixel.getX(); int dy = (int) wpPixel.getY(); poly.translate(-dx, -dy); try { Area area = wp.getMarker().getArea(zoom); if (area != null) { return area.containedIn(poly); } else { // something that has not been painted yet may // not be selected return false; } } finally { poly.translate(dx, dy); } } catch (IllegalGeoPositionException e) { log.warn("Could not convert waypoint position to pixel", e); // fall back to simple method return verifyPolygon.contains(wp.getBoundingBox().toExtent()); } } }); if (wps == null) { return new HashSet<W>(); } else { return wps; } } finally { poly.translate(-viewPort.x, -viewPort.y); } } /** * Clear the way-points */ public void clearWaypoints() { synchronized (waypoints) { waypoints.flush(); } refreshAll(); } /** * @see TileOverlayPainter#dispose() */ @Override public void dispose() { clearWaypoints(); super.dispose(); } /** * Get the way-points bounding box. * * @return the bounding box */ public BoundingBox getBoundingBox() { return waypoints.getRoot().getBoundingBox(); } }