NavAnalyst |
This reference application shows how many of the AGI.Foundation.Navigation types and methods can be used. This application is designed to show a navigation analyst a quick-look picture of the Dilution of Precision (DOP) for a site, which satellites are in view of that site, and the azimuth and elevation of the satellites at that site. An open-source charting package called ZedGraph is used to draw the graphs in the application.
Note |
---|
This sample application requires a license for the Navigation Accuracy Library. |
The source code files for the application can be found in the DME Component Libraries install at Examples\NavAnalyst\.
Since the code is fully commented, only selected classes, properties and methods are discussed in the following sections.
Only three DME Component Libraries references need to be added to support this application:
However, several namespaces from these references are needed:
The constructor for the NavAnalyst class initializes a DateTime and uses it to set the default time period for analysis to the current day, with a duration of 24 hours. A tree control is initialized, and titles and axis labels are assigned to 3 instances of the ZedGraphControl class - 1 each for DOP and Azimuth/Elevation graphs and a third for the accuracy plots.
An InitLinestyles method initializes an array of structures, each holding a color and symbol style for a given satellite, denoted by PRN. These show up in the legend for the Elevation Angles graph:
When NavAnalyst starts, you first need to supply an almanac. Almanacs provide the information needed to propagate the GPS satellite orbits. These orbits can then be used to determine visibility, azimuth, elevation and the like.
To obtain a SEM formatted almanac file, select the Almanac tab (if it is not already selected), click the browse (...) button and load the default SampleAlmanac.al3 file or get a new one from: https://ftp.agi.com/pub/Catalog/Almanacs/SEM/
Set the start and stop times for the analysis (or keep the default times), select UTC or GPS time, and set the time step, i.e. the interval between DOP calculations. The tree control expands and updates to display the name of the selected almanac.
All of these operations are handled in a fairly straightforward way in the click event handler for the SEM Almanac browse button.
After the almanac is loaded you can either output the almanac ephemeris into STK .e ephemeris files or add a receiver and perform DOP or Accuracy calculations.
The click event handler for the Export ephemeris to .e files button uses the SemAlmanac, NavstarISGps200DPropagator, DateMotionCollection<T>, and StkEphemerisFile classes, plus others, as follows:
private void OnExportEphemerisClick(object sender, EventArgs e) { // Open the almanac file SemAlmanac almanac; using (Stream stream = openAlmanacDialog.OpenFile()) using (StreamReader reader = new StreamReader(stream)) { almanac = SemAlmanac.ReadFrom(reader, 1); } // setup the progressbar // GPSElement contains a list of PRNs (PRNList) available in the almanac. progressBar1.Maximum = almanac.Count; progressBar1.Step = 1; // get just the almanac path here. This will allow us to place the new .e files in the same place the almanac was opened from. string outputPath = Path.GetDirectoryName(openAlmanacDialog.FileName); // Create a .e file for every PRN in the element set... foreach (SemAlmanacRecord record in almanac) { // provide a name and path for the new .e file. string filename = String.Format("PRN_{0:00}.e", record.PseudoRandomNumber); string path = outputPath + "\\" + filename; // check that the path exists if (File.Exists(path)) { // ask if it's OK to overwrite existing files. If it's not, advance the progress bar (passed this SV) and continue to the next SV if (MessageBox.Show(string.Format("{0} " + Localization.existsOverwrite, filename), Localization.FileExists, MessageBoxButtons.YesNo) == DialogResult.No) { progressBar1.PerformStep(); continue; } } // ok to overwrite existing files (or no file existed already) // the using block here will use the sw variable to write the .e file stream to. using (StreamWriter writer = File.CreateText(path)) { JulianDate startjd; JulianDate stopjd; // We need to know which time standard to use here. if the user has specified that GPS time is to be used // we need to create the JulianDates with the GlobalPositioningSystemTime standard. if (GPSTimeRadio.Checked) { startjd = new JulianDate(StartTime.Value, TimeStandard.GlobalPositioningSystemTime); stopjd = new JulianDate(StopTime.Value, TimeStandard.GlobalPositioningSystemTime); } else { // otherwise, the default time standard is UTC startjd = new JulianDate(StartTime.Value); stopjd = new JulianDate(StopTime.Value); } // Now that we know the start and stop times for the .e file, we need to propagate the orbits // the NavstarISGps200DPropagator take a Duration type for the timestep - let's create it // using the user-specified timestep value double timestep = double.Parse(TimeStep.Text); Duration timestepDuration = Duration.FromSeconds(timestep); // declare a NavstarISGps200DPropagator - this is used to propagate the satellite positions // assign an instance of the propagator. The propagator constructor takes an almanac element set (for a single satellite) NavstarISGps200DPropagator propagator = new NavstarISGps200DPropagator(record); // now create an StkEphemerisFile object and assign its Ephemeris property the output of the propagator StkEphemerisFile file = new StkEphemerisFile(); DateMotionCollection<Cartesian> ephemeris = propagator.Propagate(startjd, stopjd, timestepDuration, 0, propagator.ReferenceFrame); StkEphemerisFile.EphemerisTimePos ephemerisFormat = new StkEphemerisFile.EphemerisTimePos { CoordinateSystem = propagator.ReferenceFrame, EphemerisData = ephemeris }; file.Data = ephemerisFormat; // write the .e ephemeris to the stream opened in this using block file.WriteTo(writer); } // end of using block // update the progress bar progressBar1.PerformStep(); } // end of PRN List // we're done! Reset the progress bar progressBar1.Value = 0; }
In addition to triggering a number of other operations (see next section), the click event handler for the Add Receiver button provides the functionality to introduce a GPS receiver into the analysis and configure it.
Among other things, AddReceiver_Click illustrates the use of the GpsReceiver class and the GpsReceiverSolutionType enumeration. In addition, PointCartographic is used to specify receiver location.
// Let's create the GPSReceiver. The receiver stores many properties and has a defined location. This location // is the point of reference for visibility calculations. receiver = new GpsReceiver(); // add receiver to the tree TreeNode newNode = new TreeNode(Localization.Receiver); rootNode.Nodes.Add(newNode); // Easy reference to Earth Central body used to initialize the ElevationAngleAccessConstraint and // to calculate the Az/El/Range Data. EarthCentralBody earth = CentralBodiesFacet.GetFromContext().Earth; // set the receiver properties based on user selections // The receiver has a receiver FrontEnd that contains the visibility and tracking constraints // Be sure to convert your angles to Radians! double minimumAngle = Trig.DegreesToRadians(Double.Parse(MaskAngle.Text)); receiver.ReceiverConstraints.Clear(); receiver.ReceiverConstraints.Add(new ElevationAngleConstraint(earth, minimumAngle)); receiver.NumberOfChannels = (int)NumberOfChannels.Value; receiver.NoiseModel = new ConstantGpsReceiverNoiseModel(0.8); // The receiver's methods of reducing the number of visible satellites to the limit imposed by the number of channels if (BestNSolType.Checked) receiver.ReceiverSolutionType = GpsReceiverSolutionType.BestN; else receiver.ReceiverSolutionType = GpsReceiverSolutionType.AllInView; // create a new location for the receiver by using the Cartographic type from AGI.Foundation.Coordinates // again, remember to convert from degrees to Radians! (except the height of course) Cartographic position = new Cartographic(Trig.DegreesToRadians(double.Parse(Longitude.Text)), Trig.DegreesToRadians(double.Parse(Latitude.Text)), double.Parse(ReceiverHeight.Text)); // Now create an antenna for the GPS receiver. We specify the location of the antenna by assigning a // PointCartographic instance to the LocationPoint property. We specify that the antenna should be oriented // according to the typically-used East-North-Up axes by assigning an instance of AxesEastNorthUp to // the OrientationAxes property. While the orientation of the antenna won't affect which satellites are visible // or tracked in this case, it will affect the DOP values. For example, the EDOP value can be found in // DilutionOfPrecision.X, but only if we've configured the antenna to use East-North-Up axes. PointCartographic antennaLocationPoint = new PointCartographic(earth, position); Platform antenna = new Platform { LocationPoint = antennaLocationPoint, OrientationAxes = new AxesEastNorthUp(earth, antennaLocationPoint) }; receiver.Antenna = antenna;
The click event handler for the Delete Receiver button clears the data that was used in creating the graphs (more below) and clears the tree control of the receiver information.
With the receiver set up and its location defined, we need to evaluate the azimuth-elevation and Dilution of Precision (DOP) data needed for the graphs. The following code uses several types from the AGI.Foundation.Navigation namespace.
// Now, we'll open the almanac SemAlmanac almanac; using (Stream stream = openAlmanacDialog.OpenFile()) using (StreamReader reader = new StreamReader(stream)) { // Read the SEM almanac from an almanac stream reader. almanac = SemAlmanac.ReadFrom(reader, 1); } // Now create a PlatformCollection to hold GpsSatellite object instances. The SemAlmanac method CreateSatellitesFromRecords returns // just such a collection. We'll use this set of satellites as the set from which we'll try and track. There is a // GpsSatellite object for each satellite specified in the almanac. PlatformCollection gpsSatellites = almanac.CreateSatelliteCollection(); // We provide the receiver with the complete set of gpsSatellites to consider for visibility calculations. // This is usually all SVs defined in the almanac - however you may want to remove SVs that aren't healthy. This can // be done by creating the gpsSatellites collection above using another version of the CreateSatellitesFromRecords method that // takes a SatelliteOutageFileReader. receiver.NavigationSatellites = gpsSatellites; // Optimization opportunity: Add the following code in a thread. This will help for long duration analyses. // Now that we have the receiver and location setup, we need to evaluate all the pertinent data. // using a SatelliteTrackingEvaluator, we can track satellites and using a DOP Evaluator, // we can calculate DOP at a specified time. // The receiver's GetSatelliteTrackingEvaluator method will provide a SatelliteTrackingEvaluator for you. // Similarly, the GetDilutionOfPrecisionEvaluator provides the DOP evaluator. // We create all evaluators in the same EvaluatorGroup for the best performance. EvaluatorGroup group = new EvaluatorGroup(); Evaluator<int[]> satTrackingEvaluator = receiver.GetSatelliteTrackingIndexEvaluator(group); Evaluator<DilutionOfPrecision> dopEvaluator = receiver.GetDilutionOfPrecisionEvaluator(group); // We also need to create an evaluator to compute Azimuth/Elevation for each of the SVs MotionEvaluator<AzimuthElevationRange>[] aerEvaluators = new MotionEvaluator<AzimuthElevationRange>[gpsSatellites.Count]; for (int i = 0; i < gpsSatellites.Count; ++i) { Platform satellite = receiver.NavigationSatellites[i]; VectorTrueDisplacement vector = new VectorTrueDisplacement(antenna.LocationPoint, satellite.LocationPoint); aerEvaluators[i] = earth.GetAzimuthElevationRangeEvaluator(vector, group); } // First we'll initialize the data structures used to plot the data for (int i = 0; i < DOPData.Length; i++) { // PointPairList is defined in the ZedGraph reference DOPData[i] = new PointPairList(); } // We need to know which time standard to use here. If the user has specified that GPS time is to be used // we need to create the JulianDates with the GlobalPositioningSystemTime standard. if (GPSTimeRadio.Checked) { startjd = new JulianDate(StartTime.Value, TimeStandard.GlobalPositioningSystemTime); stopjd = new JulianDate(StopTime.Value, TimeStandard.GlobalPositioningSystemTime); } else { // otherwise, the default time standard is UTC startjd = new JulianDate(StartTime.Value); stopjd = new JulianDate(StopTime.Value); } // Now we''ll create the variables we'll need for iterating through time. // The propagator requires a Duration type be used to specify the timestep. Duration dur = stopjd - startjd; double timestep = Double.Parse(TimeStep.Text); // Initialize the progressbar with appropriate values progressBar1.Maximum = (int)dur.TotalSeconds; progressBar1.Step = (int)timestep; // now we'll iterate through time by adding seconds to the start time JulianDate // creating a new JulianDate 'evaluateTime' each time step. for (double t = 0; t <= dur.TotalSeconds; t += timestep) { JulianDate evaluateTime = startjd.AddSeconds(t); // The string 'trackedSVs' is the start of a string we'll continue to build through this time // iteration. It will contain the info we'll need to put in the DOP graph tooltips for the different // DOP series (VDOP, HDOP, etc.) String trackedSVs = Localization.Tracked + ": "; // The evaluator method GetTrackedSatellites will take the current time and the initial list of satellites and // determine which satellites can be tracked based on the receiver constraints setup earlier. This method // returns a PlatformCollection object as well (though we'll cast each member of the Collection to a GPSSatellite type) int[] trackedSatellites = satTrackingEvaluator.Evaluate(evaluateTime); foreach (int satelliteIndex in trackedSatellites) { Platform satellite = receiver.NavigationSatellites[satelliteIndex]; // Now we have access to a Platform object representing a GPS satellite and calculate the azimuth and elevation // of each. Note that we're just calculating the azimuth and elevation, but could just as easily get the // range as well. AzimuthElevationRange aer = aerEvaluators[satelliteIndex].Evaluate(evaluateTime); // Get the GpsSatelliteExtension attached to the platform. The extension extends a // platform with GPS-specific information. In this case, we need the // satellites PRN. GpsSatelliteExtension extension = satellite.Extensions.GetByType<GpsSatelliteExtension>(); // Create two separate PointPairLists to hold the data stored by Time and Azimuth PointPairList thisTimePointList, thisAzPointList; // Before we can arbitrarily create new PointPair Lists, we have to see if the Data Storage structures already contain a list // for this PRN. // The variables AzElData_TimeBased and AzElData_AzimuthBased are dictionaries that hold the PointPairLists using the PRN // as a key. We use this structure to store a large amount of data for every satellite in a single, easy to access, variable. // if the satellite we're currently looking at already has a list defined in the dictionary, we'll use that one, otherwise // we'll create a new list if (AzElData_TimeBased.ContainsKey(extension.PseudoRandomNumber)) { thisTimePointList = AzElData_TimeBased[extension.PseudoRandomNumber]; AzElData_TimeBased.Remove(extension.PseudoRandomNumber); } else { thisTimePointList = new PointPairList(); } if (AzElData_AzimuthBased.ContainsKey(extension.PseudoRandomNumber)) { thisAzPointList = AzElData_AzimuthBased[extension.PseudoRandomNumber]; AzElData_AzimuthBased.Remove(extension.PseudoRandomNumber); } else { thisAzPointList = new PointPairList(); } // Now to get the actual Azimuth and elevation data // Converting your Radians to degrees here makes the data appear in a more readable format. We also constrain the azimuth // to be within the interval [0, 2*pi] double azimuth = Trig.RadiansToDegrees(Trig.ZeroToTwoPi(aer.Azimuth)); double elevation = Trig.RadiansToDegrees(aer.Elevation);
Once a receiver is added, click on the Accuracy tab at the bottom. Here you can add either a PAF or PSF file.
After you select a PAF or PSF file to load, NavAnalyst will immediately calculate the navigation accuracy and display it on the third graph. Navigation accuracy is calculated using the receiver currently loaded. The methods below show how navigation accuracy is calculated:
/// <summary> /// Method to calculate the Assessed Navigation Accuracy. /// </summary> /// <param name="pafFile">Fully qualified PAF file.</param> private void ComputeAssessedAccuracy(string pafFile) { // See the documentation for an overview of calculating navigation accuracy. // Populate the satellites with PAF data, extrapolating if the user requests. PerformanceAssessmentFile paf = PerformanceAssessmentFile.ReadFrom(pafFile); paf.DefaultAllowExtrapolation = UseExtrapolationCheckBox.Checked; try { //Obtain the evaluator. using (Evaluator<NavigationAccuracyAssessed> accuracyAssessedEvaluator = receiver.GetNavigationAccuracyAssessedEvaluator(paf)) { //Now, let's set up the data structures for the graph to display. ComputeValuesForAssessedAccGraph(accuracyAssessedEvaluator); } //And now, display the graph. DisplayNavAccGraph(AsAccData, Color.Blue, AsAccCheckBox.Checked, Localization.Assessed); } catch (SystemException e) { MessageBox.Show(e.Message); } } /// <summary> /// Method to obtain the Assessed Nav Accuracy at each timestep and populate the /// data structures that will be used to draw the graph. /// </summary> /// <param name="accuracyAssessedEvaluator">Evaluator for Assesssed Nav Accuracy.</param> private void ComputeValuesForAssessedAccGraph(Evaluator<NavigationAccuracyAssessed> accuracyAssessedEvaluator) { Duration dur = stopjd - startjd; double timestep = Double.Parse(TimeStep.Text); Duration ts = Duration.FromSeconds(timestep); // Initialize the progressbar with appropriate values progressBar1.Maximum = (int)dur.TotalSeconds; progressBar1.Step = (int)timestep; // now we'll iterate through time by adding seconds to the start time JulianDate object - // creating a new JulianDate each time step. for (JulianDate jd = startjd; jd <= stopjd; jd += ts) { try { //Evaluate at this particular time. NavigationAccuracyAssessed accuracyAssessed = accuracyAssessedEvaluator.Evaluate(jd); double txd = new XDate(jd.ToDateTime()); if (accuracyAssessed != null) { // Store it away in the PointPairList. AsAccData.Add(txd, accuracyAssessed.PositionSignalInSpace); } } catch { } // update the progress bar - we're done with this time step! progressBar1.PerformStep(); } // reset the progress bar progressBar1.Value = 0; } /// <summary> /// Method to calculate the Predicted Navigation Accuracy. /// </summary> private void ComputePredictedAccuracy(string psfFile) { // Populate the satellites with PSF data PredictionSupportFile psf = PredictionSupportFile.ReadFrom(psfFile); try { // Obtain the Predicted Accuracy Evaluator. using (Evaluator<NavigationAccuracyPredicted> accuracyPredictedEvaluator = receiver.GetNavigationAccuracyPredictedEvaluator(psf)) { // Now, let's set up the data structures for the graph to display. ComputeValuesForPredictedAccGraph(accuracyPredictedEvaluator); } // And now, display the graph. DisplayNavAccGraph(PredAccData, Color.Red, PredAccCheckBox.Checked, Localization.Predicted); } catch (SystemException e) { MessageBox.Show(e.Message); } } /// <summary> /// Method to obtain the Predicted Nav Accuracy at each timestep and populate the /// data structures that will be used to draw the graph. /// </summary> /// <param name="accuracyPredictedEvaluator">Evaluator for Predicted Nav Accuracy.</param> private void ComputeValuesForPredictedAccGraph(Evaluator<NavigationAccuracyPredicted> accuracyPredictedEvaluator) { Duration dur = stopjd - startjd; double timestep = Double.Parse(TimeStep.Text); Duration ts = Duration.FromSeconds(timestep); PredAccData.Clear(); // create a new Confidence Interval ConfidenceInterval ci = new ConfidenceInterval(); // Initialize the progressbar with appropriate values progressBar1.Maximum = (int)dur.TotalSeconds; progressBar1.Step = (int)timestep; // now we'll iterate through time by adding seconds to the start time JulianDate object - // creating a new JulianDate each time step. for (JulianDate jd = startjd; jd <= stopjd; jd += ts) { try { NavigationAccuracyPredicted accuracyPredicted = accuracyPredictedEvaluator.Evaluate(jd); double txd = new XDate(jd.ToDateTime()); // Lets use the specified confidence interval for our Accuracy Predictions. if (accuracyPredicted != null) { // we're using a ConfidenceInterval instance here to convert the predicted nav accuracy to a standard // confidence percentile. PredAccData.Add(txd, ci.ConvertToGlobalPositioningSystemConfidence(accuracyPredicted.PositionSignalInSpace, (int)ConfIntvlUpDown.Value, ConfidenceIntervalVariableDimension.Three)); } } catch { } // update the progress bar - we're done with this time step! progressBar1.PerformStep(); } // reset the progress bar progressBar1.Value = 0; }
Once calculated, the accuracy plots show both the assessed accuracy and predicted accuracy for the same timeframe. Note that by changing the Confidence Interval percentage, the predicted accuracy values will change. With 50% selected, roughly 50% of the predicted values will lie above the actual values, and 50% will lie below.
The remaining code in this reference application utilizes a number of ZedGraph types to organize and display the azimuth-elevation and DOP data in graphs. On the DOP plot you can select and deselect the types of DOP data that you want to be displayed. If you hover the mouse pointer over a data point on the DOP graph, a tooltip displays identifying the PRNs of the satellites used in creating that DOP value, as well as the time and the DOP value itself; on the azimuth-elevation graph, the tooltip shows the PRN, the time and the azimuth and elevation angles. See the commented code for details.