using System; using System.IO; namespace MultiWheelC.TrajectoryPlanning.Mapping; /// 可选 PNG 导出器;它永远不参与地图构建、障碍物投影或缓存指纹计算。 public static class PlanningMapImageExporter { /// 每个规划栅格在输出图中占用的边长,单位为像素。 public const int PixelsPerCell = 4; /// 输出 PNG 单边允许的最大像素数,超过时拒绝导出。 public const int MaximumImageEdgePixels = 4000; /// 输出 PNG 允许的最大文件大小,单位为字节(50 MiB)。 public const long MaximumFileSizeBytes = 50L * 1024L * 1024L; /// 写入 PNG 的物理分辨率元数据,单位 DPI。 public const float OutputDpi = 300f; private const int MaximumFilenameAttempts = 1024; /// /// 在开关开启时导出规划地图 PNG。 /// /// 参数:enabled 为导出开关;request 包含不可变地图和输出根目录。 /// 返回:开关关闭时返回 Skipped;成功时返回 Saved、路径、字节数与像素尺寸;输入或尺寸不合法时返回失败信息。 /// 注意:该方法仅用于调试,绝不会影响地图创建和缓存结果。 /// public static PlanningMapImageExportResult ExportIfEnabled(bool enabled, PlanningMapImageExportRequest request) { if (!enabled) return new PlanningMapImageExportResult { Skipped = true, Message = "Planning map image export is disabled." }; if (request == null || request.Map == null) return Rejected("Planning map image export requires a PlanningGridMap."); if (string.IsNullOrWhiteSpace(request.OutputRootDirectory)) return Rejected("Planning map image export requires an output root directory."); long width = (long)request.Map.Cols * PixelsPerCell, height = (long)request.Map.Rows * PixelsPerCell; if (width <= 0 || height <= 0 || width > MaximumImageEdgePixels || height > MaximumImageEdgePixels) return Rejected("Planning map image dimensions exceed the permitted edge.", width, height); string temporary = null; try { string directory = Path.Combine(request.OutputRootDirectory, "PlanningMapExports"); Directory.CreateDirectory(directory); using (FileStream stream = CreateTemporaryFile(directory, out string finalPath, out temporary)) { byte[] rgba = PlanningMapImageRenderer.Render(request.Map, PixelsPerCell, out int pixelWidth, out int pixelHeight); ValidatedPngWriter.Write(rgba, pixelWidth, pixelHeight, stream); } long length = new FileInfo(temporary).Length; if (length > MaximumFileSizeBytes) return Rejected("Planning map image PNG exceeds 50 MiB.", width, height); string completed = temporary.Substring(0, temporary.Length - 4); File.Move(temporary, completed); temporary = null; return new PlanningMapImageExportResult { Saved = true, FilePath = completed, FileSizeBytes = length, PixelWidth = (int)width, PixelHeight = (int)height, Message = "Planning map image export saved." }; } catch (Exception exception) { return Rejected("Planning map image export failed: " + exception.Message, width, height); } finally { if (temporary != null && File.Exists(temporary)) File.Delete(temporary); } } private static PlanningMapImageExportResult Rejected(string message, long width = 0, long height = 0) { return new PlanningMapImageExportResult { Message = message, PixelWidth = width > int.MaxValue ? int.MaxValue : (int)width, PixelHeight = height > int.MaxValue ? int.MaxValue : (int)height }; } private static FileStream CreateTemporaryFile(string directory, out string finalPath, out string temporaryPath) { string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss_fff"); for (int index = 0; index < MaximumFilenameAttempts; index++) { string suffix = index == 0 ? string.Empty : "_" + index; finalPath = Path.Combine(directory, "PlanningMap_" + timestamp + suffix + ".png"); temporaryPath = finalPath + ".tmp"; if (File.Exists(finalPath)) continue; try { return new FileStream(temporaryPath, FileMode.CreateNew, FileAccess.Write, FileShare.None); } catch (IOException) { } } finalPath = null; temporaryPath = null; throw new IOException("Could not reserve a unique PlanningMap PNG filename."); } }