// Get the image-scaled coordinate points for all instances of this MapItem // Speeds up repeated or edited map plots by caching them public List <MapDataPoint> GetPlots() { // Cache the plots if not already active - cell mode needs to edit these values (see CellScaling), so refresh them each time if (plots == null || SettingsMap.IsCellModeActive()) { switch (type) { case Type.Standard: plots = SettingsMap.IsCellModeActive() ? DataHelper.GetCellCoords(uniqueIdentifier, SettingsCell.GetCell().formID, filteredLockTypes) : DataHelper.GetStandardCoords(uniqueIdentifier, filteredLockTypes); break; case Type.NPC: plots = DataHelper.GetNPCCoords(uniqueIdentifier, weight); break; case Type.Scrap: plots = DataHelper.GetScrapCoords(uniqueIdentifier); break; } } return(plots); }
// Write the given map image to a given location public static void WriteToFile(string filePath, Image image) { try { if (SettingsMap.IsCellModeActive()) { // Save with PNG encoding in Cell mode to maintain the transparency, and to avoid compression image.Save(filePath, ImageFormat.Png); } else { EncoderParameters encoderParams = new EncoderParameters(1); encoderParams.Param[0] = encoderParam; image.Save(filePath, jpegEncoder, encoderParams); } } catch (Exception e) { Notify.Error( "Mappalachia was unable to save your map to " + filePath + ".\n" + "Please try a different location.\n\n" + genericExceptionHelpText + e); return; } }
// Construct the map background layer, without plotted points public static void DrawBaseLayer() { // Start with the chosen base map if (SettingsMap.IsCellModeActive()) { backgroundLayer = new Bitmap(mapDimension, mapDimension); if (SettingsCell.drawOutline) { Graphics backgroundGraphics = Graphics.FromImage(backgroundLayer); DrawCellBackground(backgroundGraphics); } } else { backgroundLayer = SettingsMap.layerMilitary ? IOManager.GetImageMapMilitary() : IOManager.GetImageMapNormal(); } Graphics graphic = Graphics.FromImage(backgroundLayer); float b = SettingsMap.brightness / 100f; // Apply grayscale color matrix, or just apply brightness adjustments ColorMatrix matrix = SettingsMap.grayScale ? new ColorMatrix(new float[][] { new float[] { 0.299f * b, 0.299f * b, 0.299f * b, 0, 0 }, new float[] { 0.587f * b, 0.587f * b, 0.587f * b, 0, 0 }, new float[] { 0.114f * b, 0.114f * b, 0.114f * b, 0, 0 }, new float[] { 0, 0, 0, 1, 0 }, new float[] { 0, 0, 0, 0, 1 }, }) : new ColorMatrix(new float[][] { new float[] { b, 0, 0, 0, 0 }, new float[] { 0, b, 0, 0, 0 }, new float[] { 0, 0, b, 0, 0 }, new float[] { 0, 0, 0, 1, 0 }, new float[] { 0, 0, 0, 0, 1 }, }); ImageAttributes attributes = new ImageAttributes(); attributes.SetColorMatrix(matrix); Point[] points = { new Point(0, 0), new Point(mapDimension, 0), new Point(0, mapDimension), }; Rectangle rect = new Rectangle(0, 0, mapDimension, mapDimension); graphic.DrawImage(backgroundLayer, points, rect, GraphicsUnit.Pixel, attributes); Draw(); // Redraw the whole map since we updated the base layer }
// Draws an outline of all items in the current cell to act as background/template static void DrawCellBackground(Graphics backgroundLayer) { if (!SettingsMap.IsCellModeActive()) { return; } CellScaling cellScaling = SettingsCell.GetCell().GetScaling(); int outlineWidth = SettingsCell.outlineWidth; int outlineSize = SettingsCell.outlineSize; Image plotIconImg = new Bitmap(outlineSize, outlineSize); Graphics plotIconGraphic = Graphics.FromImage(plotIconImg); plotIconGraphic.SmoothingMode = SmoothingMode.AntiAlias; Color outlineColor = Color.FromArgb(SettingsCell.outlineAlpha, SettingsCell.outlineColor); Pen outlinePen = new Pen(outlineColor, outlineWidth); plotIconGraphic.DrawEllipse( outlinePen, new RectangleF(outlineWidth, outlineWidth, outlineSize - (outlineWidth * 2), outlineSize - (outlineWidth * 2))); // Iterate over every data point and draw it foreach (MapDataPoint point in DataHelper.GetAllCellCoords(SettingsCell.GetCell().formID)) { // If this coordinate exceeds the user-selected cell mapping height bounds, skip it // (Also accounts for the z-height of volumes) if (point.z < SettingsCell.GetMinHeightCoordBound() || point.z > 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; backgroundLayer.DrawImage(plotIconImg, (float)(point.x - (plotIconImg.Width / 2d)), (float)(point.y - (plotIconImg.Height / 2d))); } GC.Collect(); }
// 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; }