diff --git a/src/core/src/main/java/com/bbn/openmap/layer/terrain/MultiLOS/MultiLOSLayer.java b/src/core/src/main/java/com/bbn/openmap/layer/terrain/MultiLOS/MultiLOSLayer.java new file mode 100644 index 00000000..27c4ee08 --- /dev/null +++ b/src/core/src/main/java/com/bbn/openmap/layer/terrain/MultiLOS/MultiLOSLayer.java @@ -0,0 +1,536 @@ +package com.bbn.openmap.layer.terrain.MultiLOS; + +import java.awt.geom.Point2D; + +import com.bbn.openmap.dataAccess.dted.DTEDDirectoryHandler; +import com.bbn.openmap.dataAccess.dted.DTEDFrameCache; +import com.bbn.openmap.layer.OMGraphicHandlerLayer; +import com.bbn.openmap.omGraphics.OMCircle; +import com.bbn.openmap.omGraphics.OMColor; +import com.bbn.openmap.omGraphics.OMGraphicList; +import com.bbn.openmap.omGraphics.OMPoint; +import com.bbn.openmap.omGraphics.OMText; +import com.bbn.openmap.proj.Length; +import com.bbn.openmap.proj.Planet; +import com.bbn.openmap.proj.Projection; +import com.bbn.openmap.proj.coords.LatLonPoint; +import com.bbn.openmap.tools.terrain.LOSGenerator; +import com.bbn.openmap.util.PropUtils; +import java.awt.Color; +import java.awt.Component; +import java.awt.GridLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSpinner; +import javax.swing.SpinnerNumberModel; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +/** + * An OpenMap Layer to display an LOS map for a number of viewpoints, from a given altitude + * + *

+
+ ############################
+ # Example Properties for a MultiLOS layer
+ multilos.class=com.bbn.openmap.layer.terrain.MultiLOS.MultiLOSLayer
+ multilos.prettyName=MultiLOS
+ 
+ # Properties for the los calculations
+ # Altitude is MSL. This is the default altitude if it is not specified on a viewpoint
+ multilos.altitude=500
+ multilos.altitudeUnits=M
+ # Max viable sensor distance
+ # This is the default range if it is not specified on a viewpoint
+ multilos.maxRange=200
+ multilos.maxRangeUnits=KM
+ # viewpoints: Semicolon-separated list of lat,lon pairs separated by commas.
+ # lat,lon[,alt[,sensorRange]]
+ multilos.viewPoints=22.3,116.0;24.3,119.7,100,1000
+ # If you don't want to specify a list of viewpoints, use this to specify a series of lines:
+ # semicolon-separated list, StartLat,StartLon,EndLat,EndLon,NumPointsBetweenStartEnd
+ multilos.viewPointLines=
+ # Whether to indicate viewpoint properties
+ multilos.showHorizons=TRUE
+ multilos.showViewPoints=TRUE
+ multilos.showMaxRanges=TRUE
+ multilos.showNumberPoints=TRUE
+ # LOS is calculated per-screen-point, with
+ #  n_samples = pixels_between_viewpoint_and_screen_point / pixelSkip
+ # Number of pixels per point - high numbers are faster/lower resolution. don't go below 1
+ multilos.pixelsPerPoint=2
+ # Multiplier to make rendering faster but less accurate. 1 == "slowest, most accurate"
+ multilos.pixelSkip=3
+ # color of fill. Leaving out means we won't fill that type [canSee or canNotSee]
+ multilos.canSeeColor=4400ff00
+ multilos.canNotSeeColor=44ff0000
+ # DTED
+ multilos.dtedLevel=0
+ multilos.dtedDir=/data/dted/dted0
+ ############################
+ 
+ 
+ * + * @author Gary Briggs + */ +public class MultiLOSLayer extends OMGraphicHandlerLayer { + // Class that represents a viewpoint + private class MultiLOSViewPoint { + LatLonPoint p; + double altitude; + double maxRange; + + public MultiLOSViewPoint(LatLonPoint p, double altitude, double maxRange) { + this.p = p; + this.altitude = altitude; + this.maxRange = maxRange; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(p.getLatitude()); + sb.append(","); + sb.append(p.getLongitude()); + sb.append(","); + sb.append(altitude); + sb.append(","); + sb.append(maxRange); + return sb.toString(); + } + + + }; + + // Properties from the user + double altitude = 500.0; + Length altitudeUnits = Length.METER; + List viewPoints = new ArrayList(); + boolean showHorizons = true; + boolean showViewPoints = true; + boolean showMaxRanges = true; + boolean showNumberPoints = true; + String dtedDir = "/data/dted/dted0"; + int dtedLevel = 0; + int pixelsPerPoint = 2; + double pixelSkip = 2.0; + Color canSeeColor = new Color(0, 255, 0, 100); + Color canNotSeeColor = null; + double maxRange = 200; + Length maxRangeUnits = Length.KM; + + public final static String altProperty = "altitude"; + public final static String altUnitsProperty = "altitudeUnits"; + public final static String viewPointsProperty = "viewPoints"; + public final static String viewPointLinesProperty = "viewPointLines"; + public final static String showHorizonsProperty = "showHorizons"; + public final static String showViewPointsProperty = "showViewPoints"; + public final static String showMaxRangesProperty = "showMaxRanges"; + public final static String canSeeColorProperty = "canSeeColor"; + public final static String canNotSeeColorProperty = "canNotSeeColor"; + public final static String dtedLevelProperty = "dtedLevel"; + public final static String dtedDirProperty = "dtedDir"; + public final static String maxRangeProperty = "maxRange"; + public final static String maxRangeUnitsProperty = "maxRangeUnits"; + public final static String pixelsPerPointProperty = "pixelsPerPoint"; + public final static String pixelSkipProperty = "pixelSkip"; + public final static String showNumberPointsProperty = "showNumberPoints"; + + // Internal use only members + DTEDFrameCache dted; + + public MultiLOSLayer() { + dted = new DTEDFrameCache(); + } + + @Override + public void setProperties(String prefix, Properties props) { + super.setProperties(prefix, props); + String realPrefix = PropUtils.getScopedPropertyPrefix(this); + + altitude = PropUtils.doubleFromProperties(props, realPrefix + altProperty, altitude); + altitudeUnits = Length.get(props.getProperty(realPrefix + altUnitsProperty, altitudeUnits.getAbbr())); + maxRange = PropUtils.doubleFromProperties(props, realPrefix + maxRangeProperty, maxRange); + maxRangeUnits = Length.get(props.getProperty(realPrefix + maxRangeUnitsProperty, maxRangeUnits.getAbbr())); + + showHorizons = PropUtils.booleanFromProperties(props, realPrefix + showHorizonsProperty, showHorizons); + showMaxRanges = PropUtils.booleanFromProperties(props, realPrefix + showMaxRangesProperty, showMaxRanges); + showViewPoints = PropUtils.booleanFromProperties(props, realPrefix + showViewPointsProperty, showViewPoints); + showNumberPoints = PropUtils.booleanFromProperties(props, realPrefix + showNumberPointsProperty, showNumberPoints); + + pixelSkip = PropUtils.doubleFromProperties(props, realPrefix + pixelSkipProperty, pixelSkip); + pixelsPerPoint = Math.max(1, PropUtils.intFromProperties(props, realPrefix + pixelsPerPointProperty, pixelsPerPoint)); + + dtedLevel = PropUtils.intFromProperties(props, realPrefix + dtedLevelProperty, dtedLevel); + dtedDir = props.getProperty(realPrefix + dtedDirProperty, dtedDir); + dted.addDTEDDirectoryHandler(new DTEDDirectoryHandler(dtedDir)); + + String csc = props.getProperty(realPrefix + canSeeColorProperty, + (null == canSeeColor?null:canSeeColor.toString())); + if(null == csc) { + canSeeColor = null; + } else { + canSeeColor = PropUtils.parseColor(csc, true); + } + + String cnsc = props.getProperty(realPrefix + canNotSeeColorProperty, + (null == canNotSeeColor?null:canNotSeeColor.toString())); + if(null == cnsc) { + canNotSeeColor = null; + } else { + canNotSeeColor = PropUtils.parseColor(cnsc, true); + } + + viewPoints = new ArrayList(); + + // Viewpoints are semicolon-separated lat,lon pairs separated by commas + String viewPointSource = props.getProperty(realPrefix + viewPointsProperty, null); + if(null != viewPointSource) { + String[] viewPointStrings = viewPointSource.split(";"); + for(String s : viewPointStrings) { + String trimmed = s.trim(); + if(0 == trimmed.length()) { + continue; + } + String[] oneLL = trimmed.split(","); + if(oneLL.length < 2) { + Logger.getLogger(this.getClass().getName()).log(Level.SEVERE, "Error parsing \"" + trimmed + "\": must have at least lat,lon "); + continue; + } + try { + Double lat = Double.valueOf(oneLL[0]); + Double lon = Double.valueOf(oneLL[1]); + double thisAlt = altitude; + double thisMaxRange = maxRange; + if(3 <= oneLL.length) { + thisAlt = Double.valueOf(oneLL[2]); + } + if(4 <= oneLL.length) { + thisMaxRange = Double.valueOf(oneLL[3]); + } + viewPoints.add(new MultiLOSViewPoint(new LatLonPoint.Double(lat, lon, false), thisAlt, thisMaxRange)); + } catch(NumberFormatException ex) { + Logger.getLogger(this.getClass().getName()).log(Level.SEVERE, "Cannot parse \"" + trimmed + "\" numerically"); + } + } + } + + + String viewPointLinesSource = props.getProperty(realPrefix + viewPointLinesProperty, null); + if(null != viewPointLinesSource) { + for(String oneViewPointLine : viewPointLinesSource.split(";")) { + String trimmed = oneViewPointLine.trim(); + if(0 == trimmed.length()) { + continue; + } + String[] linePieces = trimmed.split(","); + if(5 != linePieces.length) { + Logger.getLogger(this.getClass().getName()).log(Level.SEVERE, "ViewPointsLine \"" + trimmed + "\" must be formatted l,l,l,l,n"); + continue; + } + + try { + Double startLat = Double.valueOf(linePieces[0]); + Double startLon = Double.valueOf(linePieces[1]); + Double endLat = Double.valueOf(linePieces[2]); + Double endLon = Double.valueOf(linePieces[3]); + // Always do the two end points. Bonus: we can skip worrying about div0 + int piececnt = 2 + Integer.valueOf(linePieces[4]); + + double dLat = ((endLat-startLat)/piececnt); + double dLon = ((endLon-startLon)/piececnt); + + for(int i = 0; i < piececnt; i++) { + double lat = startLat + (i * dLat); + double lon = startLon + (i * dLon); + LatLonPoint.Double p = new LatLonPoint.Double(lat, lon, false); + viewPoints.add(new MultiLOSViewPoint(p, altitude, maxRange)); + } + } catch(NumberFormatException ex) { + Logger.getLogger(this.getClass().getName()).log(Level.SEVERE, "Cannot parse \"" + trimmed + "\" numerically"); + } + } + } + } + + @Override + public Properties getProperties(Properties props) { + props = super.getProperties(props); + + String prefix = PropUtils.getScopedPropertyPrefix(this); + + props.put(prefix + "class", this.getClass().getName()); + props.put(prefix + altProperty, altitude); + props.put(prefix + altUnitsProperty, altitudeUnits.getAbbr()); + props.put(prefix + showNumberPointsProperty, Boolean.toString(showNumberPoints)); + props.put(prefix + pixelSkipProperty, pixelSkip); + props.put(prefix + pixelsPerPointProperty, pixelsPerPoint); + props.put(prefix + showHorizonsProperty, Boolean.toString(showHorizons)); + props.put(prefix + showMaxRangesProperty, Boolean.toString(showMaxRanges)); + props.put(prefix + showViewPointsProperty, Boolean.toString(showViewPoints)); + if(null != canSeeColor) { + props.put(prefix + canSeeColorProperty, canSeeColor.toString()); + } + if(null != canNotSeeColor) { + props.put(prefix + canNotSeeColorProperty, canNotSeeColor.toString()); + } + props.put(prefix + dtedLevelProperty, dtedLevel); + props.put(prefix + dtedDirProperty, dtedDir); + props.put(prefix + maxRangeProperty, maxRange); + props.put(prefix + maxRangeUnitsProperty, maxRangeUnits.getAbbr()); + + StringBuilder vp_prop = new StringBuilder(); + for(MultiLOSViewPoint mlvp : viewPoints) { + vp_prop.append(mlvp.toString()); + vp_prop.append(";"); + } + props.put(prefix + viewPointsProperty, vp_prop.toString()); + + return props; + } + + @Override + public Properties getPropertyInfo(Properties list) { + list = super.getPropertyInfo(list); + + list.put(altProperty, "Default altitude for viewpoints, MSL"); + list.put(altUnitsProperty, "Units for altitude"); + list.put(showHorizonsProperty, "Whether to indicate horizons"); + list.put(showMaxRangesProperty, "Whether to indicate max ranges"); + list.put(showViewPointsProperty, "Whether to indicate viewpoints"); + list.put(showNumberPointsProperty, "Whether to indicate numbers next to points"); + list.put(canSeeColorProperty, "Color to indicate if a point can be seen."); + list.put(canNotSeeColorProperty, "Color to indicate if a point cannot be seen. Leave blank to not include points"); + list.put(dtedLevelProperty, "DTED Level"); + list.put(dtedDirProperty, "DTED data directory"); + list.put(pixelsPerPointProperty, "Pixels per point"); + list.put(pixelsPerPointProperty, "Pixel Skip"); + list.put(maxRangeProperty, "Maximum sensor range"); + list.put(maxRangeUnitsProperty, "Maximum sensor range units"); + list.put(viewPointsProperty, "Semicolon-separated list of lat,lon pairs with option ',alt' suffix"); + + return list; + } + @Override + public OMGraphicList prepare() { + OMGraphicList l = new OMGraphicList(); + + if(showHorizons) { + for(MultiLOSViewPoint mlvp : viewPoints) { + LatLonPoint vp = mlvp.p; + final double thisAlt = mlvp.altitude; + final double thisAltM = Length.METER.fromRadians(altitudeUnits.toRadians(thisAlt)); + final double horizonRad = calculateHorizonDistRad(thisAltM); + OMCircle circ = new OMCircle(vp.getLatitude(), vp.getLongitude(), horizonRad, Length.RADIAN); + circ.setLinePaint(Color.BLACK); + l.add(circ); + } + } + if(showMaxRanges) { + for(MultiLOSViewPoint mlvp : viewPoints) { + LatLonPoint vp = mlvp.p; + final double maxRangeRad = maxRangeUnits.toRadians(mlvp.maxRange); + OMCircle circ = new OMCircle(vp.getLatitude(), vp.getLongitude(), maxRangeRad, Length.RADIAN); + circ.setLinePaint(Color.GRAY); + l.add(circ); + } + } + if(showViewPoints) { + for(int i = 0; i < viewPoints.size(); i++) { + MultiLOSViewPoint mlvp = viewPoints.get(i); + LatLonPoint vp = mlvp.p; + OMPoint p = new OMPoint(vp.getLatitude(), vp.getLongitude()); + l.add(p); + if(showNumberPoints) { + OMText t = new OMText(vp.getLatitude(), vp.getLongitude(), Integer.toString(i), OMText.JUSTIFY_LEFT); + l.add(t); + } + } + } + createMultiLOS(l); + l.generate(getProjection()); + return l; + } + + @Override + public Component getGUI() { + JPanel pan = new JPanel(new GridLayout(0, 2, 2, 2)); + + final JButton setAltsButton = new JButton("Set all viewpoint alts to (" + altitudeUnits.getAbbr() + "):"); + pan.add(setAltsButton); + final JSpinner altSpinner = new JSpinner(new SpinnerNumberModel(altitude, 0.0, 1000000.0, 1.0)); + pan.add(altSpinner); + + final JButton setRangesButton = new JButton("Set all viewpoint ranges to (" + maxRangeUnits.getAbbr() + "):"); + pan.add(setRangesButton); + final JSpinner maxRangeSpinner = new JSpinner(new SpinnerNumberModel(maxRange, 0.0, 1000000.0, 20.0)); + pan.add(maxRangeSpinner); + + pan.add(new JLabel("Pixel Skip")); + final JSpinner pixelSkipSpinner = new JSpinner(new SpinnerNumberModel(pixelSkip, 0.01, 1000.0, 1)); + pan.add(pixelSkipSpinner); + + pan.add(new JLabel("Pixels per Point")); + final JSpinner pixelPerPointSpinner = new JSpinner(new SpinnerNumberModel(pixelsPerPoint, 1, 1000, 1)); + pan.add(pixelPerPointSpinner); + + pan.add(new JLabel("Show horizons")); + final JCheckBox showHorizonCB = new JCheckBox((String)null, showHorizons); + pan.add(showHorizonCB); + + pan.add(new JLabel("Show max ranges")); + final JCheckBox showMaxRangesCB = new JCheckBox((String)null, showMaxRanges); + pan.add(showMaxRangesCB); + + pan.add(new JLabel("Show viewpoints")); + final JCheckBox showViewPointsCB = new JCheckBox((String)null, showViewPoints); + pan.add(showViewPointsCB); + + pan.add(new JLabel("Show Point Numbering")); + final JCheckBox showNumberPointsCB = new JCheckBox((String)null, showNumberPoints); + pan.add(showNumberPointsCB); + + ActionListener altAl = new ActionListener() { + public void actionPerformed(ActionEvent e) { + Double newAlt = ((SpinnerNumberModel)altSpinner.getModel()).getNumber().doubleValue(); + for(MultiLOSViewPoint mlvp : viewPoints) { + mlvp.altitude = newAlt; + } + doPrepare(); + } + }; + setAltsButton.addActionListener(altAl); + + ActionListener rangeAl = new ActionListener() { + public void actionPerformed(ActionEvent e) { + Double newMaxRange = ((SpinnerNumberModel)maxRangeSpinner.getModel()).getNumber().doubleValue(); + for(MultiLOSViewPoint mlvp : viewPoints) { + mlvp.maxRange = newMaxRange; + } + doPrepare(); + } + }; + setRangesButton.addActionListener(rangeAl); + + final ActionListener al = new ActionListener() { + public void actionPerformed(ActionEvent e) { + showNumberPoints = showNumberPointsCB.isSelected(); + showHorizons = showHorizonCB.isSelected(); + showMaxRanges = showMaxRangesCB.isSelected(); + showViewPoints = showViewPointsCB.isSelected(); + doPrepare(); + } + }; + + final ChangeListener spinnerListener = new ChangeListener() { + public void stateChanged(ChangeEvent e) { + pixelsPerPoint = ((SpinnerNumberModel)pixelPerPointSpinner.getModel()).getNumber().intValue(); + pixelSkip = ((SpinnerNumberModel)pixelSkipSpinner.getModel()).getNumber().doubleValue(); + doPrepare(); + } + }; + pixelPerPointSpinner.addChangeListener(spinnerListener); + pixelSkipSpinner.addChangeListener(spinnerListener); + + showNumberPointsCB.addActionListener(al); + showHorizonCB.addActionListener(al); + showMaxRangesCB.addActionListener(al); + showViewPointsCB.addActionListener(al); + return pan; + } + + public void createMultiLOS(OMGraphicList l) { + LOSGenerator los = new LOSGenerator(dted); + + Projection proj = getProjection(); + + int checkedPoints = 0; + int seenPoints = 0; + + for (int x = 0; x < proj.getWidth(); x+=pixelsPerPoint) { +// System.out.println(String.format("MultiLOS Render: %d/%d", x, proj.getWidth())); + for (int y = 0; y < proj.getHeight(); y+=pixelsPerPoint) { + if(Thread.currentThread().isInterrupted()) { + // eg, if we're mid-render and someone moves the map again + return; + } + + checkedPoints++; + + LatLonPoint testp = new LatLonPoint.Double(); + proj.inverse(x, y, testp); + double testLat = testp.getLatitude(); + double testLon = testp.getLongitude(); + + int elevation = dted.getElevation((float) testLat, (float) testLon, dtedLevel); + if(elevation > 0) { + int losCount = 0; + + for (MultiLOSViewPoint mlvp : viewPoints) { + + LatLonPoint oneVP = mlvp.p; + double thisAlt = mlvp.altitude; + double thisMaxRangeRad = maxRangeUnits.toRadians(mlvp.maxRange); + + double thisAltM = Length.METER.fromRadians(altitudeUnits.toRadians(thisAlt)); + + final double distanceRad = oneVP.distance(testp); + + if(distanceRad > thisMaxRangeRad) { + // Broadphase - skip anything outside our sensor horizon + continue; + } +// + Point2D tXY = proj.forward(oneVP.getLatitude(), oneVP.getLongitude()); + int numPixBetween = (int) (Math.sqrt( + Math.pow(tXY.getX() - x, 2) + + Math.pow(tXY.getY() - y, 2) + ) / pixelSkip); + + if (los.isLOS(oneVP, (int) thisAltM, false, testp, 0, + (int) numPixBetween)) { + losCount++; + // If one can see, that's sufficient for this layer's see/not see metric + break; + } + } + + if(0 < losCount && null != canSeeColor) { + OMPoint p = new OMPoint(testLat, testLon); + p.setLinePaint(OMColor.clear); + p.setFillPaint(canSeeColor); + p.setRadius(pixelsPerPoint / 2); + l.add(p); + seenPoints++; + } else if(0 == losCount && null != canNotSeeColor) { + OMPoint p = new OMPoint(testLat, testLon); + p.setLinePaint(OMColor.clear); + p.setFillPaint(canNotSeeColor); + l.add(p); + } + } else { + // Skipped a point because it's elevation was zero or smaller + // System.out.println("elevation " + elevation); + } + } + } +// progressSupport.fireUpdate(ProgressEvent.DONE, taskName, currProgress, maxProgress); + System.out.println("Last Render, " + seenPoints + "/" + checkedPoints + " points seen/total"); + } + + private double calculateHorizonDistRad(Double altM) { + final double horizonDistM = Math.sqrt((2 * Planet.wgs84_earthEquatorialRadiusMeters_D * altM) + (altM * altM)); + final double horizonDistRad = Length.METER.toRadians(horizonDistM); + return horizonDistRad; + } + +} diff --git a/src/core/src/main/java/com/bbn/openmap/layer/terrain/MultiLOS/package.html b/src/core/src/main/java/com/bbn/openmap/layer/terrain/MultiLOS/package.html new file mode 100644 index 00000000..8cbfc146 --- /dev/null +++ b/src/core/src/main/java/com/bbn/openmap/layer/terrain/MultiLOS/package.html @@ -0,0 +1,3 @@ + +MultiLOS is for answering the "what if I had multiple viewpoints" LOS question +