|
|
@@ -0,0 +1,593 @@
|
|
|
+using Microsoft.Win32;
|
|
|
+using Prism.Commands;
|
|
|
+using Prism.Mvvm;
|
|
|
+using System;
|
|
|
+using System.Collections.Generic;
|
|
|
+using System.IO;
|
|
|
+using System.Linq;
|
|
|
+using System.Windows;
|
|
|
+using System.Windows.Controls;
|
|
|
+using System.Windows.Media;
|
|
|
+using System.Windows.Media.Imaging;
|
|
|
+using System.Windows.Shapes;
|
|
|
+using System.Xml.Serialization;
|
|
|
+using TeamAAS_VP.Views.Dialogs;
|
|
|
+
|
|
|
+namespace TeamAAS_VP.ViewModels
|
|
|
+{
|
|
|
+ public class ImageViewModel : BindableBase
|
|
|
+ {
|
|
|
+ private const string AppFolderName = "TeamAAS-VM";
|
|
|
+ private const string ImageFileName = "LastImage.png";
|
|
|
+ private const string XmlFileName = "LastAnno.xml";
|
|
|
+
|
|
|
+ private string _folder;
|
|
|
+ private string _imagePath;
|
|
|
+ private string _xmlPath;
|
|
|
+
|
|
|
+ private FrameworkElement _renderTarget;
|
|
|
+ private Canvas _canvas;
|
|
|
+
|
|
|
+ private Grid _lastArrowGroup;
|
|
|
+
|
|
|
+ public ImageViewModel()
|
|
|
+ {
|
|
|
+ InitPaths();
|
|
|
+
|
|
|
+ ImportImageCommand = new DelegateCommand(ImportImage);
|
|
|
+ SaveCommand = new DelegateCommand(Save);
|
|
|
+ ClearCommand = new DelegateCommand(Clear);
|
|
|
+
|
|
|
+ Tool = "Rect";
|
|
|
+ ColorName = "Red";
|
|
|
+ Thickness = 2;
|
|
|
+ }
|
|
|
+
|
|
|
+ #region Commands
|
|
|
+
|
|
|
+ public DelegateCommand ImportImageCommand { get; }
|
|
|
+ public DelegateCommand SaveCommand { get; }
|
|
|
+ public DelegateCommand ClearCommand { get; }
|
|
|
+
|
|
|
+ #endregion
|
|
|
+
|
|
|
+ #region Bindable Properties
|
|
|
+
|
|
|
+ private ImageSource _ImageSource;
|
|
|
+ public ImageSource ImageSource
|
|
|
+ {
|
|
|
+ get { return _ImageSource; }
|
|
|
+ set { SetProperty(ref _ImageSource, value); }
|
|
|
+ }
|
|
|
+
|
|
|
+ private int _PixelWidth;
|
|
|
+ public int PixelWidth
|
|
|
+ {
|
|
|
+ get { return _PixelWidth; }
|
|
|
+ set { SetProperty(ref _PixelWidth, value); }
|
|
|
+ }
|
|
|
+
|
|
|
+ private int _PixelHeight;
|
|
|
+ public int PixelHeight
|
|
|
+ {
|
|
|
+ get { return _PixelHeight; }
|
|
|
+ set { SetProperty(ref _PixelHeight, value); }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Rect / Arrow / Text / Select
|
|
|
+ /// </summary>
|
|
|
+ private string _Tool;
|
|
|
+ public string Tool
|
|
|
+ {
|
|
|
+ get { return _Tool; }
|
|
|
+ set { SetProperty(ref _Tool, value); }
|
|
|
+ }
|
|
|
+
|
|
|
+ private string _ColorName;
|
|
|
+ public string ColorName
|
|
|
+ {
|
|
|
+ get { return _ColorName; }
|
|
|
+ set { SetProperty(ref _ColorName, value); }
|
|
|
+ }
|
|
|
+
|
|
|
+ private double _Thickness;
|
|
|
+ public double Thickness
|
|
|
+ {
|
|
|
+ get { return _Thickness; }
|
|
|
+ set { SetProperty(ref _Thickness, value); }
|
|
|
+ }
|
|
|
+
|
|
|
+ #endregion
|
|
|
+
|
|
|
+ #region Public API (called from ImageView.xaml.cs)
|
|
|
+
|
|
|
+ public void AttachRenderTarget(FrameworkElement renderTarget, Canvas canvas)
|
|
|
+ {
|
|
|
+ _renderTarget = renderTarget;
|
|
|
+ _canvas = canvas;
|
|
|
+ }
|
|
|
+
|
|
|
+ public void LoadLast()
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ if (File.Exists(_imagePath))
|
|
|
+ {
|
|
|
+ var bmp = LoadBitmap(_imagePath);
|
|
|
+ ImageSource = bmp;
|
|
|
+ PixelWidth = bmp.PixelWidth;
|
|
|
+ PixelHeight = bmp.PixelHeight;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (_canvas != null && File.Exists(_xmlPath))
|
|
|
+ {
|
|
|
+ var list = ReadXml<List<AnnoItem>>(_xmlPath);
|
|
|
+ if (list != null)
|
|
|
+ {
|
|
|
+ _canvas.Children.Clear();
|
|
|
+ foreach (var item in list)
|
|
|
+ {
|
|
|
+ RestoreItem(item);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ catch
|
|
|
+ {
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public Rectangle CreateRect()
|
|
|
+ {
|
|
|
+ return new Rectangle
|
|
|
+ {
|
|
|
+ Stroke = GetStrokeBrush(),
|
|
|
+ StrokeThickness = Thickness,
|
|
|
+ Fill = Brushes.Transparent
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ public void CreateTextAt(Canvas canvas, Point pixelPoint)
|
|
|
+ {
|
|
|
+ var dlg = new TextInputDialog();
|
|
|
+ dlg.Owner = Application.Current?.MainWindow;
|
|
|
+ if (dlg.ShowDialog() != true) return;
|
|
|
+
|
|
|
+ var input = dlg.InputText;
|
|
|
+ if (string.IsNullOrWhiteSpace(input)) return;
|
|
|
+
|
|
|
+ var tb = new TextBlock
|
|
|
+ {
|
|
|
+ Text = input,
|
|
|
+ Foreground = GetStrokeBrush(),
|
|
|
+ FontSize = 20,
|
|
|
+ Background = Brushes.Transparent
|
|
|
+ };
|
|
|
+
|
|
|
+ Canvas.SetLeft(tb, pixelPoint.X);
|
|
|
+ Canvas.SetTop(tb, pixelPoint.Y);
|
|
|
+ canvas.Children.Add(tb);
|
|
|
+ }
|
|
|
+
|
|
|
+ public Grid CreateArrowGroup(Point start, Point end)
|
|
|
+ {
|
|
|
+ var brush = GetStrokeBrush();
|
|
|
+
|
|
|
+ var line = new Line
|
|
|
+ {
|
|
|
+ X1 = start.X,
|
|
|
+ Y1 = start.Y,
|
|
|
+ X2 = end.X,
|
|
|
+ Y2 = end.Y,
|
|
|
+ Stroke = brush,
|
|
|
+ StrokeThickness = Thickness
|
|
|
+ };
|
|
|
+
|
|
|
+ var head = new Polygon
|
|
|
+ {
|
|
|
+ Fill = brush,
|
|
|
+ Stroke = brush,
|
|
|
+ StrokeThickness = 1
|
|
|
+ };
|
|
|
+
|
|
|
+ var group = new Grid();
|
|
|
+ group.Children.Add(line);
|
|
|
+ group.Children.Add(head);
|
|
|
+ group.Tag = line;
|
|
|
+
|
|
|
+ _lastArrowGroup = group;
|
|
|
+ UpdateArrowHead(line, head);
|
|
|
+
|
|
|
+ return group;
|
|
|
+ }
|
|
|
+
|
|
|
+ public void UpdateLastArrow(Canvas canvas, Point current)
|
|
|
+ {
|
|
|
+ if (_lastArrowGroup == null) return;
|
|
|
+
|
|
|
+ var line = _lastArrowGroup.Children.OfType<Line>().FirstOrDefault();
|
|
|
+ var head = _lastArrowGroup.Children.OfType<Polygon>().FirstOrDefault();
|
|
|
+ if (line == null || head == null) return;
|
|
|
+
|
|
|
+ line.X2 = current.X;
|
|
|
+ line.Y2 = current.Y;
|
|
|
+
|
|
|
+ UpdateArrowHead(line, head);
|
|
|
+ }
|
|
|
+
|
|
|
+ public void CommitLastArrow(Canvas canvas, Point end)
|
|
|
+ {
|
|
|
+ UpdateLastArrow(canvas, end);
|
|
|
+ }
|
|
|
+
|
|
|
+ #endregion
|
|
|
+
|
|
|
+ #region Command handlers
|
|
|
+
|
|
|
+ private void ImportImage()
|
|
|
+ {
|
|
|
+ var dlg = new OpenFileDialog();
|
|
|
+ dlg.Title = "导入图片";
|
|
|
+ dlg.Filter = "Image Files|*.png;*.jpg;*.jpeg;*.bmp|All Files|*.*";
|
|
|
+ if (dlg.ShowDialog() != true) return;
|
|
|
+
|
|
|
+ var bmp = LoadBitmap(dlg.FileName);
|
|
|
+ ImageSource = bmp;
|
|
|
+
|
|
|
+ PixelWidth = bmp.PixelWidth;
|
|
|
+ PixelHeight = bmp.PixelHeight;
|
|
|
+
|
|
|
+ _canvas?.Children.Clear();
|
|
|
+ _lastArrowGroup = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void Clear()
|
|
|
+ {
|
|
|
+ _canvas?.Children.Clear();
|
|
|
+ _lastArrowGroup = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void Save()
|
|
|
+ {
|
|
|
+ if (ImageSource == null)
|
|
|
+ {
|
|
|
+ MessageBox.Show("请先导入图片", "AAS", MessageBoxButton.OK, MessageBoxImage.Asterisk);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (_renderTarget == null || _canvas == null)
|
|
|
+ {
|
|
|
+ MessageBox.Show("保存失败:界面尚未初始化完成", "AAS", MessageBoxButton.OK, MessageBoxImage.Asterisk);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (PixelWidth <= 0 || PixelHeight <= 0)
|
|
|
+ {
|
|
|
+ MessageBox.Show("图片像素尺寸无效", "AAS", MessageBoxButton.OK, MessageBoxImage.Asterisk);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try
|
|
|
+ {
|
|
|
+ var merged = RenderPixelElement(_renderTarget, PixelWidth, PixelHeight);
|
|
|
+ SavePng(merged, _imagePath);
|
|
|
+
|
|
|
+ var items = DumpAnnoItems();
|
|
|
+ WriteXml(_xmlPath, items);
|
|
|
+
|
|
|
+ MessageBox.Show("保存成功", "AAS", MessageBoxButton.OK, MessageBoxImage.Asterisk);
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ MessageBox.Show(ex.Message, "AAS", MessageBoxButton.OK, MessageBoxImage.Asterisk);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ #endregion
|
|
|
+
|
|
|
+ #region Persistence (PNG + XML)
|
|
|
+
|
|
|
+ private void InitPaths()
|
|
|
+ {
|
|
|
+ _folder = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), AppFolderName);
|
|
|
+ Directory.CreateDirectory(_folder);
|
|
|
+
|
|
|
+ _imagePath = System.IO.Path.Combine(_folder, ImageFileName);
|
|
|
+ _xmlPath = System.IO.Path.Combine(_folder, XmlFileName);
|
|
|
+ }
|
|
|
+
|
|
|
+ private BitmapImage LoadBitmap(string path)
|
|
|
+ {
|
|
|
+ var bmp = new BitmapImage();
|
|
|
+ bmp.BeginInit();
|
|
|
+ bmp.CacheOption = BitmapCacheOption.OnLoad;
|
|
|
+ bmp.UriSource = new Uri(path, UriKind.Absolute);
|
|
|
+ bmp.EndInit();
|
|
|
+ bmp.Freeze();
|
|
|
+ return bmp;
|
|
|
+ }
|
|
|
+
|
|
|
+ private BitmapSource RenderPixelElement(FrameworkElement element, int pixelWidth, int pixelHeight)
|
|
|
+ {
|
|
|
+ element.Measure(new Size(pixelWidth, pixelHeight));
|
|
|
+ element.Arrange(new Rect(0, 0, pixelWidth, pixelHeight));
|
|
|
+ element.UpdateLayout();
|
|
|
+
|
|
|
+ var rtb = new RenderTargetBitmap(pixelWidth, pixelHeight, 96, 96, PixelFormats.Pbgra32);
|
|
|
+ rtb.Render(element);
|
|
|
+ rtb.Freeze();
|
|
|
+ return rtb;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void SavePng(BitmapSource source, string path)
|
|
|
+ {
|
|
|
+ var encoder = new PngBitmapEncoder();
|
|
|
+ encoder.Frames.Add(BitmapFrame.Create(source));
|
|
|
+ using (var fs = File.Create(path))
|
|
|
+ {
|
|
|
+ encoder.Save(fs);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void WriteXml<T>(string path, T data)
|
|
|
+ {
|
|
|
+ var ser = new XmlSerializer(typeof(T));
|
|
|
+ using (var fs = File.Create(path))
|
|
|
+ {
|
|
|
+ ser.Serialize(fs, data);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private T ReadXml<T>(string path) where T : class
|
|
|
+ {
|
|
|
+ var ser = new XmlSerializer(typeof(T));
|
|
|
+ using (var fs = File.OpenRead(path))
|
|
|
+ {
|
|
|
+ return ser.Deserialize(fs) as T;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ #endregion
|
|
|
+
|
|
|
+ #region Dump/Restore items
|
|
|
+
|
|
|
+ private List<AnnoItem> DumpAnnoItems()
|
|
|
+ {
|
|
|
+ var list = new List<AnnoItem>();
|
|
|
+ if (_canvas == null) return list;
|
|
|
+
|
|
|
+ foreach (var child in _canvas.Children)
|
|
|
+ {
|
|
|
+ if (child is Rectangle r)
|
|
|
+ {
|
|
|
+ list.Add(new AnnoItem
|
|
|
+ {
|
|
|
+ Type = "Rect",
|
|
|
+ X = SafeGetLeft(r),
|
|
|
+ Y = SafeGetTop(r),
|
|
|
+ W = r.Width,
|
|
|
+ H = r.Height,
|
|
|
+ Color = BrushToColorString(r.Stroke),
|
|
|
+ Thickness = r.StrokeThickness
|
|
|
+ });
|
|
|
+ }
|
|
|
+ else if (child is TextBlock t)
|
|
|
+ {
|
|
|
+ list.Add(new AnnoItem
|
|
|
+ {
|
|
|
+ Type = "Text",
|
|
|
+ X = SafeGetLeft(t),
|
|
|
+ Y = SafeGetTop(t),
|
|
|
+ Text = t.Text,
|
|
|
+ Color = BrushToColorString(t.Foreground),
|
|
|
+ FontSize = t.FontSize
|
|
|
+ });
|
|
|
+ }
|
|
|
+ else if (child is Grid g)
|
|
|
+ {
|
|
|
+ var line = g.Children.OfType<Line>().FirstOrDefault();
|
|
|
+ if (line != null)
|
|
|
+ {
|
|
|
+ list.Add(new AnnoItem
|
|
|
+ {
|
|
|
+ Type = "Arrow",
|
|
|
+ X1 = line.X1,
|
|
|
+ Y1 = line.Y1,
|
|
|
+ X2 = line.X2,
|
|
|
+ Y2 = line.Y2,
|
|
|
+ Color = BrushToColorString(line.Stroke),
|
|
|
+ Thickness = line.StrokeThickness
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return list;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void RestoreItem(AnnoItem item)
|
|
|
+ {
|
|
|
+ if (_canvas == null || item == null) return;
|
|
|
+
|
|
|
+ if (item.Type == "Rect")
|
|
|
+ {
|
|
|
+ var rect = new Rectangle
|
|
|
+ {
|
|
|
+ Stroke = BrushFrom(item.Color),
|
|
|
+ StrokeThickness = item.Thickness <= 0 ? 2 : item.Thickness,
|
|
|
+ Fill = Brushes.Transparent,
|
|
|
+ Width = item.W,
|
|
|
+ Height = item.H
|
|
|
+ };
|
|
|
+ Canvas.SetLeft(rect, item.X);
|
|
|
+ Canvas.SetTop(rect, item.Y);
|
|
|
+ _canvas.Children.Add(rect);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (item.Type == "Text")
|
|
|
+ {
|
|
|
+ var tb = new TextBlock
|
|
|
+ {
|
|
|
+ Text = item.Text ?? string.Empty,
|
|
|
+ Foreground = BrushFrom(item.Color),
|
|
|
+ FontSize = item.FontSize <= 0 ? 20 : item.FontSize,
|
|
|
+ Background = Brushes.Transparent
|
|
|
+ };
|
|
|
+ Canvas.SetLeft(tb, item.X);
|
|
|
+ Canvas.SetTop(tb, item.Y);
|
|
|
+ _canvas.Children.Add(tb);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (item.Type == "Arrow")
|
|
|
+ {
|
|
|
+ var brush = BrushFrom(item.Color);
|
|
|
+
|
|
|
+ var line = new Line
|
|
|
+ {
|
|
|
+ X1 = item.X1,
|
|
|
+ Y1 = item.Y1,
|
|
|
+ X2 = item.X2,
|
|
|
+ Y2 = item.Y2,
|
|
|
+ Stroke = brush,
|
|
|
+ StrokeThickness = item.Thickness <= 0 ? 2 : item.Thickness
|
|
|
+ };
|
|
|
+
|
|
|
+ var head = new Polygon
|
|
|
+ {
|
|
|
+ Fill = brush,
|
|
|
+ Stroke = brush,
|
|
|
+ StrokeThickness = 1
|
|
|
+ };
|
|
|
+
|
|
|
+ var group = new Grid();
|
|
|
+ group.Children.Add(line);
|
|
|
+ group.Children.Add(head);
|
|
|
+ group.Tag = line;
|
|
|
+
|
|
|
+ UpdateArrowHead(line, head);
|
|
|
+
|
|
|
+ _canvas.Children.Add(group);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ #endregion
|
|
|
+
|
|
|
+ #region Helpers
|
|
|
+
|
|
|
+ private Brush GetStrokeBrush()
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ var obj = ColorConverter.ConvertFromString(ColorName);
|
|
|
+ if (obj != null)
|
|
|
+ {
|
|
|
+ return new SolidColorBrush((Color)obj);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ catch { }
|
|
|
+ return Brushes.Red;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Brush BrushFrom(string color)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ if (!string.IsNullOrWhiteSpace(color))
|
|
|
+ {
|
|
|
+ var c = (Color)ColorConverter.ConvertFromString(color);
|
|
|
+ return new SolidColorBrush(c);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ catch { }
|
|
|
+ return Brushes.Red;
|
|
|
+ }
|
|
|
+
|
|
|
+ private string BrushToColorString(Brush brush)
|
|
|
+ {
|
|
|
+ if (brush is SolidColorBrush s)
|
|
|
+ {
|
|
|
+ return s.Color.ToString();
|
|
|
+ }
|
|
|
+ return Colors.Red.ToString();
|
|
|
+ }
|
|
|
+
|
|
|
+ private double SafeGetLeft(UIElement e)
|
|
|
+ {
|
|
|
+ var v = Canvas.GetLeft(e);
|
|
|
+ return double.IsNaN(v) ? 0 : v;
|
|
|
+ }
|
|
|
+
|
|
|
+ private double SafeGetTop(UIElement e)
|
|
|
+ {
|
|
|
+ var v = Canvas.GetTop(e);
|
|
|
+ return double.IsNaN(v) ? 0 : v;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void UpdateArrowHead(Line line, Polygon head)
|
|
|
+ {
|
|
|
+ var dx = line.X2 - line.X1;
|
|
|
+ var dy = line.Y2 - line.Y1;
|
|
|
+ var len = Math.Sqrt(dx * dx + dy * dy);
|
|
|
+ if (len < 1) len = 1;
|
|
|
+
|
|
|
+ var ux = dx / len;
|
|
|
+ var uy = dy / len;
|
|
|
+
|
|
|
+ const double size = 12.0;
|
|
|
+ var px = -uy;
|
|
|
+ var py = ux;
|
|
|
+
|
|
|
+ var tip = new Point(line.X2, line.Y2);
|
|
|
+ var p1 = new Point(line.X2 - ux * size + px * (size / 2), line.Y2 - uy * size + py * (size / 2));
|
|
|
+ var p2 = new Point(line.X2 - ux * size - px * (size / 2), line.Y2 - uy * size - py * (size / 2));
|
|
|
+
|
|
|
+ head.Points.Clear();
|
|
|
+ head.Points.Add(tip);
|
|
|
+ head.Points.Add(p1);
|
|
|
+ head.Points.Add(p2);
|
|
|
+ }
|
|
|
+
|
|
|
+ #endregion
|
|
|
+
|
|
|
+ [Serializable]
|
|
|
+ public class AnnoItem
|
|
|
+ {
|
|
|
+ public string Type { get; set; }
|
|
|
+
|
|
|
+ public double X { get; set; }
|
|
|
+ public double Y { get; set; }
|
|
|
+ public double W { get; set; }
|
|
|
+ public double H { get; set; }
|
|
|
+ public string Text { get; set; }
|
|
|
+ public double FontSize { get; set; }
|
|
|
+
|
|
|
+ public double X1 { get; set; }
|
|
|
+ public double Y1 { get; set; }
|
|
|
+ public double X2 { get; set; }
|
|
|
+ public double Y2 { get; set; }
|
|
|
+
|
|
|
+ public string Color { get; set; }
|
|
|
+ public double Thickness { get; set; }
|
|
|
+ }
|
|
|
+
|
|
|
+ public void RemoveAnnotation(UIElement element)
|
|
|
+ {
|
|
|
+ if (element == null) return;
|
|
|
+ if (_canvas == null) return;
|
|
|
+
|
|
|
+ if (_canvas.Children.Contains(element))
|
|
|
+ {
|
|
|
+ _canvas.Children.Remove(element);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果删的是最后一根箭头,清理引用避免后续更新异常
|
|
|
+ if (ReferenceEquals(_lastArrowGroup, element))
|
|
|
+ {
|
|
|
+ _lastArrowGroup = null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|