//------------------------------------------------------------- // // Copyright © Microsoft Corporation. All Rights Reserved. // //------------------------------------------------------------- // @owner=alexgor, deliant //================================================================= // File: ImageMap.cs // // Namespace: DataVisualization.Charting // // Classes: MapArea, MapAreasCollection // // Purpose: Collection of MapArea classes is used to generate // Chart image map, which provides functionality like // tooltip, drilldown and client-side scripting. // // Reviewed: AG - Jul 31, 2002 // AG - Microsoft 14, 2007 // //=================================================================== #region Used namespaces using System; using System.Text; using System.Collections; using System.Collections.Specialized; using System.ComponentModel; using System.ComponentModel.Design; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Design; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; #if Microsoft_CONTROL using System.Windows.Forms.DataVisualization.Charting; using System.Windows.Forms.DataVisualization.Charting.Data; using System.Windows.Forms.DataVisualization.Charting.ChartTypes; using System.Windows.Forms.DataVisualization.Charting.Utilities; using System.Windows.Forms.DataVisualization.Charting.Borders3D; #else using System.Web; using System.Web.UI; using System.Web.UI.DataVisualization.Charting; using System.Web.UI.DataVisualization.Charting.Utilities; using System.Text.RegularExpressions; using System.IO; #endif #endregion #if Microsoft_CONTROL namespace System.Windows.Forms.DataVisualization.Charting #else namespace System.Web.UI.DataVisualization.Charting #endif { #if ! Microsoft_CONTROL #region Map area shape enumeration /// /// An enumeration of map areas shapes. /// public enum MapAreaShape { /// /// The shape of the map area is rectangular. /// Rectangle, /// /// The shape of the map area is circular. /// Circle, /// /// The shape of the map area is polygonal. /// Polygon } #endregion #region IMapArea interface defenition /// /// Interface which defines common properties for the map area /// #if ASPPERM_35 [AspNetHostingPermission(System.Security.Permissions.SecurityAction.InheritanceDemand, Level = AspNetHostingPermissionLevel.Minimal)] [AspNetHostingPermission(System.Security.Permissions.SecurityAction.LinkDemand, Level = AspNetHostingPermissionLevel.Minimal)] #endif public interface IChartMapArea { /// /// Map area tooltip /// /// The tooltip. string ToolTip { set; get; } /// /// Map area Href /// /// The map area Href. [SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings")] string Url { set; get; } /// /// Map area other custom attributes /// /// The map area attributes. string MapAreaAttributes { set; get; } /// /// Map area custom data /// /// The tag. object Tag { set; get; } /// /// Map area post back value. /// /// The post back value. string PostBackValue { get; set; } } #endregion /// /// The MapArea class represents an area of the chart with end-user /// interactivity like tooltip, HREF or custom attributes. /// [ DefaultProperty("ToolTip"), SRDescription("DescriptionAttributeMapArea_MapArea") ] #if ASPPERM_35 [AspNetHostingPermission(System.Security.Permissions.SecurityAction.InheritanceDemand, Level = AspNetHostingPermissionLevel.Minimal)] [AspNetHostingPermission(System.Security.Permissions.SecurityAction.LinkDemand, Level = AspNetHostingPermissionLevel.Minimal)] #endif public class MapArea : ChartNamedElement, IChartMapArea { #region Member variables private string _toolTip = String.Empty; private string _url = String.Empty; private string _attributes = String.Empty; private string _postBackValue = String.Empty; private bool _isCustom = true; private MapAreaShape _shape = MapAreaShape.Rectangle; private float[] _coordinates = new float[4]; private static Regex _mapAttributesRegex; #endregion #region Constructors /// /// Initializes a new instance of the class. /// public MapArea() : base() { } /// /// Initializes a new instance of the class. /// /// The destination URL or anchor point of the map area. /// A GraphicsPath object that defines the shape of the map area. [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "0#")] public MapArea(string url, GraphicsPath path) : this(String.Empty, url, String.Empty, String.Empty, path, null) { } /// /// Initializes a new instance of the class. /// /// The destination URL or anchor point of the map area. /// A RectangleF structure that defines shape of the rectangular map area. [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "0#")] public MapArea(string url, RectangleF rect) : this(String.Empty, url, String.Empty, String.Empty, rect, null) { } /// /// Initializes a new instance of the class. /// /// Area shape. /// The destination URL or anchor point of the map area. /// Coordinates array that determines the location of the circle, rectangle or polygon. /// The type of shape that is being used determines the type of coordinates required. [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#")] public MapArea(MapAreaShape shape, string url, float[] coordinates) : this(shape, String.Empty, url, String.Empty, String.Empty, coordinates, null) { } /// /// Initializes a new instance of the class. /// /// Tool tip. /// Jump URL. /// Other area attributes. /// The postback value. /// Area coordinates as graphic path /// The tag. [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#")] public MapArea(string toolTip, string url, string attributes, string postBackValue, GraphicsPath path, object tag) : base() { if(path.PointCount == 0) { throw new ArgumentException(SR.ExceptionImageMapPolygonShapeInvalid); } // Flatten all curved lines path.Flatten(); // Allocate array of floats PointF[] pathPoints = path.PathPoints; float[] coord = new float[pathPoints.Length * 2]; // Transfer path points int index = 0; foreach(PointF point in pathPoints) { coord[index++] = point.X; coord[index++] = point.Y; } // Initiazize area Initialize(MapAreaShape.Polygon, toolTip, url, attributes, postBackValue, coord, tag); } /// /// Initializes a new instance of the class. /// /// Tool tip. /// Jump URL. /// Other area attributes. /// The postback value. /// Rect coordinates /// The tag. [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#")] public MapArea(string toolTip, string url, string attributes, string postBackValue, RectangleF rect, object tag) : base() { float[] coord = new float[4]; coord[0] = rect.X; coord[1] = rect.Y; coord[2] = rect.Right; coord[3] = rect.Bottom; Initialize(MapAreaShape.Rectangle, toolTip, url, attributes, postBackValue, coord, tag); } /// /// Initializes a new instance of the class. /// /// The shape. /// The tool tip. /// The URL. /// The attributes. /// The postback value. /// The coordinates. /// The tag. [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "2#")] public MapArea(MapAreaShape shape, string toolTip, string url, string attributes, string postBackValue, float[] coordinates, object tag) : base() { Initialize(shape, toolTip, url, attributes, postBackValue, coordinates, tag); } private void Initialize(MapAreaShape shape, string toolTip, string url, string attributes, string postBackValue, float[] coordinates, object tag) { // Check number of coordinates depending on the area shape if (shape == MapAreaShape.Circle && coordinates.Length != 3) { throw (new InvalidOperationException(SR.ExceptionImageMapCircleShapeInvalid)); } if (shape == MapAreaShape.Rectangle && coordinates.Length != 4) { throw (new InvalidOperationException(SR.ExceptionImageMapRectangleShapeInvalid)); } if (shape == MapAreaShape.Polygon && (coordinates.Length % 2f) != 0f) { throw (new InvalidOperationException(SR.ExceptionImageMapPolygonShapeInvalid)); } // Create new area object this._toolTip = toolTip; this._url = url; this._attributes = attributes; this._shape = shape; this._coordinates = new float[coordinates.Length]; this._postBackValue = postBackValue; this.Tag = tag; coordinates.CopyTo(this._coordinates, 0); } #endregion #region Map area HTML tag generation methods /// /// Gets the name of the shape. /// /// private string GetShapeName() { //***************************************** //** Set shape type //***************************************** if (_shape == MapAreaShape.Circle) { return "circle"; } else if (_shape == MapAreaShape.Rectangle) { return "rect"; } else if (_shape == MapAreaShape.Polygon) { return "poly"; } return String.Empty; } /// /// Gets the coordinates. /// /// The graph. /// private string GetCoordinates(ChartGraphics graph) { // Transform coordinates from relative to pixels float[] transformedCoord = new float[this.Coordinates.Length]; if (this.Shape == MapAreaShape.Circle) { PointF p = graph.GetAbsolutePoint(new PointF(this.Coordinates[0], this.Coordinates[1])); transformedCoord[0] = p.X; transformedCoord[1] = p.Y; p = graph.GetAbsolutePoint(new PointF(this.Coordinates[2], this.Coordinates[1])); transformedCoord[2] = p.X; } else if (this.Shape == MapAreaShape.Rectangle) { PointF p = graph.GetAbsolutePoint(new PointF(this.Coordinates[0], this.Coordinates[1])); transformedCoord[0] = p.X; transformedCoord[1] = p.Y; p = graph.GetAbsolutePoint(new PointF(this.Coordinates[2], this.Coordinates[3])); transformedCoord[2] = p.X; transformedCoord[3] = p.Y; // Check if rectangle has width and height if ((int)Math.Round(transformedCoord[0]) == (int)Math.Round(transformedCoord[2])) { transformedCoord[2] = (float)Math.Round(transformedCoord[2]) + 1; } if ((int)Math.Round(transformedCoord[1]) == (int)Math.Round(transformedCoord[3])) { transformedCoord[3] = (float)Math.Round(transformedCoord[3]) + 1; } } else { PointF pConverted = Point.Empty; PointF pOriginal = Point.Empty; for (int index = 0; index < this.Coordinates.Length - 1; index += 2) { pOriginal.X = this.Coordinates[index]; pOriginal.Y = this.Coordinates[index + 1]; pConverted = graph.GetAbsolutePoint(pOriginal); transformedCoord[index] = pConverted.X; transformedCoord[index + 1] = pConverted.Y; } } StringBuilder tagStringBuilder = new StringBuilder(); // Store transformed coordinates in the string bool firstElement = true; foreach (float f in transformedCoord) { if (!firstElement) { tagStringBuilder.Append(","); } firstElement = false; tagStringBuilder.Append((int)Math.Round(f)); } return tagStringBuilder.ToString(); } private static bool IsJavaScript(string value) { string checkValue = value.Trim().Replace("\r", String.Empty).Replace("\n", String.Empty); if (checkValue.StartsWith("javascript:", StringComparison.OrdinalIgnoreCase)) { return true; } return false; } /// /// Encodes the value. /// /// The chart. /// The name. /// The value. /// private static string EncodeValue(Chart chart, string name, string value) { if (chart.IsMapAreaAttributesEncoded) { if (IsJavaScript(value) || name.Trim().StartsWith("on", StringComparison.OrdinalIgnoreCase)) { return HttpUtility.UrlEncode(value); } } return value; } /// /// Renders the tag. /// /// The writer. /// The chart. [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification="We use lower case to generate html attributes.")] internal void RenderTag(HtmlTextWriter writer, Chart chart) { StringBuilder excludedAttributes = new StringBuilder(); writer.WriteLine(); writer.AddAttribute(HtmlTextWriterAttribute.Shape, this.GetShapeName(), false); writer.AddAttribute(HtmlTextWriterAttribute.Coords, this.GetCoordinates(chart.chartPicture.ChartGraph)); if (!String.IsNullOrEmpty(this.ToolTip)) { excludedAttributes.Append("title,"); writer.AddAttribute(HtmlTextWriterAttribute.Title, EncodeValue(chart, "title", this.ToolTip)); } bool postbackRendered = false; if (!String.IsNullOrEmpty(this.Url)) { excludedAttributes.Append("href,"); string resolvedUrl = chart.ResolveClientUrl(this.Url); writer.AddAttribute(HtmlTextWriterAttribute.Href, EncodeValue(chart, "href", resolvedUrl)); } else if (!String.IsNullOrEmpty(this.PostBackValue) && chart.Page != null) { postbackRendered = true; excludedAttributes.Append("href,"); writer.AddAttribute(HtmlTextWriterAttribute.Href, chart.Page.ClientScript.GetPostBackClientHyperlink(chart, this.PostBackValue)); } if (!postbackRendered && !String.IsNullOrEmpty(this.PostBackValue) && chart.Page != null) { excludedAttributes.Append("onclick,"); writer.AddAttribute(HtmlTextWriterAttribute.Onclick, chart.Page.ClientScript.GetPostBackEventReference(chart, this.PostBackValue)); } if (!String.IsNullOrEmpty(this._attributes)) { string excludedAttr = excludedAttributes.ToString(); // matches name1="value1" name2="value2", don't match name1="val"ue1" or name1="value1" /> if (_mapAttributesRegex == null) { _mapAttributesRegex = new Regex(@"\s?(?(\w+))\s?=\s?""(?[^""]+)""\s?", RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled); } foreach (Match match in _mapAttributesRegex.Matches(this._attributes)) { Group names = match.Groups["name"]; Group values = match.Groups["value"]; for (int i = 0; i < names.Captures.Count || i < values.Captures.Count; i++) { string name = names.Captures[i].Value.ToLowerInvariant(); string value = values.Captures[i].Value; // skip already rendered attributes if (!excludedAttr.Contains(name + ",")) { // is it url? if ("src,href,longdesc,background,".Contains(name + ",") && !IsJavaScript(value)) { value = chart.ResolveClientUrl(value); } else { value = HttpUtility.HtmlAttributeEncode(value); } value = EncodeValue(chart, name, value); writer.AddAttribute(name, value, false); } } } } if (this._attributes.IndexOf(" alt=", StringComparison.OrdinalIgnoreCase) == -1) { if (!String.IsNullOrEmpty(this.ToolTip)) { writer.AddAttribute(HtmlTextWriterAttribute.Alt, EncodeValue(chart, "title", this.ToolTip)); } else { writer.AddAttribute(HtmlTextWriterAttribute.Alt, ""); } } writer.RenderBeginTag(HtmlTextWriterTag.Area); writer.RenderEndTag(); } #endregion #region MapArea Properties /// /// Gets or sets a flag which indicates whether the map area is custom. /// [ Browsable(false), SRDescription("DescriptionAttributeMapArea_Custom"), DefaultValue(""), DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Hidden), SerializationVisibilityAttribute(SerializationVisibility.Hidden) ] public bool IsCustom { get { return _isCustom; } internal set { _isCustom = value; } } /// /// Gets or sets the coordinates of of the map area. /// [ SRCategory("CategoryAttributeShape"), Bindable(true), SRDescription("DescriptionAttributeMapArea_Coordinates"), DefaultValue(""), #if !Microsoft_CONTROL PersistenceMode(PersistenceMode.Attribute), #endif TypeConverter(typeof(MapAreaCoordinatesConverter)) ] [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")] public float[] Coordinates { get { return _coordinates; } set { _coordinates = value; } } /// /// Gets or sets the shape of the map area. /// [ SRCategory("CategoryAttributeShape"), Bindable(true), SRDescription("DescriptionAttributeMapArea_Shape"), DefaultValue(typeof(MapAreaShape), "Rectangle"), #if !Microsoft_CONTROL PersistenceMode(PersistenceMode.Attribute) #endif ] public MapAreaShape Shape { get { return _shape; } set { _shape = value; } } /// /// Gets or sets the name of the map area. /// [ SRCategory("CategoryAttributeData"), SRDescription("DescriptionAttributeMapArea_Name"), DefaultValue("Map Area"), Browsable(false), DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Hidden), SerializationVisibilityAttribute(SerializationVisibility.Hidden) ] public override string Name { get { return base.Name; } set { base.Name = value; } } #endregion #region IMapAreaAttributesutes Properties implementation /// /// Gets or sets the tooltip of the map area. /// [ SRCategory("CategoryAttributeMapArea"), Bindable(true), SRDescription("DescriptionAttributeToolTip"), DefaultValue(""), #if !Microsoft_CONTROL PersistenceMode(PersistenceMode.Attribute) #endif ] public string ToolTip { set { _toolTip = value; } get { return _toolTip; } } /// /// Gets or sets the URL of the map area. /// [ SRCategory("CategoryAttributeMapArea"), Bindable(true), SRDescription("DescriptionAttributeUrl"), DefaultValue(""), #if !Microsoft_CONTROL PersistenceMode(PersistenceMode.Attribute), Editor(Editors.UrlValueEditor.Editor, Editors.UrlValueEditor.Base) #endif ] public string Url { set { _url = value; } get { return _url; } } /// /// Gets or sets the attributes of the map area. /// [ SRCategory("CategoryAttributeMapArea"), Bindable(true), SRDescription("DescriptionAttributeMapAreaAttributes"), DefaultValue(""), #if !Microsoft_CONTROL PersistenceMode(PersistenceMode.Attribute) #endif ] public string MapAreaAttributes { set { _attributes = value; } get { return _attributes; } } /// /// Gets or sets the postback value which can be processed on click event. /// /// The value which is passed to click event as argument. [DefaultValue("")] [SRCategory(SR.Keys.CategoryAttributeMapArea)] [SRDescription(SR.Keys.DescriptionAttributePostBackValue)] public string PostBackValue { get { return this._postBackValue; } set { this._postBackValue = value; } } #endregion } /// /// The MapAreasCollection class is a strongly typed collection of MapAreas. /// [ SRDescription("DescriptionAttributeMapAreasCollection_MapAreasCollection") ] #if ASPPERM_35 [AspNetHostingPermission(System.Security.Permissions.SecurityAction.InheritanceDemand, Level = AspNetHostingPermissionLevel.Minimal)] [AspNetHostingPermission(System.Security.Permissions.SecurityAction.LinkDemand, Level = AspNetHostingPermissionLevel.Minimal)] #endif public class MapAreasCollection : ChartElementCollection { #region Constructors /// /// Public constructor. /// public MapAreasCollection() : base(null) { } #endregion #region Methods /// /// Insert new map area items into the collection. /// /// Index to insert at. /// Tool tip. /// Jump URL. /// Other area attributes. /// The post back value associated with this item. /// Area coordinates as graphics path. /// Absolute coordinates in the graphics path. /// Chart graphics object. internal void InsertPath( int index, string toolTip, string url, string attributes, string postBackValue, GraphicsPath path, bool absCoordinates, ChartGraphics graph) { // If there is more then one graphical path split them and create // image maps for every graphical path separately. GraphicsPathIterator iterator = new GraphicsPathIterator(path); // There is more then one path. if( iterator.SubpathCount > 1 ) { GraphicsPath subPath = new GraphicsPath(); while(iterator.NextMarker(subPath) > 0) { InsertSubpath(index, toolTip, url, attributes, postBackValue, subPath, absCoordinates, graph); subPath.Reset(); } } // There is only one path else { InsertSubpath(index, toolTip, url, attributes, postBackValue, path, absCoordinates, graph); } } /// /// Insert new map area item into the collection. /// /// Index to insert at. /// Tool tip. /// Jump URL. /// Other area attributes. /// The post back value associated with this item. /// Area coordinates as graphics path. /// Absolute coordinates in the graphics path. /// Chart graphics object. private void InsertSubpath( int index, string toolTip, string url, string attributes, string postBackValue, GraphicsPath path, bool absCoordinates, ChartGraphics graph) { if(path.PointCount > 0) { // Flatten all curved lines path.Flatten(); // Allocate array of floats PointF[] pathPoints = path.PathPoints; float[] coord = new float[pathPoints.Length * 2]; // Convert absolute coordinates to relative if(absCoordinates) { for(int pointIndex = 0; pointIndex < pathPoints.Length; pointIndex++) { pathPoints[pointIndex] = graph.GetRelativePoint( pathPoints[pointIndex] ); } } // Transfer path points int i = 0; foreach(PointF point in pathPoints) { coord[i++] = point.X; coord[i++] = point.Y; } // Add new area MapArea area = new MapArea(MapAreaShape.Polygon, toolTip, url, attributes, postBackValue, coord, null); area.IsCustom = false; this.Insert(index, area); } } /// /// Removes all non custom map areas items from the collection. /// internal void RemoveNonCustom() { for(int index = 0; index < this.Count; index++) { // Check the custom flag if(!this[index].IsCustom) { this.RemoveAt(index); --index; } } } #endregion } #endif }