// ********************************************************************** // // // // BBN Technologies, a Verizon Company // 10 Moulton Street // Cambridge, MA 02138 // (617) 873-8000 // // Copyright (C) BBNT Solutions LLC. All rights reserved. // // // ********************************************************************** // // $Source: /cvs/openmap/openmap/src/openmap/com/bbn/openmap/omGraphics/OMRasterObject.java,v $ // $RCSfile: OMRasterObject.java,v $ // $Revision: 1.5 $ // $Date: 2003/12/16 22:54:57 $ // $Author: dietrick $ // // ********************************************************************** package com.bbn.openmap.omGraphics; import java.awt.*; import java.awt.image.*; import java.awt.Point; import java.awt.geom.AffineTransform; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import com.bbn.openmap.proj.Projection; import com.bbn.openmap.util.Debug; import com.bbn.openmap.image.ImageHelper; /** * The OMRasterObject is the parent class for OMRaster and OMBitmap * objects. It manages some of the same functions that both classes * require in order to create image pixel data from bytes or integers. * *

An ImageFilter may be applied to OMRasterObjects. These can be * scale filters, color filters, or maybe (?hopefully?) projection * filters. These filters won't change the original image data, and * the original can be reconstructed by resetting the filter to null, * and generating the object.

* * For all classes in the OMRasterObject family, a java.awt.Shape * object is created for the border of the image. This Shape object * is used for distance calculations. If the OMRasterObject is * selected(), however, this Shape will be rendered with the OMGraphic * parameters that are set in the OMGraphic. */ public abstract class OMRasterObject extends OMGraphic implements Serializable, ImageObserver{ /** * The direct colormodel, for OMRasters, means the integer values * passed in as pixels, already reflect the RGB color values each * pixel should display. */ public final static int COLORMODEL_DIRECT = 0; /** * The indexed colormodel, for OMRasters, means that the byte * array passed in for the pixels has to be resolved with a * colortable in order to create a integer array of RGB pixels. */ public final static int COLORMODEL_INDEXED = 1; /** * The ImageIcon colormodel means that the image is externally * set, and we just want to to display the image at the given * location. */ public final static int COLORMODEL_IMAGEICON = 2; /** If scaling the image, use the slower, smoothing algorithm. */ public final static int SMOOTH_SCALING = 0; /** * If scaling the image, use the faster, replicating/clipping * algorithm. */ public final static int FAST_SCALING = 1; /** * colorModel helps figure out what kind of updates are * necessary, by knowing what kind of image we're dealing * with. For the images created with a ImageIcon, the attribute * updates that don't relate to position will not take affect. */ protected int colorModel = COLORMODEL_DIRECT; /** * The pixels are used for the image that is drawn on the * window. The pixels are either passed in as an int[] in some * constructors of the OMRaster, or it is constructed in the * OMBitmap and in OMRasters that have a colortable. */ protected int[] pixels = null; /** * Horizontal location of the upper left corner of the image, or the x * offset from the lon for that corner, in pixels. */ protected int x = 0; /** * Vertical location of the upper left corner of the image, or the y * offset from the lat for that corner, in pixels. */ protected int y = 0; /** * The latitude of the upper left corner for the image, in * decimal degrees. */ protected float lat = 0.0f; /** * The longitude of the upper left corner for the image, in * decimal degrees. */ protected float lon = 0.0f; /** * The width of the image, in pixels. This always reflects the * width of the original image, even if a filter is applied to the * image. */ protected int width = 0; /** * The height of the image, in pixels. This always reflects the * height of the original image, even if a filter is applied to the * image. */ protected int height = 0; /** * The byte info for the image. OMBitmaps use each bit as an * indication to use the lineColor or the fillColor for each pixel * (like a XBitmap). OMRasters only use the bits when the image * being created follows the indexed colormodel. Then, the bits * hold the colortable indexes that each pixel needs to have a * color substituted in later. */ protected byte[] bits = null; /** The bitmap is drawn to the graphics. */ protected transient Image bitmap = null; /** * Projected window pixel location of the upper left corner of * the image. */ protected Point point1 = null; /** * Projected window pixel location of the lower right corner of * the image. */ protected Point point2 = null; /** * The width of the image after scaling, if you want the image to * be a different size than the source. */ protected int filteredWidth = 0; /** * The height of the image after scaling, if you want the image to * be a different size than the source. */ protected int filteredHeight = 0; /** The image filter to use on the constructed image. */ protected ImageFilter imageFilter = null; /** * Set if the projection has had attributes change that require a * repositioning of the image, not a regeneration. */ protected boolean needToReposition = true; /** * Pixel height of the current projection. Used for efficient * zoom-in scaling. */ int projHeight; /** * Pixel width of the current projection. Used for efficient * zoom-in scaling. */ int projWidth; /** the angle by which the image is to be rotated, in radians */ protected double rotationAngle; protected boolean DEBUG = false; /** * A Contructor that sets the graphic type to raster, render type * to unknown, line type to unknown, and the declutter type to * none. */ public OMRasterObject () { super(RENDERTYPE_UNKNOWN, LINETYPE_UNKNOWN, DECLUTTERTYPE_NONE); DEBUG = Debug.debugging("omraster"); } /** * A Contructor that sets the graphic type, render type, line * type and the declutter type to the values you pass in. See * OMGraphic for the definitions of these attributes. * * @param rType render type * @param lType line type * @param dcType declutter type */ public OMRasterObject(int rType, int lType, int dcType) { super(rType, lType, dcType); DEBUG = Debug.debugging("omraster"); } /** * The color model is set based on the constructor. This setting * controls what parameter changes are posiible for different * models of images. * * @param cm the colormode that describes how the colors are being * set - COLORMODEL_DIRECT, COLORMODEL_INDEXED, or * COLORMODEL_IMAGEICON. */ protected void setColorModel(int cm) { colorModel = cm; } /** * Get the color model type of the image. * * @return COLORMODEL_DIRECT, COLORMODEL_INDEXED, or * COLORMODEL_IMAGEICON. */ public int getColorModel() { return colorModel; } /** * Set the flag for the object that lets the render method * (which draws the object) know that the object needs to be * repositioned first. */ public void setNeedToReposition(boolean value) { needToReposition = value; } /** Return the reposition status. */ public boolean getNeedToReposition() { return needToReposition; } /** * Set the angle by which the image is to rotated. * * @param angle the number of radians the image is to be * rotated. Measured clockwise from horizontal. */ public void setRotationAngle(double angle) { this.rotationAngle = angle; setNeedToRegenerate(true); } /** * Get the current rotation of the image. * * @return the image rotation. */ public double getRotationAngle() { return rotationAngle; } /** * Compute the raster objects pixels, based on the color model * and the byte values. * * @return true if everything goes OK (height*width = pixel.length, etc.). */ protected abstract boolean computePixels(); /** * Called from within render(). This method should call rotate() * on the provided Graphics2D object, setting the rotation angle * and the rotation point. By default, the rotation angle is * whatever is set in the OMRasterObject, and the rotation point * is the offset point plus half the image width in the horizonal * direction, and half the image in the vertical direction. */ protected void rotate(Graphics2D g) { ((Graphics2D)g).rotate(rotationAngle, point1.x+width/2, point1.y+height/2); } /** * Render the raster on the java.awt.Graphics * * @param g java.awt.Graphics to draw the image on. */ public void render(Graphics g) { if (getNeedToRegenerate() || getNeedToReposition() || !isVisible()) { if (DEBUG) Debug.output("OMRasterObject.render(): need to regenerate or not visible!"); return; } //copy the graphic, so our transform doesn't cascade to others... g = g.create(); // Just a little check to find out if someone is rushing // things. If a Image isn't fully loaded, the getWidth will // return -1. This is just a courtesy notification in case // someone isn't seeing their image, and don't know why. if (colorModel == COLORMODEL_IMAGEICON && (getWidth() == -1)) { if(DEBUG) Debug.error("OMRasterObject.render: Attempting to draw a Image that is not ready! Image probably wasn't available."); } if (g instanceof Graphics2D && rotationAngle != DEFAULT_ROTATIONANGLE) { //rotate about our image center point rotate((Graphics2D)g); } if (bitmap != null) { if (isSelected() || Debug.debugging("rasterobjects")) { super.render(g); } if (DEBUG) { Debug.output("OMRasterObject.render() | drawing " + width + "x" + height + " image at " + point1.x + ", " + point1.y); } if (g instanceof Graphics2D && bitmap instanceof RenderedImage) { // Affine translation for placement... ((Graphics2D)g).drawRenderedImage((BufferedImage)bitmap, new AffineTransform(1f, 0f, 0f, 1f, point1.x, point1.y)); // Undo the affine translation for future graphics?? ((Graphics2D)g).translate(-point1.x, -point1.y); } else { g.drawImage(bitmap, point1.x, point1.y, this); } } else { if (DEBUG) Debug.output("OMRasterObject.render: ignoring null bitmap"); } } /** * Set the rectangle, based on the location and size of the image. */ public void setShape() { // generate shape that is a boundary of the generated image. // We'll make it a GeneralPath rectangle. int w = width; int h = height; if (imageFilter != null) { w = filteredWidth; h = filteredHeight; } shape = createBoxShape(point1.x, point1.y, w, h); } /** * 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) Debug.error("OMRasterObject: null projection in position!"); return false; } projWidth = proj.getWidth(); projHeight = proj.getHeight(); switch (renderType) { case RENDERTYPE_LATLON: if (!proj.isPlotable(lat, lon)) { if (DEBUG) { Debug.error("OMRasterObject: point is not plotable!"); } setNeedToReposition(true);//so we don't render it! return false; } point1 = proj.forward(lat, lon); break; case RENDERTYPE_XY: point1 = new Point(x, y); break; case RENDERTYPE_OFFSET: if (!proj.isPlotable(lat, lon)) { if (DEBUG) { Debug.error("OMRasterObject: point is not plotable!"); } setNeedToReposition(true);//so we don't render it! return false; } point1 = proj.forward(lat, lon); point1.x += x; point1.y += y; break; case RENDERTYPE_UNKNOWN: if(DEBUG) Debug.output("OMRasterObject.position(): ignoring unknown rendertype, wingin' it"); if (lat == 0 && lon == 0) { if (x == 0 && y == 0) { if(DEBUG) Debug.output("OMRasterObject.position(): Not enough info in object to place it reasonably."); point1 = new Point(-width, -height); point2 = new Point(0,0); return false; } else { point1 = new Point(x, y); } } else { if (!proj.isPlotable(lat, lon)) { if(DEBUG) Debug.error("OMRasterObject: point is not plotable!"); return false; } point1 = proj.forward(lat, lon); } break; } point2 = new Point(0,0); point2.x = point1.x + width; point2.y = point1.y + height; setNeedToReposition(false); return true; } /** * Set the image to be drawn, if the color model is * COLORMODEL_IMAGEICON. * * @param ii the image icon to use. */ public void setImage(Image ii) { if (ii == null) { if(DEBUG) Debug.error("OMRasterObject.setImage(): image is null!"); return; } colorModel = COLORMODEL_IMAGEICON; bitmap = ii; // Make sure the image is ready to draw. If not, this method // will be called again by the ImageObserver method // imageUpdate. Set the height and width anyway. If they are // -1, you know the image isn't ready - another way to find // out. width = bitmap.getWidth(this); height = bitmap.getHeight(this); if (!(ii instanceof RenderedImage)) { Toolkit.getDefaultToolkit().prepareImage(bitmap, -1, -1, this); } } /** * Get the image that will be put on the window. * * @return the Image created by computePixels and generate(). */ public Image getImage() { return bitmap; } /** * Always true for images, affects distance measurements. Forces * the omGraphics package to treat the OMRasterObject as a filled * shape. */ public boolean shouldRenderFill() { return true; } /** * Set the pixels for the image for direct color model images. * Checks to see of the length matches the height * width, but * doesn't do anything if they don't match. Make sure it does. * * @param values the pixel values. */ public void setPixels(int[] values) { if (values.length != (height*width)) if(DEBUG) Debug.error("OMRasterObject: new pixel[] size (" + + values.length +") doesn't" + " match [height*width (" + height*width + ")]"); pixels = values; setNeedToRegenerate(true); } /** * Return the pixels used for the image. * * @return the integer array of ints used as integer colors for * each pixel of the image. */ public int[] getPixels() { return pixels; } /** * Change the x attribute, which matters only if the render type * is RENDERTYPE_XY or RENDERTYPE_OFFSET. * * @param value the x location in pixels. */ public void setX(int value) { if (x == value) return; x = value; setNeedToReposition(true); } /** * Returns the x attribute. * * @return the x value, pixels from left of window or image origin. */ public int getX() { return x; } /** * Change the y attribute, which matters only if the render type * is RENDERTYPE_XY or RENDERTYPE_OFFSET. * * @param value the y location in pixels */ public void setY(int value) { if (y == value) return; y = value; setNeedToReposition(true); } /** * Return the y attribute. * * @return the y value, pixels from top or image origin. */ public int getY() { return y; } /** * Return the map location of the image, after generation. * * @return Point, null if not projected yet. */ public Point getMapLocation() { return point1; } /** * Change the latitude attribute, which matters only if the render * type is RENDERTYPE_LATLON or RENDERTYPE_OFFSET. * * @param value latitude in decimal degrees. */ public void setLat(float value) { if (lat == value) return; lat = value; setNeedToReposition(true); } /** * Get the latitude. * * @return the latitude in decimal degrees. */ public float getLat() { return lat; } /** * Change the longitude attribute, which matters only if the * render type is RENDERTYPE_LATLON or RENDERTYPE_OFFSET. * * @param value the longitude in decimal degrees. */ public void setLon(float value) { if (lon == value) return; lon = value; setNeedToReposition(true); } /** * Get the longitude. * * @return longitude in decimal degrees. */ public float getLon() { return lon; } /** * Set the height of the image, in pixels. * * @param value height in pixels. */ public void setHeight(int value) { if (height == value) return; setNeedToRegenerate(true); height = value; } /** * Get the height of image. * * @return height in pixels. */ public int getHeight() { return height; } /** * Get the height of image after a filter was applied. * * @return filteredHeight in pixels. */ public int getFilteredHeight() { return filteredHeight; } /** * Set width of image. * * @param value width in pixels. */ public void setWidth(int value) { if (width == value) return; setNeedToRegenerate(true); width = value; } /** * Get width of image. * * @return width of image in pixels. */ public int getWidth() { return width; } /** * Get width of image, after a filter is applied. * * @return filteredWidth of image in pixels. */ public int getFilteredWidth() { return filteredWidth; } /** * Set the bytes used to create the pixels used to create the * image. Used for indexed color model images in OMRaster, and * OMBitmaps. * * @param values byte values */ public void setBits(byte[] values) { setNeedToRegenerate(true); bits = values; } /** * Get the byte values for indexed color model images and * OMBitmaps. * * @return the bytes used to create the pixels. */ public byte[] getBits() { return bits; } /** * Set a filter to be used on the constructed image. Applied at * generate(). * * @param filter Image filter to apply to contructed raster. */ public void setImageFilter(ImageFilter filter) { imageFilter = filter; filteredWidth = width; filteredHeight = height; setNeedToRegenerate(true); } /** * Return the image filter used on the image. * * @return imagefilter, null if one wasn't set. */ public ImageFilter getImageFilter() { return imageFilter; } /** * Convenience function to scale the Image to the xy size. Sets * the imageFilter to a ReplicateScaleFilter or * AreaAveragingScaleFilter, depending on the algorithm type. * * @param w width to scale to, in pixels * @param h height to scale to, in pixels * @param algorithmType OMRasterObject parameter describing which * scaling algorithm to use. */ public void scaleTo(int w, int h, int algorithmType) { filteredWidth = w; filteredHeight = h; imageFilter = new TrimScaleFilter(filteredWidth, filteredHeight, algorithmType); setNeedToRegenerate(true); } /** * A method used to manipulate the image according to the parameters * set by the imageFilter in the OMRasterObject. Called from * generate() if the filteredWidth and filteredHeight differ from * width and height. * * @return the filtered image. */ protected Image filterImage() { // Can we do a little clipping here?? If it's been projected, maybe. // See if the frame is getting blown up, probably by at // least a certain margin, so we know that there will be time // savings as well as memory savings. if (imageFilter instanceof TrimScaleFilter) { TrimScaleFilter tf = (TrimScaleFilter)imageFilter; Image img = tf.trimExcessPixels(); if (img != null) { bitmap = img; imageFilter = tf.getFilterWithChanges(); // we can play around with point1, since that is where the // image is getting laid out. If point1.x or point1.y < 0, we // can set it to zero. Assumes that the image has already // been positioned. if (point1.x < 0) point1.x = 0; if (point1.y < 0) point1.y = 0; if (DEBUG) Debug.output("OMRasterObject: newly located at " + point1); } else if (DEBUG) { Debug.output("OMRasterObject: not being trimmed due to projection"); } } ImageProducer prod = new FilteredImageSource(bitmap.getSource(), imageFilter); if (Toolkit.getDefaultToolkit() != null) return Toolkit.getDefaultToolkit().createImage(prod); else return bitmap; } /** * From the Image Observer Interface. Called when the image * bits have arrived, and therefore calls setImage() to reset all * the OMRasterObject parameters.
* Don't call this method! */ public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) { if ((infoflags&ImageObserver.ALLBITS) != 0) { if (colorModel == COLORMODEL_IMAGEICON) { setImage(img); } return false;// all set } return true;// need more info } /** * This is an effort to create an scaling ImageFilter that will * trim off the unused pixels, lessoning the load on the display * server. It depends on knowing several things about the * projection and the current image parameters, which is why it's * not a stand-alone filter class. */ protected class TrimScaleFilter extends AreaAveragingScaleFilter { ImageFilter actualFilter = null; int algorithmType; /** * Constructs an TrimScaleFilter that scales the pixels from * its source Image as specified by the width and height parameters. * @param width the target width to scale the image * @param height the target height to scale the image */ public TrimScaleFilter(int width, int height) { super(width, height); algorithmType = FAST_SCALING; } /** * Constructs an AreaAveragingScaleFilter that scales the pixels from * its source Image as specified by the width and height parameters. * @param width the target width to scale the image * @param height the target height to scale the image * @param algorithmType FAST_SCALING or SMOOTH_SCALING - FAST * is much faster! */ protected TrimScaleFilter(int width, int height, int algorithmType) { super(width, height); this.algorithmType = algorithmType; } /** * Detect if the data is being delivered with the necessary * hints to allow the averaging algorithm to do its work. If * the algorithmType is set to FAST, I manipulate the hints to * force the filter to act like a ReplicateScaleFilter. * @see ImageConsumer#setHints */ public void setHints(int hints) { int passthrough = hints; if (algorithmType == FAST_SCALING) { /// XOR passthrough = hints ^ ImageConsumer.TOPDOWNLEFTRIGHT; } super.setHints(passthrough); } /** * The filter must change if the requested image size changes * because of clipping. Get the good filter here, after * calling trimExcessPixels(). */ protected ImageFilter getFilterWithChanges() { if (actualFilter == null) { return this; } return actualFilter; } /** * Get a trimmed-down image to expand to the map, that * contains all the pixels that will be visible after * expansion. Returns null if the image should be used as is, * and the filter as well. */ protected Image trimExcessPixels() { if (filteredWidth <= width && filteredHeight <= height) { if (DEBUG) Debug.output("TrimScaleFilter.trimExcessPixels(): image not enlarged, using entire image."); return null; } if (DEBUG) Debug.output("TrimScaleFilter.trimExcessPixels(): clipping enlarged image."); // Figure out the pixels of the old image being used in // the new image. Figure out the proj location of the upper // left pixel of the new image. We want to subsitute this // proj location for the projection location already // calculated. This should get overwritten later for any // projection changes. float widthScale = (float)filteredWidth/(float)width; float heightScale = (float)filteredHeight/(float)height; int startXPixelInSource = point1.x < 0?(int)((-1.0*point1.x)/widthScale):0; int startYPixelInSource = point1.y < 0?(int)((-1*point1.y)/heightScale):0; Point scaledDim = new Point((int)(point1.x + (width*widthScale)), (int)(point1.y +(height*heightScale))); int endXPixelInSource = (scaledDim.x > projWidth? (int)((projWidth - point1.x)/widthScale)+1:width); int endYPixelInSource = scaledDim.y > projHeight? (int)((projHeight - point1.y)/heightScale)+1:height; if (DEBUG) { Debug.output("TrimScaleFilter.trimExcessPixels(): image contributes " + startXPixelInSource + ", " + startYPixelInSource + " to " + endXPixelInSource + ", " + endYPixelInSource); } // Create a buffered image out of the old image, clipping // out the unused pixels. if (DEBUG) { Debug.output("TrimScaleFilter.trimExcessPixels(): " + " new dimensions of scaled image " + (int)((endXPixelInSource - startXPixelInSource) * widthScale) + ", " + (int)((endYPixelInSource - startYPixelInSource) * heightScale)); } // Get only the pixels you need. // Use a pixel grabber to get the right pixels. PixelGrabber pg = new PixelGrabber(bitmap, startXPixelInSource, startYPixelInSource, endXPixelInSource - startXPixelInSource, endYPixelInSource - startYPixelInSource, true); int[] pix = ImageHelper.grabPixels(pg); if (pix == null) { return null; } // Set the filter to the demisnsions. Need to remember to ask for this!!! actualFilter = new TrimScaleFilter( (int)((endXPixelInSource - startXPixelInSource) * widthScale), (int)((endYPixelInSource - startYPixelInSource) * heightScale), algorithmType); // create the new bitmap, which holds the image that gets drawn Toolkit tk = Toolkit.getDefaultToolkit(); Image image = tk.createImage( new MemoryImageSource(endXPixelInSource - startXPixelInSource, endYPixelInSource - startYPixelInSource, pix, 0, endXPixelInSource - startXPixelInSource)); return image; } } /** * Code derived from * http://www.dcs.shef.ac.uk/~tom/Java/Power/image_serialization.html */ private void writeObject(ObjectOutputStream objectstream) throws IOException { //write non-transient, non-static data objectstream.defaultWriteObject(); PixelGrabber grabber = new PixelGrabber(bitmap, 0, 0, -1, -1, true); if (colorModel == COLORMODEL_IMAGEICON) { try { grabber.grabPixels(); } catch (InterruptedException e) { System.out.println("error grabbing pixels"); } Object pix = grabber.getPixels(); Dimension dim = new Dimension(bitmap.getWidth(this), bitmap.getHeight(this)); objectstream.writeObject(dim); objectstream.writeObject(pix); } } /** * Code derived from * http://www.dcs.shef.ac.uk/~tom/Java/Power/image_serialization.html */ private void readObject(ObjectInputStream objectstream) throws IOException, ClassNotFoundException { Toolkit toolkit = Toolkit.getDefaultToolkit(); try { //read non-transient, non-static data objectstream.defaultReadObject(); Dimension dim = (Dimension)objectstream.readObject(); Object img = objectstream.readObject(); int [] pix = (int [])img; bitmap = toolkit.createImage(new MemoryImageSource(dim.width, dim.height, pix, 0, dim.width)); } catch (ClassNotFoundException ce) { System.out.println("class not found"); } } protected boolean hasLineTypeChoice() { return false; } }