// Draws all legend text (and optional Icon beside) for every MapItem // Returns the number of items missed off the legend due to size constraints static int DrawLegend(Font font, Graphics imageGraphic) { if (FormMaster.legendItems.Count == 0) { return(0); } Dictionary <int, string> overridingLegendText = FormMaster.GatherOverriddenLegendTexts(); List <int> drawnGroups = new List <int>(); // Calculate the total height of all legend strings with their plot icons beside, combined int legendTotalHeight = 0; foreach (MapItem mapItem in FormMaster.legendItems) { // Skip legend groups that are merged/overridden and have already been accounted for if (drawnGroups.Contains(mapItem.legendGroup) && overridingLegendText.ContainsKey(mapItem.legendGroup)) { continue; } legendTotalHeight += Math.Max( (int)Math.Ceiling(imageGraphic.MeasureString(mapItem.GetLegendText(false), font, legendBounds).Height), SettingsPlot.IsIconOrTopographic() ? SettingsPlotIcon.iconSize : 0); drawnGroups.Add(mapItem.legendGroup); } int skippedLegends = 0; // How many legend items did not fit onto the map // The initial Y coord where first legend item should be written, in order to Y-center the entire legend int legendCaretHeight = (mapDimension / 2) - (legendTotalHeight / 2); // Reset the drawn groups list, as we need to iterate over the items again drawnGroups = new List <int>(); // Loop over every MapItem and draw the legend foreach (MapItem mapItem in FormMaster.legendItems) { // Skip legend groups that are merged/overridden and have already been drawn if (drawnGroups.Contains(mapItem.legendGroup) && overridingLegendText.ContainsKey(mapItem.legendGroup)) { continue; } // Calculate positions and color for legend text (plus icon) int fontHeight = (int)Math.Ceiling(imageGraphic.MeasureString(mapItem.GetLegendText(false), font, legendBounds).Height); PlotIcon icon = mapItem.GetIcon(); Image plotIconImg = SettingsPlot.IsIconOrTopographic() ? icon.GetIconImage() : null; Color legendColor = SettingsPlot.IsTopographic() ? SettingsPlotTopograph.legendColor : mapItem.GetLegendColor(); Brush textBrush = new SolidBrush(legendColor); int iconHeight = SettingsPlot.IsIconOrTopographic() ? plotIconImg.Height : 0; int legendHeight = Math.Max(fontHeight, iconHeight); // If the icon is taller than the text, offset the text it so it sits Y-centrally against the icon int textOffset = 0; if (iconHeight > fontHeight) { textOffset = (iconHeight - fontHeight) / 2; } // If the legend text/item fits on the map vertically if (legendCaretHeight > 0 && legendCaretHeight + legendHeight < mapDimension) { if (SettingsPlot.IsIconOrTopographic()) { imageGraphic.DrawImage(plotIconImg, (float)(legendIconX - (plotIconImg.Width / 2d)), (float)(legendCaretHeight - (plotIconImg.Height / 2d) + (legendHeight / 2d))); } imageGraphic.DrawString(mapItem.GetLegendText(false), font, textBrush, new RectangleF(legendXMin, legendCaretHeight + textOffset, legendWidth, legendHeight)); } else { skippedLegends++; } drawnGroups.Add(mapItem.legendGroup); legendCaretHeight += legendHeight; // Move the 'caret' down for the next item, enough to fit the icon and the text } GC.Collect(); return(skippedLegends); }
// Construct the final map by drawing plots over the background layer public static void Draw() { // Reset the current image to the background layer finalImage = (Image)backgroundLayer.Clone(); Graphics imageGraphic = Graphics.FromImage(finalImage); imageGraphic.SmoothingMode = SmoothingMode.AntiAlias; Font font = new Font(fontCollection.Families[0], fontSize, GraphicsUnit.Pixel); CellScaling cellScaling = null; // Prepare the game version and watermark to be printed later string infoText = (SettingsPlot.IsTopographic() ? "Topographic View\n" : string.Empty) + "Game version " + IOManager.GetGameVersion() + "\nMade with Mappalachia - github.com/AHeroicLlama/Mappalachia"; // Additional steps for cell mode (Add further text to watermark text, get cell height boundings) if (SettingsMap.IsCellModeActive()) { Cell currentCell = SettingsCell.GetCell(); // Assign the CellScaling property cellScaling = currentCell.GetScaling(); infoText = currentCell.displayName + " (" + currentCell.editorID + ")\n" + "Height distribution: " + SettingsCell.minHeightPerc + "% - " + SettingsCell.maxHeightPerc + "%\n" + "Scale: 1:" + Math.Round(cellScaling.scale, 2) + "\n\n" + infoText; } // Gather resources for drawing informational watermark text Brush brushWhite = new SolidBrush(Color.White); RectangleF infoTextBounds = new RectangleF(plotXMin, 0, mapDimension - plotXMin, mapDimension); StringFormat stringFormatBottomRight = new StringFormat() { Alignment = StringAlignment.Far, LineAlignment = StringAlignment.Far }; // Align the text bottom-right StringFormat stringFormatBottomLeft = new StringFormat() { Alignment = StringAlignment.Near, LineAlignment = StringAlignment.Far }; // Align the text bottom-left // Draws bottom-right info text imageGraphic.DrawString(infoText, font, brushWhite, infoTextBounds, stringFormatBottomRight); // Draw a height-color key for Topography mode if (SettingsPlot.IsTopographic()) { // Identify the sizing and locations for drawing the height-color-key strings double numHeightKeys = SettingsPlotTopograph.heightKeyIndicators; Font topographFont = new Font(fontCollection.Families[0], 62, GraphicsUnit.Pixel); float singleLineHeight = imageGraphic.MeasureString(SettingsPlotTopograph.heightKeyString, topographFont, new SizeF(infoTextBounds.Width, infoTextBounds.Height)).Height; // Identify the lower limit to start printing the key so that it ends up centered double baseHeight = (mapDimension / 2) - (singleLineHeight * (numHeightKeys / 2d)); for (int i = 0; i <= numHeightKeys - 1; i++) { Brush brush = new SolidBrush(GetTopographColor(i / (numHeightKeys - 1))); imageGraphic.DrawString(SettingsPlotTopograph.heightKeyString, topographFont, brush, new RectangleF(plotXMax, 0, mapDimension - plotXMax, (float)(mapDimension - baseHeight)), stringFormatBottomRight); baseHeight += singleLineHeight; } } // Draw all legend text for every MapItem int skippedLegends = DrawLegend(font, imageGraphic); // Adds additional text if some items were missed from legend if (skippedLegends > 0) { string extraLegendText = "+" + skippedLegends + " more item" + (skippedLegends == 1 ? string.Empty : "s") + "..."; imageGraphic.DrawString(extraLegendText, font, brushWhite, infoTextBounds, stringFormatBottomLeft); } // Start progress bar off at 0 progressBarMain.Value = progressBarMain.Minimum; float progress = 0; // Nothing else to plot - ensure we update for the background layer but then return if (FormMaster.legendItems.Count == 0) { mapFrame.Image = finalImage; return; } // Count how many Map Data Points are due to be mapped int totalMapDataPoints = 0; foreach (MapItem mapItem in FormMaster.legendItems) { totalMapDataPoints += mapItem.count; } // Loop through every MapDataPoint represented by all the MapItems to find the min/max z coord in the dataset bool first = true; int zMin = 0; int zMax = 0; double zRange = 0; if (SettingsPlot.IsTopographic()) { foreach (MapItem mapItem in FormMaster.legendItems) { foreach (MapDataPoint point in mapItem.GetPlots()) { if (first) { zMin = point.z - (point.boundZ / 2); zMax = point.z + (point.boundZ / 2); first = false; continue; } // Do not contribute outlier values to the min/max range - this ensures they have the same // color as the min/max *legitimate* item and they do not skew the color ranges if (point.z > SettingsPlotTopograph.zThreshUpper || point.z < SettingsPlotTopograph.zThreshLower) { continue; } if (point.z - (point.boundZ / 2) < zMin) { zMin = point.z - (point.boundZ / 2); } if (point.z + (point.boundZ / 2) > zMax) { zMax = point.z + (point.boundZ / 2); } } } zRange = Math.Abs(zMax - zMin); if (zRange == 0) { zRange = 1; } } if (SettingsPlot.IsIconOrTopographic()) { if (SettingsPlot.IsTopographic()) { // Somehow this line prevents a memory leak // Without it, if drawing a large topographic map on first map draw, GC will not collect the multiple PlotIcon elements used in topographic drawing. Application.DoEvents(); } // Processing each MapItem in serial, draw plots for every matching valid MapDataPoint foreach (MapItem mapItem in FormMaster.legendItems) { // Generate a Plot Icon and colours/brushes to be used for all instances of the MapItem PlotIcon plotIcon = mapItem.GetIcon(); Image plotIconImg = SettingsPlot.IsIcon() ? plotIcon.GetIconImage() : null; // Icon mode has icon per MapItem, Topography needs icons per MapDataPoint and will be generated later Color volumeColor = Color.FromArgb(volumeOpacity, plotIcon.color); Brush volumeBrush = new SolidBrush(volumeColor); // Iterate over every data point and draw it foreach (MapDataPoint point in mapItem.GetPlots()) { // Override colors in Topographic mode if (SettingsPlot.IsTopographic()) { // Clamp the z values to the percieved outlier threshold double z = point.z + (point.boundZ / 2); z = Math.Max(Math.Min(z, SettingsPlotTopograph.zThreshUpper), SettingsPlotTopograph.zThreshLower); // Normalize the height of this item between the min/max z of the whole set double colorValue = (z - zMin) / zRange; // Override the plot icon color plotIcon.color = GetTopographColor(colorValue); plotIconImg = plotIcon.GetIconImage(); // Generate a new icon with a unique color for this height color // Apply the color to volume plotting too volumeColor = Color.FromArgb(volumeOpacity, plotIcon.color); volumeBrush = new SolidBrush(volumeColor); } if (SettingsMap.IsCellModeActive()) { // If this coordinate exceeds the user-selected cell mapping height bounds, skip it // (Also accounts for the z-height of volumes) if (point.z + (point.boundZ / 2d) < SettingsCell.GetMinHeightCoordBound() || point.z - (point.boundZ / 2d) > SettingsCell.GetMaxHeightCoordBound()) { continue; } point.x += cellScaling.xOffset; point.y += cellScaling.yOffset; // Multiply the coordinates by the scaling, but multiply around 0,0 point.x = ((point.x - (mapDimension / 2)) * cellScaling.scale) + (mapDimension / 2); point.y = ((point.y - (mapDimension / 2)) * cellScaling.scale) + (mapDimension / 2); point.boundX *= cellScaling.scale; point.boundY *= cellScaling.scale; } else // Skip the point if its origin is outside the surface world if (point.x < plotXMin || point.x >= plotXMax || point.y < plotYMin || point.y >= plotYMax) { continue; } // If this meets all the criteria to be suitable to be drawn as a volume if (point.primitiveShape != string.Empty && // This is a primitive shape at all SettingsPlot.drawVolumes && // Volume drawing is enabled point.boundX >= minVolumeDimension && point.boundY >= minVolumeDimension) // This is large enough to be visible if drawn as a volume { Image volumeImage = new Bitmap((int)point.boundX, (int)point.boundY); Graphics volumeGraphic = Graphics.FromImage(volumeImage); volumeGraphic.SmoothingMode = SmoothingMode.AntiAlias; switch (point.primitiveShape) { case "Box": case "Line": case "Plane": volumeGraphic.FillRectangle(volumeBrush, new Rectangle(0, 0, (int)point.boundX, (int)point.boundY)); break; case "Sphere": case "Ellipsoid": volumeGraphic.FillEllipse(volumeBrush, new Rectangle(0, 0, (int)point.boundX, (int)point.boundY)); break; default: continue; // If we reach this, we dropped the drawing of a volume. Verify we've covered all shapes via the database summary.txt } volumeImage = ImageTools.RotateImage(volumeImage, point.rotationZ); imageGraphic.DrawImage(volumeImage, (float)(point.x - (volumeImage.Width / 2)), (float)(point.y - (volumeImage.Height / 2))); } // This MapDataPoint is not suitable to be drawn as a volume - draw a normal plot icon, or topographic plot else { imageGraphic.DrawImage(plotIconImg, (float)(point.x - (plotIconImg.Width / 2d)), (float)(point.y - (plotIconImg.Height / 2d))); } } // Increment the progress bar per MapItem progress += mapItem.count; progressBarMain.Value = (int)((progress / totalMapDataPoints) * progressBarMain.Maximum); Application.DoEvents(); } } else if (SettingsPlot.IsHeatmap()) { int resolution = SettingsPlotHeatmap.resolution; int blendRange = SettingsPlotHeatmap.blendDistance; // Create a 2D Array of HeatMapGridSquare HeatMapGridSquare[,] squares = new HeatMapGridSquare[resolution, resolution]; for (int x = 0; x < resolution; x++) { for (int y = 0; y < resolution; y++) { squares[x, y] = new HeatMapGridSquare(); } } int pixelsPerSquare = mapDimension / resolution; foreach (MapItem mapItem in FormMaster.legendItems) { int heatmapLegendGroup = SettingsPlotHeatmap.IsDuo() ? mapItem.legendGroup % 2 : 0; foreach (MapDataPoint point in mapItem.GetPlots()) { if (SettingsMap.IsCellModeActive()) { point.x += cellScaling.xOffset; point.y += cellScaling.yOffset; point.x = ((point.x - (mapDimension / 2)) * cellScaling.scale) + (mapDimension / 2); point.y = ((point.y - (mapDimension / 2)) * cellScaling.scale) + (mapDimension / 2); } // Identify which grid square this MapDataPoint falls within int squareX = (int)Math.Floor(point.x / pixelsPerSquare); int squareY = (int)Math.Floor(point.y / pixelsPerSquare); // Loop over every grid square within range, and increment by the weight proportional to the distance for (int x = squareX - blendRange; x < squareX + blendRange; x++) { for (int y = squareY - blendRange; y < squareY + blendRange; y++) { // Don't try to target squares which would lay outside of the grid if (x < 0 || x >= resolution || y < 0 || y >= resolution) { continue; } // Pythagoras on the x and y dist gives us the 'as the crow flies' distance between the squares double distance = Pythagoras(squareX - x, squareY - y); // Weight and hence brightness is modified by 1/x^2 + 1 where x is the distance from actual item double additionalWeight = point.weight * (1d / ((distance * distance) + 1)); squares[x, y].weights[heatmapLegendGroup] += additionalWeight; } } } // Increment the progress bar per MapItem progress += mapItem.count; progressBarMain.Value = (int)((progress / totalMapDataPoints) * progressBarMain.Maximum); Application.DoEvents(); } // Find the largest weight value of all squares double largestWeight = 0; for (int x = 0; x < resolution; x++) { for (int y = 0; y < resolution; y++) { double weight = squares[x, y].GetTotalWeight(); if (weight > largestWeight) { largestWeight = weight; } } } // Finally now weights are calculated, draw a square for every HeatGripMapSquare in the array for (int x = 0; x < resolution; x++) { int xCoord = x * pixelsPerSquare; // Don't draw grid squares which are entirely within the legend text area if (xCoord + pixelsPerSquare < plotXMin) { continue; } for (int y = 0; y < resolution; y++) { int yCoord = y * pixelsPerSquare; Color color = squares[x, y].GetColor(largestWeight); Brush brush = new SolidBrush(color); Rectangle heatMapSquare = new Rectangle(xCoord, yCoord, mapDimension / SettingsPlotHeatmap.resolution, mapDimension / SettingsPlotHeatmap.resolution); imageGraphic.FillRectangle(brush, heatMapSquare); } } } mapFrame.Image = finalImage; }