// (c) Copyright ESRI.
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved.
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using ESRI.ArcGIS.Client.Geometry;
namespace ESRI.ArcGIS.Client.Toolkit
{
///
/// OverviewMap Control
///
[TemplatePart(Name = "OVMapImage", Type = typeof(Map))]
[TemplatePart(Name = "AOI", Type = typeof(Grid))]
[System.Windows.Markup.ContentProperty("Layer")]
public partial class OverviewMap : Control
{
private Envelope mapExtent;
private Envelope fullExtent;
private Envelope lastMapExtent = new Envelope();
private Point startPoint;
private RotateTransform rotateTransform;
double offsetLeft = 0;
double offsetTop = 0;
private bool dragOn = false;
private double maxWidth = 0;
private double maxHeight = 0;
#region Template items
Map OVMapImage;
Grid AOI;
#endregion
#region Constructor
///
/// Initializes a new instance of the class.
///
public OverviewMap()
{
#if SILVERLIGHT
DefaultStyleKey = typeof(OverviewMap);
#endif
this.Loaded += (s, e) =>
{
UpdateOVMap();
#if !SILVERLIGHT
UpdateAOI();
#endif
};
}
///
/// Static initialization for the control.
///
static OverviewMap()
{
#if !SILVERLIGHT
DefaultStyleKeyProperty.OverrideMetadata(typeof(OverviewMap),
new FrameworkPropertyMetadata(typeof(OverviewMap)));
#endif
}
#endregion
#region Overrides
///
/// When overridden in a derived class, is invoked whenever application code
/// or internal processes (such as a rebuilding layout pass) call
/// .
///
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
if (this.Layer != null && OVMapImage != null)
{
OVMapImage.Layers.Remove(this.Layer);
}
OVMapImage = GetTemplateChild("OVMapImage") as Map;
if (OVMapImage != null)
{
OVMapImage.ExtentChanged += (s, e) => { UpdateAOI(); };
OVMapImage.Layers.LayersInitialized += (s, e) => { ZoomToNewExtent(); };
if (this.Layer != null)
this.OVMapImage.Layers.Add(this.Layer);
}
AOI = GetTemplateChild("AOI") as Grid;
if (AOI != null)
AOI.MouseLeftButtonDown += AOI_MouseLeftButtonDown;
UpdateAOI();
}
///
/// Provides the behavior for the "Arrange" pass of Silverlight layout.
/// Classes can override this method to define their own arrange pass behavior.
///
/// The final area within the parent that this
/// object should use to arrange itself and its children.
/// The actual size used.
protected override Size ArrangeOverride(Size finalSize)
{
this.Clip = new RectangleGeometry() { Rect = new Rect(0, 0, ActualWidth, ActualHeight) };
return base.ArrangeOverride(finalSize);
}
#endregion
#region Dependency Properties
///
/// Identifies the dependency property.
///
public static readonly DependencyProperty MapProperty = DependencyProperty.Register("Map", typeof(Map), typeof(OverviewMap), new PropertyMetadata(OnMapPropertyChanged));
private static void OnMapPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
OverviewMap ovmap = d as OverviewMap;
Map oldMap = e.OldValue as Map;
if (oldMap != null) //clean up
{
if (ovmap.OVMapImage != null)
ovmap.OVMapImage.Layers.Clear();
oldMap.ExtentChanged -= ovmap.UpdateOVMap;
oldMap.RotationChanged -= ovmap.map_RotationChanged;
}
Map newMap = e.NewValue as Map;
if (newMap != null)
{
newMap.ExtentChanged += ovmap.UpdateOVMap;
newMap.RotationChanged += ovmap.map_RotationChanged;
if (ovmap.Layer != null && ovmap.OVMapImage != null)
ovmap.OVMapImage.Layers.Add(ovmap.Layer);
ovmap.UpdateOVMap();
}
}
private void map_RotationChanged(object sender, DependencyPropertyChangedEventArgs e)
{
double angle = (this.FlowDirection == System.Windows.FlowDirection.LeftToRight) ? -(double)e.NewValue : (double)e.NewValue;
if (rotateTransform == null)
{
rotateTransform = new RotateTransform();
AOI.RenderTransform = rotateTransform;
AOI.RenderTransformOrigin = new Point(0.5, 0.5);
}
rotateTransform.Angle = angle;
}
///
/// Sets or gets the Map control associated with the OverviewMap.
///
public Map Map
{
get { return (Map)GetValue(MapProperty); }
set { SetValue(MapProperty, value); }
}
///
/// Identifies the dependency property.
///
public static readonly DependencyProperty MaximumExtentProperty = DependencyProperty.Register("MaximumExtent", typeof(Envelope), typeof(OverviewMap), null);
///
/// Gets or sets the maximum map extent of the overview map.
/// If undefined, the maximum extent is derived from the layer.
///
/// The maximum extent.
public Envelope MaximumExtent
{
get { return (Envelope)GetValue(MaximumExtentProperty); }
set { SetValue(MaximumExtentProperty, value); }
}
///
/// Identifies the dependency property.
///
public static readonly DependencyProperty IsStaticProperty = DependencyProperty.Register("IsStatic", typeof(bool), typeof(OverviewMap), new PropertyMetadata(false, OnIsStaticPropertyChanged));
///
/// Gets or sets a value indicating whether the overview map extent is static.
///
/// If true the extent of the overview map will never change.
/// If true the extent will remain at . If
/// MaximumExtent is not set the extent will remait at the full extent
/// of the layer.
public bool IsStatic
{
get { return (bool)GetValue(IsStaticProperty); }
set { SetValue(IsStaticProperty, value); }
}
private static void OnIsStaticPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if ((bool)e.NewValue)
(d as OverviewMap).ZoomFullExtent();
else
(d as OverviewMap).ZoomToNewExtent();
(d as OverviewMap).UpdateOVMap();
}
///
/// Identifies the dependency property.
///
public static readonly DependencyProperty LayerProperty = DependencyProperty.Register("Layer", typeof(Layer), typeof(OverviewMap), new PropertyMetadata(OnLayerPropertyChanged));
private static void OnLayerPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
OverviewMap ovmap = d as OverviewMap;
if (ovmap.OVMapImage != null)
{
ovmap.OVMapImage.Layers.Clear();
if (ovmap.Layer != null)
ovmap.OVMapImage.Layers.Add(ovmap.Layer);
}
if (ovmap.Layer != null)
{
bool isInit = ovmap.Layer.IsInitialized;
if (isInit)
ovmap.Layer_LayersInitialized(ovmap.Layer, null);
else
ovmap.Layer.Initialized += ovmap.Layer_LayersInitialized;
}
}
///
/// Gets or sets the layer used in the overview map.
///
/// The layer.
public Layer Layer
{
get { return (Layer)GetValue(LayerProperty); }
set { SetValue(LayerProperty, value); }
}
#endregion
#region Private Methods
///
/// Sets extents, limits, and events after layers have been initialized
///
///
///
private void Layer_LayersInitialized(object sender, EventArgs args)
{
if (OVMapImage != null)
{
if (MaximumExtent != null)
fullExtent = MaximumExtent.Clone();
else
fullExtent = OVMapImage.Layers.GetFullExtent();
OVMapImage.MinimumResolution = double.Epsilon;
OVMapImage.MaximumResolution = double.MaxValue;
if(fullExtent != null)
{
maxWidth = fullExtent.Width;
maxHeight = fullExtent.Height;
}
UpdateOVMap();
}
}
#region Methods for setting extent of OverviewMap
///
/// Determines if the OverviewMap extent should be changed. If so, set new
/// extent and call ZoomTo or PanTo. If not, send to UpdateAOI
///
private void UpdateOVMap()
{
if (Map == null || OVMapImage == null || OVMapImage.Extent == null || Map.Extent == null)
{
if(AOI!=null)
AOI.Visibility = Visibility.Collapsed;
return;
}
Envelope ovExtent = NormalizeExtent(OVMapImage.Extent);
Envelope mapExtent = NormalizeExtent(Map.Extent);
// update ov extent if necessary
double mapWidth = mapExtent.Width;
double mapHeight = mapExtent.Height;
double ovWidth = ovExtent.Width;
double ovHeight = ovExtent.Height;
bool sameWidthHeight = (mapWidth == lastMapExtent.Width && mapHeight == lastMapExtent.Height);
if (mapExtent.Equals(lastMapExtent))
{
UpdateAOI();
}
else if (sameWidthHeight || IsStatic)
{
double halfWidth = ovWidth / 2;
double halfHeight = ovHeight / 2;
MapPoint newCenter = mapExtent.GetCenter();
if (MaximumExtent != null && !IsStatic)
{
if (newCenter.X - halfWidth < MaximumExtent.XMin) newCenter.X = MaximumExtent.XMin + halfWidth;
if (newCenter.X + halfWidth > MaximumExtent.XMax) newCenter.X = MaximumExtent.XMax - halfWidth;
if (newCenter.Y - halfHeight < MaximumExtent.YMin) newCenter.Y = MaximumExtent.YMin + halfHeight;
if (newCenter.Y + halfHeight > MaximumExtent.YMax) newCenter.Y = MaximumExtent.YMax - halfHeight;
}
if (ovWidth >= maxWidth && !Map.WrapAroundIsActive)
UpdateAOI();
else
{
if (AOI != null)
AOI.Visibility = Visibility.Collapsed;
if (NeedUpdate(newCenter,ovExtent.GetCenter(),OVMapImage.Resolution))
OVMapImage.PanTo(newCenter);
else
UpdateAOI();
}
}
else if (mapWidth >= maxWidth && !Map.WrapAroundIsActive)
ZoomFullExtent();
else
{
ZoomToNewExtent();
}
}
private void ZoomToNewExtent()
{
if (OVMapImage == null || OVMapImage.Extent == null ||
Map == null || Map.Extent == null) return;
Envelope ovExtent = NormalizeExtent(OVMapImage.Extent);
if (ovExtent == null) return;
Envelope mapExtent = NormalizeExtent(Map.Extent);
if (mapExtent == null) return;
const double minRatio = 0.15;
const double maxRatio = 0.8;
double mapWidth = mapExtent.Width;
double mapHeight = mapExtent.Height;
double ovWidth = ovExtent.Width;
double ovHeight = ovExtent.Height;
double widthRatio = mapWidth / ovWidth;
double heightRatio = mapHeight / ovHeight;
bool isMapWithinOV = mapExtent.XMin >= ovExtent.XMin && mapExtent.XMax <= ovExtent.XMax &&
mapExtent.YMin >= ovExtent.YMin && mapExtent.YMax <= ovExtent.YMax;
Envelope extent;
if (!isMapWithinOV || widthRatio <= minRatio || heightRatio <= minRatio || widthRatio >= maxRatio || heightRatio >= maxRatio)
{
//set new size around new mapextent
if (AOI != null)
AOI.Visibility = Visibility.Collapsed;
if (maxWidth / 3 > mapWidth || Map.WrapAroundIsActive)
{
if (!IsStatic)
{
extent = new Envelope()
{
XMin = mapExtent.XMin - mapWidth,
XMax = mapExtent.XMax + mapWidth,
YMin = mapExtent.YMin - mapHeight,
YMax = mapExtent.YMax + mapHeight
};
if (MaximumExtent != null)
{
if (extent.XMin < MaximumExtent.XMin) extent.XMin = MaximumExtent.XMin;
if (extent.XMax > MaximumExtent.XMax) extent.XMax = MaximumExtent.XMax;
if (extent.YMin < MaximumExtent.YMin) extent.YMin = MaximumExtent.YMin;
if (extent.YMax > MaximumExtent.YMax) extent.YMax = MaximumExtent.YMax;
}
OVMapImage.ZoomTo(extent);
}
else
UpdateAOI();
}
else
ZoomFullExtent();
}
else
UpdateAOI();
}
///
/// Overload of UpdateOVMap - ExtentEventHandler version
///
///
///
private void UpdateOVMap(object sender, ESRI.ArcGIS.Client.ExtentEventArgs e)
{
UpdateOVMap();
}
private void ZoomFullExtent()
{
if (OVMapImage != null)
{
OVMapImage.ZoomTo(fullExtent);
UpdateAOI();
}
}
#endregion
#region Methods for setting size and position of AOI Box
///
/// Sets size and position of AOI Box
///
private void UpdateAOI()
{
if (Map == null || OVMapImage == null || OVMapImage.Extent == null || AOI == null) return;
Envelope mapExtent = NormalizeExtent(Map.Extent);
if (mapExtent == null)
{
AOI.Visibility = Visibility.Collapsed;
return;
}
MapPoint pt1 = new MapPoint(mapExtent.XMin, mapExtent.YMax);
MapPoint pt2 = new MapPoint(mapExtent.XMax, mapExtent.YMin);
Point topLeft = OVMapImage.MapToScreen(pt1);
Point bottomRight = OVMapImage.MapToScreen(pt2);
if (!double.IsNaN(topLeft.X) && !double.IsNaN(topLeft.Y) &&
!double.IsNaN(bottomRight.X) && !double.IsNaN(bottomRight.Y))
{
// Get absolute value of (bottomRight.X - topLeft.X) to avoid negative width when the
// control FlowDirection is set to RTL:
AOI.Width = Math.Max(3, Math.Abs(bottomRight.X - topLeft.X));
AOI.Height = Math.Max(3, bottomRight.Y - topLeft.Y);
// Setting the correct value for AOI width when the control FlowDirection is set to RTL:
AOI.Margin = new Thickness((this.FlowDirection == System.Windows.FlowDirection.LeftToRight) ? topLeft.X
: topLeft.X - AOI.Width, topLeft.Y, 0, 0);
// Set rotation of AOI according to map roatation
if (rotateTransform == null)
{
rotateTransform = new RotateTransform();
AOI.RenderTransform = rotateTransform;
AOI.RenderTransformOrigin = new Point(0.5, 0.5);
}
rotateTransform.Angle = (Map.Rotation * -1);
AOI.Visibility = Visibility.Visible;
}
else
AOI.Visibility = Visibility.Collapsed;
lastMapExtent = mapExtent;
}
#endregion
#region Method for setting extent of Map
///
/// Set new map extent of main map control. Called after AOI
/// Box has been repositioned by user
///
private void UpdateMap()
{
if (AOI == null) return;
mapExtent = Map.Extent;
double aoiLeft = AOI.Margin.Left;
double aoiTop = AOI.Margin.Top;
MapPoint pt = OVMapImage.ScreenToMap(new Point(aoiLeft, aoiTop));
double mapHalfWidth = mapExtent.Width / 2;
double mapHalfHeight = mapExtent.Height / 2;
MapPoint pnt = new MapPoint(pt.X + mapHalfWidth, pt.Y - mapHalfHeight);
Map.PanTo(pnt);
}
#endregion
#region AOI Box Mouse handlers
private void AOI_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
dragOn = true;
startPoint = e.GetPosition(this);
offsetLeft = startPoint.X - AOI.Margin.Left;
offsetTop = startPoint.Y - AOI.Margin.Top;
AOI.MouseMove += AOI_MouseMove;
AOI.MouseLeftButtonUp += AOI_MouseLeftButtonUp;
AOI.CaptureMouse();
}
private void AOI_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (dragOn)
{
AOI.MouseMove -= AOI_MouseMove;
AOI.MouseLeftButtonUp -= AOI_MouseLeftButtonUp;
UpdateMap();
dragOn = false;
AOI.ReleaseMouseCapture();
}
}
private void AOI_MouseMove(object sender, MouseEventArgs e)
{
if (dragOn)
{
Point pos = e.GetPosition(this);
AOI.Margin = new Thickness(pos.X - offsetLeft, pos.Y - offsetTop, 0, 0);
}
}
#endregion
#endregion
// Checks to see if the base map pan has change enough that a pan is needed
// for the overview map. A very small pan on the base map may cause a pan of less
// than 1 pixel on the overview map. if the pan is less than 1 pixel then
// then the UpdateAOI should be called because no pan less than 1 pixel is valid.
bool NeedUpdate(MapPoint newCenter, MapPoint currentCenter, double resolution )
{
int x = (int)Math.Round((currentCenter.X - newCenter.X) / resolution);
int y = (int)Math.Round((currentCenter.Y - newCenter.Y) / resolution);
return (x != 0 || y != 0);
}
// this function will check to see if the current extent needs to be normalized
// because wraping is turned on and maximum extent has been set.
private Envelope NormalizeExtent(Envelope extent)
{
// If overmap has an MaximumExtent and the base map is panned into
// another frames then extent needs to be normalized in order to determine
// if the current extent of the base map on other frames is within
// the maximum extent. If there is no MaximumExtent or WrapAround is not
// present in the overview map then there is no need to normalize extent.
if (MaximumExtent != null && Map.WrapAroundIsActive)
{
Geometry.Geometry normExtent = Envelope.NormalizeCentralMeridian(extent);
// if the entire extent is in another frame then an envelope will be returned.
if (normExtent is Envelope)
return normExtent as Envelope;
// if the extent crosses the between two frames then half of the
// extent exists in one frame andthe other half exists in the
// other frame. Polygon with two rings is returned to reprsent each
// side of the extent in each frame.
else if (normExtent is Polygon)
return CreateDateLineExtent((Polygon)normExtent);
}
// if normalizing is not needed then the extent will be
// returned unchanged.
return extent;
}
// Merges two polygon rings into a single extent based on which width is
// bigger. e.g. if an extent crosses the dateline then the half that is most
// visible should be represented on the overview map.
private static Envelope CreateDateLineExtent(Polygon polygon)
{
// left side
Polygon ring1 = new Polygon();
ring1.Rings.Add(polygon.Rings[0]);
// right side
Polygon ring2 = new Polygon();
ring2.Rings.Add(polygon.Rings[1]);
// if left side is bigger than right side.
// merge the right side width onto the left side.
if (ring1.Extent.Width > ring2.Extent.Width)
{
ring1.Extent.XMax += ring2.Extent.Width;
return ring1.Extent;
}
// if the right side is bigger than the left side.
// merge the left side width onto the right side.
else
{
ring2.Extent.XMin -= ring1.Extent.Width;
return ring2.Extent;
}
}
}
}