Skip to content

Commit

Permalink
[WIP] Finished ribbon plots
Browse files Browse the repository at this point in the history
Now correctly wraps around iamges when midnight != 00:00

Remaining work: tests!
  • Loading branch information
atruskie committed Apr 28, 2019
1 parent 2005bc8 commit 8f9e727
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 16 deletions.
3 changes: 2 additions & 1 deletion src/Acoustics.Shared/AppConfigHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@ public static class AppConfigHelper
public const string DefaultTargetSampleRateKey = "DefaultTargetSampleRate";

/// <summary>
/// Warning: do not use this format to print dates as strings - it will include a colon in the time zone offset :-(
/// Warning: do not use this format to print dates as strings - it will include a colon in the time zone offset :-(.
/// </summary>
public const string Iso8601FileCompatibleDateFormat = "yyyyMMddTHHmmsszzz";
public const string Iso8601FileCompatibleDateFormatUtcWithFractionalSeconds = "yyyyMMddTHHmmss.FFF\\Z";
public const string Iso8601FormatNoFractionalSeconds = "yyyy-MM-ddTHH:mm:sszzz";
public const string StandardDateFormatUtc = "yyyyMMdd-HHmmssZ";
public const string StandardDateFormatUtcWithFractionalSeconds = "yyyyMMdd-HHmmss.FFFZ";
public const string StandardDateFormat = "yyyyMMdd-HHmmsszzz";
Expand Down
67 changes: 67 additions & 0 deletions src/Acoustics.Shared/Extensions/DateTimeAndTimeSpanExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
// ReSharper disable once CheckNamespace
namespace System
{
using System.IO;
using Collections.Generic;
using Linq;

Expand Down Expand Up @@ -348,6 +349,63 @@ public static DateTimeOffset Round(this DateTimeOffset date, TimeSpan roundingIn
return date.AddTicks(halfIntervalTicks - ((date.Ticks + halfIntervalTicks) % roundingInterval.Ticks));
}

/// <summary>
/// Round a date to a time of day.
/// </summary>
/// <remarks>
/// Unlike the other rounding methods (which accept an interval), this method
/// will only output values that are 24-hours apart so that values always align
/// to the supplied <paramref name="timeOfDay"/>.
/// </remarks>
/// <param name="date">The date to round.</param>
/// <param name="timeOfDay">The time of day to round to.</param>
/// <param name="direction">The behaviour of the rounding operation.</param>
/// <returns>A rounded date.</returns>
public static DateTimeOffset RoundToTimeOfDay(
this DateTimeOffset date,
TimeSpan timeOfDay,
RoundingDirection direction)
{
var time = date.TimeOfDay;
if (time == timeOfDay)
{
return date;
}

TimeSpan delta;
if (direction == RoundingDirection.AwayFromZero)
{
var roundDelta = timeOfDay.Subtract(time);
if (roundDelta.Absolute() < TimeSpan.FromHours(12))
{
delta = timeOfDay;
}
else if (roundDelta < TimeSpan.Zero)
{
delta = timeOfDay + TimeSpan.FromDays(1);
}
else
{
delta = timeOfDay - TimeSpan.FromDays(1);
}
}
else if (direction == RoundingDirection.Ceiling)
{
delta = time < timeOfDay ? timeOfDay : timeOfDay + TimeSpan.FromDays(1);
}
else if (direction == RoundingDirection.Floor)
{
delta = time >= timeOfDay ? timeOfDay : timeOfDay - TimeSpan.FromDays(1);
}
else
{
throw new ArgumentOutOfRangeException(nameof(direction));
}

var dayOf = new DateTimeOffset(date.Date.Add(delta), date.Offset);
return dayOf;
}

public static TimeSpan Absolute(this TimeSpan span)
{
return span < TimeSpan.Zero ? new TimeSpan(span.Ticks * -1) : span;
Expand All @@ -362,5 +420,14 @@ public static TimeSpan Max(this TimeSpan t1, TimeSpan t2)
{
return t1 >= t2 ? t1 : t2;
}



public enum RoundingDirection
{
Floor,
Ceiling,
AwayFromZero,
}
}
}
9 changes: 7 additions & 2 deletions src/AnalysisPrograms/Production/Parsers/TimeSpanParser.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// <copyright file="TimeSpanParser.cs" company="QutEcoacoustics">
// <copyright file="TimeSpanParser.cs" company="QutEcoacoustics">
// All code in this file and all associated files are the copyright and property of the QUT Ecoacoustics Research Group (formerly MQUTeR, and formerly QUT Bioacoustics Research Group).
// </copyright>

Expand All @@ -22,7 +22,12 @@ public TimeSpan Parse(string argName, string value, CultureInfo culture)
{
if (!TimeSpan.TryParse(value, out var result))
{
throw new FormatException($"Invalid value specified for {argName}. '{value} is not a valid date time (with offset)");
if (value == "24:00" || value == "24:00:00")
{
return TimeSpan.FromSeconds(86400);
}

throw new FormatException($"Invalid value specified for {argName}. '{value}' is not a valid time");
}

return result;
Expand Down
62 changes: 49 additions & 13 deletions src/AnalysisPrograms/RibbonPlots/RibbonPlot.Entry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ void Add(string colorMap)
throw new MissingDataException("Could not find any ribbon files for any of the color maps. No ribbon plots were produced.");
}

LoggedConsole.WriteSuccessLine("Completed");
return ExceptionLookup.Ok;
}

Expand All @@ -166,7 +167,7 @@ private static Image<Rgb24> CreateRibbonPlot(
Dictionary<DateTimeOffset, FileInfo> ribbons,
RibbonPlotStats stats)
{
const int Padding = 5;
const int Padding = 2;
const int HorizontalPadding = 10;

// read random ribbon in to get height - assumes all ribbons are same height
Expand All @@ -180,7 +181,7 @@ private static Image<Rgb24> CreateRibbonPlot(
}

// get width of text
var scaledFont = new Font(SystemFonts.Find("Arial"), ribbonHeight);
var scaledFont = new Font(SystemFonts.Find("Arial"), ribbonHeight * 0.8f);
int labelWidth = (int)Math.Ceiling(TextMeasurer.Measure(someRibbon.Key.ToString(AppConfigHelper.RenderedDateFormatShort), new RendererOptions(scaledFont)).Width);

var finalHeight = Padding + ((Padding + ribbonHeight) * stats.Buckets);
Expand All @@ -192,22 +193,35 @@ private static Image<Rgb24> CreateRibbonPlot(

// draw labels and voids
Log.Debug("Rendering labels and backgrounds");
var day = stats.Start;

// draw 00:00 line
image.Mutate(context =>
{
var delta = stats.Start.RoundToTimeOfDay(TimeSpan.Zero, DateTimeAndTimeSpanExtensions.RoundingDirection.Ceiling) - stats.Start;
var left = ribbonLeft + (int)(delta.Modulo(RibbonPlotDomain).TotalSeconds / stats.First.IndexCalculationDuration.TotalSeconds).Round();
var top = Padding;
var bottom = Padding + ((Padding + ribbonHeight) * stats.Buckets);
context.DrawLines(
new GraphicsOptions(false),
Pens.Solid(NamedColors<Rgb24>.Red, 1),
new PointF(left, top),
new PointF(left, bottom));
});

var bucketDate = stats.Start;
var textGraphics = new TextGraphicsOptions(true)
{ HorizontalAlignment = HorizontalAlignment.Left, VerticalAlignment = VerticalAlignment.Top };
{ HorizontalAlignment = HorizontalAlignment.Left, VerticalAlignment = VerticalAlignment.Center };
var textColor = NamedColors<Rgb24>.Black;
var voidColor = NamedColors<Rgb24>.Gray;
var firstOffset = stats.Start.Offset;
for (var b = 0; b < stats.Buckets; b++)
{

if (Log.IsVerboseEnabled())
{
Log.Verbose($"Rendering bucket {day:O} label and void");
Log.Verbose($"Rendering bucket {bucketDate:O} label and void");
}

// get label
var dateLabel = day.ToOffset(firstOffset).ToString(AppConfigHelper.RenderedDateFormatShort);
var dateLabel = bucketDate.ToString(AppConfigHelper.RenderedDateFormatShort);

image.Mutate(Operation);

Expand All @@ -216,12 +230,14 @@ void Operation(IImageProcessingContext<Rgb24> context)
var y = Padding + ((Padding + ribbonHeight) * b);

// draw label
context.DrawText(textGraphics, dateLabel, scaledFont, textColor, new Point(HorizontalPadding, y));
context.DrawText(textGraphics, dateLabel, scaledFont, textColor, new Point(HorizontalPadding, y + (ribbonHeight / 2)));

// draw void
var @void = new RectangularPolygon(ribbonLeft, y, estimatedWidth, ribbonHeight);
context.Fill(voidColor, @void);
}

bucketDate = bucketDate.AddDays(1);
}

// copy images in
Expand Down Expand Up @@ -264,14 +280,15 @@ void Operation(IImageProcessingContext<Rgb24> context)
Log.Verbose($"Rendering {date:O} in two parts, wrapped to next day");
}

var split = estimatedWidth - (ribbonHorizontalOffset + ribbonWidth);
var split = estimatedWidth - ribbonHorizontalOffset;
var crop = source.Clone((context) => context.Crop(new Rectangle(0, 0, split, source.Height)));
image.Mutate(x => x.DrawImage(crop, new Point(left, top), GraphicsOptions.Default));

// now draw the wrap around - starting from the left, which is start of new day
top += Padding + ribbonHeight;
left = ribbonLeft;
var rest = source.Clone(context => context.Crop(new Rectangle(split, 0, source.Width, source.Height)));
var rest = source.Clone(context =>
context.Crop(new Rectangle(split, 0, ribbonWidth - split, source.Height)));
image.Mutate(x => x.DrawImage(rest, new Point(left, top), GraphicsOptions.Default));
}
else
Expand All @@ -293,8 +310,27 @@ public RibbonPlotStats(SortedDictionary<DateTimeOffset, IndexGenerationData> dat
this.First = datedIndices[this.Min];
this.Max = datedIndices.Keys.Last();
this.Last = datedIndices[this.Max];
this.Start = this.Min.Floor(midnight);
this.End = (this.Max + this.Last.RecordingDuration).Ceiling(midnight);

var itsAllTheSame = midnight == TimeSpan.FromDays(1) ? TimeSpan.Zero : midnight;

this.Start = this.Min
.RoundToTimeOfDay(itsAllTheSame, DateTimeAndTimeSpanExtensions.RoundingDirection.Floor);

if (this.Start.TimeOfDay != itsAllTheSame)
{
throw new InvalidOperationException(
$"Could not calculate start {this.Start:O} correctly for given midnight {midnight}");
}

this.End = (this.Max + this.Last.RecordingDuration)
.RoundToTimeOfDay(itsAllTheSame, DateTimeAndTimeSpanExtensions.RoundingDirection.Ceiling);

if (this.End.TimeOfDay != itsAllTheSame)
{
throw new InvalidOperationException(
$"Could not calculate end {this.End:O} correctly for given midnight {midnight}");
}

this.Buckets = (int)Math.Ceiling((this.End - this.Start).Divide(TimeSpan.FromHours(24)));
}

Expand Down
1 change: 1 addition & 0 deletions tests/Acoustics.Test/Acoustics.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@
<Compile Include="RuntimesTests.cs" />
<Compile Include="Shared\AppConfigHelperTests.cs" />
<Compile Include="Shared\ConfigTests.cs" />
<Compile Include="Shared\Extensions\DateTimeAndTimeSpanExtensions.cs" />
<Compile Include="Shared\LoggingTests\LoggingTests.cs" />
<Compile Include="Shared\PathUtilsTests.cs" />
<Compile Include="Shared\ProcessRunnerTests.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// <copyright file="DateTimeAndTimeSpanExtensions.cs" company="QutEcoacoustics">
// All code in this file and all associated files are the copyright and property of the QUT Ecoacoustics Research Group (formerly MQUTeR, and formerly QUT Bioacoustics Research Group).
// </copyright>

namespace Acoustics.Test.Shared.Extensions
{
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Accord.Math;
using Acoustics.Shared;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using static System.DateTimeAndTimeSpanExtensions;

[TestClass]
public class DateTimeAndTimeSpanExtensions
{
[DataTestMethod]
[DataRow("2019-04-08T11:30:00+10:00", RoundingDirection.AwayFromZero, "2019-04-08T11:30:00+10:00")]
[DataRow("2019-04-08T12:57:33+10:00", RoundingDirection.AwayFromZero, "2019-04-08T11:30:00+10:00")]
[DataRow("2019-04-08T09:57:33+10:00", RoundingDirection.AwayFromZero, "2019-04-08T11:30:00+10:00")]
[DataRow("2019-04-08T23:45:33+10:00", RoundingDirection.AwayFromZero, "2019-04-09T11:30:00+10:00")]
[DataRow("2019-04-07T23:15:33+10:00", RoundingDirection.AwayFromZero, "2019-04-07T11:30:00+10:00")]

[DataRow("2019-04-08T11:30:00+10:00", RoundingDirection.Floor, "2019-04-08T11:30:00+10:00")]
[DataRow("2019-04-08T12:57:33+10:00", RoundingDirection.Floor, "2019-04-08T11:30:00+10:00")]
[DataRow("2019-04-08T09:57:33+10:00", RoundingDirection.Floor, "2019-04-07T11:30:00+10:00")]
[DataRow("2019-04-08T23:45:33+10:00", RoundingDirection.Floor, "2019-04-08T11:30:00+10:00")]
[DataRow("2019-04-07T23:15:33+10:00", RoundingDirection.Floor, "2019-04-07T11:30:00+10:00")]

[DataRow("2019-04-08T11:30:00+10:00", RoundingDirection.Ceiling, "2019-04-08T11:30:00+10:00")]
[DataRow("2019-04-08T12:57:33+10:00", RoundingDirection.Ceiling, "2019-04-09T11:30:00+10:00")]
[DataRow("2019-04-08T09:57:33+10:00", RoundingDirection.Ceiling, "2019-04-08T11:30:00+10:00")]
[DataRow("2019-04-08T23:45:33+10:00", RoundingDirection.Ceiling, "2019-04-09T11:30:00+10:00")]
[DataRow("2019-04-07T23:15:33+10:00", RoundingDirection.Ceiling, "2019-04-08T11:30:00+10:00")]
public void TestRoundToTimeOfDay(string test, RoundingDirection direction, string expected)
{
var testDate = DateTimeOffset.ParseExact(test, AppConfigHelper.Iso8601FormatNoFractionalSeconds, CultureInfo.InvariantCulture);
var expectedDate = DateTimeOffset.ParseExact(expected, AppConfigHelper.Iso8601FormatNoFractionalSeconds, CultureInfo.InvariantCulture);

var actual = testDate.RoundToTimeOfDay(new TimeSpan(11, 30, 0), direction);

Assert.AreEqual(expectedDate, actual);
}
}
}

0 comments on commit 8f9e727

Please sign in to comment.