add functionality for exporting pdf pages and viewing the details page. (needs tidying)

migrate history page to knockout bound pages
add charts n bits for race details page.
This commit is contained in:
chris.watts90@outlook.com 2018-02-28 20:08:04 +00:00
parent ed345150b9
commit 3f0542ae18
7 changed files with 601 additions and 45 deletions

View File

@ -1,7 +1,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Dynamic; using System.Dynamic;
using System.IO; using System.IO;
using System.Net;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Interfaces; using Interfaces;
using Nancy; using Nancy;
using Nancy.Responses; using Nancy.Responses;
@ -9,39 +14,306 @@ using Nancy.ViewEngines;
namespace RaceLapTimer.Modules namespace RaceLapTimer.Modules
{ {
public class HistoryModule:NancyModule public class HistoryModule : NancyModule
{ {
private IContainerHelper _helper; private IContainerHelper _helper;
public HistoryModule(IContainerHelper helper):base("/history") private readonly IExportManager _exporter;
private readonly IDocumentPathProvider _docPathProvider;
public HistoryModule(IContainerHelper helper
, IExportManager exportManager
, IDocumentPathProvider docPathProvider) : base("/history")
{ {
_helper = helper; _helper = helper;
_exporter = exportManager;
_docPathProvider = docPathProvider;
Get[""] = args => GetHistoryHomePage(); Get[""] = args => GetHistoryHomePage();
Get["/pdf/{id}"] = args => GetPdfFile(args); //make this async
Get["/pdf/{id}"] = (args) => GetPdfFile(args);
Get["/details/{id}"] = args => GetExportPage(args);
}
private dynamic GetExportPage(dynamic args)
{
var rand = new Random();
var model = new RaceSessionHistory
{
Title = "testmodel",
RaceMode = RaceMode.COMPETITION,
LapCount = 21
};
#region Pilots
var pilot1 = new Pilot
{
Id = 1,
ImageUrl = "/images/pilotPic.png",
Name = "testExportPilotName",
TeamName = "tstEx",
TransponderToken = 66
};
var pilot2 = new Pilot
{
Id = 2,
ImageUrl = "/images/pilotPic.png",
Name = "testExportPilot2Name",
TeamName = "tstEx2",
TransponderToken = 68
};
var pilot3 = new Pilot
{
Id = 3,
ImageUrl = "/images/pilotPic.png",
Name = "testExportPilot3Name",
TeamName = "tstEx2",
TransponderToken = 65
};
var pilot4 = new Pilot
{
Id = 4,
ImageUrl = "/images/pilotPic.png",
Name = "testExportPilot4Name",
TeamName = "tstEx4",
TransponderToken = 14
};
var pilot5 = new Pilot
{
Id = 2,
ImageUrl = "/images/pilotPic.png",
Name = "testExportPilot5Name",
TeamName = "tstEx",
TransponderToken = 17
};
var pilot6 = new Pilot
{
Id = 2,
ImageUrl = "/images/pilotPic.png",
Name = "testExportPilot6Name",
TeamName = "tstEx3",
TransponderToken = 15
};
var pilot7 = new Pilot
{
Id = 2,
ImageUrl = "/images/pilotPic.png",
Name = "testExportPilot7Name",
TeamName = "tstEx2",
TransponderToken = 14
};
#endregion
#region laps
model.LapTimes.Add(new RaceLap
{
LapNumber = 1,
LapTimeMs = rand.Next(10000,80000),
OverallPositon = 1,
RacerDetails = pilot1
});
model.LapTimes.Add(new RaceLap
{
LapNumber = 2,
LapTimeMs = rand.Next(10000,80000),
OverallPositon = 1,
RacerDetails = pilot1
});
model.LapTimes.Add(new RaceLap
{
LapNumber = 3,
LapTimeMs = rand.Next(10000,80000),
OverallPositon = 1,
RacerDetails = pilot1
});
model.LapTimes.Add(new RaceLap
{
LapNumber = 4,
LapTimeMs = rand.Next(10000,80000),
OverallPositon = 1,
RacerDetails = pilot1
});
model.LapTimes.Add(new RaceLap
{
LapNumber = 5,
LapTimeMs = rand.Next(10000,80000),
OverallPositon = 1,
RacerDetails = pilot1
});
model.LapTimes.Add(new RaceLap
{
LapNumber = 6,
LapTimeMs = rand.Next(10000,80000),
OverallPositon = 1,
RacerDetails = pilot1
});
model.LapTimes.Add(new RaceLap
{
LapNumber = 7,
LapTimeMs = rand.Next(10000,80000),
OverallPositon = 1,
RacerDetails = pilot1
});
model.LapTimes.Add(new RaceLap
{
LapNumber = 8,
LapTimeMs = rand.Next(10000,80000),
OverallPositon = 1,
RacerDetails = pilot1
});
model.LapTimes.Add(new RaceLap
{
LapNumber = 9,
LapTimeMs = rand.Next(10000,80000),
OverallPositon = 1,
RacerDetails = pilot1
});
model.LapTimes.Add(new RaceLap
{
LapNumber = 10,
LapTimeMs = rand.Next(10000,80000),
OverallPositon = 1,
RacerDetails = pilot1
});
model.LapTimes.Add(new RaceLap
{
LapNumber = 11,
LapTimeMs = rand.Next(10000,80000),
OverallPositon = 1,
RacerDetails = pilot1
});
model.LapTimes.Add(new RaceLap
{
LapNumber = 12,
LapTimeMs = rand.Next(10000,80000),
OverallPositon = 1,
RacerDetails = pilot1
});
model.LapTimes.Add(new RaceLap
{
LapNumber = 13,
LapTimeMs = rand.Next(10000,80000),
OverallPositon = 1,
RacerDetails = pilot1
});
model.LapTimes.Add(new RaceLap
{
LapNumber = 14,
LapTimeMs = rand.Next(10000,80000),
OverallPositon = 1,
RacerDetails = pilot1
});
model.LapTimes.Add(new RaceLap
{
LapNumber = 15,
LapTimeMs = rand.Next(10000,80000),
OverallPositon = 1,
RacerDetails = pilot1
});
model.LapTimes.Add(new RaceLap
{
LapNumber = 16,
LapTimeMs = rand.Next(10000,80000),
OverallPositon = 1,
RacerDetails = pilot1
});
model.LapTimes.Add(new RaceLap
{
LapNumber = 17,
LapTimeMs = rand.Next(10000,80000),
OverallPositon = 1,
RacerDetails = pilot1
});
model.LapTimes.Add(new RaceLap
{
LapNumber = 18,
LapTimeMs = rand.Next(10000,80000),
OverallPositon = 1,
RacerDetails = pilot1
});
model.LapTimes.Add(new RaceLap
{
LapNumber = 19,
LapTimeMs = rand.Next(10000,80000),
OverallPositon = 1,
RacerDetails = pilot1
});
model.LapTimes.Add(new RaceLap
{
LapNumber = 20,
LapTimeMs = rand.Next(10000,80000),
OverallPositon = 1,
RacerDetails = pilot1
});
model.LapTimes.Add(new RaceLap
{
LapNumber = 21,
LapTimeMs = rand.Next(10000,80000),
OverallPositon = 1,
RacerDetails = pilot1
});
#endregion
model.Racers.Add(pilot1);
model.Racers.Add(pilot2);
model.Racers.Add(pilot3);
model.Racers.Add(pilot4);
model.Racers.Add(pilot5);
model.Racers.Add(pilot6);
model.Racers.Add(pilot7);
model.FastestPilot = pilot1;
return View["HistoricRaceDetails.cshtml", model];
} }
private dynamic GetPdfFile(dynamic args) private dynamic GetPdfFile(dynamic args)
{ {
var raceSessionId = (int)args.Id; #region old
ViewLocationContext viewLocationContext = new ViewLocationContext() //var raceSessionId = (int)args.Id;
//ViewLocationContext viewLocationContext = new ViewLocationContext()
//{
// Context = Context,
// ModuleName = base.GetType().Name,
// ModulePath = ModulePath
//};
//var rendered = ViewFactory.RenderView("HistoryIndex.cshtml", null, viewLocationContext);
//var content = "";
//using (var ms = new MemoryStream())
//{
// rendered.Contents.Invoke(ms);
// ms.Seek(0, SeekOrigin.Begin);
// using (TextReader reader = new StreamReader(ms))
// {
// content = reader.ReadToEnd();
// }
//};
//var filePath = string.Empty;
#endregion
int raceId = args.id;
string exportType = Request.Query["exporttype"].Value;
var baseUrl = "http://localhost:8800";
var exportFileName = "RaceSessionExport" + raceId + ".pdf";
var exportFilePath = Path.Combine(_docPathProvider.GetPath(), exportFileName);
//TODO: replace testHTML with the actual exporthtml page!
var wc = new WebClient();
var html = wc.DownloadString(string.Format("{0}/history/details/{1}", baseUrl, raceId));
//Debug.WriteLine(html);
//TBD: check if export is available already
if (!File.Exists(exportFilePath))
{ {
Context = Context, //if not..create the export
ModuleName = base.GetType().Name, Stream stream;
ModulePath = ModulePath _exporter.Export(exportType, html, baseUrl, out stream);
}; //then save it to the export to the documents folder
var rendered = ViewFactory.RenderView("HistoryIndex.cshtml", null, viewLocationContext); using (stream)
var content = ""; using (var fs = new FileStream(exportFilePath, FileMode.Create))
using (var ms = new MemoryStream())
{ {
rendered.Contents.Invoke(ms); stream.Seek(0, SeekOrigin.Begin);
ms.Seek(0, SeekOrigin.Begin); stream.CopyTo(fs);
using (TextReader reader = new StreamReader(ms)) fs.Flush(true);
{
content = reader.ReadToEnd();
} }
}; }
var filePath = string.Empty; //return the file.
throw new NotImplementedException(); var resp = new StreamResponse(() => new FileStream(exportFilePath, FileMode.Open), MimeTypes.GetMimeType(exportFileName));
return Response.AsFile(filePath); return resp.AsAttachment(exportFileName);
//throw new NotImplementedException();
//return Response.AsFile(filePath);
} }
private dynamic GetHistoryHomePage() private dynamic GetHistoryHomePage()

View File

@ -0,0 +1,255 @@
@using System.Collections.Generic
@using Interfaces
@inherits Nancy.ViewEngines.Razor.NancyRazorViewBase<RaceSessionHistory>
@{
Layout = "razor-layout.cshtml";
}
<h3>@Model.Title</h3>
<link rel="stylesheet" href="/css/site.css" />
<!-- Start with page intro containing: title, lap count, racer count, race type (compo/standard), fastest pilot.-->
<table id="RaceOverviewInfo" class="table table-striped pageBreakInsideAvoid">
<thead>
<tr>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<th>Race Title</th>
<td colspan="3">@Model.Title</td>
</tr>
<tr>
<th>Lap Count</th>
<td colspan="3">@Model.LapCount</td>
</tr>
<tr>
<th>Racer Count</th>
<td colspan="3">@Model.RacerCount</td>
</tr>
<tr>
<th>Race Type</th>
<td colspan="3">@Model.RaceType</td>
</tr>
<tr>
<th>Fastest Pilot</th>
<td>@Model.FastestPilot.Name</td>
<th>Fastest Lap Time</th>
<td>@Model.FastestLapTime</td>
</tr>
</tbody>
</table>
<!-- Racer list -->
<table class="table table-bordered table-striped pageBreakInsideAvoid pageBreakAfter">
<thead>
<tr>
<th>Racer Name</th>
<th>Team Name</th>
<th>Racer Token Id</th>
<th>Racer Image</th>
</tr>
</thead>
<tbody>
@foreach (Pilot p in Model.Racers)
{
<tr>
<td class="vertical-center">@p.Name</td>
<td class="vertical-center">@p.TeamName</td>
<td class="vertical-center">@p.TransponderToken</td>
<td class="vertical-center"><img src="@p.ImageUrl" width="50" height="50" alt="Pilot Image" /></td>
</tr>
}
</tbody>
</table>
<!-- Follow with full break down, each lap, time etc. -->
<table id="LapDetailsInfo" class="table table-striped pageBreakInsideAvoid pageBreakAfter">
<thead>
<tr>
<th>Racer Number</th>
<th>Racer Name</th>
<th>Racer Team</th>
<th>Lap Number</th>
<th>Lap Time</th>
<th>Lap Position</th>
</tr>
</thead>
<tbody>
@foreach (RaceLap lap in Model.LapTimes)
{
<tr>
<td>@lap.RacerDetails.TransponderToken</td>
<td>@lap.RacerDetails.Name</td>
<td>@lap.RacerDetails.TeamName</td>
<td>@lap.LapNumber</td>
<td>@lap.LapTimeMs</td>
<td>@lap.OverallPositon</td>
</tr>
}
</tbody>
</table>
<!-- Follow with graph of lap times (chart.js) -->
<div id="RacerPositionGraphs" class="pageBreakInsideAvoid">
<canvas id="RacerPositionGraphsCanvas"></canvas>
</div>
<div id="LapTimeGraphs" class="pageBreakInsideAvoid">
<canvas id="LapTimeGraphsCanvas"></canvas>
</div>
<script src="/js/Chart.min.js" type="text/javascript"></script>
<script>
window.chartColors = {
red: 'rgb(255, 99, 132)',
orange: 'rgb(255, 159, 64)',
yellow: 'rgb(255, 205, 86)',
green: 'rgb(75, 192, 192)',
blue: 'rgb(54, 162, 235)',
purple: 'rgb(153, 102, 255)',
grey: 'rgb(201, 203, 207)'
};
var lapTimeGraphCfg = {
type: 'line',
data: {
@{
@Html.Raw("labels: [")
for (int i = 1; i <= Model.LapCount; i++)
{
@Html.Raw(i.ToString())
if (i != Model.LapCount)
{
@Html.Raw(",")
}
}
@Html.Raw("],")
}
@{
@Html.Raw("datasets: [")
for (int j = 0; j < Model.Racers.Count; j++)
{
Pilot p = Model.Racers[j];
@Html.Raw("{ label: \"" + p.Name + "\", backgroundColor: window.chartColors.red, borderColor: window.chartColors.red,data: [")
List<RaceLap> laps = new List<RaceLap>();
foreach (RaceLap lap in Model.LapTimes)
{
if (lap.RacerDetails.Id == p.Id)
{
laps.Add(lap);
}
}
for (int i = 0; i < laps.Count; i++)
{
@Html.Raw(laps[i].LapTimeMs.ToString())
if (i != laps.Count - 1)
{
@Html.Raw(",")
}
}
@Html.Raw("], fill: false}")
if (j != Model.Racers.Count - 1)
{
@Html.Raw(",")
}
}
@Html.Raw("]")
}
},
options: {
responsive: true,
title: {
display: true,
text: 'Racer Lap Times'
},
tooltips: {
mode: 'index',
intersect: false,
},
hover: {
mode: 'nearest',
intersect: true
},
scales: {
xAxes: [{
display: true,
scaleLabel: {
display: true,
labelString: 'Lap'
}
}],
yAxes: [{
display: true,
ticks: {
beginAtZero:true
},
scaleLabel: {
display: true,
labelString: 'Time'
}
}]
}
}
};
var racerPositionGraphCfg = {
type: 'line',
data: {
labels: ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"],
datasets: [
{
label: "Racer 1 Positions",
backgroundColor: window.chartColors.red,
borderColor: window.chartColors.red,
data: [
10,8,6,4,3,2,6,4,3,2,1
],
fill: false
}
]
},
options: {
responsive: true,
title: {
display: true,
text: 'Racer Lap Positions'
},
tooltips: {
mode: 'index',
intersect: false,
},
hover: {
mode: 'nearest',
intersect: true
},
scales: {
xAxes: [{
display: true,
scaleLabel: {
display: true,
labelString: 'Lap'
}
}],
yAxes: [{
display: true,
ticks: {
reverse:true
},
scaleLabel: {
display: true,
labelString: 'Position'
}
}]
}
}
};
window.onload = function () {
var ctx = document.getElementById("LapTimeGraphsCanvas").getContext("2d");
var posCtx = document.getElementById("RacerPositionGraphsCanvas").getContext("2d");
window.lapTimeGraph = new Chart(ctx, lapTimeGraphCfg);
window.racerPositionGraph = new Chart(posCtx, racerPositionGraphCfg);
};
</script>

View File

@ -26,7 +26,7 @@
<td class="col-md-1 vertical-center" data-bind="text: mode">standard</td> <td class="col-md-1 vertical-center" data-bind="text: mode">standard</td>
<td class="text-center col-md-1 vertical-center">5</td> <td class="text-center col-md-1 vertical-center">5</td>
<td class="col-md-3 text-center"> <td class="col-md-3 text-center">
<button type="button" class="btn btn-warning" data-bind="click: $root.deleteSession">Delete</button> <button type="button" class="btn btn-warning" data-bind="click: $root.deleteSession, clickBubble:false">Delete</button>
<div class="btn-group"> <div class="btn-group">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
@{ if (Model.ExportTypes == null || Model.ExportTypes.Count == 0) @{ if (Model.ExportTypes == null || Model.ExportTypes.Count == 0)
@ -41,7 +41,7 @@
<ul class="dropdown-menu"> <ul class="dropdown-menu">
@foreach (Interfaces.ExportProviderDetails eT in Model.ExportTypes) @foreach (Interfaces.ExportProviderDetails eT in Model.ExportTypes)
{ {
<li><a data-bind="click: $root.CreateExport(id(), '@eT.Type')">@eT.Type</a></li> <li><a data-bind="attr: { href: $root.exportUrl+ id()+'?exportType=@eT.Type'}, clickBubble: false, click: function(){return true;}">@eT.Type</a></li>
} }
</ul> </ul>
} }
@ -74,9 +74,13 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="row col-md-1 col-md-push-9">
<a data-bind="attr:{ href:'/history/details/' + id()}" class="btn btn-default">Race Details</a>
</div>
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<script src="js/RaceSessionObject.js" type="text/javascript"></script> <script src="js/RaceSessionObject.js" type="text/javascript"></script>
<script src="js/raceHistory.js" type="text/javascript"></script> <script src="js/raceHistory.js" type="text/javascript"></script>
<script src="js/site.js" type="text/javascript"></script>

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,7 @@
function ViewModel() { function ViewModel() {
var self = this; var self = this;
self.deletePilotUrl = "/api/racesession/historic/"; self.deletePilotUrl = "/api/racesession/historic/";
self.exportUrl = "/history/pdf/";
self.sessions = ko.observableArray([]); self.sessions = ko.observableArray([]);
self.deleteSession = function (raceSession) { self.deleteSession = function (raceSession) {
$.ajax({ $.ajax({
@ -16,9 +17,16 @@
self.toggleIcon = function(e) { self.toggleIcon = function(e) {
$(e).toggleClass("glyphicon-plus glyphicon-minus"); $(e).toggleClass("glyphicon-plus glyphicon-minus");
}; };
self.CreateExport = function(sessionId, exportType) { self.CreateExport = function(sessionId, exportType, e) {
console.log(sessionId); console.log(sessionId);
console.log(exportType); console.log(exportType);
$.ajax({
type: "GET",
url: self.exportUrl + sessionId,
data: {
exportType: exportType
}
});
}; };
}; };
ko.applyBindings(new ViewModel()); ko.applyBindings(new ViewModel());

View File

@ -0,0 +1,4 @@
$(".excludeCollapse").on("click", function (e) {
e.stopPropagation();
});
// not sure this stop propagation ever gets called, probably as knockout is intercepting..

View File

@ -95,11 +95,10 @@ copy /B /Y "$(SolutionDir)IrDaemonNotifier\$(OutDir)IrDaemonNotifier.dll" "$(Tar
copy /B /Y "$(TargetDir)NLogConfig.xml" "$(TargetDir)Configs\NLogConfig.xml" copy /B /Y "$(TargetDir)NLogConfig.xml" "$(TargetDir)Configs\NLogConfig.xml"
copy /B /Y "$(SolutionDir)RaceLapTimer\$(OutDir)Ninject.Extensions.Xml.dll" "$(TargetDir)Ninject.Extensions.Xml.dll" copy /B /Y "$(SolutionDir)RaceLapTimer\$(OutDir)Ninject.Extensions.Xml.dll" "$(TargetDir)Ninject.Extensions.Xml.dll"
copy /B /Y "$(SolutionDir)PDFExporter\$(OutDir)PDFExporter.dll" "$(TargetDir)Plugins\PDFExporter.dll" copy /B /Y "$(SolutionDir)PDFExporter\$(OutDir)PDFExporter.dll" "$(TargetDir)Plugins\PDFExporter.dll"
copy /B /Y "$(SolutionDir)PDFExporter\$(OutDir).dll" "$(TargetDir)Plugins\PDFExporter.dll" copy /B /Y "$(SolutionDir)PDFExporter\HiQPdf.dll" "$(TargetDir)Plugins\HiQPdf.dll"
copy /B /Y "$(SolutionDir)PDFExporter\$(OutDir)Syncfusion.HtmlConverter.Base.dll" "$(TargetDir)Plugins\Syncfusion.HtmlConverter.Base.dll" copy /B /Y "$(SolutionDir)PDFExporter\HiQPdf.rda" "$(TargetDir)Plugins\HiQPdf.rda"
copy /B /Y "$(SolutionDir)PDFExporter\$(OutDir)Syncfusion.Pdf.Base.dll" "$(TargetDir)Plugins\Syncfusion.Pdf.Base.dll" copy /B /Y "$(SolutionDir)PDFExporter\HiQPdf.dep" "$(TargetDir)Plugins\HiQPdf.dep"
copy /B /Y "$(SolutionDir)PDFExporter\$(OutDir)Syncfusion.Compression.Base.dll" "$(TargetDir)Plugins\Syncfusion.Compression.Base.dll" rem copy /B /Y "$(SolutionDir)PDFExporter\$(OutDir)Microsoft.mshtml.dll" "$(TargetDir)Plugins\Microsoft.mshtml.dll"</PostBuildEvent>
copy /B /Y "$(SolutionDir)PDFExporter\$(OutDir)Microsoft.mshtml.dll" "$(TargetDir)Plugins\Microsoft.mshtml.dll"</PostBuildEvent>
</PropertyGroup> </PropertyGroup>
<!-- To modify your build process, add your task inside one of the targets below and uncomment it. <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets. Other similar extension points exist, see Microsoft.Common.targets.