Common Custom renderers
Common_CustomRenderers_CSharp\App_Code\SimpleRenderer3D.cs
// Copyright 2010 ESRI
// 
// All rights reserved under the copyright laws of the United States
// and applicable international laws, treaties, and conventions.
// 
// You may freely redistribute and use this sample code, with or
// without modification, provided you include the original copyright
// notice and use restrictions.
// 
// See the use restrictions.
// 

namespace ESRI.ADF.Samples.Renderers
{
  /// <summary>
  /// Renderer that extrudes polygons and polylines based on a height attribute
  /// and renders the elements as 2½D.
  /// </summary>
  /// <remarks>
  /// Note that this renderer doesn't fully implement 3D rendering techniques and some 
  /// artifacts might occur, but instead tries to do a simplified 3D-like rendering.
  /// Features being parsed into this renderer should be sent in back-to-front (top-to-bottom)
  /// to prevent overlapping features.
  /// </remarks>
    [System.Serializable]
    public class SimpleRenderer3D : ESRI.ADF.Samples.Renderers.RendererBase
  {
    #region Constructors

    public SimpleRenderer3D()
    {
    }

    #endregion

    #region Properties - FillColor, OutlineColor, Transparency, Ambiance, HeightColumnName, MaxHeight, ScaleHeight, LightDirection

    private System.Drawing.Color fillColor = System.Drawing.Color.White;

        /// <summary>
        /// Fill color on each surface
        /// </summary>
    [System.ComponentModel.Description("Fill color on each surface.")]
    public System.Drawing.Color FillColor
    {
      get { return fillColor; }
      set { fillColor = value; }
    }

    private System.Drawing.Color outlineColor = System.Drawing.Color.Gray;
        /// <summary>
        /// Outline color around each surface.  Set to transparent or empty for no outline.
        /// </summary>
    [System.ComponentModel.Description("Outline color around each surface. Set to transparent or empty for no outline.")]
    public System.Drawing.Color OutlineColor
    {
      get { return outlineColor; }
      set { outlineColor = value; }
    }

    private int transparency = 25;
        /// <summary>
        /// Transparency of the fill
        /// </summary>
    [System.ComponentModel.DefaultValue(25)]
    [System.ComponentModel.Description("Transparency of the fill")]
    public int Transparency
    {
      get { return transparency; }
      set { transparency = value; }
    }
  
    private double ambience = 0.25;
    /// <summary>
    /// Ambience of the 3D renderer.
    /// 0 = no ambient light, 1 = full ambience
    /// </summary>
    [System.ComponentModel.DefaultValue(0.25)]
    [System.ComponentModel.Description("Ambience of the 3D renderer.  0 = no ambient light, 1 = full ambience")]
    public double Ambience
    {
      get { return ambience; }
      set
      {
        if (ambience < 0 || ambience > 1)
        {
          throw new System.ArgumentOutOfRangeException("Ambience", "Ambience must be between 0 and 1");
        }
        ambience = value;
      }
    }

    private string heightColumnName;
    /// <summary>
    /// Name of the column to use for extrusion height
    /// </summary>
    [System.ComponentModel.DefaultValue("")]
    [System.ComponentModel.Description("Name of the column to use for extrusion height")]
    public string HeightColumnName
    {
      get { return heightColumnName; }
      set { heightColumnName = value; }
    }

    public double maxHeight = 50;
    /// <summary>
    /// Maximum feature extrusion height (in pixels)
    /// </summary>
    [System.ComponentModel.DefaultValue(50)]
    [System.ComponentModel.Description("Maximum feature extrusion height (in pixels)")]
    public double MaxHeight
    {
      get { return maxHeight; }
      set { maxHeight = value; }
    }

    private double scaleHeight = 1.0;
    /// <summary>
    /// Scale factor to apply to the value specified by the feature's extrusion height column
    /// </summary>
    [System.ComponentModel.DefaultValue(1.0)]
        [System.ComponentModel.Description("Scale factor to apply to the value specified by the feature's extrusion height column")]
    public double ScaleHeight
    {
      get { return scaleHeight; }
      set { scaleHeight = value; }
    }

    private double lightDirection = 45 / 180 * System.Math.PI; //Light direction: north-east
    /// <summary>
    /// Direction of the light rays used for calculating shading. 0 is north, 90 is east, etc.
    /// </summary>
    /// <remarks>
    /// <para>Line segments perpendicular to the rays will be the color of the symbol, and 
    /// the more they are facing away from the light, the darker they will look.</para>
    /// North = 0;<br/>
    /// East = 90;<br/>
    /// South = 180;<br/>
    /// West = 270;<br/>
    /// North-West = 315;<br/>
    /// North-East = 45;<br/>
    /// </remarks>
    [System.ComponentModel.DefaultValue(45)]
        [System.ComponentModel.Description("Direction of the light rays used for calculating shading. 0 is north, 90 is east, etc.")]
    public double LightDirection
    {
      get { return lightDirection / System.Math.PI * 180; }
      set { lightDirection = value / 180 * System.Math.PI; }
    }

    #endregion

    #region IRenderer Members - GetAllSymbols, Render

        /// <summary>
        /// Generates the fill symbol used to symbolize feature surfaces and adds it to the
        /// collection of symbols used by the renderer.
        /// </summary>
        /// <param name="symbols">The list of symbols used by the renderer</param>
    public override void GetAllSymbols(System.Collections.Generic.List<ESRI.ArcGIS.ADF.Web.Display.Symbol.FeatureSymbol> symbols)
    {
            // Create a fill symbol and apply the renderer's fill color, outline color, and transparency
      ESRI.ArcGIS.ADF.Web.Display.Symbol.SimpleFillSymbol simpleFillSymbol = 
                new ESRI.ArcGIS.ADF.Web.Display.Symbol.SimpleFillSymbol(this.FillColor, this.OutlineColor, 
                ESRI.ArcGIS.ADF.Web.Display.Symbol.PolygonFillType.Solid);
      simpleFillSymbol.Transparency = this.Transparency;

            // Add the fill symbol to the symbols used by the renderer
      symbols.Add(simpleFillSymbol);
    }

        /// <summary>
        /// Main part of the IRenderer interface, within which a feature encapsulating the specified DataRow is to be 
        /// rendered on the specified graphics surface. The geometry instance has already been transformed to screen 
        /// coordinate, so we don't have to worry about that here.
        /// </summary>
        /// <param name="row">row containing the feature's data</param>
        /// <param name="graphics">GDI+ surface on which to render the feature</param>
        /// <param name="geometryColumn">column containing the feature's geometry</param>
        public override void Render(System.Data.DataRow row, System.Drawing.Graphics graphics, 
            System.Data.DataColumn geometryColumn)
    {
            // Validate method input
      if (row == null || graphics == null || geometryColumn == null)
        return;

            // Validate input geometry.  The renderer does not support points
      ESRI.ArcGIS.ADF.Web.Geometry.Geometry geometry = 
                row[geometryColumn] as ESRI.ArcGIS.ADF.Web.Geometry.Geometry;
      if (geometry == null || geometry is ESRI.ArcGIS.ADF.Web.Geometry.Point)
        return;

            // Initialize the extrusion height with the renderer's specified maximum height
      double extrusionHeight = this.MaxHeight;

      // Get the extrusion height column attribute, if possible
      if (!string.IsNullOrEmpty(HeightColumnName) && row.Table.Columns.Contains(HeightColumnName))
      {
        double.TryParse(row[HeightColumnName].ToString(), out extrusionHeight);
      }

      extrusionHeight *= this.ScaleHeight; // Apply the extrusion height scale factor

            // If the scaled height is beyond the maximum height, clip it to the maximum
      if (extrusionHeight > maxHeight) extrusionHeight = this.MaxHeight; 
      
            // Draw the feature
      this.drawGraphic(geometry, graphics, extrusionHeight);
    }

    #endregion

    #region Rendering - drawGraphics, drawPolyline, getLineSegements, drawPolygon, drawLineSegment, LineSegment

        // Renders the specified geometry on the specified GDI+ surface with the specified extrusion height
    private void drawGraphic(ESRI.ArcGIS.ADF.Web.Geometry.Geometry adfGeometry, System.Drawing.Graphics graphics, double height)
    {
            // Check whether the passed-in geometry is an envelope and, if so, convert it to a polygon
      if (adfGeometry is ESRI.ArcGIS.ADF.Web.Geometry.Envelope)
      {
        ESRI.ArcGIS.ADF.Web.Geometry.Polygon polygon = new ESRI.ArcGIS.ADF.Web.Geometry.Polygon();
        polygon.Rings.Add(new ESRI.ArcGIS.ADF.Web.Geometry.Ring(adfGeometry as ESRI.ArcGIS.ADF.Web.Geometry.Envelope));
        adfGeometry = polygon;
      }

            // Call method to draw the feature based on its geometry type
      if (adfGeometry is ESRI.ArcGIS.ADF.Web.Geometry.Polygon)
      {
        drawPolygon(graphics, adfGeometry as ESRI.ArcGIS.ADF.Web.Geometry.Polygon, height);
      }
      else if (adfGeometry is ESRI.ArcGIS.ADF.Web.Geometry.Polyline)
      {
        drawPolyline(graphics, adfGeometry as ESRI.ArcGIS.ADF.Web.Geometry.Polyline, height);
      }
    }

        // Renders the specified polyline on the specified GDI+ surface with the specified extrusion height
    private void drawPolyline(System.Drawing.Graphics graphics, 
            ESRI.ArcGIS.ADF.Web.Geometry.Polyline adfPolyline, double height)
    {
            // Loop through the polyline's paths, rendering each
      foreach (ESRI.ArcGIS.ADF.Web.Geometry.Path adfPath in adfPolyline.Paths)
      {
                // Loop through the path's points, rendering each line segment
        for (int i = 1; i < adfPath.Points.Count; i++)
        {
                    // Get the direction of the segment formed by the current point and the previous
          double direction = getLineDirection(adfPath.Points[i - 1], adfPath.Points[i]);

                    // Render the line segment formed by the current point and the previous
          drawLineSegment(graphics, adfPath.Points[i - 1], adfPath.Points[i], height, direction, true);
        }
      }
    }

    // Converts a point collection to a set of line segments connecting the points
    private void getLineSegments(ESRI.ArcGIS.ADF.Web.Geometry.PointCollection points, 
            ref System.Collections.Generic.List<LineSegment> segmentList)
    {
      for (int i = 1; i < points.Count; i++)
      {
                // Create a segment from the current and previous points and add them to the passed-in list
        LineSegment segment = new LineSegment();
        segment.Start = points[i - 1];
        segment.End = points[i];
        segment.Direction = getLineDirection(segment.Start, segment.End);
        segmentList.Add(segment);
      }
    }

        // Renders the specified polygon on the specified GDI+ surface with the specified extrusion height
    private void drawPolygon(System.Drawing.Graphics graphics, ESRI.ArcGIS.ADF.Web.Geometry.Polygon adfPolygon, double height)
    {
            // Loop through the polygon's rings, drawing each
      foreach (ESRI.ArcGIS.ADF.Web.Geometry.Ring adfRing in adfPolygon.Rings)
      {
        // Ensure orientation (important for calculating shading).  Note that because we are in a 
                // left-handed image coordinate system, outer ring orientation is actually counter-clockwise.
        adfRing.CorrectSegmentOrientation();

                // Get the line segments in the current ring
        System.Collections.Generic.List<LineSegment> segmentList = 
                    new System.Collections.Generic.List<LineSegment>(adfRing.Points.Count);        
        getLineSegments(adfRing.Points, ref segmentList);
                
                // Get the line segments in the ring's holes
        foreach (ESRI.ArcGIS.ADF.Web.Geometry.Hole adfHole in adfRing.Holes)
        {
          getLineSegments(adfHole.Points, ref segmentList);
        }

                // We implement a very simplified back-face culling mechanism. If the normal to the surface 
                // being rendered points upward, they must be behind other surfaces, so we draw them first.
                // Lines with lowest Y (top of screen) are drawn first.

        segmentList.Sort(); // Sort so top line segments are rendered first

        // First pass - Draw back surfaces
        if (this.Transparency > 0) // Ignore if not see-through
        {
          foreach (LineSegment segment in segmentList)
          {
            if (System.Math.Abs(segment.Direction) > HALF_PI)
            {
              drawLineSegment(graphics, segment.Start, segment.End, height, segment.Direction, false);
            }
          }
        }

        // Second pass - Draw front surfaces
        foreach (LineSegment segment in segmentList)
        {
          if (System.Math.Abs(segment.Direction) <= HALF_PI)
          {
            drawLineSegment(graphics, segment.Start, segment.End, height, segment.Direction, true);
          }
        }
      }

            // Draw the footprint
      Utility.FillPolygon(graphics, adfPolygon, this.FillColor, this.Transparency, 0, (int)height);
    }

    // Render the segment specified by the start and end points on the passed-in surface, extruded by the specified height
    private void drawLineSegment(System.Drawing.Graphics graphics, ESRI.ArcGIS.ADF.Web.Geometry.Point startPoint, 
      ESRI.ArcGIS.ADF.Web.Geometry.Point endPoint, double height, double direction, bool fill)
    {
      using (System.Drawing.Drawing2D.GraphicsPath graphicsPath = new System.Drawing.Drawing2D.GraphicsPath())
      {
                // Create a screen point array with the points comprising the polygon formed by extruding the segment
                System.Drawing.Point[] extrudedSegmentPoints = new System.Drawing.Point[] {
            new System.Drawing.Point(System.Convert.ToInt32(startPoint.X),System.Convert.ToInt32(startPoint.Y)),
            new System.Drawing.Point(System.Convert.ToInt32(endPoint.X),System.Convert.ToInt32(endPoint.Y)),
            new System.Drawing.Point(System.Convert.ToInt32(endPoint.X),System.Convert.ToInt32(endPoint.Y-height)),
            new System.Drawing.Point(System.Convert.ToInt32(startPoint.X),System.Convert.ToInt32(startPoint.Y-height)),
            new System.Drawing.Point(System.Convert.ToInt32(startPoint.X),System.Convert.ToInt32(startPoint.Y))
          };

                // Add the extrusion polygon to the graphics path
        graphicsPath.AddPolygon(extrudedSegmentPoints);

                // Fill the extrusion polygon if fill is true
        if (fill)
        {
                    // Calculate the brightness.  We add PI/2 to the passed-in direction because normal to the surface is 
                    // perpendicular to the line direction
          double brightness = calculateBrightness(direction + HALF_PI); 

                    // Calculate the color resulting from applying the brightness to the renderer's fill color
          System.Drawing.Color tempColor = adjustBrightness(this.FillColor, brightness);

                    // Calculate the ultimate fill color by applying the renderer's transparency
          System.Drawing.Color fillColor = 
                        System.Drawing.Color.FromArgb(Utility.TransparencyToAlpha(this.Transparency), tempColor);

                    // Draw the fill
          using (System.Drawing.SolidBrush solidBrush = new System.Drawing.SolidBrush(fillColor))
          {
            graphics.FillPath(solidBrush, graphicsPath);
          }
        }

                // Draw an outline on the extrusion polygon, if one is specified
        if (this.OutlineColor != System.Drawing.Color.Transparent && this.OutlineColor != System.Drawing.Color.Empty)
        {
          using (System.Drawing.Pen pen = new System.Drawing.Pen(this.OutlineColor, 0.5f))
          {
            graphics.DrawPath(pen, graphicsPath);
          }
        }
      }
    }

        // Encapsulates a line segement composed of two Web ADF points
        private struct LineSegment : System.IComparable<LineSegment>
        {
            public ESRI.ArcGIS.ADF.Web.Geometry.Point Start;
            public ESRI.ArcGIS.ADF.Web.Geometry.Point End;
            public double Direction;


            #region IComparable<LineSegment> Members

            /// <summary>
            /// Compares the sourthern-most points of the current and passed-in line segment.
            /// </summary>
            /// <param name="other"></param>
            /// <returns>An integer indicating the relationship between the segments' southern-most point, as follows:
            /// Return value less than zero - the calling segment's southern-most point is more southerly
            /// Return value equal to zero - the segments' southern-most points are equally southerly
            /// Return value greater than zero - the passed-in segment's southern-most point is more southerly</returns>
            public int CompareTo(LineSegment other)
            {
                return System.Math.Min(this.End.Y, this.Start.Y).CompareTo(System.Math.Min(other.End.Y, other.Start.Y));
            }

            #endregion
        }

    #endregion

    #region Helper Methods - adjustBrightness, calculateBrightness, getLineDirection

        // Applies the specified brightness factor to the specified color
    private System.Drawing.Color adjustBrightness(System.Drawing.Color color, double brightnessFactor)
    {
      int red = color.R;
      int green = color.G;
      int blue = color.B;

      brightnessFactor *= (1 - this.Ambience);
      
            red = System.Convert.ToInt32(red * (1 - brightnessFactor));
      green = System.Convert.ToInt32(green * (1 - brightnessFactor));
      blue = System.Convert.ToInt32(blue * (1 - brightnessFactor));
      
            if (red > 255) 
                red = 255;
      else if (red < 0) 
                red = 0;

            if (green > 255) 
                green = 255;
      else if (green < 0) 
                green = 0;

            if (blue > 255) 
                blue = 255;
      else if (blue < 0) 
                blue = 0;
      
            return System.Drawing.Color.FromArgb(color.A, red, green, blue);
    }

    private const double HALF_PI = System.Math.PI * 0.5;
    // Calculates brightness factor based on normals angle towards the light direction.  Returns 0 when 
        // perpendicular, 1 when opposite, and -1 when same direction.
    private double calculateBrightness(double angle)
    {
      double diff = (this.LightDirection + HALF_PI - angle);
      return System.Math.Abs(System.Math.Sin(-diff / 2));
    }

        // Calculates the direction of the line segment represented by the passed-in points
    private static double getLineDirection(ESRI.ArcGIS.ADF.Web.Geometry.Point startPoint, 
            ESRI.ArcGIS.ADF.Web.Geometry.Point endPoint)
    {
      double dx = endPoint.X - startPoint.X;
      double dy = endPoint.Y - startPoint.Y;
      return System.Math.Atan2(dy, dx);
    }

    #endregion
  }
}