Common_CustomRenderers_CSharp\App_Code\SimpleRenderer3D.cs
// Copyright 2011 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 } }