//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.Globalization; using System.Linq; using Windows.Devices.Input; using Windows.Foundation; using Windows.UI; using Windows.UI.Text; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Data; using Windows.UI.Xaml.Input; using Windows.UI.Xaml.Media; using Windows.UI.Xaml.Shapes; using LiveCharts.Maps; using LiveCharts.Uwp.Components; using LiveCharts.Uwp.Components.MultiBinding; using LiveCharts.Uwp.Maps; using Microsoft.Xaml.Interactivity; using Path = Windows.UI.Xaml.Shapes.Path; namespace LiveCharts.Uwp { /// /// /// /// 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("ActualWidth"), Source = this }); Canvas.SetBinding(HeightProperty, new Binding { Path = new PropertyPath("ActualHeight"), Source = this }); Lands = new Dictionary(); this.SetIfNotSet(BackgroundProperty, new SolidColorBrush(Color.FromArgb(150, 96, 125, 138))); /*Current*/ SetValue(GradientStopCollectionProperty, new GradientStopCollection { new GradientStop() { Color = Color.FromArgb(100, 2, 119, 188), Offset = 0d }, new GradientStop() { Color = Color.FromArgb(255, 2, 119, 188), Offset = 1d }, }); this.SetIfNotSet(HeatMapProperty, new Dictionary()); /*Current*/ SetValue(GeoMapTooltipProperty, new DefaultGeoMapTooltip {Visibility = Visibility.Collapsed}); //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.CreateCanvasStoryBoardAndBegin(l, t, AnimationsSpeed); // } //}; } #endregion #region Events /// /// Occurs when [land click]. /// public event Action LandClick; #endregion #region Properties private Canvas Canvas { get; } private Canvas Map { get; } private Point DragOrigin { get; set; } private Point OriginalPosition { get; set; } private bool IsDrawn { get; set; } private bool IsWidthDominant { get; set; } private Dictionary Lands { get; } 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(new SolidColorBrush(Color.FromArgb(200, 255, 255, 255)))); /// /// 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(1.3d)); /// /// 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(new SolidColorBrush(Color.FromArgb(30, 55, 55, 55)))); /// /// 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(TimeSpan.FromMilliseconds(500))); /// /// 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() {ScaleX = 1, ScaleY = 1}; if (DisableAnimations) { Canvas.SetLeft(Map, OriginalPosition.X); Canvas.SetTop(Map, OriginalPosition.Y); } else { Map.CreateCanvasStoryBoardAndBegin(OriginalPosition.X, 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 (Windows.ApplicationModel.DesignMode.DesignModeEnabled) { Map.Children.Add(new TextBlock { Text = "Designer preview is not currently available", Foreground = new SolidColorBrush(Colors.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() {ScaleX = s, ScaleY = s}; foreach (var land in map.Data) { var p = new Path { Data = GeometryHelper.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("LandStroke"), Source = this }); var behavior = new MultiBindingBehavior() { Converter = new ScaleStrokeConverter(), PropertyName = "StrokeThickness" }; behavior.Items.Add(new MultiBindingItem() {Parent = behavior.Items, Value = new Binding() { Path = new PropertyPath("LandStrokeThickness"), Source = this } }); behavior.Items.Add(new MultiBindingItem() {Parent = behavior.Items, Value = new Binding() { Path = new PropertyPath("ScaleX"), Source = t } }); Interaction.SetBehaviors(p, new BehaviorCollection() {behavior}); } 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("DefaultLandFill"), 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).BeginColorAnimation(nameof(SolidColorBrush.Color), color, AnimationsSpeed); } } } private void POnMouseDown(object sender, PointerRoutedEventArgs 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.Collapsed;//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.FromArgb(255, 0, 0, 0), to = Color.FromArgb(255, 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 : MultiValueConverterBase { /// /// Modifies the source data before passing it to the target for display in the UI. /// /// The source data being passed to the target. /// The of data expected by the target dependency property. /// An optional parameter to be used in the converter logic. /// The culture of the conversion. /// /// The value to be passed to the target dependency property. /// public override object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) { return (double) values[0]/(double) values[1]; } /// /// Modifies the target data before passing it to the source object. This method is called only in bindings. /// /// The target data being passed to the source. /// The of data expected by the source object. /// An optional parameter to be used in the converter logic. /// The culture of the conversion. /// /// The value to be passed to the source object. /// /// public override Object[] ConvertBack(Object value, Type targetType, Object parameter, CultureInfo culture) { throw new NotImplementedException(); } } }