// ********************************************************************** // // <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/layer/terrain/LOSGenerator.java,v $ // $RCSfile: LOSGenerator.java,v $ // $Revision: 1.7 $ // $Date: 2005/12/09 21:09:11 $ // $Author: dietrick $ // // ********************************************************************** package com.bbn.openmap.layer.terrain; import java.awt.Color; import java.awt.Point; import java.awt.event.MouseEvent; import com.bbn.openmap.MoreMath; import com.bbn.openmap.dataAccess.dted.DTEDFrameCache; import com.bbn.openmap.event.LayerStatusEvent; import com.bbn.openmap.event.ProgressEvent; import com.bbn.openmap.event.ProgressListener; import com.bbn.openmap.event.ProgressSupport; import com.bbn.openmap.gui.ProgressListenerGauge; import com.bbn.openmap.omGraphics.OMCircle; import com.bbn.openmap.omGraphics.OMGraphicList; import com.bbn.openmap.omGraphics.OMRaster; import com.bbn.openmap.proj.Planet; import com.bbn.openmap.proj.Projection; import com.bbn.openmap.proj.coords.LatLonPoint; import com.bbn.openmap.util.Debug; import com.bbn.openmap.util.SwingWorker; import com.bbn.openmap.util.stateMachine.State; /** * The LOSGenerator uses gestures to create a mask over the map. The * circular mask of green pixels shows what places are within the * sight of the center of the circle. Additional height can be added * to the center of the circle via the TerrainLayer palette, to * represent a tower, building, or aircraft. */ public class LOSGenerator implements TerrainTool { // These are used to control the algorithm type. Right now, the // first two are eliminated, since the azimuth algorithm is // faster // and more precise. final static int PRECISE = 0; final static int GOODENOUGH = 1; final static int AZIMUTH = 2; // RED Color toolColor = new Color(255, 0, 0); // The colors of pixels final static int INVISIBLE = 0; final static int VISIBLE = 1; final static int MAYBEVISIBLE = 2; int[] colortable; Projection proj; protected LOSStateMachine stateMachine; TerrainLayer layer; /** Lat lon of the center of hte circle. */ LatLonPoint LOScenterLLP; /** The xy of the center of the circle. */ Point LOScenterP = new Point(); /** The height of the earth at the center point. */ int LOScenterHeight; /** The height of the object at the center point. */ int LOSobjectHeight = 0; /** The diameter of the circle of interest. */ int LOSedge; protected OMGraphicList graphics = new OMGraphicList(); OMRaster LOSimage; // The image for the mask OMCircle LOScirc; // The circle modified for the image definition int LOSprecision; // The flag for the algorithm type LatLonPoint LOSOffPagell = new LatLonPoint.Double(-79f, -170f); Point LOSOffPagep1 = new Point(-10, -10); /** The thread worker used to create the Terrain images. */ LOSWorker currentWorker; /** * Set when the projection has changed while a swing worker is * gathering graphics, and we want him to stop early. */ protected boolean cancelled = false; protected ProgressSupport progressSupport; class LOSWorker extends SwingWorker { /** Constructor used to create a worker thread. */ public LOSWorker() {} /** * Compute the value to be returned by the <code>get</code> * method. */ public Object construct() { Debug.message("terrain", layer.getName() + "|LOSWorker.construct()"); layer.fireStatusUpdate(LayerStatusEvent.START_WORKING); createLOSImage(); return null; } /** * Called on the event dispatching thread (not on the worker * thread) after the <code>construct</code> method has * returned. */ public void finished() { layer.fireStatusUpdate(LayerStatusEvent.FINISH_WORKING); workerComplete(); } } /** * Not the preferred way to create one of these. It's full of * defaults. */ private LOSGenerator() { init(); } /** * The creation of the tool starts here. The DTED data cache is * passed in, along with a path to the dted directory to get more * data if needed. */ public LOSGenerator(TerrainLayer tLayer) { layer = tLayer; init(); } public synchronized OMGraphicList getGraphics() { return graphics; } public State getState() { return stateMachine.getState(); } public void init() { progressSupport = new ProgressSupport(this); addProgressListener(new ProgressListenerGauge("LOS Mask Creation")); // colortable colortable = new int[3]; colortable[INVISIBLE] = new Color(0, 0, 0, 0).getRGB(); colortable[VISIBLE] = new Color(0, 255, 0, 255).getRGB(); colortable[MAYBEVISIBLE] = new Color(255, 255, 0, 255).getRGB(); stateMachine = new LOSStateMachine(this); // set the graphics reset(true, true); graphics.add(LOSimage); graphics.add(LOScirc); } public void doImage() { // If there isn't a worker thread working on this already, // create a thread that will do the real work. If there is // a thread working on this, then set the cancelled flag // in the layer. if (currentWorker == null) { currentWorker = new LOSWorker(); currentWorker.execute(); } else setCancelled(true); } /** * The TerrainWorker calls this method on the layer when it is * done working. If the calling worker is not the same as the * "current" worker, then a new worker is created. */ protected synchronized void workerComplete() { if (!isCancelled()) { currentWorker = null; layer.repaint(); } else { setCancelled(false); currentWorker = new LOSWorker(); currentWorker.execute(); } } /** * Used to set the cancelled flag in the layer. The swing worker * checks this once in a while to see if the projection has * changed since it started working. If this is set to true, the * swing worker quits when it is safe. */ public synchronized void setCancelled(boolean set) { cancelled = set; } /** Check to see if the cancelled flag has been set. */ public synchronized boolean isCancelled() { return cancelled; } /** * Without arguments, the reset() call makes both graphics go * offscreen in their smallest size. */ public void reset() { reset(true, true); } /** * Circ is for the circle to be reset, and image is for the image * to be reset. Sometimes you only want one to be moved. */ public void reset(boolean circ, boolean image) { graphics.clear(); if (image) { LOSimage = new OMRaster(LOSOffPagell.getLatitude(), LOSOffPagell.getLongitude(), LOSOffPagep1.x, LOSOffPagep1.y, 1, 1, new int[1]); } if (circ) { LOScirc = new OMCircle(LOSOffPagell.getLatitude(), LOSOffPagell.getLongitude(), 1, 1); LOScirc.setLinePaint(toolColor); } layer.repaint(); stateMachine.reset(); } /** * Called on every getRectangle, in order to let the cache get * sized right, and to reset the graphics if the scale changed * (since they won't make sense. */ public void setScreenParameters(Projection p) { //reset(true, true); proj = p; LOSprecision = AZIMUTH; graphics.generate(proj); } /** * Takes the member settings and manages the creation of the * image. A large vector of slope values are created, depending on * the size of the circle, and how many pixels are around it. Each * entry in the vector is the value of the largest slope value in * that direction. The image is created from the inside out, pixel * by pixel. The slope from the pixel to the center is calculated, * and then compared with the value for that direction (in the * vector). If the pixel's slope is larger, the point is visible, * and is colored that way. The vector is updated, and the cycle * continues. */ public synchronized void createLOSImage() { if (Debug.debugging("los")) { Debug.output("createLOSimage: Entered with diameter = " + LOSedge); } if (layer == null || layer.frameCache == null) { Debug.error("LOSGenerator: can't access the DTED data through the terrain layer."); return; } int squareRadius = LOSedge / 2 + 1; int[] newPixels = new int[LOSedge * LOSedge]; float[] azimuthVals = new float[8 * (squareRadius - 1)]; // center point of raster newPixels[((LOSedge / 2) * LOSedge) + squareRadius] = MAYBEVISIBLE; if (Debug.debugging("los")) { Debug.output("createLOSimage: size of azimuth array = " + azimuthVals.length); } fireProgressUpdate(ProgressEvent.START, "Building LOS Image Mask...", 0, 100); int x, y; boolean mark = false; int markColor = colortable[INVISIBLE]; int range; float pix_arc_interval = (float) (2 * Math.PI / azimuthVals.length); // Do this in a spiral, around the center point. for (int round = 1; round < squareRadius; round++) { if (Debug.debugging("los")) { Debug.output("createLOSimage: round " + round); } y = LOScenterP.y - round; x = LOScenterP.x - round; if (round == 1) { mark = true; markColor = colortable[MAYBEVISIBLE]; } else mark = false; if (LOSprecision == AZIMUTH) { // As of now, this is the // only option range = ((LOSedge * 4) - 4) / (round * 16); for (; x < LOScenterP.x + round; x++) // top resolveImagePoint(x, y, newPixels, azimuthVals, range, pix_arc_interval, mark, markColor); for (; y < LOScenterP.y + round; y++) // right resolveImagePoint(x, y, newPixels, azimuthVals, range, pix_arc_interval, mark, markColor); for (; x > LOScenterP.x - round; x--) // bottom resolveImagePoint(x, y, newPixels, azimuthVals, range, pix_arc_interval, mark, markColor); for (; y > LOScenterP.y - round; y--) // left resolveImagePoint(x, y, newPixels, azimuthVals, range, pix_arc_interval, mark, markColor); } int whereWeAre = (int) (100f * ((float) round / (float) squareRadius)); fireProgressUpdate(ProgressEvent.UPDATE, "Analyzing data...", whereWeAre, 100); } fireProgressUpdate(ProgressEvent.UPDATE, "Creating Mask", 100, 100); LOSimage = new OMRaster(LOScenterLLP.getLatitude(), LOScenterLLP.getLongitude(), (-1 - LOSedge / 2), (-1 - LOSedge / 2), LOSedge, LOSedge, newPixels); LOSimage.generate(proj); graphics.clear(); graphics.add(LOSimage); fireProgressUpdate(ProgressEvent.DONE, "LOS mask complete", 100, 100); if (Debug.debugging("los")) { Debug.output("createLOSimage: Done..."); } } /** * Calculates the color for each pixel. After is gets the slope * value for that pixel, it manages the comparison to get the * pixel colored correctly. */ protected void resolveImagePoint(int x, int y, int[] newPixels, float[] azimuthVals, int range, float pix_arc_interval, boolean mark, int colorForMark) { int ox = LOScenterP.x - LOSedge / 2; int oy = LOScenterP.y - LOSedge / 2; int dist = TerrainLayer.numPixelsBetween(LOScenterP.x, LOScenterP.y, x, y); if (dist > (LOSedge - 1) / 2) { mark = true; colorForMark = INVISIBLE; } if (dist == (LOSedge - 1) / 2) { mark = true; colorForMark = MAYBEVISIBLE; } // This needs to be before the next two lines after this LatLonPoint cord = proj.inverse(x, y, new LatLonPoint.Double()); x -= ox; y -= oy; if (Debug.debugging("losdetail")) { Debug.output("resolveImagePoint x = " + x + ", y = " + y); } if (mark == true) { newPixels[x + y * LOSedge] = colorForMark; mark = false; return; } double arc_dist = LOScenterLLP.distance(cord); double arc_angle = LOScenterLLP.azimuth(cord); double slope = calculateLOSslope(cord, arc_dist); int index = (int) Math.round(arc_angle / pix_arc_interval); int maxIndex = (LOSedge * 4) - 4; // 4 corners out for // redundancy if (index < 0) index += maxIndex; else if (index >= maxIndex) index -= maxIndex; if (Debug.debugging("losdetail")) { Debug.output(" angle = " + arc_angle + ", index/maxIndex = " + index + "/" + maxIndex + ", slope = " + slope + " compared to slope[index]=" + azimuthVals[index]); } int color = colortable[INVISIBLE]; if (azimuthVals[index] < slope) { for (int i = (index - range); i < index + range - 1; i++) { if (i < 0) azimuthVals[maxIndex + i] = (float)slope; else if (i >= maxIndex) azimuthVals[i - maxIndex] = (float)slope; else azimuthVals[i] = (float)slope; } color = colortable[VISIBLE]; } if (Debug.debugging("losdetail")) { Debug.output(" color = " + color); } newPixels[x + y * LOSedge] = color; } /** * CalculateLOSslope figures out the slope from the pixel to the * center, in radians. The arc_dist is in radians, and is the * radian arc distance of the point from the center point of the * image, on the earth. This slope calculation does take the * earth's curvature into account, based on the spherical model. */ protected double calculateLOSslope(LatLonPoint cord, double arc_dist) { DTEDFrameCache frameCache = layer.frameCache; if (frameCache == null) { return 0; } int xyheight = frameCache.getElevation(cord.getLatitude(), cord.getLongitude()); double ret = 0; double P = Math.sin(arc_dist) * (xyheight + Planet.wgs84_earthEquatorialRadiusMeters); double xPrime = Math.cos(arc_dist) * (xyheight + Planet.wgs84_earthEquatorialRadiusMeters); double bottom; double cutoff = LOScenterHeight + Planet.wgs84_earthEquatorialRadiusMeters; // Suggested changes, submitted by Mark Wigmore. Introduces // use of doubles, and avoidance of PI/2 tan() calculations. bottom = cutoff - xPrime; ret = MoreMath.HALF_PI_D - Math.atan(bottom / P); return ret; // Old way... // if (xPrime < cutoff) { // bottom = cutoff - xPrime; // ret = Math.atan(P/bottom); // } else if (xPrime == cutoff) { // ret = MoreMath.HALF_PI_D; // } else if (xPrime > cutoff) { // double C = xPrime - cutoff; // double gamma = Math.atan(P/C); // ret = Math.PI - gamma; // } // return ret; } /** * Called when the circle is started. It starts the circle to be * drawn, and sets the parameters that will be needed to figure * out the image. * * @param event mouse event where the circle should be started. */ public void setCenter(MouseEvent event) { graphics.clear(); LOScenterP.x = event.getX(); LOScenterP.y = event.getY(); LOScenterLLP = proj.inverse(LOScenterP.x, LOScenterP.y, new LatLonPoint.Double()); LOScenterHeight = LOSobjectHeight; if (layer.frameCache != null) { LOScenterHeight += layer.frameCache.getElevation(LOScenterLLP.getLatitude(), LOScenterLLP.getLongitude()); } LOScirc.setLatLon(LOScenterLLP.getLatitude(), LOScenterLLP.getLongitude()); LOScirc.generate(proj); graphics.add(LOScirc); } /** * Used to modify the circle parameters with another mouse event. * Takes care of resetting hte circle parameters and regenerating * the circle. */ public void addLOSEvent(MouseEvent event) { graphics.clear(); LOSedge = TerrainLayer.numPixelsBetween(LOScenterP.x, LOScenterP.y, event.getX(), event.getY()) * 2 + 1; LOScirc.setWidth(LOSedge); LOScirc.setHeight(LOSedge); LOScirc.generate(proj); graphics.add(LOScirc); } /** * Sets the new object height to use at the center of the circle. * The old object is subtracted out first to get the center height * of the ground before the new value is added. * * @param value height of the object in meters. */ public void setLOSobjectHeight(int value) { LOScenterHeight -= LOSobjectHeight; LOSobjectHeight = value; LOScenterHeight += LOSobjectHeight; } /** * Add a ProgressListener that will display build progress. */ public void addProgressListener(ProgressListener list) { progressSupport.add(list); } /** * Remove a ProgressListener that displayed build progress. */ public void removeProgressListener(ProgressListener list) { progressSupport.remove(list); } /** * Clear all progress listeners. */ public void clearProgressListeners() { progressSupport.clear(); } /** * Fire an build update to progress listeners. * * @param frameNumber the current frame count * @param totalFrames the total number of frames. */ protected void fireProgressUpdate(int type, String task, int frameNumber, int totalFrames) { progressSupport.fireUpdate(type, task, totalFrames, frameNumber); } }