public static Image <Rgb24> DrawFrameSpectrogramAtScale(
            LdSpectrogramConfig config,
            SpectrogramZoomingConfig zoomingConfig,
            TimeSpan startTimeOfData,
            TimeSpan frameScale,
            double[,] frameData,
            IndexGenerationData indexGeneration,
            ImageChrome chromeOption)
        {
            // TODO:  the following normalisation bounds could be passed instead of using hard coded.
            double min = zoomingConfig.LowerNormalizationBoundForDecibelSpectrograms;
            double max = zoomingConfig.UpperNormalizationBoundForDecibelSpectrograms;

            //need to correctly orient the matrix for this method
            frameData = MatrixTools.MatrixRotate90Clockwise(frameData);

            // Get an unchromed image
            var spectrogramImage = ZoomFocusedSpectrograms.DrawStandardSpectrogramInFalseColour(frameData);

            if (chromeOption == ImageChrome.Without)
            {
                return(spectrogramImage);
            }

            int    nyquist      = indexGeneration.SampleRateResampled / 2;
            int    herzInterval = 1000;
            string title        = $"ZOOM SCALE={frameScale.TotalMilliseconds}ms/pixel ";
            var    titleBar     = ZoomFocusedSpectrograms.DrawTitleBarOfZoomSpectrogram(title, spectrogramImage.Width);

            spectrogramImage = ZoomFocusedSpectrograms.FrameZoomSpectrogram(
                spectrogramImage,
                titleBar,
                startTimeOfData,
                frameScale,
                config.XAxisTicInterval,
                nyquist,
                herzInterval);
            return(spectrogramImage);
        }
        /// <summary>
        /// Assume that we are processing data for one minute only.
        ///     From this one minute of data, we produce images at three scales.
        ///     A one minute recording framed at 20ms should yield 3000 frames.
        ///     But to achieve this where sr= 22050 and frameSize=512, we need an overlap of 71 samples.
        ///     Consequently only 2999 frames returned per minute.
        ///     Therefore have to pad end to get 3000 frames.
        /// </summary>
        public static TimeOffsetSingleLayerSuperTile[] DrawSuperTilesFromSingleFrameSpectrogram(DirectoryInfo dataDir, LdSpectrogramConfig analysisConfig, Dictionary <string, IndexProperties> indexProperties, SpectrogramZoomingConfig zoomingConfig, int minute, double[] imageScales, string basename, IndexGenerationData indexGeneration, ImageChrome chromeOption, TimeSpan alignmentPadding)
        {
            string fileStem = basename;

            // string analysisType = analysisConfig.AnalysisType;
            TimeSpan indexScale = indexGeneration.IndexCalculationDuration;
            TimeSpan frameScale = TimeSpan.FromSeconds(zoomingConfig.SpectralFrameDuration);

            var expectedDataDurationInSeconds = (int)indexGeneration.MaximumSegmentDuration.Value.TotalSeconds;
            var expectedFrameCount            = (int)Math.Round(expectedDataDurationInSeconds / zoomingConfig.SpectralFrameDuration);

            string fileName        = fileStem + "_" + minute + "min.csv";
            string csvPath         = Path.Combine(dataDir.FullName, fileName);
            bool   skipHeader      = true;
            bool   skipFirstColumn = true;

            // read spectrogram into a list of frames
            List <double[]> frameList = CsvTools.ReadCSVFileOfDoubles(csvPath, skipHeader, skipFirstColumn);

            if (frameList == null)
            {
                LoggedConsole.WriteErrorLine(
                    "WARNING: METHOD DrawSuperTilesFromSingleFrameSpectrogram(): NO SPECTRAL DATA SUPPLIED");
                return(null);
            }

            PadEndOfListOfFrames(frameList, expectedFrameCount);
            TrimEndOfListOfFrames(frameList, expectedFrameCount);

            //// frame count will be one less than expected for the recording segment because of frame overlap
            //// Therefore pad the end of the list of frames with the last frame.
            // int frameDiscrepancy = expectedFrameCount - frameList.Count;
            // if (frameDiscrepancy > 0)
            // {
            // double[] frame = frameList[frameList.Count - 1];
            // for (int d = 0; d < frameDiscrepancy; d++)
            // {
            // frameList.Add(frame);
            // }
            // }
            var frameData = new TemporalMatrix("rows", MatrixTools.ConvertList2Matrix(frameList), frameScale);

            frameData.SwapTemporalDimension();                              // so the two data matrices have the same temporal dimension

            TimeSpan startTime       = indexGeneration.AnalysisStartOffset; // default = zero minute of day i.e. midnight
            TimeSpan startTimeOfData = startTime + TimeSpan.FromMinutes(minute);

            var str = new TimeOffsetSingleLayerSuperTile[imageScales.Length];

            // make the images
            for (int scale = 0; scale < imageScales.Length; scale++)
            {
                TimeSpan imageScale        = TimeSpan.FromSeconds(imageScales[scale]);
                var      compressionFactor =
                    (int)Math.Round(imageScale.TotalMilliseconds / frameData.DataScale.TotalMilliseconds);
                double columnDuration = imageScale.TotalSeconds;

                // int expectedFrameCount = (int)Math.Round(expectedDataDurationInSeconds / columnDuration);

                // ############## RESEARCH CHOICE HERE >>>>  compress spectrograms to correct scale using either max or average
                // Average appears to offer better contrast.
                // double[,] data = frameData.CompressMatrixInTemporalDirectionByTakingMax(imageScale);
                double[,] data = frameData.CompressMatrixInTemporalDirectionByTakingAverage(imageScale);

                var spectrogramImage = DrawFrameSpectrogramAtScale(
                    analysisConfig,
                    zoomingConfig,
                    startTimeOfData,
                    imageScale,
                    data,
                    indexGeneration,
                    chromeOption);

                str[scale] = new TimeOffsetSingleLayerSuperTile(
                    alignmentPadding,
                    SpectrogramType.Frame,
                    imageScale,
                    spectrogramImage.CloneAs <Rgba32>(),
                    startTimeOfData);
            }

            return(str);
        }
        /// <summary>
        /// Draws FC index spectrograms for an entire row.
        /// </summary>
        public static TimeOffsetSingleLayerSuperTile[] DrawSuperTilesAtScaleFromIndexSpectrograms(
            LdSpectrogramConfig analysisConfig,
            Dictionary <string, IndexProperties> indexProperties,
            SpectrogramZoomingConfig zoomingConfig,
            TimeSpan imageScale,
            Dictionary <string, double[, ]> spectra,
            IndexGenerationData indexGeneration,
            string basename,
            ImageChrome chromeOption,
            TimeSpan alignmentPadding)
        {
            Contract.Requires(!spectra.IsNullOrEmpty(), "ERROR: NO SPECTRAL DATA SUPPLIED");

            // calculate source data duration from column count of arbitrary matrix
            TimeSpan dataScale = indexGeneration.IndexCalculationDuration;

            double[,] matrix = spectra.First().Value;
            TimeSpan sourceDataDuration = TimeSpan.FromSeconds(matrix.GetLength(1) * dataScale.TotalSeconds);

            int tileWidth      = zoomingConfig.TileWidth;
            int superTileWidth = zoomingConfig.SuperTileWidthDefault();
            var superTileCount =
                (int)Math.Ceiling(zoomingConfig.SuperTileCount(sourceDataDuration, imageScale.TotalSeconds));

            TimeSpan superTileDuration = TimeSpan.FromTicks(superTileWidth * imageScale.Ticks);

            // initialize the image array to return
            var superTiles = new TimeOffsetSingleLayerSuperTile[superTileCount];

            // sometimes the whole recording is not analyzed. In this case, jump the time index forward.
            TimeSpan startTime = indexGeneration.AnalysisStartOffset;

            // start the loop
            for (int t = 0; t < superTileCount; t++)
            {
                var image = DrawOneScaledIndexSpectrogramTile(
                    analysisConfig,
                    indexGeneration,
                    indexProperties,
                    startTime,
                    dataScale,
                    imageScale,
                    superTileWidth,
                    spectra,
                    basename,
                    chromeOption);

                superTiles[t] = new TimeOffsetSingleLayerSuperTile(
                    durationToPreviousTileBoundaryAtUnitScale: alignmentPadding,
                    spectrogramType: SpectrogramType.Index,
                    scale: imageScale,
                    image: image.CloneAs <Rgba32>(),
                    timeOffset: startTime);

                startTime += superTileDuration;
                if (startTime > sourceDataDuration)
                {
                    break;
                }
            }

            return(superTiles);
        }
        private static (TilingProfile Profile, TimeSpan padding) GetTilingProfile(
            ZoomParameters common,
            SpectrogramZoomingConfig zoomConfig,
            IndexGenerationData indexGeneration,
            double maxScale)
        {
            TilingProfile namingPattern;
            TimeSpan      padding;

            switch (zoomConfig.TilingProfile)
            {
            case nameof(PanoJsTilingProfile):
                namingPattern = new PanoJsTilingProfile();
                padding       = TimeSpan.Zero;

                if (zoomConfig.TileWidth != namingPattern.TileWidth)
                {
                    throw new ConfigFileException(
                              "TileWidth must match the default PanoJS TileWidth of " + namingPattern.TileWidth);
                }

                break;

            case nameof(AbsoluteDateTilingProfile):
                // Zooming spectrograms use multiple color profiles at different levels
                // therefore unable to set a useful tag (like ACI-ENT-EVN).
                if (indexGeneration.RecordingStartDate != null)
                {
                    var recordingStartDate = (DateTimeOffset)indexGeneration.RecordingStartDate;
                    var tilingStartDate    = GetPreviousTileBoundary(
                        zoomConfig.TileWidth,
                        maxScale,
                        recordingStartDate);
                    padding = recordingStartDate - tilingStartDate;

                    // if we're not writing tiles to disk, omit the basename because whatever the container format is
                    // it will have the base name attached
                    namingPattern = new AbsoluteDateTilingProfile(
                        common.OmitBasename ? string.Empty : common.OriginalBasename,
                        "BLENDED.Tile",
                        tilingStartDate,
                        indexGeneration.FrameLength / 2,
                        zoomConfig.TileWidth);
                }
                else
                {
                    throw new ArgumentNullException(
                              nameof(zoomConfig.TilingProfile),
                              "`RecordingStateDate` from the `IndexGenerationData.json` cannot be null when `AbsoluteDateTilingProfile` specified");
                }

                break;

            default:
                throw new ConfigFileException(
                          $"The {nameof(zoomConfig.TilingProfile)} configuration property was set to an unsupported value - no profile known by that name");
            }

            Log.Info($"Tiling had a left padding duration of {padding} to align tiles");
            Log.Info(
                $"Tiling using {namingPattern.GetType().Name}, Tile Width: {namingPattern.TileWidth}, Height: {namingPattern.TileHeight}");
            return(namingPattern, padding);
        }
        /// <summary>
        /// Derives false colour varying-resolution multi-index spectrograms from provided spectral indices.
        /// </summary>
        private static void GenerateIndexSpectrogramTiles(double[] indexScales, LdSpectrogramConfig ldsConfig, Dictionary <string, IndexProperties> filteredIndexProperties, SpectrogramZoomingConfig zoomConfig, Dictionary <string, double[, ]> spectra, IndexGenerationData indexGenerationData, string fileStem, TilingProfile tilingProfile, Tiler tiler, TimeSpan alignmentPadding)
        {
            // TOP MOST ZOOMED-OUT IMAGES
            Log.Info("Begin drawwing zooming indec spectrograms");

            foreach (double scale in indexScales)
            {
                Log.Info("Starting scale: " + scale);

                if (indexGenerationData.RecordingDuration.TotalSeconds < scale)
                {
                    Log.Warn($"Skipping scale step {scale} because recording duration ({indexGenerationData.RecordingDuration}) is less than 1px of scale");
                    continue;
                }

                // TODO: eventual optimisation, remove concept of super tiles
                // TODO: optimisation, cache aggregation layers (currently every layer is reaggregated from base level)
                TimeSpan imageScale = TimeSpan.FromSeconds(scale);
                var      superTiles = DrawSuperTilesAtScaleFromIndexSpectrograms(
                    ldsConfig,
                    filteredIndexProperties,
                    zoomConfig,
                    imageScale,
                    spectra,
                    indexGenerationData,
                    fileStem,
                    tilingProfile.ChromeOption,
                    alignmentPadding);

                // tile images as we go
                Log.Debug("Writing index tiles for " + scale);
                tiler.TileMany(superTiles);
                Log.Debug("Completed writing index tiles for " + scale);
            }
        }
        private static void GenerateStandardSpectrogramTiles(
            Dictionary <string, double[, ]> spectra,
            IndexGenerationData indexGeneration,
            LdSpectrogramConfig ldsConfig,
            Dictionary <string, IndexProperties> filteredIndexProperties,
            SpectrogramZoomingConfig zoomConfig,
            double[] standardScales,
            string fileStem,
            TilingProfile namingPattern,
            Tiler tiler,
            TimeSpan alignmentPadding)
        {
            Log.Info("START DRAWING ZOOMED-IN FRAME SPECTROGRAMS");

            TimeSpan dataDuration =
                TimeSpan.FromTicks(spectra["PMN"].GetLength(1) * indexGeneration.IndexCalculationDuration.Ticks);
            TimeSpan duration = indexGeneration.RecordingDuration;

            Contract.Requires(
                (dataDuration - duration).Absolute() < indexGeneration.IndexCalculationDuration,
                "The expected amount of data was not supplied");

            var minuteCount = (int)Math.Ceiling(dataDuration.TotalMinutes);

            // window the standard spectrogram generation so that we can provide adjacent supertiles to the
            // tiler, so that bordering / overlapping tiles (for cases where tile size != multiple of supertile size)
            // don't render partial tiles (i.e. bad/partial rendering of image)

            // this is the function generator
            // use of Lazy means results will only be evaluated once
            // and only when needed. This is useful for sliding window.
            Lazy <TimeOffsetSingleLayerSuperTile[]> GenerateStandardSpectrogramGenerator(int minuteToLoad)
            {
                return(new Lazy <TimeOffsetSingleLayerSuperTile[]>(
                           () =>
                {
                    Log.Info("Starting generation for minute: " + minuteToLoad);

                    var superTilingResults = DrawSuperTilesFromSingleFrameSpectrogram(
                        null /*inputDirectory*/,         //TODO:!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
                        ldsConfig,
                        filteredIndexProperties,
                        zoomConfig,
                        minuteToLoad,
                        standardScales,
                        fileStem,
                        indexGeneration,
                        namingPattern.ChromeOption,
                        alignmentPadding);

                    return superTilingResults;
                }));
            }

            Lazy <TimeOffsetSingleLayerSuperTile[]> previous = null;
            Lazy <TimeOffsetSingleLayerSuperTile[]> current  = null;
            Lazy <TimeOffsetSingleLayerSuperTile[]> next     = null;

            for (int minute = 0; minute < minuteCount; minute++)
            {
                Log.Trace("Starting loop for minute" + minute);

                // shift each value back
                previous = current;
                current  = next ?? GenerateStandardSpectrogramGenerator(minute);

                next = minute + 1 < minuteCount?GenerateStandardSpectrogramGenerator(minute + 1) : null;

                // for each scale level of the results
                for (int i = 0; i < current.Value.Length; i++)
                {
                    // finally tile the output
                    Log.Debug("Begin tile production for minute: " + minute);
                    tiler.Tile(previous?.Value[i], current.Value[i], next?.Value[i]);
                    Log.Debug("Begin tile production for minute: " + minute);
                }
            }
        }