// ********************************************************************** // // <copyright> // // BBN Technologies // 10 Moulton Street // Cambridge, MA 02138 // (617) 873-8000 // // Copyright (C) BBNT Solutions LLC. All rights reserved. // // </copyright> // ********************************************************************** // // $Source: /cvs/distapps/openmap/src/openmap/com/bbn/openmap/omGraphics/OMScalingRaster.java,v $ // $RCSfile: OMScalingRaster.java,v $ // $Revision: 1.16 $ // $Date: 2009/01/21 01:24:41 $ // $Author: dietrick $ // // ********************************************************************** package com.bbn.openmap.omGraphics; import java.awt.Color; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Image; import java.awt.Point; import java.awt.Rectangle; import java.awt.geom.AffineTransform; import java.awt.geom.GeneralPath; import java.awt.geom.Point2D; import java.awt.image.AffineTransformOp; import java.awt.image.BufferedImage; import java.io.Serializable; import java.util.ArrayList; import javax.swing.ImageIcon; import com.bbn.openmap.omGraphics.util.ImageWarp; import com.bbn.openmap.proj.GeoProj; import com.bbn.openmap.proj.ProjMath; import com.bbn.openmap.proj.Projection; import com.bbn.openmap.proj.coords.GeoCoordTransformation; import com.bbn.openmap.proj.coords.LatLonGCT; import com.bbn.openmap.util.DataBounds; /** * This is an extension to OMRaster that automatically scales itelf to match the * current projection. It is only lat/lon based, and takes the coordinates of * the upper left and lower right corners of the image. It does straight scaling * - it does not force the image projection to match the map projection! So, * your mileage may vary - you have to understand the projection of the image, * and know how it fits the projection type of the map. Of course, at larger * scales, it might not matter so much. * * This class was inspired by, and created from parts of the ImageLayer * submission from Adrian Lumsden@sss, on 25-Jan-2002. Used the scaling and * trimming code from that submission. That code was also developed with * assistance from Steve McDonald at SiliconSpaceships.com. * * @see OMRaster * @see OMRasterObject */ public class OMScalingRaster extends OMRaster implements Serializable { /** * */ private static final long serialVersionUID = 1L; /** * The latitude of the lower right corner for the image, in decimal degrees. */ protected double lat2 = 0.0f; /** * The longitude of the lower right corner for the image, in decimal * degrees. */ protected double lon2 = 0.0f; /** * AffineTransformOp applied to the bitmap at render time. */ protected AffineTransformOp scalingXFormOp; /** * The rectangle in screen coordinates that the scaled image projects to * after clipping. */ protected transient Rectangle clipRect; protected transient ArrayList<float[]> corners; /** * Transform type for AffineTransformOp to use to scale images. */ protected int scaleTransformType = AffineTransformOp.TYPE_BILINEAR; /** * This lastProjection is used to keep track of the last projection used to * warp or scale the image, an used during the rendering process to check if * we should rework the image to be displayed. */ protected Projection lastProjection = null; /** * Construct a blank OMRaster, to be filled in with set calls. */ public OMScalingRaster() { super(); } // /////////////////////////////////// INT PIXELS - DIRECT // COLORMODEL /** * Creates an OMRaster images, Lat/Lon placement with a direct colormodel * image. * * @param ullat latitude of the top of the image. * @param ullon longitude of the left side of the image. * @param lrlat latitude of the bottom of the image. * @param lrlon longitude of the right side of the image. * @param w width of the image, in pixels. * @param h height of the image, in pixels. * @param pix color values for the pixels. */ public OMScalingRaster(double ullat, double ullon, double lrlat, double lrlon, int w, int h, int[] pix) { super(ullat, ullon, w, h, pix); lat2 = lrlat; lon2 = lrlon; } // //////////////////////////////////// IMAGEICON /** * Create an OMRaster, Lat/Lon placement with an ImageIcon. * * @param ullat latitude of the top of the image. * @param ullon longitude of the left side of the image. * @param lrlat latitude of the bottom of the image. * @param lrlon longitude of the right side of the image. * @param ii ImageIcon used for the image. */ public OMScalingRaster(double ullat, double ullon, double lrlat, double lrlon, ImageIcon ii) { this(ullat, ullon, lrlat, lrlon, ii.getImage()); } /** * Create an OMRaster, Lat/Lon placement with an Image. * * @param ullat latitude of the top of the image. * @param ullon longitude of the left side of the image. * @param lrlat latitude of the bottom of the image. * @param lrlon longitude of the right side of the image. * @param ii Image used for the image. */ public OMScalingRaster(double ullat, double ullon, double lrlat, double lrlon, Image ii) { super(); setRenderType(OMGraphic.RENDERTYPE_LATLON); setColorModel(COLORMODEL_IMAGEICON); lat = ullat; lon = ullon; lat2 = lrlat; lon2 = lrlon; setImage(ii); } // //////////////////////////////////// BYTE PIXELS with // COLORTABLE /** * Lat/Lon placement with a indexed colormodel, which is using a colortable * and a byte array to construct the int[] pixels. * * @param ullat latitude of the top of the image. * @param ullon longitude of the left side of the image. * @param lrlat latitude of the bottom of the image. * @param lrlon longitude of the right side of the image. * @param w width of the image, in pixels. * @param h height of the image, in pixels. * @param bytes colortable index values for the pixels. * @param colorTable color array corresponding to bytes * @param trans transparency of image. */ public OMScalingRaster(double ullat, double ullon, double lrlat, double lrlon, int w, int h, byte[] bytes, Color[] colorTable, int trans) { super(ullat, ullon, w, h, bytes, colorTable, trans); lat2 = lrlat; lon2 = lrlon; } /** * Since the image doesn't necessarily need to be regenerated when it is * merely moved, raster objects have this function, called from generate() * and when a placement attribute is changed. * * @return true if enough information is in the object for proper placement. * @param proj projection of window. */ protected boolean position(Projection proj) { if (proj == null) { if (DEBUG) { logger.fine("OMScalingRaster: null projection in position!"); } return false; } point1 = (Point) proj.forward(lat, lon, new Point()); point2 = (Point) proj.forward(lat2, lon2, new Point()); corners = null; if (point1.x > point2.x) { double[] coords = new double[] { lat, lon, lat, lon2, lat2, lon2, lat2, lon, lat, lon }; if (proj instanceof GeoProj) { corners = ((GeoProj) proj).forwardPoly(ProjMath.arrayDegToRad(coords), OMGraphic.LINETYPE_STRAIGHT, -1, true); } else { corners = proj.forwardPoly(coords, true); } if (corners != null && corners.size() > 2) { float[] xs = corners.get(0); float[] ys = corners.get(1); point1.setLocation(xs[0], ys[0]); point2.setLocation(xs[2], ys[2]); } } setNeedToReposition(false); return true; } /** * Prepare the graphics for rendering. For all image types, it positions the * image relative to the projection. For direct and indexed colormodel * images, it creates the ImageIcon used for drawing to the window (internal * to object). For indexed colormodel images, it also calls computePixels, * to resolve the colortable and the bytes to create the image pixels. * * @param proj Projection used to position the image on the window. * @return true if the image is ready to paint. */ public boolean generate(Projection proj) { if (!updateImageForProjection(proj)) { if (getNeedToReposition()) { position(proj); setShape(); } else { // Nothing changed with image placement, image is ready, we can // return at this point. setShape(); setNeedToRegenerate(false); return true; } } setShape(null); // Position sets the position for the OMRaster!!!! if (!position(proj)) { if (DEBUG) { logger.fine("OMRaster.generate(): positioning failed!"); } return false; } if (colorModel != COLORMODEL_IMAGEICON) { // If the sourceImage hasn't been created, and needs to // be, then just do what we normally do in OMRaster. if (bitmap == null || getNeedToRegenerate()) { if (DEBUG) { logger.fine("OMScalingRaster: generating image"); } super.generate(proj); // bitmap is set to a BufferedImage, but this does some other // stuff, too. setImage(bitmap); // Since we have a source image that is going to be reused, // let's get rid of the memory that we won't use anymore. pixels = null; bits = null; } } // point1 and point2 are already set in position() // We assume that the image doesn't cross the dateline, and // that p1 is upper left corner, and p2 is lower right. // scaleTo modifies the internal bitmap image for display. scaleTo(proj); if (bitmap != null) { if (corners == null) { GeneralPath projectedShape = createBoxShape(point1.x, point1.y, point2.x - point1.x, point2.y - point1.y); int w = bitmap.getWidth(this); int h = bitmap.getHeight(this); double anchorX = point1.x + w / 2; double anchorY = point1.y + h / 2; setShape(adjustShapeForRotation(projectedShape, anchorX, anchorY)); } else { int numRects = corners.size(); GeneralPath projectedShape = null; for (int i = 0; i < numRects; i += 2) { GeneralPath gp = createShape(corners.get(i), corners.get(i + 1), true); projectedShape = appendShapeEdge(projectedShape, gp, false); } if (projectedShape != null) { Rectangle rect = projectedShape.getBounds(); double anchorX = rect.getX() + rect.getWidth() / 2; double anchorY = rect.getY() + rect.getHeight() / 2; projectedShape = adjustShapeForRotation(projectedShape, anchorX, anchorY); } setShape(projectedShape); } setLabelLocation(getShape(), proj); setNeedToRegenerate(false); } else { // Make the label go away if it is off-screen. hasLabel = false; } return true; } /** * Called from within generate. Some render buffering calls generate to make * sure the latest projection is called on an OMGraphic before it's put into * a buffer. We're keeping track of the last projection used to generate the * warped image, and if it's the same, don't bother regenerating, use the * raster we have. This method is a question: do we need to update the image * because of a projection change? * * @param proj current projection. * @return false if the projection shouldn't cause anything to change for * the image. */ protected boolean updateImageForProjection(Projection proj) { boolean projUnchanged = proj.equals(lastProjection); boolean ret = bitmap != null && projUnchanged && !getNeedToRegenerate(); if (!projUnchanged) { lastProjection = proj.makeClone(); } evaluateRotationAngle(proj); return !ret; } /** * Since the OMScalingRaster changes height and width depending on scale, we * need to rotate the image over that point and factor in the scaled height * and width of the image. Called from within OMRasterObject.render(). * * @param g Graphics2D object to rotate and translate. */ protected void rotate(Graphics2D g) { Double angle = renderRotationAngle; if (angle != null) { int rotOffsetX = point1.x + (point2.x - point1.x) / 2; int rotOffsetY = point1.y + (point2.y - point1.y) / 2; g.rotate(angle, rotOffsetX, rotOffsetY); } } /** * Take the current projection and the sourceImage, and make the image that * gets displayed fit the projection. If the source image isn't over the * map, then this OMGraphic is set to be invisible. If part of the image is * on the map, only that part is used. The OMRaster bitmap variable is set * with an image that is created from the source image, and the point1 * variable is set to the point where the image should be placed. For * instance, if the source image upper left corner is off the map to the * NorthWest, then the OMRaster bitmap is set to a image, clipped from the * source, that is entirely on the map. The OMRaster point1 is set to 0, 0, * since that is where the clipped image should be placed. * * @param thisProj the projection that the image should be scaled to. */ protected void scaleTo(Projection thisProj) { if (DEBUG) { logger.fine("starting scaling evaluation."); } if (bitmap == null) { if (DEBUG) { logger.fine("source image is null"); } return; } // Get the projection window rectangle in pix Rectangle winRect = new Rectangle(thisProj.getWidth(), thisProj.getHeight()); // Get image projection rectangle Rectangle projRect = new Rectangle(); projRect.setLocation(point1); projRect.setSize(point2.x - point1.x, point2.y - point1.y); Rectangle sourceRect = new Rectangle(); sourceRect.width = bitmap.getWidth(this); sourceRect.height = bitmap.getHeight(this); // Now we have everything we need to sort out this new projection. // boolean currentVisibility = isVisible(); // Assume we will not see it, in order to see if any part of // the image will appear on map. If so, then reset visibility to // what's needed. // setVisible(false); clipRect = null; Rectangle iRect = projRect; // <= 2 is limiting this intersection to regular world - small world // will have multiple rects, corners We don't want to clip the bitmap // if we have to draw it on different parts of the map window (if it // wraps). if (corners == null || corners.size() <= 2) { iRect = winRect.intersection(projRect); } if (!iRect.isEmpty()) { // Now we have the visible rectangle of the projected // image we need to figure out which pixels from the // source image get scaled to produce it. // Assume will need whole image, set the clipRect so it's // on the map, somewhere. Rectangle nClipRect = new Rectangle(); nClipRect.setBounds(sourceRect); // If big enough to see if ((iRect.width >= 1) && (iRect.height >= 1)) { // If it didn't all fit if (!winRect.contains(projRect)) { // calc X scale factor double xScaleFactor = (double) sourceRect.width / (double) projRect.width; // and Y scale factor double yScaleFactor = (double) sourceRect.height / (double) projRect.height; // and the x offset int xOffset = iRect.x - projRect.x; // the y offset int yOffset = iRect.y - projRect.y; // Scale the x position nClipRect.x = (int) Math.floor(xOffset * xScaleFactor); // scale the y position nClipRect.y = (int) Math.floor(yOffset * yScaleFactor); // Do Math.ceil because the icon was getting // clipped a little if it started to move off the // screen a little. nClipRect.width = (int) Math.ceil(iRect.width * xScaleFactor); // scale // the width nClipRect.height = (int) Math.ceil(iRect.height * yScaleFactor); // scale // the height // Make sure the rounding doesn't exceed the // original icon bounds if (nClipRect.width + nClipRect.x > sourceRect.width) { nClipRect.width = sourceRect.width - nClipRect.x; } if (nClipRect.height + nClipRect.y > sourceRect.height) { nClipRect.height = sourceRect.height - nClipRect.y; } } // check width and height of clipRect, in case it got // rounded down to zero. if (nClipRect.width <= 0) { nClipRect.width = 1; } if (nClipRect.height <= 0) { nClipRect.height = 1; } // Now we can grab the bit we want out of the source // and scale it to fit the intersection. // Calc width adjustment double widthAdj = (double) iRect.width / (double) nClipRect.width; // Calc height adjustment double heightAdj = (double) iRect.height / (double) nClipRect.height; // Create the transform AffineTransform xform = new AffineTransform(); // Specify scaling xform.setToScale(widthAdj, heightAdj); clipRect = nClipRect; // Create the transform op. // AffineTransformOp xformOp = new AffineTransformOp(xform, // AffineTransformOp.TYPE_NEAREST_NEIGHBOR); this.scalingXFormOp = new AffineTransformOp(xform, getScaleTransformType()); point1.setLocation(iRect.x, iRect.y); point2.setLocation(iRect.x + iRect.width, iRect.y + iRect.height); } } } /** * Render the raster on the java.awt.Graphics. Overrides the raster method * because it checks to see if the raster is in a small-world situation, * where the image must wrap around the world. * * @param graphics java.awt.Graphics to draw the image on. */ public void render(Graphics graphics) { if (getNeedToRegenerate() || getNeedToReposition() || !isVisible()) { return; } boolean smallWorld = bitmap != null && corners != null && corners.size() >= 4; if (smallWorld) { float[] xs = corners.get(2); float[] ys = corners.get(3); Point point1 = new Point(); point1.setLocation((double) xs[0], (double) ys[0]); Point point2 = new Point(); point2.setLocation((double) xs[2], (double) ys[2]); // copy the graphic, so our transform doesn't cascade to // others... Graphics g = graphics.create(); if (g instanceof Graphics2D && renderRotationAngle != null) { // rotate about our image center point rotate((Graphics2D) g); } renderImage(g, bitmap, point1); } // render the location that is always set. super.render(graphics); } /** * Render the image at the given pixel location. This method should be * overridden for special Image handling. * * @param g the Graphics object to render the image into. Assumes this is a * derivative of the Graphics passed into the OMGraphic, and can be * modified without worrying about passing settings on to other * OMGraphics. * @param image the image to render. * @param loc the pixel location of the image. */ protected void renderImage(Graphics g, Image image, Point loc) { Rectangle visibleImageArea = getClippedRectangle(); if (image != null) { if (visibleImageArea != null) { if (DEBUG) { logger.fine("drawing " + visibleImageArea + " image at " + loc.x + ", " + loc.y); } if (g instanceof Graphics2D) { if (image instanceof BufferedImage) { ((Graphics2D) g).drawImage(((BufferedImage) image).getSubimage(visibleImageArea.x, visibleImageArea.y, visibleImageArea.width, visibleImageArea.height), scalingXFormOp, loc.x, loc.y); } else { int sx1 = visibleImageArea.x; int sy1 = visibleImageArea.y; int sx2 = sx1 + visibleImageArea.width; int sy2 = sy1 + visibleImageArea.height; int dx1 = loc.x; int dy1 = loc.y; Point2D d2 = scalingXFormOp.getPoint2D(new Point2D.Double(dx1 + visibleImageArea.width, dy1 + visibleImageArea.height), new Point2D.Double()); int dx2 = (int) d2.getX(); int dy2 = (int) d2.getY(); ((Graphics2D) g).drawImage(image, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, this); } } // else what? Never seen this test fail with Java2D } } else if (DEBUG) { logger.fine("ignoring null bitmap image"); } } /** * Return the rectangle in screen co-ordinates that the scaled image has * been clipped to. This may return a null rectangle (i.e. the image is out * of the window). Otherwise the returned rectangle should always at least * partially lie within the bounds of the window. */ public Rectangle getClippedRectangle() { return clipRect; } /** * Change the upper latitude attribute. * * @param value latitude in decimal degrees. */ public void setULLat(double value) { setLat(value); } /** * Get the upper latitude. * * @return the latitude in decimal degrees. */ public double getULLat() { return getLat(); } /** * Change the western longitude attribute. * * @param value the longitude in decimal degrees. */ public void setULLon(double value) { setLon(value); } /** * Get the western longitude. * * @return longitude in decimal degrees. */ public double getULLon() { return getLon(); } /** * Change the southern latitude attribute. * * @param value latitude in decimal degrees. */ public void setLRLat(double value) { if (lat2 == value) return; lat2 = value; setNeedToReposition(true); } /** * Get the southern latitude. * * @return the latitude in decimal degrees. */ public double getLRLat() { return lat2; } /** * Change the eastern longitude attribute. * * @param value the longitude in decimal degrees. */ public void setLRLon(double value) { if (lon2 == value) return; lon2 = value; setNeedToReposition(true); } /** * Get the eastern longitude. * * @return longitude in decimal degrees. */ public double getLRLon() { return lon2; } /** * Set the rectangle, based on the location and size of the image after * scaling. */ public void setShape() { if (point2 != null && point1 != null) { // generate shape that is a boundary of the generated image. // We'll make it a GeneralPath rectangle. int w = point2.x - point1.x; int h = point2.y - point1.y; setShape(createBoxShape(point1.x, point1.y, w, h)); } } /** * Test to see if projected image is on map. * * @param proj current projection * @return true of projected image location intersects map area. */ public boolean isOnMap(Projection proj) { Point2D p1 = proj.forward(lat, lon); Point2D p2 = proj.forward(lat2, lon2); int h = (int) Math.abs(p2.getY() - p1.getY()); int w = (int) Math.abs(p2.getX() - p1.getX()); Rectangle imageRect = new Rectangle((int) p1.getX(), (int) p1.getY(), w, h); proj.forward(proj.getUpperLeft(), p1); proj.forward(proj.getLowerRight(), p2); h = (int) Math.abs(p2.getY() - p1.getY()); w = (int) Math.abs(p2.getX() - p1.getX()); Rectangle mapRect = new Rectangle((int) p1.getX(), (int) p1.getY(), w, h); return mapRect.intersects(imageRect); } public int getScaleTransformType() { return scaleTransformType; } /** * Set the AffineTransformOp used for scaling images. Default is * AffineTransformOp.TYPE_BILINEAR. Can also be * AffineTransformOp.TYPE_BICUBIC or * AffineTransformOp.TYPE_NEAREST_NEIGHBOR. * * @param scaleTransformType */ public void setScaleTransformType(int scaleTransformType) { if (scaleTransformType == AffineTransformOp.TYPE_BILINEAR || scaleTransformType == AffineTransformOp.TYPE_BICUBIC || scaleTransformType == AffineTransformOp.TYPE_NEAREST_NEIGHBOR) { this.scaleTransformType = scaleTransformType; } } /** * Creates an ImageWarp object from the contents of the OMScalingRaster. * This can be used in an OMWarpingImage to be used for display in * projections that don't match the raster's projection. * * @param transform the OMScalingImage assumes that the coordinates/pixel * transformation of the image is equal arc. If it's not, the correct * transformation should be provided for this query. The * OMScalingRaster doesn't really know what it is, it just plots the * corner coordinates and scales the image accordingly. * @return ImageWarp an ImageWarp if all the required information was * provided, null if not. */ public ImageWarp getImageWarp(GeoCoordTransformation transform) { ImageWarp imageWarp = null; DataBounds imageBounds = new DataBounds(); imageBounds.add(lon, lat); imageBounds.add(lon2, lat2); if (pixels != null) { imageWarp = new ImageWarp(pixels, width, height, transform, imageBounds); } else { Image image = bitmap; if (image != null) { if (transform == null) { transform = new LatLonGCT(); } BufferedImage bi = null; if (image instanceof BufferedImage) { bi = (BufferedImage) image; } else { int w = image.getWidth(null); int h = image.getHeight(null); if (w <= 0 || h <= 0) { // Can't create image if one of these is -1 // (Interrupted). return imageWarp; } bi = new BufferedImage(image.getWidth(null), image.getHeight(null), BufferedImage.TYPE_INT_ARGB); bi.getGraphics().drawImage(image, 0, 0, null); } imageWarp = new ImageWarp(bi, transform, imageBounds); } } return imageWarp; } public void restore(OMGeometry source) { super.restore(source); if (source instanceof OMScalingRaster) { OMScalingRaster omsr = (OMScalingRaster) source; this.lat2 = omsr.lat2; this.lon2 = omsr.lon2; this.scaleTransformType = omsr.scaleTransformType; // OK, OK, I know this isn't a deep copy. TODO // this.sourceImage = omsr.sourceImage; } } }