Common Custom renderers
Common_CustomRenderers_VBNet\App_Code\SimpleRenderer3D.vb
' 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.
' 

Imports Microsoft.VisualBasic
Imports System
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
    Inherits ESRI.ADF.Samples.Renderers.RendererBase
    #Region "Constructors"

    Public Sub New()
    End Sub

    #End Region

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

    Private fillColor_Renamed As System.Drawing.Color = System.Drawing.Color.White

    ''' <summary>
    ''' Fill color on each surface
    ''' </summary>
    <System.ComponentModel.Description("Fill color on each surface.")> _
    Public Property FillColor() As System.Drawing.Color
      Get
        Return fillColor_Renamed
      End Get
      Set
        fillColor_Renamed = Value
      End Set
    End Property

    Private outlineColor_Renamed As System.Drawing.Color = 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 Property OutlineColor() As System.Drawing.Color
      Get
        Return outlineColor_Renamed
      End Get
      Set
        outlineColor_Renamed = Value
      End Set
    End Property

    Private transparency_Renamed As Integer = 25
    ''' <summary>
    ''' Transparency of the fill
    ''' </summary>
    <System.ComponentModel.DefaultValue(25), System.ComponentModel.Description("Transparency of the fill")> _
    Public Property Transparency() As Integer
      Get
        Return transparency_Renamed
      End Get
      Set
        transparency_Renamed = Value
      End Set
    End Property

    Private ambience_Renamed As Double = 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 Property Ambience() As Double
      Get
        Return ambience_Renamed
      End Get
      Set
        If ambience_Renamed < 0 OrElse ambience_Renamed > 1 Then
          Throw New System.ArgumentOutOfRangeException("Ambience", "Ambience must be between 0 and 1")
        End If
        ambience_Renamed = Value
      End Set
    End Property

    Private heightColumnName_Renamed As String
    ''' <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 Property HeightColumnName() As String
      Get
        Return heightColumnName_Renamed
      End Get
      Set
        heightColumnName_Renamed = Value
      End Set
    End Property

        Public maxHeight_Renamed As Double = 50
        ''' <summary>
        ''' Maximum feature extrusion height (in pixels)
        ''' </summary>
        <System.ComponentModel.DefaultValue(50), System.ComponentModel.Description("Maximum feature extrusion height (in pixels)")> _
        Public Property MaxHeight() As Double
            Get
                Return MaxHeight
            End Get
            Set(ByVal value As Double)
                maxHeight_Renamed = value
            End Set
        End Property

        Private scaleHeight_Renamed As Double = 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 Property ScaleHeight() As Double
            Get
                Return scaleHeight_Renamed
            End Get
            Set(ByVal value As Double)
                scaleHeight_Renamed = Value
            End Set
        End Property

        Private lightDirection_Renamed As Double = 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 Property LightDirection() As Double
            Get
                Return lightDirection_Renamed / System.Math.PI * 180
            End Get
            Set(ByVal value As Double)
                lightDirection_Renamed = Value / 180 * System.Math.PI
            End Set
        End Property

#End Region

#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 Overrides Sub GetAllSymbols(ByVal symbols As System.Collections.Generic.List(Of ESRI.ArcGIS.ADF.Web.Display.Symbol.FeatureSymbol))
            ' Create a fill symbol and apply the renderer's fill color, outline color, and transparency
            Dim simpleFillSymbol As ESRI.ArcGIS.ADF.Web.Display.Symbol.SimpleFillSymbol = New ESRI.ArcGIS.ADF.Web.Display.Symbol.SimpleFillSymbol(Me.FillColor, Me.OutlineColor, ESRI.ArcGIS.ADF.Web.Display.Symbol.PolygonFillType.Solid)
            simpleFillSymbol.Transparency = Me.Transparency

            ' Add the fill symbol to the symbols used by the renderer
            symbols.Add(simpleFillSymbol)
        End Sub

        ''' <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 Overrides Sub Render(ByVal row As System.Data.DataRow, ByVal graphics As System.Drawing.Graphics, ByVal geometryColumn As System.Data.DataColumn)
            ' Validate method input
            If row Is Nothing OrElse graphics Is Nothing OrElse geometryColumn Is Nothing Then
                Return
            End If

            ' Validate input geometry.  The renderer does not support points
            Dim geometry As ESRI.ArcGIS.ADF.Web.Geometry.Geometry = TryCast(row(geometryColumn), ESRI.ArcGIS.ADF.Web.Geometry.Geometry)
            If geometry Is Nothing OrElse TypeOf geometry Is ESRI.ArcGIS.ADF.Web.Geometry.Point Then
                Return
            End If

            ' Initialize the extrusion height with the renderer's specified maximum height
            Dim extrusionHeight As Double = Me.maxHeight_Renamed

            ' Get the extrusion height column attribute, if possible
            If (Not String.IsNullOrEmpty(HeightColumnName)) AndAlso row.Table.Columns.Contains(HeightColumnName) Then
                Double.TryParse(row(HeightColumnName).ToString(), extrusionHeight)
            End If

            extrusionHeight *= Me.ScaleHeight ' Apply the extrusion height scale factor

            ' If the scaled height is beyond the maximum height, clip it to the maximum
            If extrusionHeight > maxHeight_Renamed Then
                extrusionHeight = Me.maxHeight_Renamed
            End If

            ' Draw the feature
            Me.drawGraphic(geometry, graphics, extrusionHeight)
        End Sub

#End Region

    #Region "Rendering - drawGraphics, drawPolyline, getLineSegements, drawPolygon, drawLineSegment, LineSegment"

    ' Renders the specified geometry on the specified GDI+ surface with the specified extrusion height
    Private Sub drawGraphic(ByVal adfGeometry As ESRI.ArcGIS.ADF.Web.Geometry.Geometry, ByVal graphics As System.Drawing.Graphics, ByVal height As Double)
      ' Check whether the passed-in geometry is an envelope and, if so, convert it to a polygon
      If TypeOf adfGeometry Is ESRI.ArcGIS.ADF.Web.Geometry.Envelope Then
        Dim polygon As ESRI.ArcGIS.ADF.Web.Geometry.Polygon = New ESRI.ArcGIS.ADF.Web.Geometry.Polygon()
        polygon.Rings.Add(New ESRI.ArcGIS.ADF.Web.Geometry.Ring(TryCast(adfGeometry, ESRI.ArcGIS.ADF.Web.Geometry.Envelope)))
        adfGeometry = polygon
      End If

      ' Call method to draw the feature based on its geometry type
      If TypeOf adfGeometry Is ESRI.ArcGIS.ADF.Web.Geometry.Polygon Then
        drawPolygon(graphics, TryCast(adfGeometry, ESRI.ArcGIS.ADF.Web.Geometry.Polygon), height)
      ElseIf TypeOf adfGeometry Is ESRI.ArcGIS.ADF.Web.Geometry.Polyline Then
        drawPolyline(graphics, TryCast(adfGeometry, ESRI.ArcGIS.ADF.Web.Geometry.Polyline), height)
      End If
    End Sub

    ' Renders the specified polyline on the specified GDI+ surface with the specified extrusion height
    Private Sub drawPolyline(ByVal graphics As System.Drawing.Graphics, ByVal adfPolyline As ESRI.ArcGIS.ADF.Web.Geometry.Polyline, ByVal height As Double)
      ' Loop through the polyline's paths, rendering each
      For Each adfPath As ESRI.ArcGIS.ADF.Web.Geometry.Path In adfPolyline.Paths
        ' Loop through the path's points, rendering each line segment
        Dim i As Integer = 1
        Do While i < adfPath.Points.Count
          ' Get the direction of the segment formed by the current point and the previous
          Dim direction As Double = 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)
          i += 1
        Loop
      Next adfPath
    End Sub

    ' Converts a point collection to a set of line segments connecting the points
    Private Sub getLineSegments(ByVal points As ESRI.ArcGIS.ADF.Web.Geometry.PointCollection, ByRef segmentList As System.Collections.Generic.List(Of LineSegment))
      Dim i As Integer = 1
      Do While i < points.Count
        ' Create a segment from the current and previous points and add them to the passed-in list
        Dim segment As LineSegment = New LineSegment()
        segment.Start = points(i - 1)
        segment.End = points(i)
        segment.Direction = getLineDirection(segment.Start, segment.End)
        segmentList.Add(segment)
        i += 1
      Loop
    End Sub

    ' Renders the specified polygon on the specified GDI+ surface with the specified extrusion height
    Private Sub drawPolygon(ByVal graphics As System.Drawing.Graphics, ByVal adfPolygon As ESRI.ArcGIS.ADF.Web.Geometry.Polygon, ByVal height As Double)
      ' Loop through the polygon's rings, drawing each
      For Each adfRing As ESRI.ArcGIS.ADF.Web.Geometry.Ring 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
        Dim segmentList As System.Collections.Generic.List(Of LineSegment) = New System.Collections.Generic.List(Of LineSegment)(adfRing.Points.Count)
        getLineSegments(adfRing.Points, segmentList)

        ' Get the line segments in the ring's holes
        For Each adfHole As ESRI.ArcGIS.ADF.Web.Geometry.Hole In adfRing.Holes
          getLineSegments(adfHole.Points, segmentList)
        Next adfHole

        ' 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 Me.Transparency > 0 Then ' Ignore if not see-through
          For Each segment As LineSegment In segmentList
            If System.Math.Abs(segment.Direction) > HALF_PI Then
              drawLineSegment(graphics, segment.Start, segment.End, height, segment.Direction, False)
            End If
          Next segment
        End If

        ' Second pass - Draw front surfaces
        For Each segment As LineSegment In segmentList
          If System.Math.Abs(segment.Direction) <= HALF_PI Then
            drawLineSegment(graphics, segment.Start, segment.End, height, segment.Direction, True)
          End If
        Next segment
      Next adfRing

      ' Draw the footprint
      Utility.FillPolygon(graphics, adfPolygon, Me.FillColor, Me.Transparency, 0, CInt(Fix(height)))
    End Sub

    ' Render the segment specified by the start and end points on the passed-in surface, extruded by the specified height
    Private Sub drawLineSegment(ByVal graphics As System.Drawing.Graphics, ByVal startPoint As ESRI.ArcGIS.ADF.Web.Geometry.Point, ByVal endPoint As ESRI.ArcGIS.ADF.Web.Geometry.Point, ByVal height As Double, ByVal direction As Double, ByVal fill As Boolean)
      Using graphicsPath As System.Drawing.Drawing2D.GraphicsPath = New System.Drawing.Drawing2D.GraphicsPath()
        ' Create a screen point array with the points comprising the polygon formed by extruding the segment
        Dim extrudedSegmentPoints As System.Drawing.Point() = 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 Then
          ' Calculate the brightness.  We add PI/2 to the passed-in direction because normal to the surface is 
          ' perpendicular to the line direction
          Dim brightness As Double = calculateBrightness(direction + HALF_PI)

          ' Calculate the color resulting from applying the brightness to the renderer's fill color
          Dim tempColor As System.Drawing.Color = adjustBrightness(Me.FillColor, brightness)

          ' Calculate the ultimate fill color by applying the renderer's transparency
          Dim fillColor_Renamed As System.Drawing.Color = System.Drawing.Color.FromArgb(Utility.TransparencyToAlpha(Me.Transparency), tempColor)

          ' Draw the fill
          Using solidBrush As System.Drawing.SolidBrush = New System.Drawing.SolidBrush(fillColor_Renamed)
            graphics.FillPath(solidBrush, graphicsPath)
          End Using
        End If

        ' Draw an outline on the extrusion polygon, if one is specified
        If Me.OutlineColor <> System.Drawing.Color.Transparent AndAlso Me.OutlineColor <> System.Drawing.Color.Empty Then
          Using pen As System.Drawing.Pen = New System.Drawing.Pen(Me.OutlineColor, 0.5f)
            graphics.DrawPath(pen, graphicsPath)
          End Using
        End If
      End Using
    End Sub

    ' Encapsulates a line segement composed of two Web ADF points
    Private Structure LineSegment
      Implements System.IComparable(Of LineSegment)
      Public Start As ESRI.ArcGIS.ADF.Web.Geometry.Point
      Public [End] As ESRI.ArcGIS.ADF.Web.Geometry.Point
      Public Direction As Double


      #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 Function CompareTo(ByVal other As LineSegment) As Integer Implements System.IComparable(Of ESRI.ADF.Samples.Renderers.SimpleRenderer3D.LineSegment).CompareTo
                Return System.Math.Min(Me.End.Y, Me.Start.Y).CompareTo(System.Math.Min(other.End.Y, other.Start.Y))
            End Function

      #End Region
    End Structure

    #End Region

    #Region "Helper Methods - adjustBrightness, calculateBrightness, getLineDirection"

    ' Applies the specified brightness factor to the specified color
    Private Function adjustBrightness(ByVal color As System.Drawing.Color, ByVal brightnessFactor As Double) As System.Drawing.Color
      Dim red As Integer = color.R
      Dim green As Integer = color.G
      Dim blue As Integer = color.B

      brightnessFactor *= (1 - Me.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 Then
        red = 255
      ElseIf red < 0 Then
        red = 0
      End If

      If green > 255 Then
        green = 255
      ElseIf green < 0 Then
        green = 0
      End If

      If blue > 255 Then
        blue = 255
      ElseIf blue < 0 Then
        blue = 0
      End If

      Return System.Drawing.Color.FromArgb(color.A, red, green, blue)
    End Function

    Private Const HALF_PI As Double = 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 Function calculateBrightness(ByVal angle As Double) As Double
      Dim diff As Double = (Me.LightDirection + HALF_PI - angle)
      Return System.Math.Abs(System.Math.Sin(-diff / 2))
    End Function

    ' Calculates the direction of the line segment represented by the passed-in points
    Private Shared Function getLineDirection(ByVal startPoint As ESRI.ArcGIS.ADF.Web.Geometry.Point, ByVal endPoint As ESRI.ArcGIS.ADF.Web.Geometry.Point) As Double
      Dim dx As Double = endPoint.X - startPoint.X
      Dim dy As Double = endPoint.Y - startPoint.Y
      Return System.Math.Atan2(dy, dx)
    End Function

    #End Region
  End Class
End Namespace