// XAML Map Control - http://xamlmapcontrol.codeplex.com/ // Copyright © Clemens Fischer 2012-2013 // Licensed under the Microsoft Public License (Ms-PL) using System; using System.Collections.Specialized; using System.Linq; #if NETFX_CORE using Windows.Foundation; using Windows.UI.Xaml; using Windows.UI.Xaml.Media; using Windows.UI.Xaml.Media.Animation; #else using System.Windows; using System.Windows.Media; using System.Windows.Media.Animation; #endif namespace MapControl { /// /// The map control. Draws map content provided by the TileLayers or the TileLayer property. /// The visible map area is defined by the Center and ZoomLevel properties. The map can be rotated /// by an angle that is given by the Heading property. /// MapBase is a MapPanel and hence can contain map overlays like other MapPanels or MapItemsControls. /// public partial class MapBase : MapPanel { public static TimeSpan AnimationDuration = TimeSpan.FromSeconds(0.5); public static EasingFunctionBase AnimationEasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut }; public static readonly DependencyProperty TileLayersProperty = DependencyProperty.Register( "TileLayers", typeof(TileLayerCollection), typeof(MapBase), new PropertyMetadata(null, (o, e) => ((MapBase)o).TileLayersPropertyChanged((TileLayerCollection)e.OldValue, (TileLayerCollection)e.NewValue))); public static readonly DependencyProperty TileLayerProperty = DependencyProperty.Register( "TileLayer", typeof(TileLayer), typeof(MapBase), new PropertyMetadata(null, (o, e) => ((MapBase)o).TileLayerPropertyChanged((TileLayer)e.NewValue))); public static readonly DependencyProperty TileOpacityProperty = DependencyProperty.Register( "TileOpacity", typeof(double), typeof(MapBase), new PropertyMetadata(1d, (o, e) => ((MapBase)o).tileContainer.Opacity = (double)e.NewValue)); public static readonly DependencyProperty CenterProperty = DependencyProperty.Register( "Center", typeof(Location), typeof(MapBase), new PropertyMetadata(new Location(), (o, e) => ((MapBase)o).CenterPropertyChanged((Location)e.NewValue))); public static readonly DependencyProperty TargetCenterProperty = DependencyProperty.Register( "TargetCenter", typeof(Location), typeof(MapBase), new PropertyMetadata(new Location(), (o, e) => ((MapBase)o).TargetCenterPropertyChanged((Location)e.NewValue))); internal static readonly DependencyProperty CenterPointProperty = DependencyProperty.Register( "CenterPoint", typeof(Point), typeof(MapBase), new PropertyMetadata(new Point(), (o, e) => ((MapBase)o).CenterPointPropertyChanged((Point)e.NewValue))); public static readonly DependencyProperty MinZoomLevelProperty = DependencyProperty.Register( "MinZoomLevel", typeof(double), typeof(MapBase), new PropertyMetadata(1d, (o, e) => ((MapBase)o).MinZoomLevelPropertyChanged((double)e.NewValue))); public static readonly DependencyProperty MaxZoomLevelProperty = DependencyProperty.Register( "MaxZoomLevel", typeof(double), typeof(MapBase), new PropertyMetadata(18d, (o, e) => ((MapBase)o).MaxZoomLevelPropertyChanged((double)e.NewValue))); public static readonly DependencyProperty ZoomLevelProperty = DependencyProperty.Register( "ZoomLevel", typeof(double), typeof(MapBase), new PropertyMetadata(1d, (o, e) => ((MapBase)o).ZoomLevelPropertyChanged((double)e.NewValue))); public static readonly DependencyProperty TargetZoomLevelProperty = DependencyProperty.Register( "TargetZoomLevel", typeof(double), typeof(MapBase), new PropertyMetadata(1d, (o, e) => ((MapBase)o).TargetZoomLevelPropertyChanged((double)e.NewValue))); public static readonly DependencyProperty HeadingProperty = DependencyProperty.Register( "Heading", typeof(double), typeof(MapBase), new PropertyMetadata(0d, (o, e) => ((MapBase)o).HeadingPropertyChanged((double)e.NewValue))); public static readonly DependencyProperty TargetHeadingProperty = DependencyProperty.Register( "TargetHeading", typeof(double), typeof(MapBase), new PropertyMetadata(0d, (o, e) => ((MapBase)o).TargetHeadingPropertyChanged((double)e.NewValue))); public static readonly DependencyProperty CenterScaleProperty = DependencyProperty.Register( "CenterScale", typeof(double), typeof(MapBase), null); private readonly TileContainer tileContainer = new TileContainer(); private readonly MapTransform mapTransform = new MercatorTransform(); private readonly MatrixTransform scaleTransform = new MatrixTransform(); private readonly MatrixTransform rotateTransform = new MatrixTransform(); private readonly MatrixTransform scaleRotateTransform = new MatrixTransform(); private Location transformOrigin; private Point viewportOrigin; private PointAnimation centerAnimation; private DoubleAnimation zoomLevelAnimation; private DoubleAnimation headingAnimation; private Brush storedBackground; private Brush storedForeground; private bool internalPropertyChange; public MapBase() { SetParentMap(); TileLayers = new TileLayerCollection(); Initialize(); Loaded += OnLoaded; } partial void Initialize(); /// /// Raised when the current viewport has changed. /// public event EventHandler ViewportChanged; /// /// Gets or sets the map foreground Brush. /// public Brush Foreground { get { return (Brush)GetValue(ForegroundProperty); } set { SetValue(ForegroundProperty, value); } } /// /// Gets or sets the TileLayers used by this Map. /// public TileLayerCollection TileLayers { get { return (TileLayerCollection)GetValue(TileLayersProperty); } set { SetValue(TileLayersProperty, value); } } /// /// Gets or sets the base TileLayer used by this Map, i.e. TileLayers[0]. /// public TileLayer TileLayer { get { return (TileLayer)GetValue(TileLayerProperty); } set { SetValue(TileLayerProperty, value); } } /// /// Gets or sets the opacity of the tile layers. /// public double TileOpacity { get { return (double)GetValue(TileOpacityProperty); } set { SetValue(TileOpacityProperty, value); } } /// /// Gets or sets the location of the center point of the Map. /// public Location Center { get { return (Location)GetValue(CenterProperty); } set { SetValue(CenterProperty, value); } } /// /// Gets or sets the target value of a Center animation. /// public Location TargetCenter { get { return (Location)GetValue(TargetCenterProperty); } set { SetValue(TargetCenterProperty, value); } } /// /// Gets or sets the minimum value of the ZoomLevel and TargetZommLevel properties. /// Must be greater than or equal to zero and less than or equal to MaxZoomLevel. /// public double MinZoomLevel { get { return (double)GetValue(MinZoomLevelProperty); } set { SetValue(MinZoomLevelProperty, value); } } /// /// Gets or sets the maximum value of the ZoomLevel and TargetZommLevel properties. /// Must be greater than or equal to MinZoomLevel and less than or equal to 20. /// public double MaxZoomLevel { get { return (double)GetValue(MaxZoomLevelProperty); } set { SetValue(MaxZoomLevelProperty, value); } } /// /// Gets or sets the map zoom level. /// public double ZoomLevel { get { return (double)GetValue(ZoomLevelProperty); } set { SetValue(ZoomLevelProperty, value); } } /// /// Gets or sets the target value of a ZoomLevel animation. /// public double TargetZoomLevel { get { return (double)GetValue(TargetZoomLevelProperty); } set { SetValue(TargetZoomLevelProperty, value); } } /// /// Gets or sets the map heading, or clockwise rotation angle in degrees. /// public double Heading { get { return (double)GetValue(HeadingProperty); } set { SetValue(HeadingProperty, value); } } /// /// Gets or sets the target value of a Heading animation. /// public double TargetHeading { get { return (double)GetValue(TargetHeadingProperty); } set { SetValue(TargetHeadingProperty, value); } } /// /// Gets the map scale at the Center location as viewport coordinate units (pixels) per meter. /// public double CenterScale { get { return (double)GetValue(CenterScaleProperty); } private set { SetValue(CenterScaleProperty, value); } } /// /// Gets the transformation from geographic coordinates to cartesian map coordinates. /// public MapTransform MapTransform { get { return mapTransform; } } /// /// Gets the transformation from cartesian map coordinates to viewport coordinates. /// public Transform ViewportTransform { get { return tileContainer.ViewportTransform; } } /// /// Gets the scaling transformation from meters to viewport coordinate units (pixels) /// at the viewport center point. /// public Transform ScaleTransform { get { return scaleTransform; } } /// /// Gets the transformation that rotates by the value of the Heading property. /// public Transform RotateTransform { get { return rotateTransform; } } /// /// Gets the combination of ScaleTransform and RotateTransform /// public Transform ScaleRotateTransform { get { return scaleRotateTransform; } } /// /// Gets the conversion factor from longitude degrees to meters, at latitude = 0. /// public double MetersPerDegree { get { return (TileLayer != null && TileLayer.TileSource != null) ? TileLayer.TileSource.MetersPerDegree : (TileSource.EarthRadius * Math.PI / 180d); } } /// /// Gets the map scale at the specified location as viewport coordinate units (pixels) per meter. /// public double GetMapScale(Location location) { return mapTransform.RelativeScale(location) * Math.Pow(2d, ZoomLevel) * TileSource.TileSize / (MetersPerDegree * 360d); } /// /// Transforms a geographic location to a viewport coordinates point. /// public Point LocationToViewportPoint(Location location) { return ViewportTransform.Transform(mapTransform.Transform(location)); } /// /// Transforms a viewport coordinates point to a geographic location. /// public Location ViewportPointToLocation(Point point) { return mapTransform.Transform(ViewportTransform.Inverse.Transform(point)); } /// /// Sets a temporary origin location in geographic coordinates for scaling and rotation transformations. /// This origin location is automatically removed when the Center property is set by application code. /// public void SetTransformOrigin(Location origin) { transformOrigin = origin; viewportOrigin = LocationToViewportPoint(origin); } /// /// Sets a temporary origin point in viewport coordinates for scaling and rotation transformations. /// This origin point is automatically removed when the Center property is set by application code. /// public void SetTransformOrigin(Point origin) { viewportOrigin.X = Math.Min(Math.Max(origin.X, 0d), RenderSize.Width); viewportOrigin.Y = Math.Min(Math.Max(origin.Y, 0d), RenderSize.Height); transformOrigin = ViewportPointToLocation(viewportOrigin); } /// /// Removes the temporary transform origin point set by SetTransformOrigin. /// public void ResetTransformOrigin() { transformOrigin = null; viewportOrigin = new Point(RenderSize.Width / 2d, RenderSize.Height / 2d); } /// /// Changes the Center property according to the specified translation in viewport coordinates. /// public void TranslateMap(Point translation) { if (translation.X != 0d || translation.Y != 0d) { if (transformOrigin != null) { viewportOrigin.X += translation.X; viewportOrigin.Y += translation.Y; UpdateTransform(); } else { Center = ViewportPointToLocation(new Point(viewportOrigin.X - translation.X, viewportOrigin.Y - translation.Y)); } } } /// /// Changes the Center, Heading and ZoomLevel properties according to the specified /// viewport coordinate translation, rotation and scale delta values. Rotation and scaling /// is performed relative to the specified origin point in viewport coordinates. /// public void TransformMap(Point origin, Point translation, double rotation, double scale) { SetTransformOrigin(origin); viewportOrigin.X += translation.X; viewportOrigin.Y += translation.Y; if (rotation != 0d) { var heading = (((Heading + rotation) % 360d) + 360d) % 360d; InternalSetValue(HeadingProperty, heading); InternalSetValue(TargetHeadingProperty, heading); } if (scale != 1d) { var zoomLevel = Math.Min(Math.Max(ZoomLevel + Math.Log(scale, 2d), MinZoomLevel), MaxZoomLevel); InternalSetValue(ZoomLevelProperty, zoomLevel); InternalSetValue(TargetZoomLevelProperty, zoomLevel); } UpdateTransform(); ResetTransformOrigin(); } /// /// Sets the value of the ZoomLevel property while retaining the specified origin point /// in viewport coordinates. /// public void ZoomMap(Point origin, double zoomLevel) { var targetZoomLevel = TargetZoomLevel; TargetZoomLevel = zoomLevel; if (TargetZoomLevel != targetZoomLevel) // TargetZoomLevel might be coerced { SetTransformOrigin(origin); } } protected override void OnViewportChanged() { base.OnViewportChanged(); if (ViewportChanged != null) { ViewportChanged(this, EventArgs.Empty); } } private void OnLoaded(object sender, RoutedEventArgs e) { Loaded -= OnLoaded; if (TileLayer == null) { TileLayer = TileLayer.Default; } UpdateTransform(); } private void TileLayerCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { switch (e.Action) { case NotifyCollectionChangedAction.Add: tileContainer.AddTileLayers(e.NewStartingIndex, e.NewItems.Cast()); break; case NotifyCollectionChangedAction.Remove: tileContainer.RemoveTileLayers(e.OldStartingIndex, e.OldItems.Count); break; #if !SILVERLIGHT case NotifyCollectionChangedAction.Move: #endif case NotifyCollectionChangedAction.Replace: tileContainer.RemoveTileLayers(e.NewStartingIndex, e.OldItems.Count); tileContainer.AddTileLayers(e.NewStartingIndex, e.NewItems.Cast()); break; case NotifyCollectionChangedAction.Reset: tileContainer.ClearTileLayers(); if (e.NewItems != null) { tileContainer.AddTileLayers(0, e.NewItems.Cast()); } break; default: break; } UpdateTileLayer(); } private void TileLayersPropertyChanged(TileLayerCollection oldTileLayers, TileLayerCollection newTileLayers) { tileContainer.ClearTileLayers(); if (oldTileLayers != null) { oldTileLayers.CollectionChanged -= TileLayerCollectionChanged; } if (newTileLayers != null) { newTileLayers.CollectionChanged += TileLayerCollectionChanged; tileContainer.AddTileLayers(0, newTileLayers); } UpdateTileLayer(); } private void TileLayerPropertyChanged(TileLayer tileLayer) { if (tileLayer != null) { if (TileLayers == null) { TileLayers = new TileLayerCollection(); } if (TileLayers.Count == 0) { TileLayers.Add(tileLayer); } else if (TileLayers[0] != tileLayer) { TileLayers[0] = tileLayer; } } if (tileLayer != null && tileLayer.Background != null) { if (storedBackground == null) { storedBackground = Background; } Background = tileLayer.Background; } else if (storedBackground != null) { Background = storedBackground; storedBackground = null; } if (tileLayer != null && tileLayer.Foreground != null) { if (storedForeground == null) { storedForeground = Foreground; } Foreground = tileLayer.Foreground; } else if (storedForeground != null) { Foreground = storedForeground; storedForeground = null; } } private void UpdateTileLayer() { var tileLayer = TileLayers.FirstOrDefault(); if (TileLayer != tileLayer) { TileLayer = tileLayer; } } private void InternalSetValue(DependencyProperty property, object value) { internalPropertyChange = true; SetValue(property, value); internalPropertyChange = false; } private bool CoerceLocation(Location location, double latitudeEpsilon = 0d) { var maxLatitude = mapTransform.MaxLatitude + latitudeEpsilon; var latitude = Math.Min(Math.Max(location.Latitude, -maxLatitude), maxLatitude); var longitude = Location.NormalizeLongitude(location.Longitude); if (location.Latitude != latitude || location.Longitude != longitude) { location.Latitude = latitude; location.Longitude = longitude; return true; } return false; } private void CoerceCenterProperty(DependencyProperty property, Location center) { if (CoerceLocation(center)) { InternalSetValue(property, center); } } private void CenterPropertyChanged(Location center) { if (!internalPropertyChange) { CoerceCenterProperty(CenterProperty, center); ResetTransformOrigin(); UpdateTransform(); if (centerAnimation == null) { InternalSetValue(TargetCenterProperty, center); InternalSetValue(CenterPointProperty, new Point(center.Longitude, center.Latitude)); } } } private void TargetCenterPropertyChanged(Location targetCenter) { if (!internalPropertyChange) { CoerceCenterProperty(TargetCenterProperty, targetCenter); if (!targetCenter.Equals(Center)) { if (centerAnimation != null) { centerAnimation.Completed -= CenterAnimationCompleted; } // animate private CenterPoint property by PointAnimation centerAnimation = new PointAnimation { From = new Point(Center.Longitude, Center.Latitude), To = new Point(targetCenter.Longitude, targetCenter.Latitude), Duration = AnimationDuration, EasingFunction = AnimationEasingFunction, FillBehavior = animationFillBehavior }; centerAnimation.Completed += CenterAnimationCompleted; this.BeginAnimation(CenterPointProperty, centerAnimation); } } } private void CenterAnimationCompleted(object sender, object e) { if (centerAnimation != null) { centerAnimation.Completed -= CenterAnimationCompleted; centerAnimation = null; InternalSetValue(CenterProperty, TargetCenter); InternalSetValue(CenterPointProperty, new Point(TargetCenter.Longitude, TargetCenter.Latitude)); ResetTransformOrigin(); UpdateTransform(); } } private void CenterPointPropertyChanged(Point centerPoint) { if (!internalPropertyChange) { InternalSetValue(CenterProperty, new Location(centerPoint.Y, centerPoint.X)); ResetTransformOrigin(); UpdateTransform(); } } private void MinZoomLevelPropertyChanged(double minZoomLevel) { var coercedValue = Math.Min(Math.Max(minZoomLevel, 0d), MaxZoomLevel); if (coercedValue != minZoomLevel) { InternalSetValue(MinZoomLevelProperty, coercedValue); } else if (ZoomLevel < minZoomLevel) { ZoomLevel = minZoomLevel; } } private void MaxZoomLevelPropertyChanged(double maxZoomLevel) { var coercedValue = Math.Min(Math.Max(maxZoomLevel, MinZoomLevel), 22d); if (coercedValue != maxZoomLevel) { InternalSetValue(MaxZoomLevelProperty, coercedValue); } else if (ZoomLevel > maxZoomLevel) { ZoomLevel = maxZoomLevel; } } private bool CoerceZoomLevelProperty(DependencyProperty property, ref double zoomLevel) { var coercedValue = Math.Min(Math.Max(zoomLevel, MinZoomLevel), MaxZoomLevel); if (coercedValue != zoomLevel) { InternalSetValue(property, coercedValue); return true; } return false; } private void ZoomLevelPropertyChanged(double zoomLevel) { if (!internalPropertyChange && !CoerceZoomLevelProperty(ZoomLevelProperty, ref zoomLevel)) { UpdateTransform(); if (zoomLevelAnimation == null) { InternalSetValue(TargetZoomLevelProperty, zoomLevel); } } } private void TargetZoomLevelPropertyChanged(double targetZoomLevel) { if (!internalPropertyChange && !CoerceZoomLevelProperty(TargetZoomLevelProperty, ref targetZoomLevel) && targetZoomLevel != ZoomLevel) { if (zoomLevelAnimation != null) { zoomLevelAnimation.Completed -= ZoomLevelAnimationCompleted; } zoomLevelAnimation = new DoubleAnimation { To = targetZoomLevel, Duration = AnimationDuration, EasingFunction = AnimationEasingFunction, FillBehavior = animationFillBehavior }; zoomLevelAnimation.Completed += ZoomLevelAnimationCompleted; this.BeginAnimation(ZoomLevelProperty, zoomLevelAnimation); } } private void ZoomLevelAnimationCompleted(object sender, object e) { if (zoomLevelAnimation != null) { zoomLevelAnimation.Completed -= ZoomLevelAnimationCompleted; zoomLevelAnimation = null; InternalSetValue(ZoomLevelProperty, TargetZoomLevel); UpdateTransform(); ResetTransformOrigin(); } } private void CoerceHeadingProperty(DependencyProperty property, ref double heading) { var coercedValue = (heading >= -180d && heading <= 360d) ? heading : (((heading % 360d) + 360d) % 360d); if (coercedValue != heading) { InternalSetValue(property, coercedValue); } } private void HeadingPropertyChanged(double heading) { if (!internalPropertyChange) { CoerceHeadingProperty(HeadingProperty, ref heading); UpdateTransform(); if (headingAnimation == null) { InternalSetValue(TargetHeadingProperty, heading); } } } private void TargetHeadingPropertyChanged(double targetHeading) { if (!internalPropertyChange) { CoerceHeadingProperty(TargetHeadingProperty, ref targetHeading); if (targetHeading != Heading) { var delta = targetHeading - Heading; if (delta > 180d) { delta -= 360d; } else if (delta < -180d) { delta += 360d; } if (headingAnimation != null) { headingAnimation.Completed -= HeadingAnimationCompleted; } headingAnimation = new DoubleAnimation { By = delta, Duration = AnimationDuration, EasingFunction = AnimationEasingFunction, FillBehavior = animationFillBehavior }; headingAnimation.Completed += HeadingAnimationCompleted; this.BeginAnimation(HeadingProperty, headingAnimation); } } } private void HeadingAnimationCompleted(object sender, object e) { if (headingAnimation != null) { headingAnimation.Completed -= HeadingAnimationCompleted; headingAnimation = null; InternalSetValue(HeadingProperty, TargetHeading); UpdateTransform(); } } private void UpdateTransform() { var center = Center; var scale = SetViewportTransform(transformOrigin ?? center); if (transformOrigin != null) { center = ViewportPointToLocation(new Point(RenderSize.Width / 2d, RenderSize.Height / 2d)); var coerced = CoerceLocation(center, 1e-3); InternalSetValue(CenterProperty, center); if (coerced) { ResetTransformOrigin(); scale = SetViewportTransform(center); } } scale *= mapTransform.RelativeScale(center) / MetersPerDegree; // Pixels per meter at center latitude CenterScale = scale; SetTransformMatrixes(scale); OnViewportChanged(); } private double SetViewportTransform(Location origin) { return tileContainer.SetViewportTransform(ZoomLevel, Heading, mapTransform.Transform(origin), viewportOrigin, RenderSize); } } }