// 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 DelftTools.Utils.Aop; using GeoAPI.Extensions.Feature; using NetTopologySuite.Extensions.Features; using SharpMap.Api; using SharpMap.Styles; namespace SharpMap.Rendering.Thematics { /// /// The GradientTheme class defines a gradient color thematic rendering of features based by a numeric attribute. /// [Entity(FireOnCollectionChange=false)] public class GradientTheme : Theme { private double maxValue; private double minValue; private GradientThemeItem maxItem; private GradientThemeItem minItem; private Color minColor; private Color maxColor; private ColorBlend fillColorBlend; private ColorBlend lineColorBlend; private ColorBlend textColorBlend; private int numberOfClasses;//stored to update the items when mix/max changes /// /// Initializes a new instance of the GradientTheme class /// /// /// The gradient theme interpolates linearly between two styles based on a numerical attribute in the datasource. /// This is useful for scaling symbols, line widths, line and fill colors from numerical attributes. /// Colors are interpolated between two colors, but if you want to interpolate through more colors (fx. a rainbow), /// set the , and properties /// to a custom . /// /// The following properties are scaled (properties not mentioned here are not interpolated): /// /// PropertyRemarks /// Red, Green, Blue and Alpha values are linearly interpolated. /// The color, width, color of pens are interpolated. MiterLimit,StartCap,EndCap,LineJoin,DashStyle,DashPattern,DashOffset,DashCap,CompoundArray, and Alignment are switched in the middle of the min/max values. /// SolidBrush color are interpolated. Other brushes are not supported. /// MaxVisible, MinVisible, Line, Outline, Fill and SymbolScale are scaled linearly. Symbol, EnableOutline and Enabled switch in the middle of the min/max values. /// FontSize, BackColor, ForeColor, MaxVisible, MinVisible, Offset are scaled linearly. All other properties use min-style. /// /// /// /// Creating a rainbow colorblend showing colors from red, through yellow, green and blue depicting /// the population density of a country. /// /// //Create two vector styles to interpolate between /// SharpMap.Styles.VectorStyle min = new SharpMap.Styles.VectorStyle(); /// SharpMap.Styles.VectorStyle max = new SharpMap.Styles.VectorStyle(); /// min.Outline.Width = 1f; //Outline width of the minimum value /// max.Outline.Width = 3f; //Outline width of the maximum value /// //Create a theme interpolating population density between 0 and 400 /// SharpMap.Rendering.Thematics.GradientTheme popdens = new SharpMap.Rendering.Thematics.GradientTheme("PopDens", 0, 400, min, max); /// //Set the fill-style colors to be a rainbow blend from red to blue. /// popdens.FillColorBlend = SharpMap.Rendering.Thematics.ColorBlend.Rainbow5; /// myVectorLayer.Theme = popdens; /// /// /// /// Name of column to extract the attribute /// Minimum value /// Maximum value /// Color for minimum value /// Color for maximum value /// /// /// /// private GradientTheme() { // used for cloning to prevent overhead of "CreateThemeItems" method } public GradientTheme(string attributeName, double minValue, double maxValue, IStyle minStyle, IStyle maxStyle, ColorBlend fillColorBlend, ColorBlend lineColorBlend, ColorBlend textColorBlend) : this(attributeName, minValue, maxValue, minStyle, maxStyle, fillColorBlend, lineColorBlend, textColorBlend, 8) { } public GradientTheme(string attributeName, double minValue, double maxValue, IStyle minStyle, IStyle maxStyle, ColorBlend fillColorBlend, ColorBlend lineColorBlend, ColorBlend textColorBlend, int numberOfClasses) { this.numberOfClasses = numberOfClasses;//store for updates later on.. this.minValue = minValue; this.maxValue = maxValue; this.fillColorBlend = fillColorBlend; this.lineColorBlend = lineColorBlend; this.textColorBlend = textColorBlend; AttributeName = attributeName; //create themeitems only for the extremes. Other values are interpolated. CreateThemeItems(minStyle, maxStyle, numberOfClasses); minColor = ThemeHelper.ExtractFillColorFromThemeItem(minItem); minItem.Symbol = ((VectorStyle) minStyle).LegendSymbol; maxColor = ThemeHelper.ExtractFillColorFromThemeItem(maxItem); maxItem.Symbol = ((VectorStyle) maxStyle).LegendSymbol; } private void CreateThemeItems(IStyle minStyle, IStyle maxStyle, int numberOfThemeItems) { minItem = new GradientThemeItem(minStyle, string.Format("{0:g4}", minValue),string.Format("{0:g4}", minValue)); themeItems.Add(minItem); maxItem = new GradientThemeItem(maxStyle, string.Format("{0:g4}", maxValue), string.Format("{0:g4}", maxValue)); if (maxValue != minValue) //don't generate in between items if min == max { double step = (maxValue - minValue)/(numberOfThemeItems - 1);//for 3 themeItems step should be halfway the data for (int i = 1; i <= numberOfThemeItems - 2; i++) { double value = minValue + i * step; IStyle style = GetStyle(value); var gradientThemeItem = new GradientThemeItem(style, string.Format("{0:g4}", value), string.Format("{0:g4}", value)); themeItems.Add(gradientThemeItem); } } themeItems.Add(maxItem); } private void UpdateThemeItems() { themeItems.Clear(); CreateThemeItems(MinStyle,MaxStyle,numberOfClasses); vectorStyleCache.Clear(); } /// /// Gets or sets the minimum value of the gradient /// public double Min { get { return minValue; } private set { minValue = value; minItem.Label = minValue.ToString(); } } /// /// Gets or sets the maximum value of the gradient /// public double Max { get { return maxValue; } private set { maxValue = value; maxItem.Label = maxValue.ToString(); } } /// /// Gets or sets the style for the minimum value /// public IStyle MinStyle { get { return minItem.Style; } //minStyle; } set { minItem.Style = value; } } /// /// Gets or sets the style for the maximum value /// public IStyle MaxStyle { get { return maxItem.Style; } //maxStyle; } set { maxItem.Style = value; } } /// /// Gets or sets the used on labels /// public ColorBlend TextColorBlend { get { return textColorBlend; } set { textColorBlend = value; } } /// /// Gets or sets the used on lines /// public ColorBlend LineColorBlend { get { return lineColorBlend; } set { lineColorBlend = value; } } /// /// Gets or sets the used as Fill /// public ColorBlend FillColorBlend { get { return fillColorBlend; } set { fillColorBlend = value; } } public int NumberOfClasses { get { return numberOfClasses; } set { numberOfClasses = value; UpdateThemeItems(); } } public bool UseCustomRange { get; set; } /// /// Returns the style based on a numeric DataColumn, where style /// properties are linearly interpolated between max and min values. /// /// Feature /// Style calculated by a linear interpolation between the min/max styles public override IStyle GetStyle(IFeature feature) { double attr; try { attr = FeatureAttributeAccessorHelper.GetAttributeValue(feature, AttributeName); } catch { throw new ApplicationException( "Invalid Attribute type in Gradient Theme - Couldn't parse attribute (must be numerical)"); } if (MinStyle.GetType() != MaxStyle.GetType()) throw new ArgumentException("MinStyle and MaxStyle must be of the same type"); switch (MinStyle.GetType().FullName) { case "SharpMap.Styles.VectorStyle": return CalculateVectorStyle(attr); case "SharpMap.Styles.LabelStyle": return CalculateLabelStyle(MinStyle as LabelStyle, MaxStyle as LabelStyle, attr); default: throw new ArgumentException( "Only SharpMap.Styles.VectorStyle and SharpMap.Styles.LabelStyle are supported for the gradient theme"); } } public override IStyle GetStyle(T value) { // Assumes this value is a double, float or int and gets the vector style for this numeric value if (!(value is double || value is float || value is int)) { throw new NotSupportedException( "Gradient theme only supports numeric value types (double, float or int)."); } if (value is double && double.IsNaN(Convert.ToDouble(value))) { var transparentBrush = new SolidBrush(Color.Transparent); return new VectorStyle { Fill = transparentBrush, Line = new Pen(transparentBrush) }; } return CalculateVectorStyle(Convert.ToDouble(value)); } public IStyle GetStyle(double attributeValue) { return CalculateVectorStyle(attributeValue); } private Dictionary vectorStyleCache = new Dictionary(); /// /// Calculates the style for the gradient Theme. Use the constructor when all values are known because /// it will also update the symbol (bitmap). /// /// /// protected VectorStyle CalculateVectorStyle(double value) { var min = (VectorStyle) MinStyle; var max = (VectorStyle) MaxStyle; bool isNoDataValue = noDataValues != null && noDataValues.Contains(value); double dFrac = Fraction(value); //There are some theoretical issues with this caching if the number of color transitions is high //(and non-smooth). However, for all intents and purposes this approach will be visually equal //to non-cached styling. const int numberOfCachedStyles = 512; int cacheIndex = isNoDataValue ? -1 : (int)(dFrac*numberOfCachedStyles); VectorStyle cachedResult; if (vectorStyleCache.TryGetValue(cacheIndex, out cachedResult)) return cachedResult; float fFrac = Convert.ToSingle(dFrac); //bool enabled = (dFrac > 0.5 ? min.Enabled : max.Enabled); bool enableOutline = (dFrac > 0.5 ? min.EnableOutline : max.EnableOutline); Brush fillStyle = null; if (isNoDataValue) fillStyle = new SolidBrush(NoDataColor); else if (fillColorBlend != null) fillStyle = new SolidBrush(fillColorBlend.GetColor(fFrac)); else if (min.Fill != null && max.Fill != null) fillStyle = InterpolateBrush(min.Fill, max.Fill, value); Pen lineStyle; if (isNoDataValue) lineStyle = new Pen(NoDataColor, min.Line.Width); else if (lineColorBlend != null) lineStyle = new Pen(lineColorBlend.GetColor(fFrac), InterpolateFloat(min.Line.Width, max.Line.Width, value)); else lineStyle = InterpolatePen(min.Line, max.Line, value); // assume line and outline same for gradient theme Pen outLineStyle = null; if (min.Outline != null && max.Outline != null) outLineStyle = InterpolatePen(min.Outline, max.Outline, value); ShapeType shapeType = min.Shape; float symbolScale = InterpolateFloat(min.SymbolScale, max.SymbolScale, value); Type geometryType = min.GeometryType; int shapeSize = InterpolateInt(min.ShapeSize, max.ShapeSize, value); var style = new VectorStyle(fillStyle, outLineStyle, enableOutline, lineStyle, symbolScale, geometryType, shapeType, shapeSize) { MinVisible = InterpolateDouble(min.MinVisible, max.MinVisible, value), MaxVisible = InterpolateDouble(min.MaxVisible, max.MaxVisible, value), Enabled = (dFrac > 0.5 ? min.Enabled : max.Enabled), Line = { StartCap = min.Line.StartCap, EndCap = min.Line.EndCap } }; vectorStyleCache[cacheIndex] = style; return style; } protected LabelStyle CalculateLabelStyle(LabelStyle min, LabelStyle max, double value) { var style = new LabelStyle(); style.CollisionDetection = min.CollisionDetection; style.Enabled = InterpolateBool(min.Enabled, max.Enabled, value); float FontSize = InterpolateFloat(min.Font.Size, max.Font.Size, value); style.Font = new Font(min.Font.FontFamily, FontSize, min.Font.Style); if (min.BackColor != null && max.BackColor != null) { style.BackColor = InterpolateBrush(min.BackColor, max.BackColor, value); } if (textColorBlend != null) { style.ForeColor = lineColorBlend.GetColor(Convert.ToSingle(Fraction(value))); } else { style.ForeColor = InterpolateColor(min.ForeColor, max.ForeColor, value); } if (min.Halo != null && max.Halo != null) { style.Halo = InterpolatePen(min.Halo, max.Halo, value); } style.MinVisible = InterpolateDouble(min.MinVisible, max.MinVisible, value); style.MaxVisible = InterpolateDouble(min.MaxVisible, max.MaxVisible, value); style.Offset = new PointF(InterpolateFloat(min.Offset.X, max.Offset.X, value), InterpolateFloat(min.Offset.Y, max.Offset.Y, value)); return style; } private double Fraction(double attr) { var infinitedDelta = double.IsInfinity(Math.Abs(maxValue - minValue)); const int rangeCorrection = 2; var fractionValue = (!infinitedDelta) ? attr : attr/rangeCorrection; var minValueToUse = (!infinitedDelta) ? minValue : minValue/rangeCorrection; var maxValueToUse = (!infinitedDelta) ? maxValue : maxValue/rangeCorrection; if (fractionValue < minValueToUse) return 0; if (fractionValue > maxValueToUse) return 1; double delta = Math.Abs(maxValueToUse - minValueToUse); if (delta < 1e-8) return 0; return (fractionValue - minValueToUse) / (delta); } private bool InterpolateBool(bool min, bool max, double attr) { double frac = Fraction(attr); return frac > 0.5 ? max : min; } private float InterpolateFloat(float min, float max, double attr) { return Convert.ToSingle((max - min)*Fraction(attr) + min); } private double InterpolateDouble(double min, double max, double attr) { return (max - min) * Fraction(attr) + min; } private int InterpolateInt(int min, int max, double attr) { return (int) ((max - min) * Fraction(attr) + min); } private SolidBrush InterpolateBrush(Brush min, Brush max, double attr) { if (min.GetType() != typeof (SolidBrush) || max.GetType() != typeof (SolidBrush)) throw (new ArgumentException("Only SolidBrush brushes are supported in GradientTheme")); return new SolidBrush(InterpolateColor((min as SolidBrush).Color, (max as SolidBrush).Color, attr)); } private Pen InterpolatePen(Pen min, Pen max, double attr) { if (min.PenType != PenType.SolidColor || max.PenType != PenType.SolidColor) throw (new ArgumentException("Only SolidColor pens are supported in GradientTheme")); Pen pen = new Pen(InterpolateColor(min.Color, max.Color, attr), InterpolateFloat(min.Width, max.Width, attr)); double frac = Fraction(attr); pen.MiterLimit = InterpolateFloat(min.MiterLimit, max.MiterLimit, attr); pen.StartCap = (frac > 0.5 ? max.StartCap : min.StartCap); pen.EndCap = (frac > 0.5 ? max.EndCap : min.EndCap); pen.LineJoin = (frac > 0.5 ? max.LineJoin : min.LineJoin); pen.DashStyle = (frac > 0.5 ? max.DashStyle : min.DashStyle); if (min.DashStyle == DashStyle.Custom && max.DashStyle == DashStyle.Custom) pen.DashPattern = (frac > 0.5 ? max.DashPattern : min.DashPattern); pen.DashOffset = (frac > 0.5 ? max.DashOffset : min.DashOffset); pen.DashCap = (frac > 0.5 ? max.DashCap : min.DashCap); if (min.CompoundArray.Length > 0 && max.CompoundArray.Length > 0) pen.CompoundArray = (frac > 0.5 ? max.CompoundArray : min.CompoundArray); pen.Alignment = (frac > 0.5 ? max.Alignment : min.Alignment); //pen.CustomStartCap = (frac > 0.5 ? max.CustomStartCap : min.CustomStartCap); //Throws ArgumentException //pen.CustomEndCap = (frac > 0.5 ? max.CustomEndCap : min.CustomEndCap); //Throws ArgumentException return pen; } private Color InterpolateColor(Color minCol, Color maxCol, double attr) { double frac = Fraction(attr); if (frac == 1) { return maxCol; } if ((frac == 0) || (double.IsNaN(frac))) { return minCol; } double r = (maxCol.R - minCol.R)*frac + minCol.R; double g = (maxCol.G - minCol.G)*frac + minCol.G; double b = (maxCol.B - minCol.B)*frac + minCol.B; double a = (maxCol.A - minCol.A)*frac + minCol.A; if (r > 255) r = 255; if (g > 255) g = 255; if (b > 255) b = 255; if (a > 255) a = 255; if (a < 0) a = 0; return Color.FromArgb((int) a, (int) r, (int) g, (int) b); } public override object Clone() { var gradientTheme = new GradientTheme { AttributeName = AttributeName, minValue = minValue, maxValue = maxValue, fillColorBlend = (null != FillColorBlend) ? (ColorBlend) FillColorBlend.Clone() : null, lineColorBlend = (null != LineColorBlend) ? (ColorBlend) LineColorBlend.Clone() : null, textColorBlend = (null != TextColorBlend) ? (ColorBlend) TextColorBlend.Clone() : null, numberOfClasses = numberOfClasses, minColor = minColor, maxColor = maxColor, UseCustomRange = UseCustomRange }; gradientTheme.themeItems.AddRange(ThemeItems.Select(ti => (IThemeItem)((GradientThemeItem)ti).Clone())); gradientTheme.minItem = (GradientThemeItem) gradientTheme.themeItems.First(); gradientTheme.maxItem = (GradientThemeItem) gradientTheme.themeItems.Last(); if (NoDataValues != null) { gradientTheme.noDataValues = NoDataValues.Cast().ToArray(); } return gradientTheme; } public override void ScaleTo(double min, double max) { if (UseCustomRange && min <= minValue && max >= maxValue) return; UseCustomRange = false; ScaleToCore(min, max); } public void SetMinMax(double min, double max) { ScaleToCore(min, max); } private void ScaleToCore(double min, double max) { Min = min; Max = max; UpdateThemeItems(); } public override Color GetFillColor(T value) { // hack: get value using field because field getter is less performand due to aop aspect. if (noDataValues != null && noDataValues.Contains(value)) { return NoDataColor; } if (fillColorBlend != null) { double fraction = Fraction(Convert.ToDouble(value)); return fillColorBlend.GetColor((float) fraction); } return InterpolateColor(minColor, maxColor, Convert.ToDouble(value)); } /// /// Fills array of colors based on current configuration of theme. /// This function is optimized for performance. Keep large loop as simple as possible. /// note method is unsafe to allow direct manipulation of colors in bitmap /// /// /// pointer to colors in bitmap /// the number of colors /// array with values to convert to colors. Length should equal lenght public override unsafe void GetFillColors(int* colors, int length, T[] values) { //update color due to changes in the min or max theme. minColor = ThemeHelper.ExtractFillColorFromThemeItem(minItem); maxColor = ThemeHelper.ExtractFillColorFromThemeItem(maxItem); if (length != values.Length) { throw new ArgumentException("GetFillColors: length of targer array should match number of source values", "length"); } for (int i = 0; i < length; i++) { if (noDataValues != null && noDataValues.Contains(values[i])) { colors[i] = NoDataColor.ToArgb(); continue; } if (fillColorBlend != null) { double fraction = Fraction(Convert.ToDouble(values[i])); if (double.IsNaN(fraction)) { fraction = 0.0; } colors[i] = fillColorBlend.GetColor((float) fraction).ToArgb(); } else { colors[i] = InterpolateColor(minColor, maxColor, Convert.ToDouble(values[i])).ToArgb(); } } } } }