/* Copyright (C) 2001, 2007 United States Government as represented by the Administrator of the National Aeronautics and Space Administration. All Rights Reserved. */ package gov.nasa.worldwind.applications.sar; import gov.nasa.worldwind.WorldWindow; import gov.nasa.worldwind.avlist.AVKey; import gov.nasa.worldwind.examples.ApplicationTemplate; import gov.nasa.worldwind.geom.*; import gov.nasa.worldwind.globes.Globe; import gov.nasa.worldwind.layers.CrosshairLayer; import gov.nasa.worldwind.layers.RenderableLayer; import gov.nasa.worldwind.view.FlyToOrbitViewStateIterator; import gov.nasa.worldwind.view.OrbitView; import javax.swing.*; import java.awt.*; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; /** * @author tag * @version $Id: AnalysisPanel.java 5066 2008-04-16 20:54:27Z dcollins $ */ public class AnalysisPanel extends JPanel { private WorldWindow wwd; private SARTrack currentTrack; private TrackViewPanel trackViewPanel; private TerrainProfilePanel terrainProfilePanel; private CrosshairLayer crosshairLayer; private RenderableLayer planeModelLayer; private PlaneModel planeModel; private final PropertyChangeListener propertyChangeListener = new PropertyChangeListener() { @SuppressWarnings({"StringEquality"}) public void propertyChange(PropertyChangeEvent propertyChangeEvent) { if (propertyChangeEvent.getPropertyName() == TrackViewPanel.VIEW_CHANGE) { // When the track mode has changed, update the view parameters gradually. updateView(true); } else if (propertyChangeEvent.getPropertyName() == TrackViewPanel.POSITION_CHANGE) { // When the track position has changed, update the view parameters immediately. updateView(false); } else if (propertyChangeEvent.getPropertyName() == AVKey.ELEVATION_MODEL && trackViewPanel.isExamineViewMode() && !wwd.getView().hasStateIterator()) { // When the elevation model changes, and the view is examining the terrain beneath the track // (but has not active state iterators), update the view parameters immediately. updateView(false); } else if (propertyChangeEvent.getPropertyName() == SAR2.ELEVATION_UNIT) { updateElevationUnit(propertyChangeEvent.getNewValue()); } if ((propertyChangeEvent.getPropertyName() == AVKey.VIEW || propertyChangeEvent.getPropertyName() == AVKey.VIEW_QUIET) && trackViewPanel.isFollowViewMode()) { updateCrosshair(); } } }; public AnalysisPanel() { initComponents(); // Init plane model layer this.planeModel = new PlaneModel(100d, 100d, Color.YELLOW); this.planeModel.setShadowScale(0.1); this.planeModel.setShadowColor(new Color(255, 255, 0, 192)); this.planeModelLayer = new RenderableLayer(); this.planeModelLayer.setName("Plane Model"); this.planeModelLayer.addRenderable(this.planeModel); //Init crosshair layer this.crosshairLayer = new CrosshairLayer("images/64x64-crosshair.png"); this.crosshairLayer.setOpacity(0.4); this.crosshairLayer.setEnabled(false); this.trackViewPanel.addPropertyChangeListener(this.propertyChangeListener); } public void setWwd(WorldWindow wwd) { if (this.wwd != null) { this.wwd.removePropertyChangeListener(this.propertyChangeListener); this.wwd.getModel().getGlobe().getElevationModel().removePropertyChangeListener(this.propertyChangeListener); this.wwd.getView().removePropertyChangeListener(this.propertyChangeListener); } this.wwd = wwd; this.terrainProfilePanel.setWwd(wwd); if (this.wwd != null) { this.wwd.addPropertyChangeListener(this.propertyChangeListener); this.wwd.getModel().getGlobe().getElevationModel().addPropertyChangeListener(this.propertyChangeListener); this.wwd.getView().addPropertyChangeListener(this.propertyChangeListener); ApplicationTemplate.insertBeforeCompass(wwd, this.planeModelLayer); ApplicationTemplate.insertBeforeCompass(wwd, this.crosshairLayer); } } public void setCurrentTrack(SARTrack currentTrack) { this.currentTrack = currentTrack; this.trackViewPanel.setCurrentTrack(currentTrack); } private void updateElevationUnit(Object newValue) { if (newValue != null) { this.trackViewPanel.setElevationUnit(newValue.toString()); this.trackViewPanel.updateReadout(this.getPositionAlongSegment()); } } private Angle getControlHeading() { return Angle.ZERO; } @SuppressWarnings({"UnusedDeclaration"}) private Angle getControlPitch() { return Angle.fromDegrees(80); } private Angle getControlFOV() { return Angle.fromDegrees(45); } private void updateView(boolean goSmoothly) { OrbitView view = (OrbitView) this.wwd.getView(); view.setFieldOfView(this.getControlFOV()); if (this.trackViewPanel.isOverrideClipDistance()) { view.setNearClipDistance(this.trackViewPanel.getClipDistance()); view.setDetectCollisions(false); } else { view.setNearClipDistance(-1); // Tells View to auto-compute the near clip distance. view.setDetectCollisions(!trackViewPanel.isSubsurfaceOkay()); } Position pos = this.getPositionAlongSegment(); if (pos != null) { Angle heading = this.getHeading().add(this.getControlHeading()); this.terrainProfilePanel.updatePosition(pos, heading); this.planeModel.setPosition(pos); this.planeModel.setHeading(heading); this.crosshairLayer.setEnabled(false); // Turn off crosshair by default if (this.trackViewPanel.isExamineViewMode()) { this.terrainProfilePanel.setFollowObject(); // Set the view center point to the current track position on the ground - spheroid. // This gets the eye looking at the cross section. Position groundPos = getSmoothedGroundPositionAlongSegment(); if (groundPos == null) groundPos = getGroundPositionAlongSegment(); if (goSmoothly) { Angle initialPitch = Angle.fromDegrees(Math.min(60, view.getPitch().degrees)); double initialZoom = 10000; // If the player is active, set initial parameters immediately. // Otherwise, set initial parameters gradually. if (this.trackViewPanel.isPlayerActive()) { view.setCenterPosition(groundPos); view.setZoom(initialZoom); view.setPitch(initialPitch); } else { view.applyStateIterator(FlyToOrbitViewStateIterator.createPanToIterator( view, this.wwd.getModel().getGlobe(), groundPos, view.getHeading(), initialPitch, initialZoom, true)); } } else { // Stop any state iterators, and center movement only. view.stopStateIterators(); view.stopMovementOnCenter(); // Set the view to center on the track position, // while keeping the eye altitude constant. try { Position eyePos = view.getCurrentEyePosition(); // New eye lat/lon will follow the ground position. LatLon newEyeLatLon = eyePos.add(groundPos.subtract(view.getCenterPosition())).getLatLon(); // Eye elevation will not change unless it is below the ground position elevation. double newEyeElev = eyePos.getElevation() < groundPos.getElevation() ? groundPos.getElevation() : eyePos.getElevation(); Position newEyePos = new Position(newEyeLatLon, newEyeElev); view.setOrientation(newEyePos, groundPos); } // Fallback to setting center position. catch (Exception e) { view.setCenterPosition(groundPos); // View/OrbitView will have logged the exception, no need to log it here. } } } else if (this.trackViewPanel.isFollowViewMode()) { Angle pitch = Angle.POS90; double zoom = 0; this.updateCrosshair(); this.terrainProfilePanel.setFollowObject(); // Place the eye at the track current lat-lon and altitude, with the proper heading // and pitch from slider. Intended to simulate the view from the plane. if (goSmoothly) { // If the player is active, set initial parameters immediately. // Otherwise, set initial parameters gradually. if (this.trackViewPanel.isPlayerActive()) { view.setCenterPosition(pos); view.setHeading(heading); view.setPitch(pitch); view.setZoom(zoom); } else { view.applyStateIterator(FlyToOrbitViewStateIterator.createPanToIterator( view, this.wwd.getModel().getGlobe(), pos, heading, pitch, zoom)); } } else { // Stop any state iterators, and any view movement. view.stopStateIterators(); view.stopMovement(); // Set the view values to follow the track. view.setCenterPosition(pos); view.setHeading(heading); view.setPitch(pitch); view.setZoom(zoom); } } else if (this.trackViewPanel.isFreeViewMode()) { if (goSmoothly) { // Stop any state iterators, and any view movement. view.stopStateIterators(); view.stopMovement(); // Set the view's center position to a point on the ground (without moving the eye). // This is needed to ensure normal interactions immediately. try { view.focusOnViewportCenter(); } catch (Exception e) { // View/OrbitView will have logged the exception, no need to log it here. } } } } this.trackViewPanel.updateReadout(pos); this.wwd.redraw(); } private int getCurrentPositionNumber() { return this.trackViewPanel.getCurrentPositionNumber(); } private boolean isLastPosition(int n) { return n >= this.currentTrack.size() - 1; } private Position getCurrentSegmentStartPosition() { if (this.currentTrack == null || this.currentTrack.size() == 0) return null; Position pos; int n = this.getCurrentPositionNumber(); if (isLastPosition(n)) pos = this.currentTrack.get(this.currentTrack.size() - 1); else pos = this.currentTrack.get(n); return new Position(pos.getLatitude(), pos.getLongitude(), pos.getElevation() + this.currentTrack.getOffset()); } private Position getCurrentSegmentEndPosition() { if (this.currentTrack == null || this.currentTrack.size() == 0) return null; Position pos; int n = this.getCurrentPositionNumber(); if (isLastPosition(n + 1)) pos = this.currentTrack.get(this.currentTrack.size() - 1); else pos = this.currentTrack.get(n + 1); return new Position(pos.getLatitude(), pos.getLongitude(), pos.getElevation() + this.currentTrack.getOffset()); } private Position getPositionAlongSegment() { double t = this.trackViewPanel.getPositionDelta(); return this.getPositionAlongSegment(t); } private Position getPositionAlongSegment(double t) { Position pa = this.getCurrentSegmentStartPosition(); if (pa == null) return null; Position pb = this.getCurrentSegmentEndPosition(); if (pb == null) return pa; return interpolateTrackPosition(t, pa, pb); } private Angle getHeading() { Position pA; Position pB; int cpn = this.getCurrentPositionNumber(); if (!this.isLastPosition(cpn)) { pA = this.currentTrack.get(cpn); pB = this.currentTrack.get(cpn + 1); } else { pA = this.currentTrack.get(cpn - 1); pB = this.currentTrack.get(cpn); } return LatLon.greatCircleAzimuth(pA.getLatLon(), pB.getLatLon()); } @SuppressWarnings({"UnusedDeclaration"}) private Position getGroundPositionAlongSegment() { if (this.wwd == null) return null; Position pos = getPositionAlongSegment(); if (pos == null) return null; double elevation = this.wwd.getModel().getGlobe().getElevation(pos.getLatitude(), pos.getLongitude()); return new Position(pos.getLatLon(), elevation); } // TODO: weighted average should be over actual polyline track points private Position getSmoothedGroundPositionAlongSegment() { if (this.currentTrack == null || this.currentTrack.size() == 0) return null; Position start = getCurrentSegmentStartPosition(); Position end = getCurrentSegmentEndPosition(); if (start == null || end == null) return null; Globe globe = this.wwd.getModel().getGlobe(); if (globe == null) return null; int n = this.getCurrentPositionNumber(); double t = this.trackViewPanel.getPositionDelta(); // Limit t to 0 if this is the last position. if (isLastPosition(n)) t = 0; double tstep = 1 / 100.0; int numWeights = 15; // TODO: extract to configurable property double elev = 0; double sumOfWeights = 0; // Compute the moving weighted average of track positions on both sides of the current track position. for (int i = 0; i < numWeights; i++) { double tt; Position pos; // Previous ground positions. tt = t - i * tstep; pos = null; if (tt >= 0) // Position is in the current track segment. pos = interpolateTrackPosition(tt, start, end); else if (tt < 0 && n > 0) // Position is in the previous track segment. pos = interpolateTrackPosition(tt + 1, this.currentTrack.get(n-1), start); if (pos != null) { double e = globe.getElevation(pos.getLatitude(), pos.getLongitude()); elev += (numWeights - i) * e; sumOfWeights += (numWeights - i); } // Next ground positions. // We don't want to count the first position twice. if (i != 0) { tt = t + i * tstep; pos = null; if (tt <= 1) // Position is in the current track segment. pos = interpolateTrackPosition(tt, start, end); else if (tt > 1 && !isLastPosition(n + 1)) // Position is in the next track segment. pos = interpolateTrackPosition(tt - 1, end, this.currentTrack.get(n+2)); if (pos != null) { double e = globe.getElevation(pos.getLatitude(), pos.getLongitude()); elev += (numWeights - i) * e; sumOfWeights += (numWeights - i); } } } elev /= sumOfWeights; Position actualPos = interpolateTrackPosition(t, start, end); return new Position(actualPos.getLatLon(), elev); } /** * SAR tracks points are connected with lines of constant heading (rhumb lines). In order to compute * an interpolated position between two track points, we must use rhumb computations, rather * than linearly interpolate the position. * @param t a decimal number between 0 and 1 * @param begin first position * @param end second position * @return Position in between begin and end */ private Position interpolateTrackPosition(double t, Position begin, Position end) { if (begin == null || end == null) return null; LatLon lla = begin.getLatLon(); LatLon llb = end.getLatLon(); // The track is drawn as a rhumb line. // Therefore we must use rhumb computations to interpolate lat/lon. Angle az = LatLon.rhumbAzimuth(lla, llb); Angle dist = LatLon.rhumbDistance(lla, llb); dist = dist.multiply(t); LatLon ll = LatLon.rhumbEndPosition(lla, az, dist); // Elevation is independent of track line type (i.e. rhumb, great-circle, linear), // so we interpolate elevation normally. double e = (1d - t) * begin.getElevation() + t * end.getElevation(); return new Position(ll, e); } // /** // * Compute crosshair location in viewport for 'follow' - 'fly-it' mode. // * <p> // * It computes the intersection of the air track with the near clipping plane // * and determines the corresponding crosshair position in the viewport.</p> // * <p> // * This assumes the view is headed in the same direction as the air track, // * and the eye is set to look at the aircraft from a distance and angle.</p> // * // * @param view the current <code>View</code> // * @param a view pitch angle relative to the air track (0 degree = horizontal) // * @param distance eye distance from the aircraft // * @return the crosshair center position in the viewport. // */ // private Vec4 computeCrosshairPosition(View view, Angle a, double distance) // { // double hfovH = view.getFieldOfView().radians / 2; // half horizontal fov in radians // double hw = view.getViewport().width / 2; // half viewport width // double hh = view.getViewport().height / 2; // half viewport height // double d = hw / Math.tan(hfovH); // distance to viewport plane in pixels // // distance to near plane in meters // double dNearMeter = Math.abs(view.getFrustum().getNear().getDistance()); // // crosshair elevation above viewport center in meter // double dyMeter = (dNearMeter - distance) * Math.sin(a.radians); // // corresponding vertical fov half angle // double ay = Math.atan(dyMeter / dNearMeter); // // corresponding viewport crosshair elevation in pixels // double dy = Math.tan(ay) * d; // // final crosshair viewport position // return new Vec4(hw, hh + dy, 0, 0); // } // Update crosshair position to follow the air track private void updateCrosshair() { Vec4 crosshairPos = computeCrosshairPosition(); if (crosshairPos != null) { this.crosshairLayer.setEnabled(true); this.crosshairLayer.setLocationCenter(crosshairPos); } else this.crosshairLayer.setEnabled(false); } // Compute cartesian intersection between the current air track segment and the near plane. // Follow rhumb line segments. private Vec4 computeCrosshairPosition() { Position posA = getCurrentSegmentStartPosition(); Position posB = getCurrentSegmentEndPosition(); Angle segmentAzimuth = LatLon.rhumbAzimuth(posA.getLatLon(), posB.getLatLon()); Angle segmentDistance = LatLon.rhumbDistance(posA.getLatLon(), posB.getLatLon()); Globe globe = this.wwd.getModel().getGlobe(); Plane near = this.wwd.getView().getFrustumInModelCoordinates().getNear(); int numSubsegments = 10; // TODO: get from track polyline double step = 1d / numSubsegments; Position p1 = null, p2; for (double s = 0; s <= 1; s += step) { if (s == 0) p2 = posA; else if (s >= 1) p2 = posB; else { Angle distance = Angle.fromRadians(s * segmentDistance.radians); LatLon latLon = LatLon.rhumbEndPosition(posA.getLatLon(), segmentAzimuth, distance); p2 = new Position(latLon, (1 - s) * posA.getElevation() + s * posB.getElevation()); } if (p1 != null) { Vec4 pa = globe.computePointFromPosition(p1); Vec4 pb = globe.computePointFromPosition(p2); if(pa.distanceTo3(pb) > 0) { Vec4 intersection = near.intersect(pa, pb); if (intersection != null) { return this.wwd.getView().project(intersection); } } } p1 = p2; } return null; } private void initComponents() { setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS)); this.trackViewPanel = new TrackViewPanel(); this.trackViewPanel.setAlignmentX(Component.LEFT_ALIGNMENT); add(this.trackViewPanel); this.terrainProfilePanel = new TerrainProfilePanel(); this.terrainProfilePanel.setAlignmentX(Component.LEFT_ALIGNMENT); add(this.terrainProfilePanel); } }