// Copyright 2005, 2006 - Morten Nielsen (www.iter.dk) // // This file is part of SharpMap. // SharpMap is free software; you can redistribute it and/or modify // it under the terms of the GNU Lesser General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. // // SharpMap is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. // You should have received a copy of the GNU Lesser General Public License // along with SharpMap; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.Linq; using System.Reflection; using GeoAPI.Geometries; using SharpMap.Rendering.Symbolizer; using SharpMap.Styles; using Point=GeoAPI.Geometries.Coordinate; using System.Runtime.CompilerServices; namespace SharpMap.Rendering { /// /// This class renders individual geometry features to a graphics object using the settings of a map object. /// public static class VectorRenderer { internal const float ExtremeValueLimit = 1E+8f; internal const float NearZero = 1E-30f; // 1/Infinity static VectorRenderer() { SizeOfString = SizeOfStringCeiling; } private static readonly Bitmap _defaultSymbol = (Bitmap) Image.FromStream( Assembly.GetExecutingAssembly().GetManifestResourceStream("SharpMap.Styles.DefaultSymbol.png")); /// /// Renders a MultiLineString to the map. /// /// Graphics reference /// MultiLineString to be rendered /// Pen style used for rendering /// Map reference /// Offset by which line will be moved to right [MethodImpl(MethodImplOptions.Synchronized)] public static void DrawMultiLineString(Graphics g, IMultiLineString lines, Pen pen, MapViewport map, float offset) { DrawMultiLineStringEx(g, lines, pen, map, offset); } /// /// Renders a MultiLineString to the map. /// /// Graphics reference /// MultiLineString to be rendered /// Pen style used for rendering /// Map reference /// Offset by which line will be moved to right /// The area of the map that was affected by the drawing operation [MethodImpl(MethodImplOptions.Synchronized)] public static RectangleF DrawMultiLineStringEx(Graphics g, IMultiLineString lines, Pen pen, MapViewport map, float offset) { var canvasArea = RectangleF.Empty; for(int i = 0; i < lines.NumGeometries; i++) { var line = (ILineString) lines[i]; var rect = DrawLineStringEx(g, line, pen, map, offset); canvasArea = rect.ExpandToInclude(canvasArea); } return canvasArea; } /// /// Offset drawn linestring by given pixel width /// /// /// /// internal static PointF[] OffsetRight(PointF[] points, float offset) { int length = points.Length; var newPoints = new PointF[(length - 1) * 2]; float space = (offset * offset / 4) + 1; //if there are two or more points if (length >= 2) { var counter = 0; float x = 0, y = 0; for (var i = 0; i < length - 1; i++) { var b = -(points[i + 1].X - points[i].X); if (b != 0) { var a = points[i + 1].Y - points[i].Y; var c = a / b; y = 2 * (float)Math.Sqrt(space / (c * c + 1)); y = b < 0 ? y : -y; x = c * y; if (offset < 0) { y = -y; x = -x; } newPoints[counter] = new PointF(points[i].X + x, points[i].Y + y); newPoints[counter + 1] = new PointF(points[i + 1].X + x, points[i + 1].Y + y); } else { newPoints[counter] = new PointF(points[i].X + x, points[i].Y + y); newPoints[counter + 1] = new PointF(points[i + 1].X + x, points[i + 1].Y + y); } counter += 2; } return newPoints; } return points; } /// /// Renders a LineString to the map. /// /// Graphics reference /// LineString to render /// Pen style used for rendering /// Map reference [MethodImpl(MethodImplOptions.Synchronized)] [Obsolete("Not called, will be removed")] public static void DrawLineString(Graphics g, ILineString line, Pen pen, MapViewport map) { DrawLineString(g, line, pen, map, 0f); } /// /// Renders a LineString to the map. /// /// Graphics reference /// LineString to render /// Pen style used for rendering /// Map reference /// Offset by which line will be moved to right public static void DrawLineString(Graphics g, ILineString line, Pen pen, MapViewport map, float offset) { DrawLineStringEx(g, line, pen, map, offset); } /// /// Renders a LineString to the map. /// /// Graphics reference /// LineString to render /// Pen style used for rendering /// Map reference /// Offset by which line will be moved to right /// The area of the map that was affected by the drawing of the geometry. public static RectangleF DrawLineStringEx(Graphics g, ILineString line, Pen pen, MapViewport map, float offset) { var points = line.TransformToImage(map); if (points.Length < 2) return RectangleF.Empty; using (var gp = new GraphicsPath()) { if (offset != 0d) points = OffsetRight(points, offset); gp.AddLines(LimitValues(points, ExtremeValueLimit)); g.DrawPath(pen, gp); var bounds = gp.GetBounds(); bounds.Inflate(pen.Width / 2f, pen.Width / 2f); return bounds; } } /// /// Renders a multipolygon byt rendering each polygon in the collection by calling DrawPolygon. /// /// Graphics reference /// MultiPolygon to render /// Brush used for filling (null or transparent for no filling) /// Outline pen style (null if no outline) /// Specifies whether polygon clipping should be applied /// Map reference [MethodImpl(MethodImplOptions.Synchronized)] public static void DrawMultiPolygon(Graphics g, IMultiPolygon multiPolygon, Brush brush, Pen pen, bool clip, MapViewport map) { DrawMultiPolygonEx(g, multiPolygon, brush, pen, clip, map); } /// /// Renders a multipolygon byt rendering each polygon in the collection by calling DrawPolygon. /// /// Graphics reference /// MultiPolygon to render /// Brush used for filling (null or transparent for no filling) /// Outline pen style (null if no outline) /// Specifies whether polygon clipping should be applied /// Map reference /// The area of the map that was affected by the drawing of the geometry. [MethodImpl(MethodImplOptions.Synchronized)] public static RectangleF DrawMultiPolygonEx(Graphics g, IMultiPolygon multiPolygon, Brush brush, Pen pen, bool clip, MapViewport map) { var canvasArea = RectangleF.Empty; for (var i = 0; i < multiPolygon.NumGeometries;i++ ) { var p = (IPolygon)multiPolygon[i]; var rect = DrawPolygonEx(g, p, brush, pen, clip, map); canvasArea = rect.ExpandToInclude(canvasArea); } return canvasArea; } /// /// Renders a polygon to the map. /// /// Graphics reference /// Polygon to render /// Brush used for filling (null or transparent for no filling) /// Outline pen style (null if no outline) /// Specifies whether polygon clipping should be applied /// Map reference [MethodImpl(MethodImplOptions.Synchronized)] public static void DrawPolygon(Graphics g, IPolygon pol, Brush brush, Pen pen, bool clip, MapViewport map) { DrawPolygonEx(g, pol, brush, pen, clip, map); } /// /// Renders a polygon to the map. /// /// Graphics reference /// Polygon to render /// Brush used for filling (null or transparent for no filling) /// Outline pen style (null if no outline) /// Specifies whether polygon clipping should be applied /// Map reference /// The area of the map that was affected by the drawing of the geometry. public static RectangleF DrawPolygonEx(Graphics g, IPolygon pol, Brush brush, Pen pen, bool clip, MapViewport map) { if (pol.ExteriorRing == null) return RectangleF.Empty; var points = pol.ExteriorRing.TransformToImage(map); if (points.Length <= 2) return RectangleF.Empty; //Use a graphics path instead of DrawPolygon. DrawPolygon has a problem with several interior holes using (var gp = new GraphicsPath()) { //Add the exterior polygon if (!clip) gp.AddPolygon(LimitValues(points, ExtremeValueLimit)); else DrawPolygonClipped(gp, LimitValues(points, ExtremeValueLimit), map.Size.Width, map.Size.Height); //Add the interior polygons (holes) if (pol.NumInteriorRings > 0) { foreach (ILinearRing ring in pol.InteriorRings) { points = ring.TransformToImage(map); if (!clip) gp.AddPolygon(LimitValues(points, ExtremeValueLimit)); else DrawPolygonClipped(gp, LimitValues(points, ExtremeValueLimit), map.Size.Width, map.Size.Height); } } // Only render inside of polygon if the brush isn't null or isn't transparent if (brush != null && brush != Brushes.Transparent) g.FillPath(brush, gp); // Create an outline if a pen style is available if (pen != null) g.DrawPath(pen, gp); // refer to comments on DrawLineStringEx for determining bounds var bounds = gp.GetBounds(); if (pen != null) bounds.Inflate(pen.Width / 2f, pen.Width / 2f); return bounds; } } private static void DrawPolygonClipped(GraphicsPath gp, PointF[] polygon, int width, int height) { var clipState = DetermineClipState(polygon, width, height); if (clipState == ClipState.Within) { gp.AddPolygon(polygon); } else if (clipState == ClipState.Intersecting) { var clippedPolygon = ClipPolygon(polygon, width, height); if (clippedPolygon.Length > 2) gp.AddPolygon(clippedPolygon); } } /// /// Purpose of this method is to prevent the 'overflow error' exception in the FillPath method. /// This Exception is thrown when the coordinate values become too big (values over -2E+9f always /// throw an exception, values under 1E+8f seem to be okay). This method limits the coordinates to /// the values given by the second parameter (plus an minus). Theoretically the lines to and from /// these limited points are not correct but GDI+ paints incorrect even before that limit is reached. /// /// The vertices that need to be limited /// The limit at which coordinate values will be cutoff /// The limited vertices public static PointF[] LimitValues(PointF[] vertices, float limit) { for (var i = 0; i < vertices.Length; i++) { vertices[i].X = Math.Max(-limit, Math.Min(limit, vertices[i].X)); vertices[i].Y = Math.Max(-limit, Math.Min(limit, vertices[i].Y)); } return vertices; } /// /// Signature for a function that evaluates the length of a string when rendered on a Graphics object with a given font /// /// object /// the text to render /// the font to use /// the size public delegate SizeF SizeOfStringDelegate(Graphics g, string text, Font font); private static SizeOfStringDelegate _sizeOfString; /// /// Delegate used to determine the of a given string. /// public static SizeOfStringDelegate SizeOfString { get { return _sizeOfString ?? (_sizeOfString = SizeOfStringCeiling); } set { if (value != null ) _sizeOfString = value; } } /// /// Function to get the of a string when rendered with the given font. /// /// object /// the text to render /// the font to use /// the size public static SizeF SizeOfStringBase(Graphics g, string text, Font font) { return g.MeasureString(text, font); } /// /// Function to get the of a string when rendered with the given font. /// /// object /// the text to render /// the font to use /// the size [Obsolete] public static SizeF SizeOfString74(Graphics g, string text, Font font) { var s = g.MeasureString(text, font); return new SizeF(s.Width * 0.74f+1f, s.Height * 0.74f); } /// /// Function to get the of a string when rendered with the given font. /// /// object /// the text to render /// the font to use /// the size public static SizeF SizeOfStringCeiling(Graphics g, string text, Font font) { SizeF f = g.MeasureString(text, font); return new SizeF((float)Math.Ceiling(f.Width), (float)Math.Ceiling(f.Height)); } /// /// Renders a label to the map. /// /// Graphics reference /// Label placement /// Offset of label in screen coordinates /// Font used for rendering /// Font foreground color /// Background color /// Color of halo /// Text rotation in degrees /// Text to render /// Map reference /// Horizontal alignment for multi line labels. If not set is used /// Point where the rotation should take place [MethodImpl(MethodImplOptions.Synchronized)] public static void DrawLabel(Graphics g, PointF labelPoint, PointF offset, Font font, Color foreColor, Brush backColor, Pen halo, float rotation, string text, MapViewport map, LabelStyle.HorizontalAlignmentEnum alignment = LabelStyle.HorizontalAlignmentEnum.Left, PointF? rotationPoint = null) { DrawLabelEx(g, labelPoint, offset, font, foreColor, backColor, halo, rotation, text, map, alignment, rotationPoint); } /// /// Renders a label to the map. /// /// Graphics reference /// Label placement /// Offset of label in screen coordinates /// Font used for rendering /// Font foreground color /// Background color /// Color of halo /// Text rotation in degrees /// Text to render /// Map reference /// Horizontal alignment for multi line labels. If not set is used /// Point where the rotation should take place [MethodImpl(MethodImplOptions.Synchronized)] public static RectangleF DrawLabelEx(Graphics g, PointF labelPoint, PointF offset, Font font, Color foreColor, Brush backColor, Pen halo, float rotation, string text, MapViewport map, LabelStyle.HorizontalAlignmentEnum alignment = LabelStyle.HorizontalAlignmentEnum.Left, PointF? rotationPoint = null) { //Calculate the size of the text var labelSize = _sizeOfString(g, text, font); //Add label offset labelPoint.X += offset.X; labelPoint.Y += offset.Y; //Translate alignment to stringalignment StringAlignment sAlign; switch (alignment) { case LabelStyle.HorizontalAlignmentEnum.Left: sAlign = StringAlignment.Near; break; case LabelStyle.HorizontalAlignmentEnum.Center: sAlign = StringAlignment.Center; break; default: sAlign = StringAlignment.Far; break; } Matrix origTrans = null; Matrix symTrans = null; if (rotation != 0 && !float.IsNaN(rotation)) { rotationPoint = rotationPoint ?? labelPoint; origTrans = g.Transform.Clone(); g.TranslateTransform(rotationPoint.Value.X, rotationPoint.Value.Y); g.RotateTransform(rotation); labelPoint = new PointF(labelPoint.X - rotationPoint.Value.X, labelPoint.Y - rotationPoint.Value.Y); symTrans = new Matrix(); symTrans.Translate(rotationPoint.Value.X, rotationPoint.Value.Y); symTrans.Rotate(rotation); } var background = new RectangleF(labelPoint.X, labelPoint.Y, labelSize.Width, labelSize.Height); if (backColor != null && backColor != Brushes.Transparent) g.FillRectangle(backColor, background); using (var path = new GraphicsPath()) { path.AddString(text, font.FontFamily, (int) font.Style, font.Size, new RectangleF(labelPoint, labelSize), new StringFormat {Alignment = sAlign}); if (halo != null) { g.DrawPath(halo, path); // excessive halo can bleed outside of background background.Inflate(halo.Width / 2f, halo.Width / 2f); } g.FillPath(new SolidBrush(foreColor), path); } if (origTrans != null) { g.Transform = origTrans; origTrans.Dispose(); } if (symTrans == null) return background; var pts = background.ToPointArray(); symTrans.TransformPoints(pts); symTrans.Dispose(); return pts.ToRectangleF(); } private static ClipState DetermineClipState(PointF[] vertices, int width, int height) { float minX = float.MaxValue; float minY = float.MaxValue; float maxX = float.MinValue; float maxY = float.MinValue; for (int i = 0; i < vertices.Length; i++) { minX = Math.Min(minX, vertices[i].X); minY = Math.Min(minY, vertices[i].Y); maxX = Math.Max(maxX, vertices[i].X); maxY = Math.Max(maxY, vertices[i].Y); } if (maxX < 0) return ClipState.Outside; if (maxY < 0) return ClipState.Outside; if (minX > width) return ClipState.Outside; if (minY > height) return ClipState.Outside; if (minX > 0 && maxX < width && minY > 0 && maxY < height) return ClipState.Within; return ClipState.Intersecting; } /// /// Clips a polygon to the view. /// Based on UMN Mapserver renderer /// /// vertices in image coordinates /// Width of map in image coordinates /// Height of map in image coordinates /// Clipped polygon internal static PointF[] ClipPolygon(PointF[] vertices, int width, int height) { var line = new List(); if (vertices.Length <= 1) /* nothing to clip */ return vertices; for (int i = 0; i < vertices.Length - 1; i++) { var x1 = vertices[i].X; var y1 = vertices[i].Y; var x2 = vertices[i + 1].X; var y2 = vertices[i + 1].Y; var deltaX = x2 - x1; if (deltaX == 0f) { // bump off of the vertical deltaX = (x1 > 0) ? -NearZero : NearZero; } var deltaY = y2 - y1; if (deltaY == 0f) { // bump off of the horizontal deltaY = (y1 > 0) ? -NearZero : NearZero; } float xIn; float xOut; if (deltaX > 0) { // points to right xIn = 0; xOut = width; } else { xIn = width; xOut = 0; } float yIn; float yOut; if (deltaY > 0) { // points up yIn = 0; yOut = height; } else { yIn = height; yOut = 0; } var tinX = (xIn - x1) / deltaX; var tinY = (yIn - y1) / deltaY; float tIn1; float tIn2; if (tinX < tinY) { // hits x first tIn1 = tinX; tIn2 = tinY; } else { // hits y first tIn1 = tinY; tIn2 = tinX; } if (1 >= tIn1) { if (0 < tIn1) line.Add(new PointF(xIn, yIn)); if (1 >= tIn2) { var tOutX = (xOut - x1) / deltaX; var tOutY = (yOut - y1) / deltaY; var tOut = (tOutX < tOutY) ? tOutX : tOutY; if (0 < tIn2 || 0 < tOut) { if (tIn2 <= tOut) { if (0 < tIn2) { line.Add(tinX > tinY ? new PointF(xIn, y1 + tinX * deltaY) : new PointF(x1 + tinY * deltaX, yIn)); } if (1 > tOut) { line.Add(tOutX < tOutY ? new PointF(xOut, y1 + tOutX * deltaY) : new PointF(x1 + tOutY * deltaX, yOut)); } else line.Add(new PointF(x2, y2)); } else { line.Add(tinX > tinY ? new PointF(xIn, yOut) : new PointF(xOut, yIn)); } } } } } if (line.Count > 0) line.Add(new PointF(line[0].X, line[0].Y)); return line.ToArray(); } /// /// Renders a point to the map. /// /// Graphics reference /// Point to render /// Brush reference /// Size of drawn Point /// Symbol offset af scale=1 /// Map reference [MethodImpl(MethodImplOptions.Synchronized)] public static void DrawPoint(Graphics g, IPoint point, Brush b, float size, PointF offset, MapViewport map) { DrawPointEx(g, point, b, size, offset, map); } /// /// Renders a point to the map. /// /// Graphics reference /// Point to render /// Brush reference /// Size of drawn Point /// Symbol offset af scale=1 /// Map reference [MethodImpl(MethodImplOptions.Synchronized)] public static RectangleF DrawPointEx(Graphics g, IPoint point, Brush b, float size, PointF offset, MapViewport map) { if (point == null) return RectangleF.Empty; var pp = map.WorldToImage(point.Coordinate); var width = size; var height = size; float minX = (int)pp.X - width / 2 + offset.X; float minY = (int) pp.Y - height / 2 + offset.Y; g.FillEllipse(b, minX, minY, width, height); return new RectangleF(minX, minY, width, height); } /// /// Renders a point to the map. /// /// Graphics reference /// Point to render /// Symbolizer to decorate point /// Map reference [MethodImpl(MethodImplOptions.Synchronized)] public static void DrawPoint(IPointSymbolizer symbolizer, Graphics g, IPoint point, MapViewport map) => DrawPointEx(symbolizer, g, point, map); /// /// Renders a point to the map. /// /// Graphics reference /// Point to render /// Symbolizer to decorate point /// Map reference [MethodImpl(MethodImplOptions.Synchronized)] public static RectangleF DrawPointEx(IPointSymbolizer symbolizer, Graphics g, IPoint point, MapViewport map) { if (point == null) return RectangleF.Empty; symbolizer.Render(map, point, g); return ((IPointSymbolizerEx)symbolizer).CanvasArea; } /// /// Renders a point to the map. /// /// Graphics reference /// Point to render /// Symbol to place over point /// The amount that the symbol should be scaled. A scale of '1' equals to no scaling /// Symbol offset af scale=1 /// Symbol rotation in degrees /// Map reference [MethodImpl(MethodImplOptions.Synchronized)] public static void DrawPoint(Graphics g, IPoint point, Image symbol, float symbolscale, PointF offset, float rotation, MapViewport map) => DrawPointEx(g, point, symbol, symbolscale, offset, rotation, map); /// /// Renders a point to the map. /// /// Graphics reference /// Point to render /// Symbol to place over point /// The amount that the symbol should be scaled. A scale of '1' equals to no scaling /// Symbol offset af scale=1 /// Symbol rotation in degrees /// Map reference [MethodImpl(MethodImplOptions.Synchronized)] public static RectangleF DrawPointEx(Graphics g, IPoint point, Image symbol, float symbolScale, PointF offset, float rotation, MapViewport map) { if (point == null) return RectangleF.Empty; if (symbol == null) //We have no point style - Use a default symbol symbol = _defaultSymbol; var pp = map.WorldToImage(point.Coordinate); float width = symbol.Width * symbolScale; float height = symbol.Height * symbolScale; float left = pp.X - width / 2 + offset.X * symbolScale; float top = pp.Y - height / 2 + offset.Y * symbolScale; Matrix symTrans = null; Matrix origTrans = null; if (rotation != 0 && !Single.IsNaN(rotation)) { origTrans = g.Transform.Clone(); using (var t = g.Transform.Clone()) { t.RotateAt(rotation, pp); g.Transform = t; } symTrans = new Matrix(); symTrans.RotateAt(rotation, pp); } lock (symbol) { g.DrawImage(symbol, left, top, width, height); } if (origTrans != null) { g.Transform = origTrans; origTrans.Dispose(); } if (symTrans== null) return new RectangleF(left, top, width, height); var pts = new[] { new PointF(left, top), new PointF(left + width, top), new PointF(left + width, top + height), new PointF(left, top + height) }; symTrans.TransformPoints(pts); symTrans.Dispose(); return pts.ToRectangleF(); } /// /// Renders a to the map. /// /// Graphics reference /// MultiPoint to render /// Symbol to place over point /// The amount that the symbol should be scaled. A scale of '1' equals to no scaling /// Symbol offset af scale=1 /// Symbol rotation in degrees /// Map reference [MethodImpl(MethodImplOptions.Synchronized)] public static void DrawMultiPoint(Graphics g, IMultiPoint points, Image symbol, float symbolScale, PointF offset, float rotation, MapViewport map) => DrawMultiPointEx(g, points, symbol, symbolScale, offset, rotation, map); /// /// Renders a to the map. /// /// Graphics reference /// MultiPoint to render /// Symbol to place over point /// The amount that the symbol should be scaled. A scale of '1' equals to no scaling /// Symbol offset af scale=1 /// Symbol rotation in degrees /// Map reference [MethodImpl(MethodImplOptions.Synchronized)] public static RectangleF DrawMultiPointEx(Graphics g, IMultiPoint points, Image symbol, float symbolScale, PointF offset, float rotation, MapViewport map) { var canvasArea = RectangleF.Empty; for (var i = 0; i < points.NumGeometries; i++) { var rect = DrawPointEx(g, (IPoint) points[i], symbol, symbolScale, offset, rotation, map); canvasArea = rect.ExpandToInclude(canvasArea); } return canvasArea; } /// /// Renders a to the map. /// /// Graphics reference /// MultiPoint to render /// Symbolizer to decorate point /// Map reference [MethodImpl(MethodImplOptions.Synchronized)] public static void DrawMultiPoint(IPointSymbolizer symbolizer, Graphics g, IMultiPoint points, MapViewport map) => DrawMultiPointEx(symbolizer, g, points, map); /// /// Renders a to the map. /// /// Graphics reference /// MultiPoint to render /// Symbolizer to decorate point /// Map reference [MethodImpl(MethodImplOptions.Synchronized)] public static RectangleF DrawMultiPointEx(IPointSymbolizer symbolizer, Graphics g, IMultiPoint points, MapViewport map) { symbolizer.Render(map, points, g); return ((IPointSymbolizerEx)symbolizer).CanvasArea; } /// /// Renders a to the map. /// /// Graphics reference /// MultiPoint to render /// Brush reference /// Size of drawn Point /// Symbol offset af scale=1 /// Map reference [MethodImpl(MethodImplOptions.Synchronized)] public static void DrawMultiPoint(Graphics g, IMultiPoint points, Brush brush, float size, PointF offset, MapViewport map) => DrawMultiPointEx(g, points, brush, size, offset, map); /// /// Renders a to the map. /// /// Graphics reference /// MultiPoint to render /// Brush reference /// Size of drawn Point /// Symbol offset af scale=1 /// Map reference [MethodImpl(MethodImplOptions.Synchronized)] public static RectangleF DrawMultiPointEx(Graphics g, IMultiPoint points, Brush brush, float size, PointF offset, MapViewport map) { var canvasArea = RectangleF.Empty; for (var i = 0; i < points.NumGeometries; i++) { var point = (IPoint) points[i]; var rect = DrawPointEx(g, point, brush, size, offset, map); canvasArea = rect.ExpandToInclude(canvasArea); } return canvasArea; } #region Nested type: ClipState private enum ClipState { Within, Outside, Intersecting } #endregion /// /// Equivalent of Envelope.ExpandToInclude, allowing for RectangleF.Empty /// /// The base rectangle /// The rectangle to include /// /// /// RectangleF.Union does not take into account RectangleF.Empty. For example, /// when A = (0, 0; 0, 0) and B = (1, 1; 2, 2) then A.Union(B) = (0, 0; 2, 2) /// internal static RectangleF ExpandToInclude(this RectangleF self, RectangleF other) { if (other.IsEmpty) return self; if (self.IsEmpty) return other; return RectangleF.FromLTRB( Math.Min(self.X, other.X), Math.Min(self.Y, other.Y), Math.Max(self.Right, other.Right), Math.Max(self.Bottom, other.Bottom) ); } /// /// Equivalent of Envelope.ExpandToInclude, allowing for Rectangle.Empty /// /// The base rectangle /// The rectangle to include /// /// /// Rectangle.Union does not take into account Rectangle.Empty. For example, /// when A = (0, 0; 0, 0) and B = (1, 1; 2, 2) then A.Union(B) = (0, 0; 2, 2) /// public static Rectangle ExpandToInclude(this Rectangle self, Rectangle other) { if (other.IsEmpty) return self; if (self.IsEmpty) return other; return Rectangle.FromLTRB( Math.Min(self.X, other.X), Math.Min(self.Y, other.Y), Math.Max(self.Right, other.Right), Math.Max(self.Bottom, other.Bottom) ); } /// /// Utility method to return Rectangle enclosing given RectangleF. /// Top-left coordinate is rounded towards origin, while bottom-right coordinate is rounded away from origin. /// /// /// internal static Rectangle ToRectangle(this RectangleF self) { if (self.IsEmpty) return Rectangle.Empty; return Rectangle.FromLTRB( (int)Math.Truncate(self.X), (int)Math.Truncate(self.Y), (int)Math.Ceiling(self.Right), (int)Math.Ceiling(self.Bottom)); } /// /// Utility method to return Rectangle enclosing given RectangleF. /// Top-left coordinate is rounded towards origin, while bottom-right coordinate is rounded away from origin. /// /// /// internal static Rectangle ToRectangle(this System.Drawing.Point[] self) { if (self.Length < 4) return Rectangle.Empty; int minX = self.Min(p => p.X); int maxX = self.Max(p => p.X); int minY = self.Min(p => p.Y); int maxY = self.Max(p => p.Y); return new Rectangle(minX, minY, maxX - minX, maxY - minY); } /// /// Utility method to return enclosing rectangle. Source array must have 4 or more points. /// /// /// internal static RectangleF ToRectangleF(this PointF[] self) { if (self.Length < 4) return RectangleF.Empty; float minX = self.Min(p => p.X); float maxX = self.Max(p => p.X); float minY = self.Min(p => p.Y); float maxY = self.Max(p => p.Y); return new RectangleF(minX, minY, maxX - minX, maxY - minY); } /// /// Utility method to return points defining rectangle, ordered clockwise from top left /// /// /// internal static PointF[] ToPointArray(this RectangleF self) { return new [] { new PointF(self.X, self.Y), new PointF(self.X + self.Width, self.Y), new PointF(self.X + self.Width, self.Y +self.Height), new PointF(self.X, self.Y + self.Height), }; } /// /// Utility method to return points defining rectangle, ordered clockwise from top left /// /// /// internal static System.Drawing.Point[] ToPointArray(this Rectangle self) { return new[] { new System.Drawing.Point(self.X, self.Y), new System.Drawing.Point(self.X + self.Width, self.Y), new System.Drawing.Point(self.X + self.Width, self.Y +self.Height), new System.Drawing.Point(self.X, self.Y + self.Height) }; } /// /// Basic rectilinear union. Rectangles are assumed to be a common graphics coordinate system. /// /// /// /// /// Either of the arrays could be empty, so must return new array internal static PointF[] Union(this PointF[] self, PointF[] other) { if (other.Length == 0) return self; if (self.Length == 0) return other; float minX = Math.Min(self.Min(p => p.X), other.Min(p => p.X)); float maxX = Math.Max(self.Max(p => p.X), other.Max(p => p.X)); float minY = Math.Min(self.Min(p => p.Y), other.Min(p => p.Y)); float maxY = Math.Max(self.Max(p => p.Y), other.Max(p => p.Y)); float width = maxX - minX; float height = maxY - minY; return new [] { new PointF(minX, minY), new PointF(minX + width, minY), new PointF(minX + width, minY + height), new PointF(minX, minY + height), }; } } }