//The MIT License(MIT) //Copyright(c) 2016 Alberto Rodriguez & LiveCharts Contributors //Permission is hereby granted, free of charge, to any person obtaining a copy //of this software and associated documentation files (the "Software"), to deal //in the Software without restriction, including without limitation the rights //to use, copy, modify, merge, publish, distribute, sublicense, and/or sell //copies of the Software, and to permit persons to whom the Software is //furnished to do so, subject to the following conditions: //The above copyright notice and this permission notice shall be included in all //copies or substantial portions of the Software. //THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR //IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, //FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE //AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER //LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, //OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE //SOFTWARE. using System; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Media.Effects; using System.Windows.Shapes; using LiveCharts.Maps; using LiveCharts.Wpf.Maps; using Path = System.Windows.Shapes.Path; namespace LiveCharts.Wpf { /// /// /// /// public class GeoMap : UserControl { #region Constructors /// /// Initializes a new instance of the class. /// public GeoMap() { Canvas = new Canvas(); Map = new Canvas(); Canvas.Children.Add(Map); Content = Canvas; Canvas.SetBinding(WidthProperty, new Binding { Path = new PropertyPath(ActualWidthProperty), Source = this }); Canvas.SetBinding(HeightProperty, new Binding { Path = new PropertyPath(ActualHeightProperty), Source = this }); Lands = new Dictionary(); SetCurrentValue(DefaultLandFillProperty, new SolidColorBrush(Color.FromArgb(200,255,255,255))); SetCurrentValue(LandStrokeProperty, new SolidColorBrush(Color.FromArgb(30, 55,55, 55))); SetCurrentValue(LandStrokeThicknessProperty, 1.3d); SetCurrentValue(AnimationsSpeedProperty, TimeSpan.FromMilliseconds(500)); SetCurrentValue(BackgroundProperty, new SolidColorBrush(Color.FromArgb(150, 96, 125, 138))); SetCurrentValue(GradientStopCollectionProperty, new GradientStopCollection { new GradientStop(Color.FromArgb(100,2,119,188), 0d), new GradientStop(Color.FromRgb(2,119,188), 1d), }); SetCurrentValue(HeatMapProperty, new Dictionary()); SetCurrentValue(GeoMapTooltipProperty, new DefaultGeoMapTooltip {Visibility = Visibility.Hidden}); Canvas.Children.Add(GeoMapTooltip); SizeChanged += (sender, e) => { Draw(); }; MouseWheel += (sender, e) => { if (!EnableZoomingAndPanning) return; e.Handled = true; var rt = Map.RenderTransform as ScaleTransform; var p = rt == null ? 1 : rt.ScaleX; p += e.Delta > 0 ? .05 : -.05; p = p < 1 ? 1 : p; var o = e.GetPosition(this); if (e.Delta > 0) Map.RenderTransformOrigin = new Point(o.X/ActualWidth,o.Y/ActualHeight); Map.RenderTransform = new ScaleTransform(p, p); }; MouseDown += (sender, e) => { if (!EnableZoomingAndPanning) return; DragOrigin = e.GetPosition(this); }; MouseUp += (sender, e) => { if (!EnableZoomingAndPanning) return; var end = e.GetPosition(this); var delta = new Point(DragOrigin.X - end.X, DragOrigin.Y - end.Y); var l = Canvas.GetLeft(Map) - delta.X; var t = Canvas.GetTop(Map) - delta.Y; if (DisableAnimations) { Canvas.SetLeft(Map, l); Canvas.SetTop(Map, t); } else { Map.BeginAnimation(Canvas.LeftProperty, new DoubleAnimation(l, AnimationsSpeed)); Map.BeginAnimation(Canvas.TopProperty, new DoubleAnimation(t, AnimationsSpeed)); } }; } #endregion #region Events /// /// Occurs when [land click]. /// public event Action LandClick; #endregion #region Properties private Canvas Canvas { get; set; } private Canvas Map { get; set; } private Point DragOrigin { get; set; } private Point OriginalPosition { get; set; } private bool IsDrawn { get; set; } private bool IsWidthDominant { get; set; } private Dictionary Lands { get; set; } private static readonly DependencyProperty GeoMapTooltipProperty = DependencyProperty.Register( "GeoMapTooltip", typeof (DefaultGeoMapTooltip), typeof (GeoMap), new PropertyMetadata(default(DefaultGeoMapTooltip))); private DefaultGeoMapTooltip GeoMapTooltip { get { return (DefaultGeoMapTooltip) GetValue(GeoMapTooltipProperty); } set { SetValue(GeoMapTooltipProperty, value); } } /// /// The language pack property /// public static readonly DependencyProperty LanguagePackProperty = DependencyProperty.Register( "LanguagePack", typeof (Dictionary), typeof (GeoMap), new PropertyMetadata(default(Dictionary))); /// /// Gets or sets the language dictionary /// public Dictionary LanguagePack { get { return (Dictionary) GetValue(LanguagePackProperty); } set { SetValue(LanguagePackProperty, value); } } /// /// The default land fill property /// public static readonly DependencyProperty DefaultLandFillProperty = DependencyProperty.Register( "DefaultLandFill", typeof (Brush), typeof (GeoMap), new PropertyMetadata(default(Brush))); /// /// Gets or sets default land fill /// public Brush DefaultLandFill { get { return (Brush) GetValue(DefaultLandFillProperty); } set { SetValue(DefaultLandFillProperty, value); } } /// /// The land stroke thickness property /// public static readonly DependencyProperty LandStrokeThicknessProperty = DependencyProperty.Register( "LandStrokeThickness", typeof (double), typeof (GeoMap), new PropertyMetadata(default(double))); /// /// Gets or sets every land stroke thickness property /// public double LandStrokeThickness { get { return (double) GetValue(LandStrokeThicknessProperty); } set { SetValue(LandStrokeThicknessProperty, value); } } /// /// The land stroke property /// public static readonly DependencyProperty LandStrokeProperty = DependencyProperty.Register( "LandStroke", typeof (Brush), typeof (GeoMap), new PropertyMetadata(default(Brush))); /// /// Gets or sets every land stroke /// public Brush LandStroke { get { return (Brush) GetValue(LandStrokeProperty); } set { SetValue(LandStrokeProperty, value); } } /// /// The disable animations property /// public static readonly DependencyProperty DisableAnimationsProperty = DependencyProperty.Register( "DisableAnimations", typeof (bool), typeof (GeoMap), new PropertyMetadata(default(bool))); /// /// Gets or sets whether the chart is animated /// public bool DisableAnimations { get { return (bool) GetValue(DisableAnimationsProperty); } set { SetValue(DisableAnimationsProperty, value); } } /// /// The animations speed property /// public static readonly DependencyProperty AnimationsSpeedProperty = DependencyProperty.Register( "AnimationsSpeed", typeof (TimeSpan), typeof (GeoMap), new PropertyMetadata(default(TimeSpan))); /// /// Gets or sets animations speed /// public TimeSpan AnimationsSpeed { get { return (TimeSpan) GetValue(AnimationsSpeedProperty); } set { SetValue(AnimationsSpeedProperty, value); } } /// /// The hoverable property /// public static readonly DependencyProperty HoverableProperty = DependencyProperty.Register( "Hoverable", typeof (bool), typeof (GeoMap), new PropertyMetadata(default(bool))); /// /// Gets or sets whether the chart reacts when a user moves the mouse over a land /// public bool Hoverable { get { return (bool) GetValue(HoverableProperty); } set { SetValue(HoverableProperty, value); } } /// /// The heat map property /// public static readonly DependencyProperty HeatMapProperty = DependencyProperty.Register( "HeatMap", typeof (Dictionary), typeof (GeoMap), new PropertyMetadata(default(Dictionary), OnHeapMapChanged)); /// /// Gets or sets the current heat map /// public Dictionary HeatMap { get { return (Dictionary) GetValue(HeatMapProperty); } set { SetValue(HeatMapProperty, value); } } /// /// The gradient stop collection property /// public static readonly DependencyProperty GradientStopCollectionProperty = DependencyProperty.Register( "GradientStopCollection", typeof(GradientStopCollection), typeof(GeoMap), new PropertyMetadata(default(GradientStopCollection))); /// /// Gets or sets the gradient stop collection, use every gradient offset and color properties to define your gradient. /// public GradientStopCollection GradientStopCollection { get { return (GradientStopCollection)GetValue(GradientStopCollectionProperty); } set { SetValue(GradientStopCollectionProperty, value); } } /// /// The source property /// public static readonly DependencyProperty SourceProperty = DependencyProperty.Register( "Source", typeof (string), typeof (GeoMap), new PropertyMetadata(default(string))); /// /// Gets or sets the map source /// public string Source { get { return (string) GetValue(SourceProperty); } set { SetValue(SourceProperty, value); } } /// /// The enable zooming and panning property /// public static readonly DependencyProperty EnableZoomingAndPanningProperty = DependencyProperty.Register( "EnableZoomingAndPanning", typeof (bool), typeof (GeoMap), new PropertyMetadata(default(bool))); /// /// Gets or sets whether the map allows zooming and panning /// public bool EnableZoomingAndPanning { get { return (bool) GetValue(EnableZoomingAndPanningProperty); } set { SetValue(EnableZoomingAndPanningProperty, value); } } #endregion #region Publics ///// ///// Moves the map to a specif area ///// ///// target area //public void MoveTo(MapData data) //{ // var s = (Path) data.Shape; // var area = s.Data.Bounds; // var t = (ScaleTransform) s.RenderTransform; // //if (DisableAnimations) // //{ // double scale; // double cx = 0; // double cy = 0; // if (IsWidthDominant) // { // scale = data.LvcMap.DesiredWidth / area.Width; // } // else // { // scale = data.LvcMap.DesiredHeight / area.Height; // } // Map.RenderTransformOrigin = new Point(0, 0); // Map.RenderTransform = new ScaleTransform(scale, scale); // Canvas.SetLeft(Map, -area.X*t.ScaleX*scale + cx); // Canvas.SetTop(Map, -area.Y*t.ScaleY*scale + cy); // //} // //else // //{ // // Map.BeginAnimation(Canvas.LeftProperty, new DoubleAnimation(-area.X, AnimationsSpeed)); // // //Map.BeginAnimation(Canvas.TopProperty, new DoubleAnimation(area.Y, AnimationsSpeed)); // //} //} /// /// Restarts the current map view /// public void Restart() { Map.RenderTransform = new ScaleTransform(1, 1); if (DisableAnimations) { Canvas.SetLeft(Map, OriginalPosition.X); Canvas.SetTop(Map, OriginalPosition.Y); } else { Map.BeginAnimation(Canvas.LeftProperty, new DoubleAnimation(OriginalPosition.X, TimeSpan.FromMilliseconds(1))); Map.BeginAnimation(Canvas.TopProperty, new DoubleAnimation(OriginalPosition.Y, TimeSpan.FromMilliseconds(1))); } } /// /// Sets a heat map value with a given key, then updates every land heat color /// /// key /// new value public void UpdateKey(string key, double value) { HeatMap[key] = value; ShowMeSomeHeat(); } #endregion #region Privates private void Draw() { IsDrawn = true; Map.Children.Clear(); if (DesignerProperties.GetIsInDesignMode(this)) { Map.Children.Add(new TextBlock { Text = "Designer preview is not currently available", Foreground = Brushes.White, FontWeight = FontWeights.Bold, FontSize = 12, Effect = new DropShadowEffect { ShadowDepth = 2, RenderingBias = RenderingBias.Performance } }); return; } var map = MapResolver.Get(Source); if (map == null) return; var desiredSize = new Size(map.DesiredWidth, map.DesiredHeight); var r = desiredSize.Width/desiredSize.Height; var wr = ActualWidth/desiredSize.Width; var hr = ActualHeight/desiredSize.Height; double s; if (wr < hr) { IsWidthDominant = true; Map.Width = ActualWidth; Map.Height = Map.Width/r; s = wr; OriginalPosition = new Point(0, (ActualHeight - Map.Height)*.5); Canvas.SetLeft(Map, OriginalPosition.X); Canvas.SetTop(Map, OriginalPosition.Y); } else { IsWidthDominant = false; Map.Height = ActualHeight; Map.Width = r*ActualHeight; s = hr; OriginalPosition = new Point((ActualWidth - Map.Width)*.5, 0d); Canvas.SetLeft(Map, OriginalPosition.X); Canvas.SetTop(Map, OriginalPosition.Y); } var t = new ScaleTransform(s, s); foreach (var land in map.Data) { var p = new Path { Data = Geometry.Parse(land.Data), RenderTransform = t }; land.Shape = p; Lands[land.Id] = land; Map.Children.Add(p); p.MouseEnter += POnMouseEnter; p.MouseLeave += POnMouseLeave; p.MouseMove += POnMouseMove; p.MouseDown += POnMouseDown; p.SetBinding(Shape.StrokeProperty, new Binding { Path = new PropertyPath(LandStrokeProperty), Source = this }); p.SetBinding(Shape.StrokeThicknessProperty, new MultiBinding { Converter = new ScaleStrokeConverter(), Bindings = { new Binding("LandStrokeThickness") {Source = this}, new Binding("ScaleX") {Source = t} } }); } ShowMeSomeHeat(); } private void ShowMeSomeHeat() { var max = double.MinValue; var min = double.MaxValue; foreach (var i in HeatMap.Values) { max = i > max ? i : max; min = i < min ? i : min; } foreach (var land in Lands) { double temperature; var shape = ((Shape) land.Value.Shape); shape.SetBinding(Shape.FillProperty, new Binding {Path = new PropertyPath(DefaultLandFillProperty), Source = this}); if (!HeatMap.TryGetValue(land.Key, out temperature)) continue; temperature = LinealInterpolation(0, 1, min, max, temperature); var color = ColorInterpolation(temperature); if (DisableAnimations) { shape.Fill = new SolidColorBrush(color); } else { shape.Fill = new SolidColorBrush(); ((SolidColorBrush) shape.Fill).BeginAnimation(SolidColorBrush.ColorProperty, new ColorAnimation(color, AnimationsSpeed)); } } } private void POnMouseDown(object sender, MouseButtonEventArgs mouseButtonEventArgs) { var land = Lands.Values.FirstOrDefault(x => x.Shape == sender); if (land == null) return; if (LandClick != null) LandClick.Invoke(sender, land); } private void POnMouseLeave(object sender, MouseEventArgs mouseEventArgs) { var path = (Path)sender; path.Opacity = 1; GeoMapTooltip.Visibility = Visibility.Hidden; } private void POnMouseEnter(object sender, MouseEventArgs mouseEventArgs) { var path = (Path) sender; path.Opacity = .8; var land = Lands.Values.FirstOrDefault(x => x.Shape == sender); if (land == null) return; double value; if (!HeatMap.TryGetValue(land.Id, out value)) return; if (!Hoverable) return; GeoMapTooltip.Visibility = Visibility.Visible; string name = null; if (LanguagePack != null) LanguagePack.TryGetValue(land.Id, out name); GeoMapTooltip.GeoData = new GeoData { Name = name ?? land.Name, Value = value }; } private void POnMouseMove(object sender, MouseEventArgs mouseEventArgs) { var location = mouseEventArgs.GetPosition(this); Canvas.SetTop(GeoMapTooltip, location.Y + 5); Canvas.SetLeft(GeoMapTooltip, location.X + 5); } private Color ColorInterpolation(double weight) { Color from = Color.FromRgb(0, 0, 0), to = Color.FromRgb(0, 0, 0); double fromOffset = 0, toOffset = 0; for (var i = 0; i < GradientStopCollection.Count-1; i++) { // ReSharper disable once InvertIf if (GradientStopCollection[i].Offset <= weight && GradientStopCollection[i + 1].Offset >= weight) { from = GradientStopCollection[i].Color; to = GradientStopCollection[i + 1].Color; fromOffset = GradientStopCollection[i].Offset; toOffset = GradientStopCollection[i + 1].Offset; break; } } return Color.FromArgb( (byte) LinealInterpolation(from.A, to.A, fromOffset, toOffset, weight), (byte) LinealInterpolation(from.R, to.R, fromOffset, toOffset, weight), (byte) LinealInterpolation(from.G, to.G, fromOffset, toOffset, weight), (byte) LinealInterpolation(from.B, to.B, fromOffset, toOffset, weight)); } private static double LinealInterpolation(double fromComponent, double toComponent, double fromOffset, double toOffset, double value) { var p1 = new Point(fromOffset, fromComponent); var p2 = new Point(toOffset, toComponent); var deltaX = p2.X - p1.X; // ReSharper disable once CompareOfFloatsByEqualityOperator var m = (p2.Y - p1.Y) / (deltaX == 0 ? double.MinValue : deltaX); return m * (value - p1.X) + p1.Y; } private static void OnHeapMapChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) { var geoMap = (GeoMap)o; if (!geoMap.IsDrawn) return; geoMap.ShowMeSomeHeat(); } #endregion } /// /// /// /// public class ScaleStrokeConverter : IMultiValueConverter { /// /// Converts source values to a value for the binding target. The data binding engine calls this method when it propagates the values from source bindings to the binding target. /// /// The array of values that the source bindings in the produces. The value indicates that the source binding has no value to provide for conversion. /// The type of the binding target property. /// The converter parameter to use. /// The culture to use in the converter. /// /// A converted value.If the method returns null, the valid null value is used.A return value of . indicates that the converter did not produce a value, and that the binding will use the if it is available, or else will use the default value.A return value of . indicates that the binding does not transfer the value or use the or the default value. /// public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) { return (double) values[0]/(double) values[1]; } /// /// Converts a binding target value to the source binding values. /// /// The value that the binding target produces. /// The array of types to convert to. The array length indicates the number and types of values that are suggested for the method to return. /// The converter parameter to use. /// The culture to use in the converter. /// /// An array of values that have been converted from the target value back to the source values. /// /// public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) { throw new NotImplementedException(); } } }