// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.IO; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; using PerfReportTool; using CSVStats; using System.Drawing; using System.Drawing.Imaging; namespace PerfSummaries { class MapOverlaySummary : Summary { class MapOverlayEvent { public MapOverlayEvent(string inName) { name = inName; } public MapOverlayEvent(XElement element) { } public string name; public string summaryStatName; public string shortName; public string lineColor; }; class MapOverlay { public MapOverlay(XElement element) { positionStatNames[0] = element.GetSafeAttibute("xStat"); positionStatNames[1] = element.GetSafeAttibute("yStat"); positionStatNames[2] = element.GetSafeAttibute("zStat"); summaryStatNamePrefix = element.GetSafeAttibute("summaryStatNamePrefix"); // unused! lineColor = element.GetSafeAttibute("lineColor", "#ffffff"); foreach (XElement eventEl in element.Elements("event")) { MapOverlayEvent ev = new MapOverlayEvent(eventEl.Attribute("name").Value); ev.shortName = eventEl.GetSafeAttibute("shortName"); ev.summaryStatName = eventEl.GetSafeAttibute("summaryStatName"); // unused! ev.lineColor = eventEl.GetSafeAttibute("lineColor"); if (eventEl.GetSafeAttibute("isStartEvent", false)) { if (startEvent != null) { throw new Exception("Can't have multiple start events!"); } startEvent = ev; } events.Add(ev); } } public string[] positionStatNames = new string[3]; public string summaryStatNamePrefix; public MapOverlayEvent startEvent; public string lineColor; public List events = new List(); } public MapOverlaySummary(XElement element, string baseXmlDirectory) { ReadStatsFromXML(element); if (stats.Count != 0) { throw new Exception(" element is not supported"); } sourceImagePath = element.GetSafeAttibute("sourceImage"); if (baseXmlDirectory == null) { throw new Exception("BaseXmlDirectory not specified"); } if (!System.IO.Path.IsPathRooted(sourceImagePath)) { sourceImagePath = System.IO.Path.GetFullPath(System.IO.Path.Combine(baseXmlDirectory, sourceImagePath)); } offsetX = element.GetSafeAttibute("offsetX", 0.0f); offsetY = element.GetSafeAttibute("offsetY", 0.0f); scale = element.GetSafeAttibute("scale", 1.0f); title = element.GetSafeAttibute("title", "Events"); destImageFilename = element.Attribute("destImage").Value; imageWidth = element.GetSafeAttibute("width", 250.0f); imageHeight = element.GetSafeAttibute("height", 250.0f); framesPerLineSegment = element.GetSafeAttibute("framesPerLineSegment", 5); lineSplitDistanceThreshold = element.GetSafeAttibute("lineSplitDistanceThreshold", float.MaxValue); foreach (XElement overlayEl in element.Elements("overlay")) { MapOverlay overlay = new MapOverlay(overlayEl); overlays.Add(overlay); stats.Add(overlay.positionStatNames[0]); stats.Add(overlay.positionStatNames[1]); stats.Add(overlay.positionStatNames[2]); } } public MapOverlaySummary() { } public override string GetName() { return "mapoverlay"; } int toSvgX(float worldX, float worldY) { float svgX = (worldY * scale + offsetX) * 0.5f + 0.5f; svgX *= imageWidth; return (int)(svgX + 0.5f); } int toSvgY(float worldX, float worldY) { float svgY = 1.0f - (worldX * scale + offsetY) * 0.5f - 0.5f; svgY *= imageHeight; return (int)(svgY + 0.5f); } private void CopyAndResizeImage(string sourceImagePath, string destImagePath, int destWidth, int destHeight) { Console.WriteLine("Downsampling map image.\n Source: " + sourceImagePath+"\n Dest : "+destImagePath); using (FileStream fileStream = new FileStream(sourceImagePath, FileMode.Open, FileAccess.Read)) { Console.WriteLine("Reading source image"); var image = System.Drawing.Image.FromStream(fileStream); Console.WriteLine("Generating downsampled image"); var thumbnail = image.GetThumbnailImage(destWidth, destHeight, null, IntPtr.Zero); using (var destImageStream = new FileStream(destImagePath, FileMode.OpenOrCreate, FileAccess.Write)) { Console.WriteLine("Saving downsampled map image: " + destImageStream); thumbnail.Save(destImageStream, ImageFormat.Jpeg); } } } public override void WriteSummaryData(System.IO.StreamWriter htmlFile, CsvStats csvStats, CsvStats csvStatsUnstripped, bool bWriteSummaryCsv, SummaryTableRowData rowData, string htmlFileName) { // Output HTML if (htmlFile != null) { string outputDirectory = System.IO.Path.GetDirectoryName(System.IO.Path.GetFullPath(htmlFileName)); string outputMapFilename = System.IO.Path.Combine(outputDirectory, destImageFilename); // Skip the copy if the output file already exists if (!File.Exists(outputMapFilename)) { if (File.Exists(sourceImagePath)) { // Copy the file to the reports folder and reset attributes to ensure it's not readonly if the source is // File.Copy(sourceImagePath, outputMapFilename); // File.SetAttributes(outputMapFilename, FileAttributes.Normal); CopyAndResizeImage(sourceImagePath, outputMapFilename, (int)imageWidth, (int)imageHeight); } else { Console.WriteLine("[Warning] Can't find source map image: " + sourceImagePath); } } // Check if the file exists in the output directory htmlFile.WriteLine("

" + title + "

"); htmlFile.WriteLine(""); htmlFile.WriteLine(""); // Draw the overlays foreach (MapOverlay overlay in overlays) { StatSamples xStat = csvStats.GetStat(overlay.positionStatNames[0]); StatSamples yStat = csvStats.GetStat(overlay.positionStatNames[1]); if (xStat == null || yStat == null) { continue; } // If a startevent is specified, update the start frame int startFrame = 0; if (overlay.startEvent != null) { foreach (CsvEvent ev in csvStats.Events) { if (CsvStats.DoesSearchStringMatch(ev.Name, overlay.startEvent.name)) { startFrame = ev.Frame; break; } } } // Make a mapping from frame to map indices List> frameEvents = new List>(); foreach (MapOverlayEvent mapEvent in overlay.events) { foreach (CsvEvent ev in csvStats.Events) { if (CsvStats.DoesSearchStringMatch(ev.Name, mapEvent.name)) { frameEvents.Add(new KeyValuePair(ev.Frame, mapEvent)); } } } frameEvents.Sort((pair0, pair1) => pair0.Key.CompareTo(pair1.Key)); int eventIndex = 0; // Draw the lines string currentLineColor = overlay.lineColor; string lineStartTemplate = "= frameEvents[eventIndex].Key) { MapOverlayEvent mapEvent = frameEvents[eventIndex].Value; string newLineColor = mapEvent.lineColor != null ? mapEvent.lineColor : overlay.lineColor; // If we changed color, restart the line strip if (newLineColor != currentLineColor) { currentLineColor = newLineColor; restartLineStrip = true; } eventIndex++; } // If the distance between this point and the last is over the threshold, restart the line strip float maxManhattanDist = Math.Max(Math.Abs(x - oldx), Math.Abs(y - oldy)); if (maxManhattanDist > adjustedLineSplitDistanceThreshold) { restartLineStrip = true; } else { htmlFile.Write(lineCoordsStr); } if (restartLineStrip) { htmlFile.WriteLine("'/>"); htmlFile.Write(lineStartTemplate.Replace("{LINECOLOUR}", currentLineColor)); htmlFile.Write(lineCoordsStr); } oldx = x; oldy = y; lastFrameIndex = i; } htmlFile.WriteLine("'/>"); // Plot the events float circleRadius = 3; string eventColourString = "#ffffff"; foreach (MapOverlayEvent mapEvent in overlay.events) { foreach (CsvEvent ev in csvStats.Events) { if (CsvStats.DoesSearchStringMatch(ev.Name, mapEvent.name)) { string eventText = mapEvent.shortName != null ? mapEvent.shortName : ev.Name; float x = xStat.samples[ev.Frame]; float y = yStat.samples[ev.Frame]; int svgX = toSvgX(x, y); int svgY = toSvgY(x, y); htmlFile.Write(""); htmlFile.WriteLine("" + eventText + ""); } } } } //htmlFile.WriteLine("" + title + ""); htmlFile.WriteLine(""); } // Output row data if (rowData != null) { } } public override void PostInit(ReportTypeInfo reportTypeInfo, CsvStats csvStats) { } string title; string sourceImagePath; float offsetX; float offsetY; float scale; string destImageFilename; float imageWidth; float imageHeight; float lineSplitDistanceThreshold; int framesPerLineSegment; List overlays = new List(); }; }