/* * Copyright (c) 2012 Data Harmonisation Panel * * 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: * HUMBOLDT EU Integrated Project #030962 * Data Harmonisation Panel <http://www.dhpanel.eu> */ package eu.esdihumboldt.hale.ui.views.styledmap.painter; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.geom.Point2D; import java.util.ArrayList; import java.util.Collection; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import org.jdesktop.swingx.mapviewer.GeoPosition; import org.jdesktop.swingx.mapviewer.IllegalGeoPositionException; import org.jdesktop.swingx.mapviewer.PixelConverter; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.TransformException; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.GeometryCollection; import com.vividsolutions.jts.geom.GeometryFactory; import com.vividsolutions.jts.geom.LineString; import com.vividsolutions.jts.geom.Point; import com.vividsolutions.jts.geom.Polygon; import de.fhg.igd.geom.BoundingBox; import de.fhg.igd.geom.Point3D; import de.fhg.igd.geom.shape.Line2D; import de.fhg.igd.geom.shape.Surface; import de.fhg.igd.mapviewer.marker.AbstractMarker; import de.fhg.igd.mapviewer.marker.BoundingBoxMarker; import de.fhg.igd.mapviewer.marker.Marker; import de.fhg.igd.mapviewer.marker.SimpleCircleMarker; import de.fhg.igd.mapviewer.marker.area.Area; import de.fhg.igd.mapviewer.marker.area.MultiArea; import de.fhg.igd.mapviewer.marker.area.PolygonArea; import de.fhg.igd.mapviewer.waypoints.SelectableWaypoint; import de.fhg.igd.slf4jplus.ALogger; import de.fhg.igd.slf4jplus.ALoggerFactory; import eu.esdihumboldt.hale.common.schema.geometry.CRSDefinition; import eu.esdihumboldt.hale.common.schema.geometry.GeometryProperty; import eu.esdihumboldt.hale.ui.style.StyleHelper; import eu.esdihumboldt.hale.ui.style.service.internal.StylePreferences; import eu.esdihumboldt.hale.ui.views.styledmap.util.CRSConverter; import eu.esdihumboldt.hale.ui.views.styledmap.util.CRSDecode; /** * Instance marker painter. * * @author Simon Templer */ @SuppressWarnings("restriction") public class InstanceMarker extends BoundingBoxMarker<InstanceWaypoint> { /** * Get the geometry factory instance for internal usage. * * @return the geometry factory */ private static GeometryFactory getGeometryFactory() { if (geometryFactory == null) { geometryFactory = new GeometryFactory(); } return geometryFactory; } private static final ALogger log = ALoggerFactory.getLogger(InstanceMarker.class); private static volatile GeometryFactory geometryFactory; /** * Overlap for geometry pixel bounding boxes when checking against graphics * bounds. */ private static final int GEOMETRY_PIXEL_BB_OVERLAP = 5; private final int defaultPointSize = 7; /** * Cache for geometry bounding boxes in the map CRS. Will be cleared on map * change. */ private final Map<Geometry, BoundingBox> geometryMapBBs = new IdentityHashMap<Geometry, BoundingBox>(); /** * @see AbstractMarker#reset() */ @Override public void reset() { synchronized (geometryMapBBs) { geometryMapBBs.clear(); } super.reset(); } /** * Reset the marker areas. */ protected void areaReset() { super.reset(); } /** * @see BoundingBoxMarker#doPaintMarker(Graphics2D, SelectableWaypoint, * PixelConverter, int, int, int, int, int, Rectangle, boolean) */ @Override protected Area doPaintMarker(Graphics2D g, InstanceWaypoint context, PixelConverter converter, int zoom, int minX, int minY, int maxX, int maxY, Rectangle gBounds, boolean calulateArea) { List<Area> areas = (!calulateArea) ? (null) : (new ArrayList<Area>()); List<GeometryProperty<?>> geometries = context.getGeometries(); // map CRS CoordinateReferenceSystem mapCRS; try { mapCRS = CRSDecode.getLonLatCRS(converter.getMapEpsg()); // map (GeoPosition) assumes lon/lat order } catch (Throwable e) { log.error("Could not decode map CRS", e); return null; } // paint each geometry for (GeometryProperty<?> geometry : geometries) { Area geometryArea = paintGeometry(g, geometry.getCRSDefinition(), geometry.getGeometry(), context, converter, zoom, geometries.size() == 1, gBounds, mapCRS, calulateArea); if (areas != null && geometryArea != null) { areas.add(geometryArea); } } if (areas == null) { return null; } if (areas.size() == 1) { return areas.get(0); } else if (!areas.isEmpty()) { return new MultiArea(areas); } return null; } /** * Paint a geometry. * * @param g the graphics to paint on * @param crsDefinition the CRS definition associated with the geometry * @param geometry the geometry * @param context the context * @param converter the pixel converter * @param zoom the zoom level * @param singleGeometry if this is the only geometry associated to the * marker * @param gBounds the graphics bounds * @param mapCRS the map coordinate reference system * @param calculateArea if the area representing the marker should be * calculated, if <code>false</code> is given here the return * value is ignored and should be <code>null</code> * @return the area the geometry occupies (in pixel coordinates), or * <code>null</code> if nothing has been painted */ protected Area paintGeometry(Graphics2D g, CRSDefinition crsDefinition, Geometry geometry, InstanceWaypoint context, PixelConverter converter, int zoom, boolean singleGeometry, Rectangle gBounds, CoordinateReferenceSystem mapCRS, boolean calculateArea) { if (geometry instanceof GeometryCollection) { // paint each geometry in a geometry collection List<Area> areas = (calculateArea) ? (new ArrayList<Area>()) : (null); GeometryCollection collection = (GeometryCollection) geometry; for (int i = 0; i < collection.getNumGeometries(); i++) { Geometry geom = collection.getGeometryN(i); Area geomArea = paintGeometry(g, crsDefinition, geom, context, converter, zoom, singleGeometry && collection.getNumGeometries() == 1, gBounds, mapCRS, calculateArea); if (areas != null && geomArea != null) { areas.add(geomArea); } } if (areas == null || areas.isEmpty()) { return null; } else { return new MultiArea(areas); } } // check if geometry lies inside tile // if the area must be calculated we must process all geometries // if it is the only geometry the check that was already made is OK if (!calculateArea && !singleGeometry) { // we can safely return null inside this method, as no area has to // be calculated // determine bounding box BoundingBox geometryBB; synchronized (geometryMapBBs) { // retrieve cached bounding box geometryBB = geometryMapBBs.get(geometry); if (geometryBB == null) { // if none available, try to calculate BB BoundingBox calcBB = BoundingBox.compute(geometry); if (calcBB != null && calcBB.checkIntegrity()) { try { // get CRS converter CRSConverter conv = CRSConverter.getConverter(crsDefinition.getCRS(), mapCRS); // manually convert to map CRS geometryBB = conv.convert(calcBB); // put BB in cache geometryMapBBs.put(geometry, geometryBB); } catch (Throwable e) { log.error("Error checking geometry bounding box", e); return null; } } } } if (geometryBB != null) { try { GeoPosition minCorner = new GeoPosition(geometryBB.getMinX(), geometryBB.getMinY(), converter.getMapEpsg()); GeoPosition maxCorner = new GeoPosition(geometryBB.getMaxX(), geometryBB.getMaxY(), converter.getMapEpsg()); // determine pixel coordinates Point2D minPixels = converter.geoToPixel(minCorner, zoom); Point2D maxPixels = converter.geoToPixel(maxCorner, zoom); // geometry pixel bounding box int minX = Math.min((int) minPixels.getX(), (int) maxPixels.getX()); int minY = Math.min((int) minPixels.getY(), (int) maxPixels.getY()); int maxX = Math.max((int) minPixels.getX(), (int) maxPixels.getX()); int maxY = Math.max((int) minPixels.getY(), (int) maxPixels.getY()); // add overlap minX -= GEOMETRY_PIXEL_BB_OVERLAP; minY -= GEOMETRY_PIXEL_BB_OVERLAP; maxX += GEOMETRY_PIXEL_BB_OVERLAP; maxY += GEOMETRY_PIXEL_BB_OVERLAP; // create bounding box Rectangle geometryPixelBB = new Rectangle(minX, minY, maxX - minX, maxY - minY); if (!gBounds.intersects(geometryPixelBB) && !gBounds.contains(geometryPixelBB)) { // geometry does not lie in tile return null; } } catch (Throwable e) { log.error("Error checking geometry bounding box", e); return null; } } else { return null; // empty or invalid bounding box } } if (geometry instanceof Point) { return paintPoint((Point) geometry, g, crsDefinition, context, converter, zoom, mapCRS, calculateArea); } if (geometry instanceof Polygon) { return paintPolygon((Polygon) geometry, g, crsDefinition, context, converter, zoom, mapCRS, calculateArea); } // if (geometry instanceof LinearRing) { // //TODO any special handling needed? // } if (geometry instanceof LineString) { return paintLine((LineString) geometry, g, crsDefinition, context, converter, zoom, mapCRS, calculateArea); } return null; } /** * Paint a point geometry. * * @param geometry the point * @param g the graphics object to paint on * @param crsDefinition the CRS definition associated to the geometry * @param context the context * @param converter the pixel converter * @param zoom the zoom level * @param mapCRS the map coordinate reference system * @param calculateArea if the area representing the marker should be * calculated, if <code>false</code> is given here the return * value is ignored and should be <code>null</code> * @return the point marker area or <code>null</code> if painting failed */ protected Area paintPoint(Point geometry, Graphics2D g, CRSDefinition crsDefinition, InstanceWaypoint context, PixelConverter converter, int zoom, CoordinateReferenceSystem mapCRS, boolean calculateArea) { try { /* * Conversion to map pixel coordinates: Though most of the time the * result will be the origin (0,0), e.g. for way-points representing * a single point, the coordinates may also be different, e.g. for * MultiPoint way-points. */ // get CRS converter CRSConverter conv = CRSConverter.getConverter(crsDefinition.getCRS(), mapCRS); // manually convert to map CRS Point3D mapPoint = conv.convert(geometry.getX(), geometry.getY(), 0); GeoPosition pos = new GeoPosition(mapPoint.getX(), mapPoint.getY(), converter.getMapEpsg()); // determine pixel coordinates Point2D point = converter.geoToPixel(pos, zoom); int x = (int) point.getX(); int y = (int) point.getY(); // TODO support style // fall-back: circle if (applyFill(g, context)) { g.fillOval(x - defaultPointSize / 2, y - defaultPointSize / 2, defaultPointSize, defaultPointSize); } if (applyStroke(g, context)) { // TODO respect stroke width? g.drawOval(x - defaultPointSize / 2 - 1, y - defaultPointSize / 2 - 1, defaultPointSize + 1, defaultPointSize + 1); } if (calculateArea) { return new PolygonArea(new java.awt.Polygon( new int[] { x - defaultPointSize / 2 - 1, x + defaultPointSize / 2 + 1, x + defaultPointSize / 2 + 1, x - defaultPointSize / 2 - 1 }, new int[] { y - defaultPointSize / 2 - 1, y - defaultPointSize / 2 - 1, y + defaultPointSize / 2 + 1, y + defaultPointSize / 2 + 1 }, 4)); } else { return null; } } catch (Exception e) { log.error("Error painting instance point geometry", e); return null; } } /** * Paint a polygon geometry. * * @param geometry the polygon * @param g the graphics object to paint on * @param crsDefinition the CRS definition associated to the geometry * @param context the context * @param converter the pixel converter * @param zoom the zoom level * @param mapCRS the map coordinate reference system * @param calculateArea if the area representing the marker should be * calculated, if <code>false</code> is given here the return * value is ignored and should be <code>null</code> * @return the polygon area or <code>null</code> if painting failed */ protected Area paintPolygon(Polygon geometry, Graphics2D g, CRSDefinition crsDefinition, InstanceWaypoint context, PixelConverter converter, int zoom, CoordinateReferenceSystem mapCRS, boolean calculateArea) { try { // get CRS converter CRSConverter conv = CRSConverter.getConverter(crsDefinition.getCRS(), mapCRS); // exterior Coordinate[] coordinates = geometry.getExteriorRing().getCoordinates(); java.awt.Polygon outerPolygon = createPolygon(coordinates, conv, converter, zoom); if (geometry.getNumInteriorRing() > 0) { // polygon has interior geometries java.awt.geom.Area drawArea = new java.awt.geom.Area(outerPolygon); // interior for (int i = 0; i < geometry.getNumInteriorRing(); i++) { LineString interior = geometry.getInteriorRingN(i); java.awt.Polygon innerPolygon = createPolygon(interior.getCoordinates(), conv, converter, zoom); drawArea.subtract(new java.awt.geom.Area(innerPolygon)); } if (applyFill(g, context)) { g.fill(drawArea); } if (applyStroke(g, context)) { g.draw(drawArea); } if (calculateArea) { return new AdvancedPolygonArea(drawArea, outerPolygon); } } else { // polygon has no interior // use polygon instead of Area for painting, as painting small // Areas sometimes produces strange results (some are not // visible) if (applyFill(g, context)) { g.fill(outerPolygon); } if (applyStroke(g, context)) { g.draw(outerPolygon); } if (calculateArea) { return new PolygonArea(outerPolygon); } } return null; // no calculateArea set } catch (Exception e) { log.error("Error painting instance polygon geometry", e); return null; } } private java.awt.Polygon createPolygon(Coordinate[] coordinates, CRSConverter geoConverter, PixelConverter pixelConverter, int zoom) throws TransformException, IllegalGeoPositionException { java.awt.Polygon result = new java.awt.Polygon(); for (Coordinate coord : coordinates) { // manually convert to map CRS Point3D mapPoint = geoConverter.convert(coord.x, coord.y, 0); GeoPosition pos = new GeoPosition(mapPoint.getX(), mapPoint.getY(), pixelConverter.getMapEpsg()); Point2D point = pixelConverter.geoToPixel(pos, zoom); result.addPoint((int) point.getX(), (int) point.getY()); } return result; } /** * Paint a line string geometry. * * @param geometry the line string * @param g the graphics object to paint on * @param crsDefinition the CRS definition associated to the geometry * @param context the context * @param converter the pixel converter * @param zoom the zoom level * @param mapCRS the map coordinate reference system * @param calculateArea if the area representing the marker should be * calculated, if <code>false</code> is given here the return * value is ignored and should be <code>null</code> * @return the polygon area or <code>null</code> if painting failed */ protected Area paintLine(LineString geometry, Graphics2D g, CRSDefinition crsDefinition, InstanceWaypoint context, PixelConverter converter, int zoom, CoordinateReferenceSystem mapCRS, boolean calculateArea) { Coordinate[] coordinates = geometry.getCoordinates(); if (coordinates.length <= 0) { return null; } if (coordinates.length == 1) { // fall back to point drawing Point point = getGeometryFactory().createPoint(coordinates[0]); return paintPoint(point, g, crsDefinition, context, converter, zoom, mapCRS, calculateArea); } try { // get CRS converter CRSConverter conv = CRSConverter.getConverter(crsDefinition.getCRS(), mapCRS); List<Point2D> mapPoints = new ArrayList<Point2D>(coordinates.length); for (Coordinate coord : coordinates) { // manually convert to map CRS Point3D mapPoint = conv.convert(coord.x, coord.y, 0); GeoPosition pos = new GeoPosition(mapPoint.getX(), mapPoint.getY(), converter.getMapEpsg()); Point2D point = converter.geoToPixel(pos, zoom); mapPoints.add(point); } if (applyStroke(g, context)) { for (int i = 0; i < mapPoints.size() - 1; i++) { // draw each connecting line Point2D p1 = mapPoints.get(i); Point2D p2 = mapPoints.get(i + 1); g.drawLine((int) p1.getX(), (int) p1.getY(), (int) p2.getX(), (int) p2.getY()); } } else { log.warn("Stroke disabled in style, LineString is not rendered"); } if (!calculateArea) { return null; } // use a buffer around the line as area java.awt.Polygon[] buffer = createBufferPolygon(mapPoints, 3); // XXX // buffer // size // is // in // pixels, // which // value // is // ok? if (buffer.length == 0) { return null; } else if (buffer.length == 1) { return new PolygonArea(buffer[0]); } else { Collection<Area> areas = new ArrayList<Area>(); for (java.awt.Polygon bufferPoly : buffer) { areas.add(new PolygonArea(bufferPoly)); } return new MultiArea(areas); } } catch (Exception e) { log.error("Error painting instance polygon geometry", e); return null; } } /** * Create a buffered polygon for the given line and a distance. * * @param linePoints the points defining the line * @param distance the buffer size * @return the buffer polygon(s) */ private static java.awt.Polygon[] createBufferPolygon(List<Point2D> linePoints, double distance) { // create metamodel line de.fhg.igd.geom.Point2D[] convertedLinePoints = new de.fhg.igd.geom.Point2D[linePoints .size()]; int index = 0; for (Point2D point : linePoints) { convertedLinePoints[index] = new de.fhg.igd.geom.Point2D(point.getX(), point.getY()); index++; } Line2D line = new Line2D(convertedLinePoints); Surface buffer = line.computeBuffer(distance); return buffer.toAWTPolygons(1, 1, new de.fhg.igd.geom.Point2D(0, 0)); } /** * @see BoundingBoxMarker#getFallbackMarker(SelectableWaypoint) */ @Override protected Marker<? super InstanceWaypoint> getFallbackMarker(InstanceWaypoint context) { return new SimpleCircleMarker(7, getPaintColor(context), getBorderColor(context), Color.BLACK, false); } /** * @see BoundingBoxMarker#getPaintColor(SelectableWaypoint) */ @Override protected Color getPaintColor(InstanceWaypoint context) { // default color with applied transparency Color color = getBorderColor(context); return new Color(color.getRed(), color.getGreen(), color.getRed(), (int) (255 * StyleHelper.DEFAULT_FILL_OPACITY)); } /** * Get the stroke for drawing lines. * * @param context the context * @return the stroke */ protected java.awt.Stroke getLineStroke(InstanceWaypoint context) { if (context.isSelected()) { return new BasicStroke(StylePreferences.getSelectionWidth()); } else { return new BasicStroke(StylePreferences.getDefaultWidth()); } } /** * @see BoundingBoxMarker#applyStroke(Graphics2D, SelectableWaypoint) */ @Override protected boolean applyStroke(Graphics2D g, InstanceWaypoint context) { g.setStroke(getLineStroke(context)); g.setColor(getBorderColor(context)); return true; } /** * @see BoundingBoxMarker#getBorderColor(SelectableWaypoint) */ @Override protected Color getBorderColor(InstanceWaypoint context) { if (context.isSelected()) { // get selection color return StylePreferences.getSelectionColor(); } // get default color return StylePreferences.getDefaultColor(context.getValue().getDataSet()); } }