void WriteSafeHavenOverlay(float[,] ary, string path)
        {
            var map = new SafeHavenColorTable();
            Func <float, byte> func = v => (byte)Math.Min(255f, v);  // color entries map to hours

            using (var bmp = BitmapHelper.ToFormat8bppIndexed(ary, func))
            {
                map.LoadPalette(bmp);
                bmp.Save(LandingSiteDataset.AppendToFilename(path, "", ".png"), System.Drawing.Imaging.ImageFormat.Png);
                using (var bmp2 = map.AddLegend(bmp))
                    bmp2.Save(LandingSiteDataset.AppendToFilename(path, "_legend", ".png"), System.Drawing.Imaging.ImageFormat.Png);
            }
        }
        public void WriteSafeHavenGeotiffs(string path)
        {
            var outer_start = Start.AddDays(-28);
            var outer_stop  = Stop.AddDays(28);

            GetLowEarthTimes(outer_start, outer_stop, out List <DateTime> times_start_to_stop, out List <int> indices_of_minima_earth_elevation);

            for (var i = 0; i < indices_of_minima_earth_elevation.Count; i++)
            {
                Console.WriteLine($"i={i} index={indices_of_minima_earth_elevation[i]} time={times_start_to_stop[indices_of_minima_earth_elevation[i]]}");
            }
            var region_width  = Region.Width;
            var region_height = Region.Height;

            var combined     = new float[region_height, region_width];
            var month_images = Enumerable.Range(0, indices_of_minima_earth_elevation.Count).Select(i => new float[region_height, region_width]).ToList();

            var sun_vectors   = times_start_to_stop.Select(time => CSpice.SunPosition(time)).ToList();
            var earth_vectors = times_start_to_stop.Select(time => CSpice.EarthPosition(time)).ToList();

            // Get the indexes in the time and vector arrays of Start and Stop
            var first_inside = times_start_to_stop.Select((time, index) => (time, index)).Where(pair => pair.time >= Start).Select(pair => pair.index).First();
            var last_inside  = -1;

            for (var i = times_start_to_stop.Count - 1; i >= 0; i--)
            {
                if (times_start_to_stop[i] <= Stop)
                {
                    last_inside = i;
                    break;
                }
            }

            //var debugpt = new Point(Region.Left + 545, Region.Top + 490);
            //debugpt = new Point(Region.Left + 494, Region.Top + 438);
            var ids = TerrainPatch.EnumerateIds(TerrainPatch.CoveringIdRectangle(Region)).ToList();

            {
                var not_generated = ids.Where(id => !File.Exists(TerrainPatch.FromId(id, ObserverHeightInMeters).Path)).ToList();
                Console.WriteLine($"{not_generated.Count} patches haven't been generated yet.  Generating them.");
                var gpuProcessor = ViperEnvironment.Processor as CPUHorizons;
                var queue        = not_generated.Select(id => TerrainPatch.FromId(id)).ToList();
                gpuProcessor.RunQueue(queue, writeHorizons: WriteHorizons, unloadHorizons: true);
            }

            Parallel.ForEach(ids,
                             new ParallelOptions {
                MaxDegreeOfParallelism = ViperEnvironment.MaxDegreeOfParallelism
            },
                             id =>
            {
                var patch = GetPatchWithHorizons(id, ObserverHeightInMeters);
                for (var row = 0; row < TerrainPatch.DefaultSize; row++)
                {
                    var region_row = (patch.Line + row) - Region.Y;
                    if (region_row < 0 || region_row >= region_height)
                    {
                        continue;
                    }
                    for (var col = 0; col < TerrainPatch.DefaultSize; col++)
                    {
                        //if ((patch.Line + row) != debugpt.Y || (patch.Sample + col) != debugpt.X)
                        //    continue;
                        var region_col = patch.Sample + col - Region.X;
                        if (region_col < 0 || region_col >= region_width)
                        {
                            continue;
                        }
                        var has_sun     = patch.GetLightCurve(row, col, sun_vectors).Select(sun => sun >= SafeHavenSunThreshold).ToList();
                        var has_comm    = patch.GetEarthOverHorizonCurve(row, col, earth_vectors).Select(rise => rise >= EarthMultipathThreshold).ToList();
                        var month_steps = MaxShadowWithoutComm2(indices_of_minima_earth_elevation, has_sun, has_comm);
                        combined[region_row, region_col] = 2f * month_steps.Max();
                        for (var i = 0; i < month_steps.Count; i++)
                        {
                            month_images[i][region_row, region_col] = 2f * month_steps[i];
                        }
                    }
                }
            });

            {
                var path_combined = LandingSiteDataset.AppendToFilename(path, "_combined");
                GeotiffHelper.WriteArrayAsGeotiff(combined, Region, path_combined);
                WriteSafeHavenOverlay(combined, path_combined);
            }

            for (var i = 0; i < month_images.Count; i++)
            {
                var time  = times_start_to_stop[indices_of_minima_earth_elevation[i]];
                var path1 = LandingSiteDataset.AppendToFilename(path, "_" + time.ToString("yyyy-MM-dd"));
                GeotiffHelper.WriteArrayAsGeotiff(month_images[i], Region, path1);
                WriteSafeHavenOverlay(month_images[i], path1);
            }
        }