/* * GeoRefGrid.java * * Created on February 15, 205, 2:37 PM */ package ika.geo; import java.util.*; import java.awt.*; import java.awt.geom.*; import java.awt.image.*; /** * * @author Bernhard Jenny, Institute of Cartography, ETH Zurich. */ public class GeoRefGrid extends GeoObject { /* LinkedHashSet keeps the order in which the elements were added. */ private LinkedHashSet<GeoObject>[] refs; private int cols; private int rows; private double west; private double south; private double cellSize; private BufferedImage rasterizerImage; private GeoImage visualizerImage; private Graphics2D rasterizerG2d; public final static int STATS_1_REF = 0; public final static int STATS_2_REF = 1; public final static int STATS_3_REF = 2; public final static int STATS_4_REF = 3; public final static int STATS_TOTAL = 4; public final static int STATS_OCCUPIED = 5; public static GeoRefGrid createGeoRefGrid(java.awt.geom.Rectangle2D bounds, double cellSize) { if (cellSize <= 0) throw new IllegalArgumentException("Cell size must be > 0"); int cols = (int)Math.ceil(bounds.getWidth() / cellSize); int rows = (int)Math.ceil(bounds.getHeight() / cellSize); // make sure grid is at least 1x1 pixel large. cols = Math.max(cols, 1); rows = Math.max(rows, 1); return new GeoRefGrid(cols, rows, bounds.getX(), bounds.getY(), cellSize); } /** Creates a new instance of GeoRefGrid */ public GeoRefGrid(int cols, int rows, double west, double south, double cellSize) { refs = new LinkedHashSet [cols*rows]; this.cols = cols; this.rows = rows; this.west = west; this.south = south; this.cellSize = cellSize; // setup image to rasterize objects this.rasterizerImage = new BufferedImage(cols, rows, BufferedImage.TYPE_BYTE_GRAY); this.rasterizerG2d = rasterizerImage.createGraphics(); this.rasterizerG2d.setColor(Color.white); this.rasterizerG2d.setBackground(Color.black); this.rasterizerG2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // enable antialiasing // transform from Geo space to user space. // last transformation first! /* * Transformation: * x_ = (x-west)*scale; * y_ = (north-y)*scale = (y-north)*(-scale); */ final double scale = 1./cellSize; rasterizerG2d.scale(scale, -scale); rasterizerG2d.translate(-west, -this.getNorth()); this.visualizerImage = null; } public double getWidth() { return cellSize * cols; } public double getHeight() { return cellSize * rows; } public double getNorth(){ return this.south + this.rows * this.cellSize; } private void rasterizeGeoSet(GeoSet geoSet){ if (geoSet == null) return; MapEventTrigger trigger = new MapEventTrigger(this); try { final int nbrChildren = geoSet.getNumberOfChildren(); for (int i = 0; i < nbrChildren; i++) { GeoObject geoObject = geoSet.getGeoObject(i); if (geoObject instanceof GeoPath){ GeoPath geoPath = (GeoPath)geoObject; VectorSymbol symbol = geoPath.getVectorSymbol(); rasterizerG2d.setStroke(new BasicStroke((float)symbol.getStrokeWidth())); this.rasterize(geoPath.toPathIterator(null), symbol); } else if (geoObject instanceof GeoSet){ this.add((GeoSet)geoObject, false); } else if (geoObject instanceof GeoImage){ } else if (geoObject instanceof GeoPoint){ } } } finally { trigger.inform(); } } public void add(GeoSet geoSet, boolean treatGeoSetAsOneObject) { if (geoSet == null) return; MapEventTrigger trigger = new MapEventTrigger(this); try { if (treatGeoSetAsOneObject){ clearRasterizer(); rasterizeGeoSet(geoSet); copyRasterizer(geoSet.getBounds2D(GeoObject.UNDEFINED_SCALE), geoSet, null); } else { final int nbrChildren = geoSet.getNumberOfChildren(); for (int i = 0; i < nbrChildren; i++) { GeoObject geoObject = geoSet.getGeoObject(i); if (geoObject instanceof GeoPath) this.add((GeoPath)geoObject); else if (geoObject instanceof GeoSet) this.add((GeoSet)geoObject, false); else if (geoObject instanceof GeoImage) this.add((GeoImage)geoObject); else if (geoObject instanceof GeoPoint) this.add((GeoPoint)geoObject); } } } finally { trigger.inform(); } } public void add(GeoPoint geoPoint) { PointSymbol pointSymbol = geoPoint.getPointSymbol(); rasterizerG2d.setStroke(new BasicStroke((float)pointSymbol.getStrokeWidth())); GeoPath geoPath = pointSymbol.getPointSymbol(1, geoPoint.getX(), geoPoint.getY()); PathIterator pathIterator = geoPath.toPathIterator(null); java.awt.geom.Rectangle2D objBounds = geoPath.getBounds2D(GeoPath.UNDEFINED_SCALE); this.addPathIterator(pathIterator, pointSymbol, objBounds, geoPoint); MapEventTrigger.inform(this); } public void add(GeoPath geoPath) { // apply symbol of GeoPath VectorSymbol vectorSymbol = geoPath.getVectorSymbol(); rasterizerG2d.setStroke(new BasicStroke((float)vectorSymbol.getStrokeWidth())); Rectangle2D objBounds = geoPath.getBounds2D(GeoObject.UNDEFINED_SCALE); this.addPathIterator(geoPath.toPathIterator(null), vectorSymbol, objBounds, geoPath); MapEventTrigger.inform(this); } public boolean isAddingCausingOverlay(GeoSet geoSet, boolean treatGeoSetAsOneObject) { if (treatGeoSetAsOneObject){ clearRasterizer(); rasterizeGeoSet(geoSet); Rectangle2D objBounds = geoSet.getBounds2D(GeoObject.UNDEFINED_SCALE); return isCopyingRasterizerCausingOverlay(objBounds, null); } else { final int nbrChildren = geoSet.getNumberOfChildren(); for (int i = 0; i < nbrChildren; i++) { GeoObject geoObject = geoSet.getGeoObject(i); if (geoObject instanceof GeoPath) return this.isAddingCausingOverlay((GeoPath)geoObject); else if (geoObject instanceof GeoSet) return this.isAddingCausingOverlay((GeoSet)geoObject, false); } } return false; } public boolean isAddingCausingOverlay(GeoPath geoPath){ // apply symbol of GeoPath VectorSymbol vectorSymbol = geoPath.getVectorSymbol(); rasterizerG2d.setStroke(new BasicStroke((float)vectorSymbol.getStrokeWidth())); clearRasterizer(); rasterize(geoPath.toPathIterator(null), vectorSymbol); Rectangle2D objBounds = geoPath.getBounds2D(GeoObject.UNDEFINED_SCALE); return isCopyingRasterizerCausingOverlay(objBounds, vectorSymbol); } public int getNbrOverlayedCellsWhenAdding(GeoSet geoSet, boolean treatGeoSetAsOneObject, GeoObject[] geoObjectsToIgnore) { if (geoSet == null) return 0; if (treatGeoSetAsOneObject){ clearRasterizer(); rasterizeGeoSet(geoSet); Rectangle2D objBounds = geoSet.getBounds2D(GeoObject.UNDEFINED_SCALE); return getNbrOverlayedCellsWhenAdding(objBounds, null, geoObjectsToIgnore); } else { final int nbrChildren = geoSet.getNumberOfChildren(); for (int i = 0; i < nbrChildren; i++) { GeoObject geoObject = geoSet.getGeoObject(i); if (geoObject instanceof GeoPath) return this.getNbrOverlayedCellsWhenAdding( (GeoPath)geoObject, geoObjectsToIgnore); else if (geoObject instanceof GeoSet) return this.getNbrOverlayedCellsWhenAdding((GeoSet)geoObject, false, geoObjectsToIgnore); } } return 0; } public int getNbrOverlayedCellsWhenAdding(GeoPath geoPath, GeoObject[] geoObjectsToIgnore){ // apply symbol of GeoPath VectorSymbol vectorSymbol = geoPath.getVectorSymbol(); rasterizerG2d.setStroke(new BasicStroke((float)vectorSymbol.getStrokeWidth())); clearRasterizer(); rasterize(geoPath.toPathIterator(null), vectorSymbol); Rectangle2D objBounds = geoPath.getBounds2D(GeoObject.UNDEFINED_SCALE); return getNbrOverlayedCellsWhenAdding(objBounds, vectorSymbol, geoObjectsToIgnore); } public void add(GeoImage geoImage) { throw new IllegalArgumentException("GeoImage not supported yet"); } private void addPathIterator(PathIterator pathIterator, VectorSymbol vectorSymbol, Rectangle2D objBounds, GeoObject geoObject) { clearRasterizer(); rasterize(pathIterator, vectorSymbol); copyRasterizer(objBounds, geoObject, vectorSymbol); } /** Clears the rasterizer. */ private void clearRasterizer(){ this.visualizerImage = null; // clear the rasterizer image AffineTransform trans = rasterizerG2d.getTransform(); rasterizerG2d.setTransform(new AffineTransform()); rasterizerG2d.clearRect(0, 0, cols, rows); rasterizerG2d.setTransform(trans); } /** * Draws a PathIterator into the rasterizer */ private void rasterize(PathIterator pathIterator, VectorSymbol vectorSymbol){ GeneralPath path = new GeneralPath(); path.append(pathIterator, false); if (vectorSymbol.isFilled()) rasterizerG2d.fill(path); if (vectorSymbol.isStroked()) rasterizerG2d.draw(path); } /** Extracts painted pixels from the rasterizer and stores * references to GeoObject. */ private void copyRasterizer(Rectangle2D objBounds, GeoObject geoObject, VectorSymbol vectorSymbol) { // copy occupied cells from rasterizer image to this GeoRefGrid // compute section that has to be copied from the rasterized image final double lineWidth = vectorSymbol != null ? vectorSymbol.getStrokeWidth()*2. : 0; final double objWest = objBounds.getMinX(); final double objEast = objBounds.getMaxX(); final double objSouth = objBounds.getMinY(); final double objNorth = objBounds.getMaxY(); int firstCol = (int)((objWest - lineWidth - this.west)/cellSize); int firstRow = (int)((objNorth - lineWidth - this.getNorth())/cellSize); firstCol = Math.max(0, firstCol); firstRow = Math.max(0, firstRow); int lastCol = (int)Math.ceil((objEast + lineWidth - this.west)/cellSize); int lastRow = (int)Math.ceil((this.getNorth() - objSouth + lineWidth)/cellSize); lastCol = Math.min(rasterizerImage.getWidth(), lastCol); lastRow = Math.min(rasterizerImage.getHeight(), lastRow); int imgWidth = lastCol - firstCol; int imgHeight = lastRow - firstRow; int[] grayValues = null; final int w = Math.max(0, lastCol-firstCol); final int h = Math.max(0, lastRow-firstRow); grayValues = rasterizerImage.getRaster().getSamples(firstCol, firstRow, w, h, 0, grayValues); LinkedHashSet set; int refGridIndex; int imgIndex = 0; for (int r = 0; r < imgHeight; ++r) { refGridIndex = (firstRow+r) * cols + firstCol; for (int c = 0; c < imgWidth; ++c) { if (grayValues[imgIndex++] > 0) { if (refs[refGridIndex] == null) { set = new LinkedHashSet(); refs[refGridIndex] = set; } else set = refs[refGridIndex]; set.add(geoObject); } ++refGridIndex; } } } private boolean isCopyingRasterizerCausingOverlay(Rectangle2D objBounds, VectorSymbol vectorSymbol) { final double lineWidth = vectorSymbol != null ? vectorSymbol.getStrokeWidth()*2. : 0; final double objWest = objBounds.getMinX(); final double objEast = objBounds.getMaxX(); final double objSouth = objBounds.getMinY(); final double objNorth = objBounds.getMaxY(); int firstCol = (int)((objWest - lineWidth - this.west)/cellSize); int firstRow = (int)((objNorth - lineWidth - this.getNorth())/cellSize); firstCol = Math.max(0, firstCol); firstRow = Math.max(0, firstRow); int lastCol = (int)Math.ceil((objEast + lineWidth - this.west)/cellSize); int lastRow = (int)Math.ceil((this.getNorth() - objSouth + lineWidth)/cellSize); lastCol = Math.min(rasterizerImage.getWidth(), lastCol); lastRow = Math.min(rasterizerImage.getHeight(), lastRow); int imgWidth = lastCol - firstCol; int imgHeight = lastRow - firstRow; int[] grayValues = null; final int w = Math.max(0, lastCol-firstCol); final int h = Math.max(0, lastRow-firstRow); grayValues = rasterizerImage.getRaster().getSamples(firstCol, firstRow, w, h, 0, grayValues); int refGridIndex; int imgIndex = 0; for (int r = 0; r < imgHeight; ++r) { refGridIndex = (firstRow+r) * cols + firstCol; for (int c = 0; c < imgWidth; ++c) { if (grayValues[imgIndex++] > 0) { // found a grid cell covered by the passed GeoObject // that contains a reference to another GeoObject if (refs[refGridIndex] != null) { return true; } } ++refGridIndex; } } return false; } private int getNbrOverlayedCellsWhenAdding (Rectangle2D objBounds, VectorSymbol vectorSymbol, GeoObject[] geoObjectsToIgnore) { final double lineWidth = vectorSymbol != null ? vectorSymbol.getStrokeWidth()*2. : 0; final double objWest = objBounds.getMinX(); final double objEast = objBounds.getMaxX(); final double objSouth = objBounds.getMinY(); final double objNorth = objBounds.getMaxY(); int firstCol = (int)((objWest - lineWidth - this.west)/cellSize); int firstRow = (int)((objNorth - lineWidth - this.getNorth())/cellSize); firstCol = Math.max(0, firstCol); firstRow = Math.max(0, firstRow); int lastCol = (int)Math.ceil((objEast + lineWidth - this.west)/cellSize); int lastRow = (int)Math.ceil((this.getNorth() - objSouth + lineWidth)/cellSize); lastCol = Math.min(rasterizerImage.getWidth(), lastCol); lastRow = Math.min(rasterizerImage.getHeight(), lastRow); int imgWidth = lastCol - firstCol; int imgHeight = lastRow - firstRow; int[] grayValues = null; final int w = Math.max(0, lastCol-firstCol); final int h = Math.max(0, lastRow-firstRow); grayValues = rasterizerImage.getRaster().getSamples(firstCol, firstRow, w, h, 0, grayValues); LinkedHashSet set; int refGridIndex; int imgIndex = 0; int nbrOverlayedCells = 0; for (int r = 0; r < imgHeight; ++r) { refGridIndex = (firstRow+r) * cols + firstCol; for (int c = 0; c < imgWidth; ++c) { if (grayValues[imgIndex++] > 0) { // found a grid cell covered by the passed GeoObject // that contains a reference to another GeoObject if (refs[refGridIndex] != null) { set = refs[refGridIndex]; int nbrOverlays = set.size(); for (int i = 0; i < geoObjectsToIgnore.length; ++i){ if (set.contains(geoObjectsToIgnore[i])) --nbrOverlays; } if (nbrOverlays > 0) ++nbrOverlayedCells; } } ++refGridIndex; } } return nbrOverlayedCells; } public BufferedImage toBufferedImage() { GeoImage geoImage = this.toGeoImage(); return geoImage.getBufferedImage(); } public GeoImage toGeoImage() { if (visualizerImage != null) return visualizerImage; // index colors. First color will be replaced with complete transparency. // can be expanded to up to 16 indexed colors. byte ff = (byte)0xff; byte[] r = {ff, 0, 0, ff}; byte[] g = {ff, ff, 0, 0}; byte[] b = {ff, 0, ff, 0}; // use 2 bits for currently 4 colors, index 0 transparent. final int nbrColors = 4; final int lastColorIndex = nbrColors-1; IndexColorModel cm = new IndexColorModel(2, nbrColors, r, g, b, 0); BufferedImage image = new BufferedImage(cols, rows, BufferedImage.TYPE_BYTE_BINARY, cm); // fill image int nbrCells = this.cols * this.rows; int [] grayValues = new int [nbrCells]; for (int i = 0; i < nbrCells; ++i) { Set set = refs[i]; grayValues[i] = (set != null) ? Math.min(lastColorIndex, set.size()) : 0; } image.getRaster().setSamples(0, 0, image.getWidth(), image.getHeight(), 0, grayValues); this.visualizerImage = new GeoImage(image, this.west, this.getNorth(), this.cellSize); return this.visualizerImage; } public void drawNormalState(RenderParams rp) { rp.g2d.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF ); rp.g2d.setRenderingHint( RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR ); GeoImage geoImage = toGeoImage(); geoImage.drawNormalState(rp); GeoPath.newRect(getBounds2D(rp.scale)).drawNormalState(rp); } public void drawSelectedState(RenderParams rp) { if (!isSelected()) { return; } GeoPath.newRect(getBounds2D(rp.scale)).drawNormalState(rp); } public boolean isPointOnSymbol(java.awt.geom.Point2D point, double tolDist, double scale) { Rectangle2D rect = new Rectangle2D.Double(west-tolDist, south-tolDist, cellSize * cols + 2 * tolDist, cellSize * rows + 2 * tolDist); return rect.contains(point); } public boolean isIntersectedByRectangle(Rectangle2D rect, double scale) { return false; } public Rectangle2D getBounds2D(double scale) { return new Rectangle2D.Double(west, south, cellSize * cols, cellSize * rows); } /** * Returns the objects referenced at a specified position in the grid. * @param col Column of the cell from the left border. * @param row Row of the cell from the top border. * @return A shared instance of a set containing the referenced objects. */ public Set<GeoObject> getObjectsAtColRow(int col, int row) { if (col < 0 || col >= cols || row < 0 || row >= rows) return null; int id = col + row * cols; return this.refs[id]; } public Set getObjectsAtPosition(Point2D pt) { return this.getObjectsAtPosition(pt.getX(), pt.getY()); } public Set getObjectsAtPosition(double x, double y) { // find cell at position west / south int col = (int)((x - west) / cellSize); int row = (int)((this.getNorth() - y) / cellSize); return this.getObjectsAtColRow(col, row); } public Set<GeoObject> getCloseObjects(double x, double y, int searchRadiusInCells) { int col = (int)((x - west) / cellSize); int row = (int)((this.getNorth() - y) / cellSize); LinkedHashSet<GeoObject> set = new LinkedHashSet<GeoObject>(); for (int r = row - searchRadiusInCells; r <= row + searchRadiusInCells; r++) { for (int c = col - searchRadiusInCells; c <= col + searchRadiusInCells; c++) { Set<GeoObject> cellSet = getObjectsAtColRow(c, r); if (cellSet != null) set.addAll(cellSet); } } return set; } public int[] getStatistics(){ int[] stats = new int[6]; stats[STATS_TOTAL] = this.cols * this.rows; if (this.refs == null) return stats; for (int i = 0; i < this.refs.length; i++){ LinkedHashSet set = this.refs[i]; if (set != null) { int nbrRefs = set.size(); if (nbrRefs > 4) nbrRefs = 4; ++stats[nbrRefs - 1]; ++stats[STATS_OCCUPIED]; } } return stats; } @Override public String toString() { StringBuffer str = new StringBuffer(); str.append(super.toString()); str.append("\nCell Size\t: "); str.append(this.cellSize); str.append("\nColums\t: "); str.append(this.cols); str.append("\nRows\t: "); str.append(this.rows); int[] stats = this.getStatistics(); str.append("\nTotal number of cells\t: "); str.append(stats[STATS_TOTAL]); str.append("\nNumber of occupied cells\t: "); str.append(stats[STATS_OCCUPIED]); for (int i = 0; i < 4; i++){ str.append("\nNumber of cells with "); str.append(i + 1); str.append(" references\t: "); str.append(stats[i]); } return str.toString(); } @Override public void move(double dx, double dy) { this.west += dx; this.south += dy; MapEventTrigger.inform(this); } @Override public void scale(double scale) { this.west *= scale; this.south *= scale; this.cellSize *= scale; MapEventTrigger.inform(this); } public void transform(AffineTransform affineTransform) { throw new IllegalArgumentException("not supported yet"); } }