/// <summary>
        /// Parse key information about the first raster: the geotransform, shape (n pixels X and Y), and
        /// nodata value (of the first band). Also calculate the latitude of each row and the longitude of
        /// each column.
        /// </summary>
        private void PopulateRasterProperties()
        {
            string firstFilename = m_FileDates.First().Value;

            GeoTransform = GDAL_Operations.GetGeoTransform(firstFilename);
            Shape        = GDAL_Operations.GetRasterShape(firstFilename);
            NoDataValue  = GDAL_Operations.GetNoDataValue(firstFilename);
            Projection   = GDAL_Operations.GetProjection(firstFilename);
            double[] latcoords = new double[Shape.Item1];
            double[] loncoords = new double[Shape.Item2];
            double   originX   = GeoTransform[0];
            double   cellsizeX = GeoTransform[1];
            double   originY   = GeoTransform[3];
            double   cellsizeY = GeoTransform[5];

            for (int i = 0; i < Shape.Item1; i++)
            {
                latcoords[i] = originY + i * cellsizeY;
            }
            for (int i = 0; i < Shape.Item2; i++)
            {
                loncoords[i] = originX + i * cellsizeX;
            }
            GlobalLatCoords = latcoords;
            GlobalLonCoords = loncoords;
        }
        public float[][] ReadRegionAcrossFiles(int xOffset, int yOffset, int xSize, int ySize)
        {
            int nPixPerTile = xSize * ySize;

            float[][] tileData = new float[m_FileDates.Count][];
            int       fileNum  = 0;

            foreach (var t in m_FileDates)
            {
                var newshape = GDAL_Operations.GetRasterShape(t.Value);
                if (newshape.Item1 != Shape.Item1 || newshape.Item2 != Shape.Item2)
                {
                    throw new ArgumentException("Raster shapes don't match");
                }
                var newGT = GDAL_Operations.GetGeoTransform(t.Value);
                if (!GeoTransform.SequenceEqual(newGT))
                {
                    throw new ArgumentException("Raster geotransforms don't match");
                }
                var newNDV = GDAL_Operations.GetNoDataValue(t.Value);
                if (newNDV != NoDataValue)
                {
                    throw new ArgumentException("Raster nodata values don't match");
                }
                var newProj = GDAL_Operations.GetProjection(t.Value);
                if (newProj != Projection)
                {
                    throw new ArgumentException("Raster projections don't match");
                }
                tileData[fileNum] = GDAL_Operations.ReadGDALRasterBandsToFlatArray(
                    t.Value, xSize, ySize, xOffset, yOffset, 1);
                fileNum += 1;
            }
            return(tileData);
        }
        public double[] GetSubsetGeoTransform(PixelLims TileCoords)
        {
            string firstFilename = m_FileDates.First().Value;
            var    res           = GDAL_Operations.GetClippedGeoTransform(firstFilename, TileCoords);

            return(res);
        }
        static float[] TestReadTileAcrossTime(int column, int row, int xSize = 512, int ySize = 512)
        {
            int xOff = column * xSize;
            int yOff = row * ySize;

            string fileWildCard = "F:\\MOD11A2_Gapfilled_Output\\LST_Day\\Output_Final_30k_2030pc\\*Data.tif";
            IFilenameDateParser modisFileParse = new FilenameDateParser_MODIS8DayRaw();
            var    details       = GetFilenamesAndDates(fileWildCard, modisFileParse);
            string firstFileName = details[0].Item1;

            double[] overallGT = GDAL_Operations.GetGeoTransform(firstFileName);
            var      shape     = GDAL_Operations.GetRasterShape(firstFileName);

            if (yOff > shape.Item1 || xOff > shape.Item2)
            {
                throw new ArgumentException("you specified a column or row greater than the number of tiles available");
            }
            if (yOff + ySize > shape.Item1)
            {
                ySize = shape.Item1 - yOff;
            }
            if (xOff + xSize > shape.Item2)
            {
                xSize = shape.Item2 - xOff;
            }
            int nPix = xSize * ySize;

            float[] tileData = new float[nPix * details.Count];

            for (int t = 0; t < details.Count; t++)
            {
                int      pxStart  = nPix * t;
                string   filename = details[t].Item1;
                DateTime filedate = details[t].Item2;
                var      newshape = GDAL_Operations.GetRasterShape(filename);
                if (newshape.Item1 != shape.Item1 || newshape.Item2 != shape.Item2)
                {
                    throw new ArgumentException("Raster shapes don't match");
                }
                var bandarr = GDAL_Operations.ReadGDALRasterBandsToFlatArray(filename, xSize, ySize, xOff, yOff, 1);
                Array.Copy(bandarr, 0, tileData, pxStart, bandarr.Length);
                // tileData[pxStart : pxStart+nPix] = arr;
            }
            return(tileData);
        }
        // see also https://www.codeproject.com/Articles/14465/Specify-a-Configuration-File-at-Runtime-for-a-C-Co


        private TSModelRunner(IFilenameDateParser _parser, TSIModelConfig cfg)
        {
            _fnParser = _parser;
            //var set = Properties.Settings.Default;
            //System.Console.WriteLine("Looking for files in " + m_FileWildCard);
            //var d = new System.Diagnostics.DefaultTraceListener();

            _maxReader = new TiffCubeReader(cfg.dataPathConfig.MaxTempFiles, _fnParser,
                                            cfg.modelRunConfig.ReadFromDate, cfg.modelRunConfig.ReadToDate);
            var nMax = _maxReader.Filenames.Count;

            Console.WriteLine("Looking for max temp files in " + cfg.dataPathConfig.MaxTempFiles +
                              " - found " + nMax.ToString());
            _minReader = new TiffCubeReader(cfg.dataPathConfig.MinTempFiles, _fnParser,
                                            cfg.modelRunConfig.ReadFromDate, cfg.modelRunConfig.ReadToDate);
            var nMin = _minReader.Filenames.Count;

            Console.WriteLine("Looking for min temp files in " + cfg.dataPathConfig.MinTempFiles +
                              " - found " + nMin.ToString());

            if (nMax == 0 || nMin == 0)
            {
                throw new ArgumentException("Can't continue without data");
                // I don't see any reason not to continue if the numbers for day and night aren't actually equal
            }
            if (!_maxReader.GeoTransform.SequenceEqual(_minReader.GeoTransform))
            {
                throw new ArgumentException("max and min temp image transforms do not match!");
            }
            if (!GDAL_Operations.GetGeoTransform(cfg.dataPathConfig.MaskFile).SequenceEqual(_maxReader.GeoTransform))
            {
                throw new ArgumentException("Land-sea mask doesn't match images!");
            }
            if (!System.IO.Directory.Exists(cfg.dataPathConfig.OutputFolder))
            {
                System.IO.Directory.CreateDirectory(cfg.dataPathConfig.OutputFolder);
            }
            _cfg = cfg;
        }
        public float[][] ReadRegionAcrossFiles_MP(int xOffset, int yOffset, int xSize, int ySize)
        {
            int nPixPerTile = xSize * ySize;

            float[][]       tileData = new float[m_FileDates.Count][];
            ParallelOptions b        = new ParallelOptions();

            b.MaxDegreeOfParallelism = 6;
            var keys = m_FileDates.Keys;

            Parallel.For(0, keys.Count, b, c =>
            {
                var fDate    = keys[c];
                var fName    = m_FileDates[fDate];
                var newshape = GDAL_Operations.GetRasterShape(fName);
                if (newshape.Item1 != Shape.Item1 || newshape.Item2 != Shape.Item2)
                {
                    throw new ArgumentException("Raster shapes don't match");
                }
                var newGT = GDAL_Operations.GetGeoTransform(fName);
                if (!GeoTransform.SequenceEqual(newGT))
                {
                    throw new ArgumentException("Raster geotransforms don't match");
                }
                var newNDV = GDAL_Operations.GetNoDataValue(fName);
                if (newNDV != NoDataValue)
                {
                    throw new ArgumentException("Raster nodata values don't match");
                }
                var newProj = GDAL_Operations.GetProjection(fName);
                if (newProj != Projection)
                {
                    throw new ArgumentException("Raster projections don't match");
                }
                tileData[c] = GDAL_Operations.ReadGDALRasterBandsToFlatArray(
                    fName, xSize, ySize, xOffset, yOffset, 1);
            });
            return(tileData);
        }
        static void Main(string [] args)
        {
            Environment.SetEnvironmentVariable("PATH", Environment.GetEnvironmentVariable("PATH")
                                               + ";C:\\Users\\zool1301.NDPH\\Documents\\Code_General\\temp-suitability\\TempSuitability_CSharp\\packages\\GDAL.Native.1.11.1\\gdal\\x64");

            //string testFile = "G:\\DataPrep\\ts_global\\TempSuitability.Pf.AnnualInfectiousDays.1k.2010.global.tif";
            string testFile = "F:\\MOD11A2_Gapfilled_Output\\LST_Day\\Output_Final_30k_2030pc\\LST_Day_All.vrt";

            int xsize  = 512;
            int ysize  = 512;
            int nbands = 727;

            Stopwatch sw = new Stopwatch();

            sw.Start();
            //var arr = GDAL_Operations.ReadGDALRasterBandsToFlatArray(testFile, xsize, ysize, 20480, 9216, null);
            var arr = TestReadTileAcrossTime(40, 18);

            sw.Stop();
            Console.WriteLine("Time elapsed reading tif data into flat array via pointers: {0}", sw.Elapsed);
            sw.Restart();
            int nPx = xsize * ysize;

            var cellArr = new float[nPx][];

            for (int y = 0; y < ysize; y++)
            {
                for (int x = 0; x < xsize; x++)
                {
                    int cellNum = y * xsize + x;
                    cellArr[cellNum] = new float[nbands];
                    for (int z = 0; z < nbands; z++)
                    {
                        cellArr[cellNum][z] = arr[nPx * z + cellNum];
                    }
                }
            }
            Console.WriteLine("Time elapsed reformatting flat data into cell-band order: {0}", sw.Elapsed);
            arr = null;

            sw.Restart();
            var arr3 = GDAL_Operations.ReadGDALRasterBandsToFlatArray(testFile, xsize, ysize, 20480, 9216, null);

            sw.Stop();
            Console.WriteLine("Time elapsed reading vrt data into flat array via pointers: {0}", sw.Elapsed);
            sw.Restart();
            var cellArr3 = new float[nPx][];

            for (int y = 0; y < ysize; y++)
            {
                for (int x = 0; x < xsize; x++)
                {
                    int cellNum = y * xsize + x;
                    cellArr3[cellNum] = new float[nbands];
                    for (int z = 0; z < nbands; z++)
                    {
                        cellArr3[cellNum][z] = arr3[nPx * z + cellNum];
                    }
                }
            }
            Console.WriteLine("Time elapsed reformatting flat vrt data into cell-band order: {0}", sw.Elapsed);
            arr3 = null;

            sw.Restart();
            var arr2 = GDAL_Operations.ReadGDALRasterBandsToJaggedArray(testFile, xsize, ysize, 20480, 9216, null);

            sw.Stop();
            Console.WriteLine("Time elapsed reading data into jagged array via pointers: {0}", sw.Elapsed);
            sw.Restart();
            var cellArr2 = new float[xsize * ysize][];

            for (int y = 0; y < ysize; y++)
            {
                for (int x = 0; x < xsize; x++)
                {
                    int cellNum = y * xsize + x;
                    cellArr2[cellNum] = new float[nbands];
                    for (int z = 0; z < nbands; z++)
                    {
                        cellArr2[cellNum][z] = arr2[z][cellNum];
                    }
                }
            }
            Console.WriteLine("Time elapsed reformatting jagged data into cell-band order: {0}", sw.Elapsed);

            System.Console.ReadKey();
        }
        /// <summary>
        /// Runs a temperature suitability model for all pixels in a region specified by pixel limits.
        /// Output is a jagged array with one value for each cell starting at top left and then row by
        /// row to bottom right, EXCEPT if no pixel in the tile is in a data area in which case the output
        /// is an array with length zero.
        /// Otherwise, each value is an array with one TS value for each month of the run period,
        /// EXCEPT if the cell is in the sea / masked area, in which case it is an array of length 0.
        /// Each cell location is done independently and this is therefore multithreaded.
        /// </summary>
        /// <param name="xOff"></param>
        /// <param name="yOff"></param>
        /// <param name="xSize"></param>
        /// <param name="ySize"></param>
        /// <returns></returns>
        public float[][] RunTile(int xOff, int yOff, int xSize, int ySize)
        {
            // test area: int xOff=20480, int yOff=9216, int xSize=512, int ySize=512
            var lsMask = GDAL_Operations.ReadGDALRasterBandsToFlatArray(
                _cfg.dataPathConfig.MaskFile,
                xSize, ySize, xOff, yOff, 1);

            if (!lsMask.Any(v => v == _cfg.modelRunConfig.MaskValidValue))
            {
                // this whole tile is in the sea, no need to run, return as a special case a zero length cell array
                return(new float[0][]);
            }
            var maxTempData = _maxReader.ReadCellData(xOff, yOff, xSize, ySize);
            var lats        = _maxReader.GetSubsetLatitudeCoords(yOff, ySize);
            var lons        = _maxReader.GetSubsetLongitudeCoords(xOff, xSize);
            var minTempData = _minReader.ReadCellData(xOff, yOff, xSize, ySize);
            int numCells    = xSize * ySize;

            // there's no reason in principle why we couldn't allow different numbers of min and max temp files although of
            // course we'd have to pass separate dates arrays into the model then, but hey. It would be a bit more effort
            // to handle different geotransforms in the max and min temps data and this way we make it less likely that
            // they've come from separate sources.
            Debug.Assert(minTempData.Length == maxTempData.Length);
            Debug.Assert(maxTempData.Length == numCells);
            var dates  = _maxReader.Filedates.ToArray();
            var nFiles = dates.Length;

            // get the model parameters from the default settings file, bearing in mind that this could have been re-set
            // at initialisation to a file specified on the commandline, rather than just being the default app config
            //var set = Properties.Settings.Default;
            var set = _cfg.modelConfig;
            PopulationParams popParams = new PopulationParams();

            popParams.DegreeDayThreshold       = set.SporogenesisDegreeDays;
            popParams.MinTempThreshold         = set.MinTempThresholdCelsius;
            popParams.MosquitoDeathTemperature = set.DeathTempCelsius;;
            popParams.MosquitoLifespanDays     = new TimeSpan((int)set.LifespanDays, 0, 0, 0);;
            popParams.SliceLength        = new TimeSpan((int)set.SliceLengthHours, 0, 0);
            popParams.MaxTempSuitability = set.MaxTSNormaliser;
            // max ts for default settings is 34.2467; the Weiss code had 33.89401 , not sure how generated.
            // I got this using iterative solver in excel.
            Console.WriteLine("Tile data loaded, computation beginning");

            var mConfig = _cfg.modelRunConfig;

            float[][]       tsOut       = new float[numCells][];
            DateTime[]      outputDates = null;
            ParallelOptions b           = new ParallelOptions();

            if (mConfig.MaxThreads != 0)
            {
                // set threads to 1 for easier step-through debugging or some other number to not hog the whole machine
                b.MaxDegreeOfParallelism = (int)mConfig.MaxThreads;
                //System.Threading.ThreadPool.SetMaxThreads(set.MaxThreads, set.MaxThreads);
                //System.Threading.ThreadPool.SetMinThreads(set.MinThreads, set.MinThreads);
            }
            int testnum = 0;

            while (outputDates == null && testnum < numCells)
            {
                // if we haven't got at least 50% of the data and 100+ points it's probably crap
                // (unless we're playing with synoptic data or something, so make this threshold configurable).
                // This doesn't affect the date calculation but the spline will throw an error
                // on initialisation
                var nValid = Math.Min(
                    maxTempData[testnum].Count(v => v != _maxReader.NoDataValue),
                    minTempData[testnum].Count(v => v != _minReader.NoDataValue));
                if (nValid < nFiles / 2 || nValid < mConfig.MinRequiredDataPoints)
                {
                    testnum += 1;
                    continue;
                }
                // set up a dummy model purely to parse the output dates (that they will all share)
                // avoids the need to test for the first one to do this in the parallel loop, which needs a lock,
                // which slows things right down with >>20 cores
                TSCellModel tsModel = new TSCellModel(
                    popParams,
                    new GeographicCellLocation()
                {
                    Latitude = lats[0], Longitude = lons[0]
                },
                    PopulationTypes.Pointers);
                if (!tsModel.SetData(maxTempData[testnum], minTempData[testnum], dates, _maxReader.NoDataValue.Value,
                                     mConfig.MaxTempFilesAreLST, mConfig.MinTempFilesAreLST))
                {
                    throw new ApplicationException("Pop goes the weasel");
                }
                ;
                outputDates = tsModel.OutputDates;
                break;
            }

            Parallel.For(0, numCells, b, c =>
            {
                if (lsMask[c] != mConfig.MaskValidValue)
                {
                    // this cell is in the sea, no need to run, return as a special case a zero length result array
                    tsOut[c] = new float[0];
                }
                else
                {
                    int rownum = c / xSize;
                    int colnum = c % xSize;
                    GeographicCellLocation geogParams = new GeographicCellLocation();
                    geogParams.Latitude  = lats[rownum];
                    geogParams.Longitude = lons[colnum];

                    //geogParams.ModelRuntimeDays = nDays;
                    //geogParams.StartJulianDay = 0;

                    // run only if we've got at least 50% of the data and 100+ points (or whatever is configured)
                    var nValid = Math.Min(
                        maxTempData[c].Count(v => v != _maxReader.NoDataValue),
                        minTempData[c].Count(v => v != _minReader.NoDataValue));
                    if (nValid < nFiles / 2 || nValid < mConfig.MinRequiredDataPoints)
                    {
                        tsOut[c] = new float[0];
                    }
                    else
                    {
                        TSCellModel tsModel = new TSCellModel(popParams, geogParams, PopulationTypes.Pointers);
                        // var dd = dayData[c];
                        // var nd = nightData[c];
                        // float optimal = 28.6857194664029F;
                        // for (int i = 0; i < 727; i++) {
                        //     dd[i] = optimal;
                        //                if (i%3==0) { dd[i] = 43; }
                        //     nd[i] = optimal; }
                        //tsModel.SetData(dd, nd, dates, _maxReader.NoDataValue.Value, false);

                        if (!tsModel.SetData(maxTempData[c], minTempData[c], dates, _maxReader.NoDataValue.Value,
                                             mConfig.MaxTempFilesAreLST, mConfig.MinTempFilesAreLST))
                        {
                            throw new ApplicationException("Pop goes the weasel");
                        }
                        ;
                        // run the entire ts model for this location
                        float[] tsCell    = tsModel.RunModel();
                        int nOutputPoints = tsCell.Length;
                        tsOut[c]          = tsCell;

                        /*lock (_lockobj) // ensure only 1 thread makes this check
                         * {
                         *  // calculate and record the dates of the outputs for an arbitrary one of the cell models
                         *  if (outputDates == null)
                         *  {
                         *      outputDates = tsModel.OutputDates;
                         *  }
                         * }*/
                    }
                }
            }
                         );
            if (_outputDates == null)
            {
                _outputDates = outputDates;
            }
            else if (outputDates != null && !_outputDates.SequenceEqual(outputDates))
            {
                throw new ApplicationException("Dates have changed between tiles somehow, this shouldn't happen...");
            }
            return(tsOut);
        }
        /// <summary>
        /// Runs TS model for all cells of all tiles required to cover the given bounding box at the given tile size.
        /// Any tile wholly in the sea will be skipped (no output files generated)
        /// </summary>
        /// <param name="WestDegrees"></param>
        /// <param name="EastDegrees"></param>
        /// <param name="NorthDegrees"></param>
        /// <param name="SouthDegrees"></param>
        /// <param name="TileSize"></param>
        public void RunAllTiles()
        {
            var globalGT  = _maxReader.GeoTransform;
            var lims      = _cfg.spatialLimits;
            var pxOverall = GDAL_Operations.CalculatePixelCoordsOfBlock(globalGT,
                                                                        lims.WestLimitDegrees, lims.EastLimitDegrees,
                                                                        lims.NorthLimitDegrees, lims.SouthLimitDegrees
                                                                        );
            var TileSize  = _cfg.modelRunConfig.MaxTileSizePx;
            var latsToRun = _maxReader.GetSubsetLatitudeCoords((int)pxOverall.NorthPixelCoord, (int)(pxOverall.SouthPixelCoord - pxOverall.NorthPixelCoord));
            var lonsToRun = _maxReader.GetSubsetLongitudeCoords((int)pxOverall.WestPixelCoord, (int)(pxOverall.EastPixelCoord - pxOverall.WestPixelCoord));
            var nTilesX   = (int)Math.Ceiling((double)lonsToRun.Length / TileSize);
            var nTilesY   = (int)Math.Ceiling((double)latsToRun.Length / TileSize);

            var    nTilesTotal = nTilesX * nTilesY;
            string tileDir     = lims.WestLimitDegrees.ToString() + "W-"
                                 + lims.EastLimitDegrees.ToString() + "E-"
                                 + lims.NorthLimitDegrees.ToString() + "N-"
                                 + lims.SouthLimitDegrees.ToString() + "S-"
                                 + TileSize.ToString() + "px_tiles";

            Console.WriteLine("Initiating run of  " + nTilesTotal.ToString() + " tiles");
            var runDir = nTilesTotal == 1 ?
                         _cfg.dataPathConfig.OutputFolder :
                         System.IO.Path.Combine(_cfg.dataPathConfig.OutputFolder, tileDir);

            System.IO.Directory.CreateDirectory(runDir);
            for (int tileRow = 0; tileRow < nTilesY; tileRow++)
            {
                int yOff          = (int)pxOverall.NorthPixelCoord + tileRow * TileSize;
                int yEnd          = yOff + TileSize;
                int thisTileYSize = TileSize;

                if (yEnd > pxOverall.SouthPixelCoord)
                {
                    thisTileYSize = (int)pxOverall.SouthPixelCoord - yOff;
                }
                for (int tileCol = 0; tileCol < nTilesX; tileCol++)
                {
                    var    tileNum = tileRow * nTilesX + tileCol + 1;
                    string tilenameLocPart;
                    if (nTilesTotal > 1)
                    {
                        tilenameLocPart = ".r" + tileRow.ToString("D3") + "_c" + tileCol.ToString("D3") + ".tif";
                    }
                    else
                    {
                        tilenameLocPart = ".tif";
                    }
                    if (System.IO.Directory.EnumerateFiles(runDir, "*" + tilenameLocPart).Count() != 0)
                    {
                        System.Console.WriteLine("Tile " + tileNum.ToString() + " appears to be already done, skipping");
                        continue;
                    }
                    int xOff          = (int)pxOverall.WestPixelCoord + tileCol * TileSize;
                    int xEnd          = xOff + TileSize;
                    int thisTileXSize = TileSize;
                    if (xEnd > pxOverall.EastPixelCoord)
                    {
                        thisTileXSize = (int)pxOverall.EastPixelCoord - xOff;
                    }

                    var tileCoords = new PixelLims(xOff, xOff + thisTileXSize, yOff, yOff + thisTileYSize);
                    var cellRes    = RunTile(xOff, yOff, thisTileXSize, thisTileYSize);
                    if (cellRes.Length == 0)
                    {
                        // whole tile was in masked / sea area. Do not bother to write output.
                        Console.WriteLine("Tile " + tileNum.ToString() + " was wholly in sea - skipped");
                        continue;
                    }
                    Console.WriteLine("Tile computation completed, writing output");
                    var tileRes = TransposeCellData(
                        cellRes,
                        thisTileXSize, thisTileYSize);
                    var tileGT   = _maxReader.GetSubsetGeoTransform(tileCoords);
                    var tileProj = _maxReader.Projection;
                    // write each tile to a separate tiff file - we can mosaic them later.
                    var tileLims = new PixelLims(0, thisTileXSize, 0, thisTileYSize);
                    for (int t = 0; t < tileRes.Length; t++)
                    {
                        var    tData = tileRes[t];
                        string fn    = _cfg.modelRunConfig.OutputFileTag + "." +
                                       _outputDates[t].Date.ToString("yyyy.MM") + tilenameLocPart;
                        string outFile = System.IO.Path.Combine(runDir, fn);
                        GDAL_Operations.WriteWholeTiff(outFile, tData, tileGT, tileProj, tileLims, true, _maxReader.NoDataValue);
                    }
                    Console.WriteLine("Tile " + tileNum.ToString() + " finished - wrote " + tileRes.Length.ToString() + " files");
                }
            }
        }