/// <summary>
        /// Recursively calculates the nearest sound speed profiles along a given radial using a binary search-like algorithm
        /// 1. If start and end points are provided, use them, otherwise find the nearest SSP to each of those points
        /// 2. If the start point was calculated, add the SSP closest to the calculated start point to the enumerable
        /// 2. If the SSPs closest to the start and end points are within 10m of each other they are considered identical and there are 
        ///    assumed to be no more intervening points
        /// 3. If the SSPs closest to the start and end points are NOT within 10m of each other, calculate the midpoint of the segment 
        ///    and find the nearest SSP to that point.
        /// 4. If the SSP nearest the midpoint is not within 10m of the SSP nearest to the start point, recursively call this function to
        ///    find the new midpoint between the start point and the current midpoint
        /// 5. Return the
        /// </summary>
        /// <param name="segment"></param>
        /// <param name="startDistance"></param>
        /// <param name="startProfile"></param>
        /// <param name="endProfile"></param>
        /// <param name="bottomProfile"></param>
        /// <param name="soundSpeedData"></param>
        /// <param name="deepestProfile"></param>
        /// <returns></returns>
        static IEnumerable<Tuple<double, SoundSpeedProfile>> ProfilesAlongRadial(GeoSegment segment, double startDistance, SoundSpeedProfile startProfile, SoundSpeedProfile endProfile, BottomProfile bottomProfile, EnvironmentData<SoundSpeedProfile> soundSpeedData, SoundSpeedProfile deepestProfile)
        {
            var returnStartProfile = false;
            var returnEndProfile = false;
            if (startProfile == null)
            {
                returnStartProfile = true;
                startProfile = soundSpeedData.IsFast2DLookupAvailable
                                   ? soundSpeedData.GetNearestPointAsync(segment[0]).Result.Extend(deepestProfile)
                                   : soundSpeedData.GetNearestPoint(segment[0]).Extend(deepestProfile);
            }
            if (endProfile == null)
            {
                returnEndProfile = true;
                endProfile = soundSpeedData.IsFast2DLookupAvailable
                                 ? soundSpeedData.GetNearestPointAsync(segment[1]).Result.Extend(deepestProfile)
                                 : soundSpeedData.GetNearestPoint(segment[1]).Extend(deepestProfile);
            }
            if (returnStartProfile) yield return Tuple.Create(NearestBottomProfileDistanceTo(bottomProfile, startDistance), startProfile);
            // If the start and end profiles are the same, we're done
            if (startProfile.DistanceKilometers(endProfile) <= 0.01) yield break;

            // If not, create a middle profile
            var middleProfile = soundSpeedData.IsFast2DLookupAvailable
                                    ? soundSpeedData.GetNearestPointAsync(segment.Center).Result.Extend(deepestProfile)
                                    : soundSpeedData.GetNearestPoint(segment.Center).Extend(deepestProfile);
            // If the center profile is different from BOTH endpoints
            if (startProfile.DistanceKilometers(middleProfile) > 0.01 && middleProfile.DistanceKilometers(endProfile) > 0.01)
            {
                // Recursively create and return any new sound speed profiles between the start and the center
                var firstHalfSegment = new GeoSegment(segment[0], segment.Center);
                foreach (var tuple in ProfilesAlongRadial(firstHalfSegment, startDistance, startProfile, middleProfile, bottomProfile, soundSpeedData, deepestProfile)) yield return tuple;

                var centerDistance = startDistance + Geo.RadiansToKilometers(segment[0].DistanceRadians(segment.Center));
                // return the center profile
                yield return Tuple.Create(NearestBottomProfileDistanceTo(bottomProfile, centerDistance), middleProfile);

                // Recursively create and return any new sound speed profiles between the center and the end
                var secondHalfSegment = new GeoSegment(segment.Center, segment[1]);
                foreach (var tuple in ProfilesAlongRadial(secondHalfSegment, centerDistance, middleProfile, endProfile, bottomProfile, soundSpeedData, deepestProfile)) yield return tuple;
            }
            var endDistance = startDistance + Geo.RadiansToKilometers(segment.LengthRadians);
            // return the end profile
            if (returnEndProfile) yield return Tuple.Create(NearestBottomProfileDistanceTo(bottomProfile, endDistance), endProfile);
        }
        public static void CreateBellhopEnvironment(string outputDirectory, string name, 
                                                    double sourceDepth, double frequency, double verticalBeamWidth, double depressionElevationAngle, 
                                                    List<double> bathymetryRanges, List<double> bathymetryDepths, List<double> soundspeedDepths, 
                                                    List<double> soundspeedSpeeds, List<double> receiverRanges, List<double> receiverDepths, 
                                                    int sedimentType, int beamCount, double maxDepth)
        {
            if (!Directory.Exists(outputDirectory)) Directory.CreateDirectory(outputDirectory);

            // Write the bathymetry file
            var bathymetryFilename = Path.Combine(outputDirectory, name + ".bty");
            using (var writer = new StreamWriter(bathymetryFilename))
            {
                writer.WriteLine("'C'");
                writer.WriteLine(bathymetryRanges.Count);
                for (var index = 0; index < bathymetryRanges.Count; index++)
                    writer.WriteLine("{0} {1}", bathymetryRanges[index], bathymetryDepths[index]);
            }

            var acousticProperties = new AcousticProperties
            {
                HighFrequency = (float)frequency,
                LowFrequency = (float)frequency,
                DepressionElevationAngle = (float)depressionElevationAngle,
                SourceDepth = (float)sourceDepth,
                VerticalBeamWidth = (float)verticalBeamWidth,
            };
            var maxRadius = (int)Math.Ceiling(receiverRanges.Last() * 1.01); // Allow an extra 1% of range so the beams don't run off the end before they hit the last column of receivers

            var sspData = soundspeedDepths.Select((t, index) => new SoundSpeedSample((float)t, (float)soundspeedSpeeds[index])).ToList();
            var soundSpeedProfile = new SoundSpeedProfile
            {
                Data = sspData
            };
            var result = GetRadialConfiguration(acousticProperties, soundSpeedProfile, SedimentTypes.Find(sedimentType), 
                                                maxDepth, maxRadius, receiverRanges, receiverDepths, 
                                                false, true, true, beamCount);
            File.WriteAllText(Path.Combine(outputDirectory, name + ".env"), result, new ASCIIEncoding());
        }
        public static string GetRadialConfiguration(AcousticProperties acousticProperties, SoundSpeedProfile ssp, SedimentType sediment, 
                                                    double maxDepth, double maxRadius, List<double> ranges, List<double> depths, 
                                                    bool useSurfaceReflection, bool useVerticalBeamforming, bool generateArrivalsFile, int beamCount)
        {
            using (var sw = new StringWriter())
            {
                sw.WriteLine("'TL' ! Title");
                sw.WriteLine("{0} ! Frequency (Hz)", acousticProperties.Frequency);
                sw.WriteLine("1 ! NMedia"); // was NMEDIA in gui_genbellhopenv.m
                sw.WriteLine(useSurfaceReflection ? "'CFFT ' ! Top Option" : "'CVFT ' ! Top Option");

                sw.WriteLine("0  0.00 {0} ! N sigma depth", ssp.Data[ssp.Data.Count - 1].Depth);

                // If SSP is shallower than the bathymetry then extrapolate an SSP entry for the deepest part of the water
                //if (SSP.DepthVector[SSP.DepthVector.Length - 1] < RealBottomDepth_Meters)
                //    SoundSpeedProfile = ExtrapolateSSP(SoundSpeedProfile, RealBottomDepth_Meters);

                foreach (var sample in ssp.Data)
                    sw.WriteLine("{0:0.00} {1:0.00} 0.00 1.00 0.00 0.00 / ! z c cs rho", sample.Depth, sample.SoundSpeed);

                sw.WriteLine("'A*' 0.00 ! Bottom Option, sigma"); // A = Acoustic halfspace, ~ = read bathymetry file, 0.0 = bottom roughness (currently ignored)
                sw.WriteLine("{0} {1} {2} {3} {4} {5} / ! lower halfspace", maxDepth, sediment.CompressionWaveSpeed, sediment.ShearWaveSpeed, sediment.Density, sediment.LossParameter, 0);
                // Source and Receiver Depths and Ranges
                sw.WriteLine("1"); // Number of Source Depths
                sw.WriteLine("{0} /", acousticProperties.SourceDepth); // source depth
                sw.WriteLine("{0}", depths.Count); // Number of Receiver Depths
                foreach (var depth in depths) sw.Write("{0} ", depth);
                sw.WriteLine("/ ! Receiver Depths (m)");
                sw.WriteLine("{0}", ranges.Count); // Number of Receiver Ranges
                foreach (var range in ranges) sw.Write("{0} ", range);
                sw.WriteLine("/ ! Receiver Ranges (km)");

                if (generateArrivalsFile) sw.WriteLine("'aG'");  // aB
                else sw.WriteLine(useVerticalBeamforming ? "'IG*'" : "'I'");
                // if useVerticalBeamforming is true, then SBPFIL must be present (Source Beam Pattern file)
                sw.WriteLine("{0}", beamCount); // Number of beams
                //sw.WriteLine("0"); // Number of beams
                var verticalHalfAngle = acousticProperties.VerticalBeamWidth / 2;
                var angle1 = acousticProperties.DepressionElevationAngle - verticalHalfAngle;
                var angle2 = acousticProperties.DepressionElevationAngle + verticalHalfAngle;
                sw.WriteLine("{0} {1} /", angle1, angle2); // Beam fan half-angles (negative angles are toward the surface
                //sw.WriteLine("-60.00 60.00 /"); // Beam fan half-angles (negative angles are toward the surface
                //sw.WriteLine("{0:F} {1:F} {2:F} ! step zbox(meters) rbox(km)", experiment.TransmissionLossSettings.DepthCellSize, RealBottomDepth_Meters + 100, (bottomProfile.Length / 1000.0) * 1.01);
                sw.WriteLine("{0} {1} {2}", (ranges[0] / 2) * 1000, maxDepth, maxRadius);
                return sw.ToString();
            }
        }
        public static SoundSpeedField ReadFile(string gdemDirectory, TimePeriod month, GeoRect region)
        {
            var temperatureFile = NetCDFFile.Open(FindTemperatureFile(month, gdemDirectory));
            var temperatureLatitudes = ((NcVarDouble)temperatureFile.Variables.Single(var => var.Name == "lat")).ToArray();
            var temperatureLongitudes = ((NcVarDouble)temperatureFile.Variables.Single(var => var.Name == "lon")).ToArray();
            var temperatureDepths = ((NcVarDouble)temperatureFile.Variables.Single(var => var.Name == "depth")).ToArray();
            var temperatureData = ((NcVarShort)temperatureFile.Variables.Single(var => var.Name == "water_temp"));
            var temperatureMissingValue = ((NcAttShort)temperatureData.Attributes.Single(att => att.Name == "missing_value"))[0];
            var temperatureScaleFactor = ((NcAttFloat)temperatureData.Attributes.Single(att => att.Name == "scale_factor"))[0];
            var temperatureAddOffset = ((NcAttFloat)temperatureData.Attributes.Single(att => att.Name == "add_offset"))[0];
            temperatureData.Filename = FindTemperatureFile(month, gdemDirectory);

            var salinityFile = NetCDFFile.Open(FindSalinityFile(month, gdemDirectory));
            //var salinityLatitudes = ((NcVarDouble)salinityFile.Variables.Single(var => var.Name == "lat")).ToArray();
            //var salinityLongitudes = ((NcVarDouble)salinityFile.Variables.Single(var => var.Name == "lon")).ToArray();
            //var salinityDepths = ((NcVarDouble)salinityFile.Variables.Single(var => var.Name == "depth")).ToArray();
            var salinityData = ((NcVarShort)salinityFile.Variables.Single(var => var.Name == "salinity"));
            var salinityMissingValue = ((NcAttShort)salinityData.Attributes.Single(att => att.Name == "missing_value"))[0];
            var salinityScaleFactor = ((NcAttFloat)salinityData.Attributes.Single(att => att.Name == "scale_factor"))[0];
            var salinityAddOffset = ((NcAttFloat)salinityData.Attributes.Single(att => att.Name == "add_offset"))[0];
            salinityData.Filename = FindSalinityFile(month, gdemDirectory);

            var north = region.North;
            var south = region.South;
            var east = region.East;
            var west = region.West;

            if (temperatureLongitudes.First() > west) west += 360;
            if (temperatureLongitudes.Last() < west) west -= 360;
            if (temperatureLongitudes.First() > east) east += 360;
            if (temperatureLongitudes.Last() < east) east -= 360;

            var lonMap = new List<AxisMap>();
            var latMap = new List<AxisMap>();
            int i;
            if (east < west)
            {
                for (i = 0; i < temperatureLongitudes.Length; i++) 
                    if ((temperatureLongitudes[i] <= east) || (temperatureLongitudes[i] >= west)) 
                        lonMap.Add(new AxisMap((float)temperatureLongitudes[i], i));
            }
            else
            {
                for (i = 0; i < temperatureLongitudes.Length; i++) 
                    if ((temperatureLongitudes[i] >= west) && (temperatureLongitudes[i] <= east)) 
                        lonMap.Add(new AxisMap((float)temperatureLongitudes[i], i));
            }
            for (i = 0; i < temperatureLatitudes.Length; i++) if (temperatureLatitudes[i] >= south && temperatureLatitudes[i] <= north) latMap.Add(new AxisMap((float)temperatureLatitudes[i], i));
            var selectedLons = lonMap.Select(x => x.Value).ToArray();
            var selectedLats = latMap.Select(y => y.Value).ToArray();

            var latCount = selectedLats.Length;
            var lonCount = selectedLons.Length;

            var newFieldEnvironmentData = new List<SoundSpeedProfile>();

            for (var lonIndex = 0; lonIndex < lonCount; lonIndex++)
            {
                var lon = lonMap[lonIndex].Value;
                var wrappedLon = lon;
                while (wrappedLon > 180) wrappedLon -= 360;
                while (wrappedLon < -180) wrappedLon += 360;

                var lonSourceIndex = lonMap[lonIndex].Index;
                for (var latIndex = 0; latIndex < latCount; latIndex++)
                {
                    var lat = latMap[latIndex].Value;
                    var latSourceIndex = latMap[latIndex].Index;
                    var newProfile = new SoundSpeedProfile(new Geo(lat, wrappedLon));
                    for (var depthIndex = 0; depthIndex < temperatureDepths.Length; depthIndex++)
                    {
                        var temperatureValue = temperatureData[(uint)depthIndex, (uint)latSourceIndex, (uint)lonSourceIndex];
                        var salinityValue = salinityData[(uint)depthIndex, (uint)latSourceIndex, (uint)lonSourceIndex];
                        if ((Math.Abs(temperatureValue - temperatureMissingValue) < 0.0001) || (Math.Abs(salinityValue - salinityMissingValue) < 0.0001)) break;
                        var temperature = (temperatureValue * temperatureScaleFactor) + temperatureAddOffset;
                        var salinity = (salinityValue * salinityScaleFactor) + salinityAddOffset;
                        newProfile.Add(new SoundSpeedSample((float)temperatureDepths[depthIndex], temperature, salinity,
                                                            ChenMilleroLi.SoundSpeed(newProfile, (float)temperatureDepths[depthIndex], temperature, salinity)));
                    }
                    if (newProfile.Data.Count > 0) newFieldEnvironmentData.Add(newProfile);
                }
            }
            var newField = new SoundSpeedField { TimePeriod = month };
            newField.EnvironmentData.AddRange(newFieldEnvironmentData);
            newField.EnvironmentData.Sort();
            newField.EnvironmentData.TrimToNearestPoints(region);
            return newField;
        }