commit caab7208a3c774ad741d04966b1607b57fc3b2e3 Author: ClemensF Date: Wed Apr 25 22:02:53 2012 +0200 . diff --git a/MapControl.sln b/MapControl.sln new file mode 100644 index 00000000..838e0c8f --- /dev/null +++ b/MapControl.sln @@ -0,0 +1,30 @@ + +Microsoft Visual Studio Solution File, Format Version 11.00 +# Visual Studio 2010 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapControl", "MapControl\MapControl.csproj", "{06481252-2310-414A-B9FC-D5739FDF6BD3}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|Mixed Platforms = Debug|Mixed Platforms + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|Mixed Platforms = Release|Mixed Platforms + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {06481252-2310-414A-B9FC-D5739FDF6BD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {06481252-2310-414A-B9FC-D5739FDF6BD3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {06481252-2310-414A-B9FC-D5739FDF6BD3}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {06481252-2310-414A-B9FC-D5739FDF6BD3}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {06481252-2310-414A-B9FC-D5739FDF6BD3}.Debug|x86.ActiveCfg = Debug|Any CPU + {06481252-2310-414A-B9FC-D5739FDF6BD3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {06481252-2310-414A-B9FC-D5739FDF6BD3}.Release|Any CPU.Build.0 = Release|Any CPU + {06481252-2310-414A-B9FC-D5739FDF6BD3}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {06481252-2310-414A-B9FC-D5739FDF6BD3}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {06481252-2310-414A-B9FC-D5739FDF6BD3}.Release|x86.ActiveCfg = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/MapControl/GlyphRunText.cs b/MapControl/GlyphRunText.cs new file mode 100644 index 00000000..641f2d4b --- /dev/null +++ b/MapControl/GlyphRunText.cs @@ -0,0 +1,61 @@ +using System; +using System.Windows; +using System.Windows.Media; + +namespace MapControl +{ + public static class GlyphRunText + { + public static GlyphRun Create(string text, Typeface typeface, double emSize, Point baselineOrigin) + { + GlyphTypeface glyphTypeface; + + if (!typeface.TryGetGlyphTypeface(out glyphTypeface)) + { + throw new ArgumentException(string.Format("{0}: no GlyphTypeface found", typeface.FontFamily)); + } + + ushort[] glyphIndices = new ushort[text.Length]; + double[] advanceWidths = new double[text.Length]; + + for (int i = 0; i < text.Length; i++) + { + ushort glyphIndex = glyphTypeface.CharacterToGlyphMap[text[i]]; + glyphIndices[i] = glyphIndex; + advanceWidths[i] = glyphTypeface.AdvanceWidths[glyphIndex] * emSize; + } + + return new GlyphRun(glyphTypeface, 0, false, emSize, glyphIndices, baselineOrigin, advanceWidths, + null, null, null, null, null, null); + } + + public static GlyphRun Create(string text, Typeface typeface, double emSize, Vector centerOffset) + { + GlyphTypeface glyphTypeface; + + if (!typeface.TryGetGlyphTypeface(out glyphTypeface)) + { + throw new ArgumentException(string.Format("{0}: no GlyphTypeface found", typeface.FontFamily)); + } + + ushort[] glyphIndices = new ushort[text.Length]; + double[] advanceWidths = new double[text.Length]; + + for (int i = 0; i < text.Length; i++) + { + ushort glyphIndex = glyphTypeface.CharacterToGlyphMap[text[i]]; + glyphIndices[i] = glyphIndex; + advanceWidths[i] = glyphTypeface.AdvanceWidths[glyphIndex] * emSize; + } + + GlyphRun glyphRun = new GlyphRun(glyphTypeface, 0, false, emSize, glyphIndices, new Point(), advanceWidths, + null, null, null, null, null, null); + + Rect bbox = glyphRun.ComputeInkBoundingBox(); + Point baselineOrigin = new Point(centerOffset.X - bbox.X - bbox.Width / 2d, centerOffset.Y - bbox.Y - bbox.Height / 2d); + + return new GlyphRun(glyphTypeface, 0, false, emSize, glyphIndices, baselineOrigin, advanceWidths, + null, null, null, null, null, null); + } + } +} diff --git a/MapControl/Map.cs b/MapControl/Map.cs new file mode 100644 index 00000000..bf511993 --- /dev/null +++ b/MapControl/Map.cs @@ -0,0 +1,708 @@ +using System; +using System.Collections.Generic; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows.Media.Animation; + +namespace MapControl +{ + public partial class Map : MapPanel + { + public const double MeterPerDegree = 1852d * 60d; + + public static readonly DependencyProperty FontSizeProperty = Control.FontSizeProperty.AddOwner(typeof(Map)); + public static readonly DependencyProperty FontFamilyProperty = Control.FontFamilyProperty.AddOwner(typeof(Map)); + public static readonly DependencyProperty FontStyleProperty = Control.FontStyleProperty.AddOwner(typeof(Map)); + public static readonly DependencyProperty FontWeightProperty = Control.FontWeightProperty.AddOwner(typeof(Map)); + public static readonly DependencyProperty FontStretchProperty = Control.FontStretchProperty.AddOwner(typeof(Map)); + public static readonly DependencyProperty ForegroundProperty = Control.ForegroundProperty.AddOwner(typeof(Map)); + + public static readonly DependencyProperty LightForegroundProperty = DependencyProperty.Register( + "LightForeground", typeof(Brush), typeof(Map)); + + public static readonly DependencyProperty DarkForegroundProperty = DependencyProperty.Register( + "DarkForeground", typeof(Brush), typeof(Map)); + + public static readonly DependencyProperty LightBackgroundProperty = DependencyProperty.Register( + "LightBackground", typeof(Brush), typeof(Map)); + + public static readonly DependencyProperty DarkBackgroundProperty = DependencyProperty.Register( + "DarkBackground", typeof(Brush), typeof(Map)); + + public static readonly DependencyProperty TileLayersProperty = DependencyProperty.Register( + "TileLayers", typeof(TileLayerCollection), typeof(Map), new FrameworkPropertyMetadata( + (o, e) => ((Map)o).SetTileLayers((TileLayerCollection)e.NewValue))); + + public static readonly DependencyProperty BaseTileLayerProperty = DependencyProperty.Register( + "BaseTileLayer", typeof(TileLayer), typeof(Map), new FrameworkPropertyMetadata( + (o, e) => ((Map)o).SetBaseTileLayer((TileLayer)e.NewValue), + (o, v) => ((Map)o).CoerceBaseTileLayer((TileLayer)v))); + + public static readonly DependencyProperty TileOpacityProperty = DependencyProperty.Register( + "TileOpacity", typeof(double), typeof(Map), new FrameworkPropertyMetadata(1d, + (o, e) => ((Map)o).tileContainer.Opacity = (double)e.NewValue)); + + public static readonly DependencyProperty CenterProperty = DependencyProperty.Register( + "Center", typeof(Point), typeof(Map), new FrameworkPropertyMetadata(new Point(), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, + (o, e) => ((Map)o).SetCenter((Point)e.NewValue), + (o, v) => ((Map)o).CoerceCenter((Point)v))); + + public static readonly DependencyProperty TargetCenterProperty = DependencyProperty.Register( + "TargetCenter", typeof(Point), typeof(Map), new FrameworkPropertyMetadata(new Point(), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, + (o, e) => ((Map)o).SetTargetCenter((Point)e.NewValue), + (o, v) => ((Map)o).CoerceCenter((Point)v))); + + public static readonly DependencyProperty ZoomLevelProperty = DependencyProperty.Register( + "ZoomLevel", typeof(double), typeof(Map), new FrameworkPropertyMetadata(1d, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, + (o, e) => ((Map)o).SetZoomLevel((double)e.NewValue), + (o, v) => ((Map)o).CoerceZoomLevel((double)v))); + + public static readonly DependencyProperty TargetZoomLevelProperty = DependencyProperty.Register( + "TargetZoomLevel", typeof(double), typeof(Map), new FrameworkPropertyMetadata(1d, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, + (o, e) => ((Map)o).SetTargetZoomLevel((double)e.NewValue), + (o, v) => ((Map)o).CoerceZoomLevel((double)v))); + + public static readonly DependencyProperty HeadingProperty = DependencyProperty.Register( + "Heading", typeof(double), typeof(Map), new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, + (o, e) => ((Map)o).SetHeading((double)e.NewValue), + (o, v) => ((Map)o).CoerceHeading((double)v))); + + public static readonly DependencyProperty TargetHeadingProperty = DependencyProperty.Register( + "TargetHeading", typeof(double), typeof(Map), new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, + (o, e) => ((Map)o).SetTargetHeading((double)e.NewValue), + (o, v) => ((Map)o).CoerceHeading((double)v))); + + private static readonly DependencyPropertyKey CenterScalePropertyKey = DependencyProperty.RegisterReadOnly( + "CenterScale", typeof(double), typeof(Map), null); + + public static readonly DependencyProperty CenterScaleProperty = CenterScalePropertyKey.DependencyProperty; + + private readonly TileContainer tileContainer = new TileContainer(); + private readonly MapViewTransform mapViewTransform = new MapViewTransform(); + private readonly ScaleTransform scaleTransform = new ScaleTransform(); + private readonly RotateTransform rotateTransform = new RotateTransform(); + private readonly MatrixTransform scaleRotateTransform = new MatrixTransform(); + private Point? transformOrigin; + private Point viewOrigin; + private PointAnimation centerAnimation; + private DoubleAnimation zoomLevelAnimation; + private DoubleAnimation headingAnimation; + private bool updateTransform = true; + + public Map() + { + MinZoomLevel = 1; + MaxZoomLevel = 20; + + AddVisualChild(tileContainer); + mapViewTransform.ViewTransform = tileContainer.ViewTransform; + + BaseTileLayer = new TileLayer + { + Description = "© {y} OpenStreetMap Contributors, CC-BY-SA", + TileSource = new OpenStreetMapTileSource { UriFormat = "http://{c}.tile.openstreetmap.org/{z}/{x}/{y}.png" } + }; + + SetValue(ParentMapProperty, this); + } + + /// + /// Raised when the ViewTransform property has changed. + /// + public event EventHandler ViewTransformChanged; + + /// + /// Raised when the TileLayers property has changed. + /// + public event EventHandler TileLayersChanged; + + public double MinZoomLevel { get; set; } + public double MaxZoomLevel { get; set; } + + public double FontSize + { + get { return (double)GetValue(FontSizeProperty); } + set { SetValue(FontSizeProperty, value); } + } + + public FontFamily FontFamily + { + get { return (FontFamily)GetValue(FontFamilyProperty); } + set { SetValue(FontFamilyProperty, value); } + } + + public FontStyle FontStyle + { + get { return (FontStyle)GetValue(FontStyleProperty); } + set { SetValue(FontStyleProperty, value); } + } + + public FontWeight FontWeight + { + get { return (FontWeight)GetValue(FontWeightProperty); } + set { SetValue(FontWeightProperty, value); } + } + + public FontStretch FontStretch + { + get { return (FontStretch)GetValue(FontStretchProperty); } + set { SetValue(FontStretchProperty, value); } + } + + public Brush Foreground + { + get { return (Brush)GetValue(ForegroundProperty); } + set { SetValue(ForegroundProperty, value); } + } + + public Brush LightForeground + { + get { return (Brush)GetValue(LightForegroundProperty); } + set { SetValue(LightForegroundProperty, value); } + } + + public Brush DarkForeground + { + get { return (Brush)GetValue(DarkForegroundProperty); } + set { SetValue(DarkForegroundProperty, value); } + } + + public Brush LightBackground + { + get { return (Brush)GetValue(LightBackgroundProperty); } + set { SetValue(LightBackgroundProperty, value); } + } + + public Brush DarkBackground + { + get { return (Brush)GetValue(DarkBackgroundProperty); } + set { SetValue(DarkBackgroundProperty, 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 BaseTileLayer + { + get { return (TileLayer)GetValue(BaseTileLayerProperty); } + set { SetValue(BaseTileLayerProperty, 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 world coordinates (latitude and longitude) of the center point. + /// + public Point Center + { + get { return (Point)GetValue(CenterProperty); } + set { SetValue(CenterProperty, value); } + } + + /// + /// Gets or sets the target value of a Center animation. + /// + public Point TargetCenter + { + get { return (Point)GetValue(TargetCenterProperty); } + set { SetValue(TargetCenterProperty, 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 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 point as view coordinate units (pixels) per meter. + /// + public double CenterScale + { + get { return (double)GetValue(CenterScaleProperty); } + private set { SetValue(CenterScalePropertyKey, value); } + } + + /// + /// Gets the transformation from world coordinates (latitude and longitude) + /// to cartesian map coordinates. + /// + public MapTransform MapTransform + { + get { return mapViewTransform.MapTransform; } + } + + /// + /// Gets the transformation from cartesian map coordinates to view coordinates. + /// + public Transform ViewTransform + { + get { return mapViewTransform.ViewTransform; } + } + + /// + /// Gets the combination of MapTransform and ViewTransform, i.e. the transformation + /// from longitude and latitude values to view coordinates. + /// + public GeneralTransform MapViewTransform + { + get { return mapViewTransform; } + } + + /// + /// Gets the scaling transformation from meters to view coordinate units (pixels) + /// at the view 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; } + } + + /// + /// Sets an intermittent origin point in world 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) + { + transformOrigin = origin; + viewOrigin = MapViewTransform.Transform(origin); + } + + /// + /// Sets an intermittent origin point in view coordinates for scaling and rotation transformations. + /// This origin point is automatically removed when the Center property is set by application code. + /// + public void SetTransformViewOrigin(Point origin) + { + viewOrigin.X = Math.Min(Math.Max(origin.X, 0d), RenderSize.Width); + viewOrigin.Y = Math.Min(Math.Max(origin.Y, 0d), RenderSize.Height); + transformOrigin = CoerceCenter(MapViewTransform.Inverse.Transform(viewOrigin)); + } + + /// + /// Removes the intermittent transform origin point set by SetTransformOrigin. + /// + public void ResetTransformOrigin() + { + transformOrigin = null; + viewOrigin = new Point(RenderSize.Width / 2d, RenderSize.Height / 2d); + } + + /// + /// Changes the Center property according to the specified translation in view coordinates. + /// + public void TranslateMap(Vector translation) + { + if (translation.X != 0d || translation.Y != 0d) + { + ResetTransformOrigin(); + Center = MapViewTransform.Inverse.Transform(viewOrigin - translation); + } + } + + /// + /// Changes the Center, Heading and ZoomLevel properties according to the specified + /// view coordinate translation, rotation and scale delta values. Rotation and scaling + /// is performed relative to the specified origin point in view coordinates. + /// + public void TransformMap(Point origin, Vector translation, double rotation, double scale) + { + if (rotation != 0d || scale != 1d) + { + SetTransformViewOrigin(origin); + updateTransform = false; + Heading += rotation; + ZoomLevel += Math.Log(scale, 2d); + updateTransform = true; + SetViewTransform(); + } + + TranslateMap(translation); + } + + /// + /// Sets the value of the ZoomLevel property while retaining the specified origin point + /// in view coordinates. + /// + public void ZoomMap(Point origin, double zoomLevel) + { + SetTransformViewOrigin(origin); + TargetZoomLevel = zoomLevel; + } + + /// + /// Gets the map scale at the specified world coordinate point + /// as view coordinate units (pixels) per meter. + /// + public double GetMapScale(Point point) + { + return MapTransform.RelativeScale(point) * Math.Pow(2d, ZoomLevel) * 256d / (MeterPerDegree * 360d); + } + + protected override int VisualChildrenCount + { + get { return InternalChildren.Count + 1; } + } + + protected override Visual GetVisualChild(int index) + { + if (index == 0) + { + return tileContainer; + } + + return InternalChildren[index - 1]; + } + + protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo) + { + base.OnRenderSizeChanged(sizeInfo); + + ResetTransformOrigin(); + SetViewTransform(); + } + + protected override void OnRender(DrawingContext drawingContext) + { + drawingContext.DrawRectangle(Background, null, new Rect(RenderSize)); + } + + protected override void OnViewTransformChanged(Map map) + { + base.OnViewTransformChanged(map); + + if (ViewTransformChanged != null) + { + ViewTransformChanged(this, EventArgs.Empty); + } + } + + protected internal virtual void OnTileLayersChanged() + { + if (tileContainer.TileLayers != null && + tileContainer.TileLayers.Count > 0 && + tileContainer.TileLayers[0].HasDarkBackground) + { + if (DarkForeground != null) + { + Foreground = DarkForeground; + } + + if (DarkBackground != null) + { + Background = DarkBackground; + } + } + else + { + if (LightForeground != null) + { + Foreground = LightForeground; + } + + if (LightBackground != null) + { + Background = LightBackground; + } + } + + if (TileLayersChanged != null) + { + TileLayersChanged(this, EventArgs.Empty); + } + } + + private void SetTileLayers(TileLayerCollection tileLayers) + { + BaseTileLayer = tileLayers.Count > 0 ? tileLayers[0] : null; + tileContainer.TileLayers = tileLayers; + } + + private void SetBaseTileLayer(TileLayer baseTileLayer) + { + if (baseTileLayer != null) + { + if (TileLayers == null) + { + TileLayers = new TileLayerCollection(baseTileLayer); + } + else if (TileLayers.Count == 0) + { + TileLayers.Add(baseTileLayer); + } + else if (TileLayers[0] != baseTileLayer) + { + TileLayers[0] = baseTileLayer; + } + } + } + + private TileLayer CoerceBaseTileLayer(TileLayer baseTileLayer) + { + if (baseTileLayer == null && TileLayers.Count > 0) + { + baseTileLayer = TileLayers[0]; + } + + return baseTileLayer; + } + + private void SetCenter(Point center) + { + if (updateTransform) + { + ResetTransformOrigin(); + SetViewTransform(); + } + + if (centerAnimation == null) + { + TargetCenter = center; + } + } + + private void SetTargetCenter(Point targetCenter) + { + if (targetCenter != Center) + { + if (centerAnimation != null) + { + centerAnimation.Completed -= CenterAnimationCompleted; + } + + centerAnimation = new PointAnimation + { + From = Center, + To = targetCenter, + Duration = TimeSpan.FromSeconds(0.5), + FillBehavior = FillBehavior.Stop, + EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut } + }; + + centerAnimation.Completed += CenterAnimationCompleted; + + updateTransform = false; + Center = targetCenter; + updateTransform = true; + + BeginAnimation(CenterProperty, centerAnimation); + } + } + + private void CenterAnimationCompleted(object sender, EventArgs eventArgs) + { + centerAnimation = null; + } + + private Point CoerceCenter(Point point) + { + point.X = ((point.X + 180d) % 360d + 360d) % 360d - 180d; + point.Y = Math.Min(Math.Max(point.Y, -MapTransform.MaxLatitude), MapTransform.MaxLatitude); + return point; + } + + private void SetZoomLevel(double zoomLevel) + { + if (updateTransform) + { + SetViewTransform(); + } + + if (zoomLevelAnimation == null) + { + TargetZoomLevel = zoomLevel; + } + } + + private void SetTargetZoomLevel(double targetZoomLevel) + { + if (targetZoomLevel != ZoomLevel) + { + if (zoomLevelAnimation != null) + { + zoomLevelAnimation.Completed -= ZoomLevelAnimationCompleted; + } + + zoomLevelAnimation = new DoubleAnimation + { + From = ZoomLevel, + To = targetZoomLevel, + Duration = TimeSpan.FromSeconds(0.5), + FillBehavior = FillBehavior.Stop, + EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut } + }; + + zoomLevelAnimation.Completed += ZoomLevelAnimationCompleted; + + updateTransform = false; + ZoomLevel = targetZoomLevel; + updateTransform = true; + + BeginAnimation(ZoomLevelProperty, zoomLevelAnimation); + } + } + + private void ZoomLevelAnimationCompleted(object sender, EventArgs eventArgs) + { + zoomLevelAnimation = null; + ResetTransformOrigin(); + } + + private double CoerceZoomLevel(double zoomLevel) + { + return Math.Min(Math.Max(zoomLevel, MinZoomLevel), MaxZoomLevel); + } + + private void SetHeading(double heading) + { + if (updateTransform) + { + SetViewTransform(); + } + + if (headingAnimation == null) + { + TargetHeading = heading; + } + } + + private void SetTargetHeading(double targetHeading) + { + if (targetHeading != Heading) + { + if (headingAnimation != null) + { + headingAnimation.Completed -= HeadingAnimationCompleted; + } + + double delta = targetHeading - Heading; + + if (delta > 180d) + { + delta -= 360d; + } + else if (delta < -180d) + { + delta += 360d; + } + + headingAnimation = new DoubleAnimation + { + From = Heading, + By = delta, + Duration = TimeSpan.FromSeconds(0.5), + FillBehavior = FillBehavior.Stop, + EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut } + }; + + headingAnimation.Completed += HeadingAnimationCompleted; + + updateTransform = false; + Heading = targetHeading; + updateTransform = true; + + BeginAnimation(HeadingProperty, headingAnimation); + } + } + + private void HeadingAnimationCompleted(object sender, EventArgs eventArgs) + { + headingAnimation = null; + } + + private double CoerceHeading(double heading) + { + return ((heading % 360d) + 360d) % 360d; + } + + private void SetViewTransform() + { + double scale; + + if (transformOrigin.HasValue) + { + scale = tileContainer.SetTransform(ZoomLevel, Heading, MapTransform.Transform(transformOrigin.Value), viewOrigin, RenderSize); + updateTransform = false; + Center = MapViewTransform.Inverse.Transform(new Point(RenderSize.Width / 2d, RenderSize.Height / 2d)); + updateTransform = true; + } + else + { + scale = tileContainer.SetTransform(ZoomLevel, Heading, MapTransform.Transform(Center), viewOrigin, RenderSize); + } + + scale *= MapTransform.RelativeScale(Center) / MeterPerDegree; // Pixels per meter at center latitude + + CenterScale = scale; + scaleTransform.ScaleX = scale; + scaleTransform.ScaleY = scale; + rotateTransform.Angle = Heading; + scaleRotateTransform.Matrix = scaleTransform.Value * rotateTransform.Value; + + OnViewTransformChanged(this); + } + } +} diff --git a/MapControl/MapControl.csproj b/MapControl/MapControl.csproj new file mode 100644 index 00000000..0e714c36 --- /dev/null +++ b/MapControl/MapControl.csproj @@ -0,0 +1,82 @@ + + + + Debug + AnyCPU + 8.0.30703 + 2.0 + {06481252-2310-414A-B9FC-D5739FDF6BD3} + Library + Properties + MapControl + MapControl + v4.0 + 512 + Client + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + Code + + + + + + + + + + + + + + + + + + + MSBuild:Compile + Designer + + + + + \ No newline at end of file diff --git a/MapControl/MapElement.cs b/MapControl/MapElement.cs new file mode 100644 index 00000000..342074f2 --- /dev/null +++ b/MapControl/MapElement.cs @@ -0,0 +1,44 @@ +using System; +using System.Windows; + +namespace MapControl +{ + internal interface INotifyParentMapChanged + { + void ParentMapChanged(Map oldParentMap, Map newParentMap); + } + + public abstract class MapElement : FrameworkElement, INotifyParentMapChanged + { + protected MapElement() + { + HorizontalAlignment = HorizontalAlignment.Stretch; + VerticalAlignment = VerticalAlignment.Stretch; + } + + public Map ParentMap + { + get { return MapPanel.GetParentMap(this); } + } + + protected abstract void OnViewTransformChanged(Map parentMap); + + private void OnViewTransformChanged(object sender, EventArgs eventArgs) + { + OnViewTransformChanged((Map)sender); + } + + void INotifyParentMapChanged.ParentMapChanged(Map oldParentMap, Map newParentMap) + { + if (oldParentMap != null) + { + oldParentMap.ViewTransformChanged -= OnViewTransformChanged; + } + + if (newParentMap != null) + { + newParentMap.ViewTransformChanged += OnViewTransformChanged; + } + } + } +} diff --git a/MapControl/MapGraticule.cs b/MapControl/MapGraticule.cs new file mode 100644 index 00000000..86ea3982 --- /dev/null +++ b/MapControl/MapGraticule.cs @@ -0,0 +1,203 @@ +using System; +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows.Shapes; + +namespace MapControl +{ + public class MapGraticule : MapElement + { + public static readonly DependencyProperty ForegroundProperty = Control.ForegroundProperty.AddOwner( + typeof(MapGraticule), new FrameworkPropertyMetadata((o, e) => ((MapGraticule)o).UpdateBrush())); + + public static readonly DependencyProperty FontSizeProperty = Control.FontSizeProperty.AddOwner( + typeof(MapGraticule)); + + public static readonly DependencyProperty FontFamilyProperty = Control.FontFamilyProperty.AddOwner( + typeof(MapGraticule), new FrameworkPropertyMetadata((o, e) => ((MapGraticule)o).typeface = null)); + + public static readonly DependencyProperty FontStyleProperty = Control.FontStyleProperty.AddOwner( + typeof(MapGraticule), new FrameworkPropertyMetadata((o, e) => ((MapGraticule)o).typeface = null)); + + public static readonly DependencyProperty FontWeightProperty = Control.FontWeightProperty.AddOwner( + typeof(MapGraticule), new FrameworkPropertyMetadata((o, e) => ((MapGraticule)o).typeface = null)); + + public static readonly DependencyProperty FontStretchProperty = Control.FontStretchProperty.AddOwner( + typeof(MapGraticule), new FrameworkPropertyMetadata((o, e) => ((MapGraticule)o).typeface = null)); + + public static readonly DependencyProperty StrokeProperty = Shape.StrokeProperty.AddOwner( + typeof(MapGraticule), new FrameworkPropertyMetadata((o, e) => ((MapGraticule)o).UpdateBrush())); + + public static readonly DependencyProperty StrokeThicknessProperty = Shape.StrokeThicknessProperty.AddOwner( + typeof(MapGraticule), new FrameworkPropertyMetadata(0.5, (o, e) => ((MapGraticule)o).pen.Thickness = (double)e.NewValue)); + + public static readonly DependencyProperty MinSpacingPixelsProperty = DependencyProperty.Register( + "MinSpacingPixels", typeof(double), typeof(MapGraticule), new FrameworkPropertyMetadata(100d)); + + public static double[] GridSpacings = + new double[] { 1d / 60d, 1d / 30d, 1d / 12d, 1d / 6d, 1d / 4d, 1d / 3d, 1d / 2d, 1d, 2d, 5d, 10d, 15d, 20d, 30d, 45d }; + + private readonly DrawingVisual visual = new DrawingVisual(); + private readonly Pen pen; + private Typeface typeface; + + public MapGraticule() + { + pen = new Pen(null, StrokeThickness); + IsHitTestVisible = false; + AddVisualChild(visual); + } + + public Brush Foreground + { + get { return (Brush)GetValue(ForegroundProperty); } + set { SetValue(ForegroundProperty, value); } + } + + public double FontSize + { + get { return (double)GetValue(FontSizeProperty); } + set { SetValue(FontSizeProperty, value); } + } + + public FontFamily FontFamily + { + get { return (FontFamily)GetValue(FontFamilyProperty); } + set { SetValue(FontFamilyProperty, value); } + } + + public FontStyle FontStyle + { + get { return (FontStyle)GetValue(FontStyleProperty); } + set { SetValue(FontStyleProperty, value); } + } + + public FontWeight FontWeight + { + get { return (FontWeight)GetValue(FontWeightProperty); } + set { SetValue(FontWeightProperty, value); } + } + + public FontStretch FontStretch + { + get { return (FontStretch)GetValue(FontStretchProperty); } + set { SetValue(FontStretchProperty, value); } + } + + public Brush Stroke + { + get { return (Brush)GetValue(StrokeProperty); } + set { SetValue(StrokeProperty, value); } + } + + public double StrokeThickness + { + get { return (double)GetValue(StrokeThicknessProperty); } + set { SetValue(StrokeThicknessProperty, value); } + } + + public double MinSpacingPixels + { + get { return (double)GetValue(MinSpacingPixelsProperty); } + set { SetValue(MinSpacingPixelsProperty, value); } + } + + protected override int VisualChildrenCount + { + get { return 1; } + } + + protected override Visual GetVisualChild(int index) + { + return visual; + } + + protected override void OnViewTransformChanged(Map parentMap) + { + Rect bounds = parentMap.MapViewTransform.Inverse.TransformBounds(new Rect(parentMap.RenderSize)); + double minSpacing = MinSpacingPixels * 360d / (Math.Pow(2d, parentMap.ZoomLevel) * 256d); + double spacing = GridSpacings[GridSpacings.Length - 1]; + + if (spacing >= minSpacing) + { + spacing = GridSpacings.FirstOrDefault(s => s >= minSpacing); + } + + double longitudeStart = Math.Ceiling(bounds.Left / spacing) * spacing; + double latitudeStart = Math.Ceiling(bounds.Top / spacing) * spacing; + + if (pen.Brush == null) + { + pen.Brush = Stroke != null ? Stroke : Foreground; + } + + using (DrawingContext drawingContext = visual.RenderOpen()) + { + for (double lon = longitudeStart; lon <= bounds.Right; lon += spacing) + { + drawingContext.DrawLine(pen, + parentMap.MapViewTransform.Transform(new Point(lon, bounds.Bottom)), + parentMap.MapViewTransform.Transform(new Point(lon, bounds.Top))); + } + + for (double lat = latitudeStart; lat <= bounds.Bottom; lat += spacing) + { + drawingContext.DrawLine(pen, + parentMap.MapViewTransform.Transform(new Point(bounds.Left, lat)), + parentMap.MapViewTransform.Transform(new Point(bounds.Right, lat))); + } + + if (Foreground != null && Foreground != Brushes.Transparent) + { + string format = spacing < 1d ? "{0} {1}°{2:00}'" : "{0} {1}°"; + + if (typeface == null) + { + typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch); + } + + for (double lon = longitudeStart; lon <= bounds.Right; lon += spacing) + { + for (double lat = latitudeStart; lat <= bounds.Bottom; lat += spacing) + { + double t = StrokeThickness / 2d; + Point p = parentMap.MapViewTransform.Transform(new Point(lon, lat)); + Point latPos = new Point(p.X + t + 2d, p.Y - t - FontSize / 4d); + Point lonPos = new Point(p.X + t + 2d, p.Y + t + FontSize); + string latString = CoordinateString(lat, format, "NS"); + string lonString = CoordinateString(((lon + 180d) % 360d + 360d) % 360d - 180d, format, "EW"); + + drawingContext.PushTransform(new RotateTransform(parentMap.Heading, p.X, p.Y)); + drawingContext.DrawGlyphRun(Foreground, GlyphRunText.Create(latString, typeface, FontSize, latPos)); + drawingContext.DrawGlyphRun(Foreground, GlyphRunText.Create(lonString, typeface, FontSize, lonPos)); + drawingContext.Pop(); + } + } + } + } + } + + private void UpdateBrush() + { + pen.Brush = null; + OnViewTransformChanged(ParentMap); + } + + private static string CoordinateString(double value, string format, string hemispheres) + { + char hemisphere = hemispheres[0]; + + if (value < -1e-8) // ~1mm + { + value = -value; + hemisphere = hemispheres[1]; + } + + int minutes = (int)(value * 60d + 0.5); + + return string.Format(format, hemisphere, minutes / 60, (double)(minutes % 60)); + } + } +} diff --git a/MapControl/MapInput.cs b/MapControl/MapInput.cs new file mode 100644 index 00000000..df9bc807 --- /dev/null +++ b/MapControl/MapInput.cs @@ -0,0 +1,81 @@ +using System; +using System.Windows; +using System.Windows.Input; + +namespace MapControl +{ + public partial class Map + { + private double mouseWheelZoom = 0.25; + private Point? mousePosition; + + public double MouseWheelZoom + { + get { return mouseWheelZoom; } + set { mouseWheelZoom = value; } + } + + protected override void OnMouseWheel(MouseWheelEventArgs eventArgs) + { + base.OnMouseWheel(eventArgs); + + ZoomMap(eventArgs.GetPosition(this), TargetZoomLevel + mouseWheelZoom * Math.Sign(eventArgs.Delta)); + } + + protected override void OnMouseRightButtonDown(MouseButtonEventArgs eventArgs) + { + base.OnMouseRightButtonDown(eventArgs); + + if (eventArgs.ClickCount == 2) + { + ZoomMap(eventArgs.GetPosition(this), Math.Ceiling(ZoomLevel - 1.5)); + } + } + + protected override void OnMouseLeftButtonDown(MouseButtonEventArgs eventArgs) + { + base.OnMouseLeftButtonDown(eventArgs); + + if (eventArgs.ClickCount == 1) + { + mousePosition = eventArgs.GetPosition(this); + CaptureMouse(); + } + else if (eventArgs.ClickCount == 2) + { + ZoomMap(eventArgs.GetPosition(this), Math.Floor(ZoomLevel + 1.5)); + } + } + + protected override void OnMouseLeftButtonUp(MouseButtonEventArgs eventArgs) + { + base.OnMouseLeftButtonUp(eventArgs); + + if (mousePosition.HasValue) + { + mousePosition = null; + ReleaseMouseCapture(); + } + } + + protected override void OnMouseMove(MouseEventArgs eventArgs) + { + base.OnMouseMove(eventArgs); + + if (mousePosition.HasValue) + { + Point position = eventArgs.GetPosition(this); + TranslateMap(position - mousePosition.Value); + mousePosition = position; + } + } + + protected override void OnManipulationDelta(ManipulationDeltaEventArgs eventArgs) + { + base.OnManipulationDelta(eventArgs); + + ManipulationDelta d = eventArgs.DeltaManipulation; + TransformMap(eventArgs.ManipulationOrigin, d.Translation, d.Rotation, (d.Scale.X + d.Scale.Y) / 2d); + } + } +} diff --git a/MapControl/MapItem.cs b/MapControl/MapItem.cs new file mode 100644 index 00000000..0b877071 --- /dev/null +++ b/MapControl/MapItem.cs @@ -0,0 +1,210 @@ +using System; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Input; +using System.Windows.Media; + +namespace MapControl +{ + [TemplateVisualState(GroupName = "CommonStates", Name = "Normal")] + [TemplateVisualState(GroupName = "CommonStates", Name = "Disabled")] + [TemplateVisualState(GroupName = "CommonStates", Name = "MouseOver")] + [TemplateVisualState(GroupName = "SelectionStates", Name = "Unselected")] + [TemplateVisualState(GroupName = "SelectionStates", Name = "Selected")] + [TemplateVisualState(GroupName = "CurrentStates", Name = "NonCurrent")] + [TemplateVisualState(GroupName = "CurrentStates", Name = "Current")] + public class MapItem : ContentControl + { + public static readonly RoutedEvent SelectedEvent = ListBoxItem.SelectedEvent.AddOwner(typeof(MapItem)); + public static readonly RoutedEvent UnselectedEvent = ListBoxItem.UnselectedEvent.AddOwner(typeof(MapItem)); + + public static readonly DependencyProperty LocationProperty = MapPanel.LocationProperty.AddOwner(typeof(MapItem)); + public static readonly DependencyProperty ViewPositionProperty = MapPanel.ViewPositionProperty.AddOwner(typeof(MapItem)); + public static readonly DependencyProperty ViewPositionTransformProperty = MapPanel.ViewPositionTransformProperty.AddOwner(typeof(MapItem)); + + public static readonly DependencyProperty IsSelectedProperty = Selector.IsSelectedProperty.AddOwner( + typeof(MapItem), new FrameworkPropertyMetadata((o, e) => ((MapItem)o).IsSelectedChanged((bool)e.NewValue))); + + private static readonly DependencyPropertyKey IsCurrentPropertyKey = DependencyProperty.RegisterReadOnly( + "IsCurrent", typeof(bool), typeof(MapItem), null); + + public static readonly DependencyProperty IsCurrentProperty = IsCurrentPropertyKey.DependencyProperty; + + private static readonly DependencyPropertyKey IsInsideMapBoundsPropertyKey = DependencyProperty.RegisterReadOnly( + "IsInsideMapBounds", typeof(bool), typeof(MapItem), null); + + public static readonly DependencyProperty IsInsideMapBoundsProperty = IsInsideMapBoundsPropertyKey.DependencyProperty; + + private object item; + + static MapItem() + { + FrameworkElement.DefaultStyleKeyProperty.OverrideMetadata(typeof(MapItem), + new FrameworkPropertyMetadata(typeof(MapItem))); + + UIElement.IsEnabledProperty.OverrideMetadata(typeof(MapItem), + new FrameworkPropertyMetadata((o, e) => ((MapItem)o).CommonStateChanged())); + + MapPanel.ViewPositionPropertyKey.OverrideMetadata(typeof(MapItem), + new FrameworkPropertyMetadata((o, e) => ((MapItem)o).ViewPositionChanged((Point)e.NewValue))); + } + + public event RoutedEventHandler Selected + { + add { AddHandler(SelectedEvent, value); } + remove { RemoveHandler(SelectedEvent, value); } + } + + public event RoutedEventHandler Unselected + { + add { AddHandler(UnselectedEvent, value); } + remove { RemoveHandler(UnselectedEvent, value); } + } + + public Map ParentMap + { + get { return MapPanel.GetParentMap(this); } + } + + public Point? Location + { + get { return (Point?)GetValue(LocationProperty); } + set { SetValue(LocationProperty, value); } + } + + public bool HasViewPosition + { + get { return ReadLocalValue(ViewPositionProperty) != DependencyProperty.UnsetValue; } + } + + public Point ViewPosition + { + get { return (Point)GetValue(ViewPositionProperty); } + } + + public Transform ViewPositionTransform + { + get { return (Transform)GetValue(ViewPositionTransformProperty); } + } + + public bool IsSelected + { + get { return (bool)GetValue(IsSelectedProperty); } + set { SetValue(IsSelectedProperty, value); } + } + + public bool IsCurrent + { + get { return (bool)GetValue(IsCurrentProperty); } + internal set + { + if (IsCurrent != value) + { + SetValue(IsCurrentPropertyKey, value); + int zIndex = Panel.GetZIndex(this); + Panel.SetZIndex(this, value ? (zIndex | 0x40000000) : (zIndex & ~0x40000000)); + VisualStateManager.GoToState(this, value ? "Current" : "NonCurrent", true); + } + } + } + + public bool IsInsideMapBounds + { + get { return (bool)GetValue(IsInsideMapBoundsProperty); } + } + + public object Item + { + get { return item; } + internal set + { + item = value; + if (HasViewPosition) + { + ViewPositionChanged(ViewPosition); + } + } + } + + protected override void OnMouseEnter(MouseEventArgs e) + { + base.OnMouseEnter(e); + CommonStateChanged(); + } + + protected override void OnMouseLeave(MouseEventArgs e) + { + base.OnMouseLeave(e); + CommonStateChanged(); + } + + protected override void OnMouseLeftButtonDown(MouseButtonEventArgs eventArgs) + { + base.OnMouseLeftButtonDown(eventArgs); + eventArgs.Handled = true; + IsSelected = !IsSelected; + } + + protected override void OnTouchDown(TouchEventArgs eventArgs) + { + base.OnTouchDown(eventArgs); + eventArgs.Handled = true; // get TouchUp event + } + + protected override void OnTouchUp(TouchEventArgs eventArgs) + { + base.OnTouchUp(eventArgs); + eventArgs.Handled = true; + IsSelected = !IsSelected; + } + + protected virtual void OnViewPositionChanged(Point viewPosition) + { + } + + private void ViewPositionChanged(Point viewPosition) + { + Map map = ParentMap; + + if (map != null) + { + SetValue(IsInsideMapBoundsPropertyKey, + viewPosition.X >= 0d && viewPosition.X <= map.ActualWidth && + viewPosition.Y >= 0d && viewPosition.Y <= map.ActualHeight); + + OnViewPositionChanged(viewPosition); + } + } + + private void CommonStateChanged() + { + if (!IsEnabled) + { + VisualStateManager.GoToState(this, "Disabled", true); + } + else if (IsMouseOver) + { + VisualStateManager.GoToState(this, "MouseOver", true); + } + else + { + VisualStateManager.GoToState(this, "Normal", true); + } + } + + private void IsSelectedChanged(bool isSelected) + { + if (isSelected) + { + VisualStateManager.GoToState(this, "Selected", true); + RaiseEvent(new RoutedEventArgs(SelectedEvent)); + } + else + { + VisualStateManager.GoToState(this, "Unselected", true); + RaiseEvent(new RoutedEventArgs(UnselectedEvent)); + } + } + } +} diff --git a/MapControl/MapItemsControl.cs b/MapControl/MapItemsControl.cs new file mode 100644 index 00000000..58ec9cbd --- /dev/null +++ b/MapControl/MapItemsControl.cs @@ -0,0 +1,128 @@ +using System; +using System.ComponentModel; +using System.Windows; +using System.Windows.Controls.Primitives; +using System.Windows.Media; + +namespace MapControl +{ + public enum MapItemSelectionMode { Single, Extended } + + public class MapItemsControl : MultiSelector + { + public static readonly DependencyProperty SelectionModeProperty = DependencyProperty.Register( + "SelectionMode", typeof(MapItemSelectionMode), typeof(MapItemsControl), + new FrameworkPropertyMetadata((o, e) => ((MapItemsControl)o).CanSelectMultipleItems = (MapItemSelectionMode)e.NewValue != MapItemSelectionMode.Single)); + + public static readonly DependencyProperty SelectionGeometryProperty = DependencyProperty.Register( + "SelectionGeometry", typeof(Geometry), typeof(MapItemsControl), + new FrameworkPropertyMetadata((o, e) => ((MapItemsControl)o).SelectionGeometryChanged((Geometry)e.NewValue))); + + public MapItemsControl() + { + CanSelectMultipleItems = false; + Style = (Style)FindResource(typeof(MapItemsControl)); + Items.CurrentChanging += OnCurrentItemChanging; + Items.CurrentChanged += OnCurrentItemChanged; + } + + public MapItemSelectionMode SelectionMode + { + get { return (MapItemSelectionMode)GetValue(SelectionModeProperty); } + set { SetValue(SelectionModeProperty, value); } + } + + public Geometry SelectionGeometry + { + get { return (Geometry)GetValue(SelectionGeometryProperty); } + set { SetValue(SelectionGeometryProperty, value); } + } + + public MapItem GetMapItem(object item) + { + return item != null ? ItemContainerGenerator.ContainerFromItem(item) as MapItem : null; + } + + public object GetHitItem(Point point) + { + DependencyObject obj = InputHitTest(point) as DependencyObject; + + while (obj != null) + { + if (obj is MapItem) + { + return ((MapItem)obj).Item; + } + + obj = VisualTreeHelper.GetParent(obj); + } + + return null; + } + + protected override DependencyObject GetContainerForItemOverride() + { + return new MapItem(); + } + + protected override void PrepareContainerForItemOverride(DependencyObject element, object item) + { + MapItem mapItem = (MapItem)element; + mapItem.Item = item; + base.PrepareContainerForItemOverride(element, item); + } + + protected override void ClearContainerForItemOverride(DependencyObject element, object item) + { + MapItem mapItem = (MapItem)element; + mapItem.Item = null; + base.ClearContainerForItemOverride(element, item); + } + + private void OnCurrentItemChanging(object sender, CurrentChangingEventArgs eventArgs) + { + MapItem mapItem = GetMapItem(Items.CurrentItem); + + if (mapItem != null) + { + mapItem.IsCurrent = false; + } + } + + private void OnCurrentItemChanged(object sender, EventArgs eventArgs) + { + MapItem mapItem = GetMapItem(Items.CurrentItem); + + if (mapItem != null) + { + mapItem.IsCurrent = true; + } + } + + private void SelectionGeometryChanged(Geometry geometry) + { + if (geometry != null) + { + SelectionMode = MapItemSelectionMode.Extended; + + BeginUpdateSelectedItems(); + SelectedItems.Clear(); + + if (!geometry.IsEmpty()) + { + foreach (object item in Items) + { + MapItem mapItem = GetMapItem(item); + + if (mapItem != null && mapItem.HasViewPosition && geometry.FillContains(mapItem.ViewPosition)) + { + SelectedItems.Add(item); + } + } + } + + EndUpdateSelectedItems(); + } + } + } +} diff --git a/MapControl/MapPanel.cs b/MapControl/MapPanel.cs new file mode 100644 index 00000000..e899387e --- /dev/null +++ b/MapControl/MapPanel.cs @@ -0,0 +1,253 @@ +using System; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; + +namespace MapControl +{ + public class MapPanel : Panel, INotifyParentMapChanged + { + public static readonly DependencyProperty ParentMapProperty = DependencyProperty.RegisterAttached( + "ParentMap", typeof(Map), typeof(MapPanel), + new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.Inherits, ParentMapPropertyChanged)); + + public static readonly DependencyProperty LocationProperty = DependencyProperty.RegisterAttached( + "Location", typeof(Point?), typeof(MapPanel), + new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender, LocationPropertyChanged)); + + internal static readonly DependencyPropertyKey ViewPositionPropertyKey = DependencyProperty.RegisterAttachedReadOnly( + "ViewPosition", typeof(Point), typeof(MapPanel), null); + + private static readonly DependencyPropertyKey ViewPositionTransformPropertyKey = DependencyProperty.RegisterAttachedReadOnly( + "ViewPositionTransform", typeof(Transform), typeof(MapPanel), null); + + public static readonly DependencyProperty ViewPositionProperty = ViewPositionPropertyKey.DependencyProperty; + public static readonly DependencyProperty ViewPositionTransformProperty = ViewPositionTransformPropertyKey.DependencyProperty; + + public MapPanel() + { + ClipToBounds = true; + } + + public Map ParentMap + { + get { return (Map)GetValue(ParentMapProperty); } + } + + public static Map GetParentMap(UIElement element) + { + return (Map)element.GetValue(ParentMapProperty); + } + + public static Point? GetLocation(UIElement element) + { + return (Point?)element.GetValue(LocationProperty); + } + + public static void SetLocation(UIElement element, Point? value) + { + element.SetValue(LocationProperty, value); + } + + public static Point GetViewPosition(UIElement element) + { + return (Point)element.GetValue(ViewPositionProperty); + } + + public static Transform GetViewPositionTransform(UIElement element) + { + return (Transform)element.GetValue(ViewPositionTransformProperty); + } + + protected override Size MeasureOverride(Size availableSize) + { + Size infiniteSize = new Size(double.PositiveInfinity, double.PositiveInfinity); + + foreach (UIElement element in InternalChildren) + { + element.Measure(infiniteSize); + } + + return new Size(); + } + + protected override Size ArrangeOverride(Size finalSize) + { + foreach (UIElement element in InternalChildren) + { + object viewPosition = element.ReadLocalValue(ViewPositionProperty); + + if (viewPosition == DependencyProperty.UnsetValue || + !ArrangeElement(element, (Point)viewPosition)) + { + ArrangeElement(element, finalSize); + } + } + + return finalSize; + } + + protected virtual void OnViewTransformChanged(Map parentMap) + { + foreach (UIElement element in InternalChildren) + { + Point? location = GetLocation(element); + + if (location.HasValue) + { + SetViewPosition(element, parentMap, location); + } + } + } + + private void OnViewTransformChanged(object sender, EventArgs eventArgs) + { + OnViewTransformChanged((Map)sender); + } + + void INotifyParentMapChanged.ParentMapChanged(Map oldParentMap, Map newParentMap) + { + if (oldParentMap != null && oldParentMap != this) + { + oldParentMap.ViewTransformChanged -= OnViewTransformChanged; + } + + if (newParentMap != null && newParentMap != this) + { + newParentMap.ViewTransformChanged += OnViewTransformChanged; + } + } + + private static void ParentMapPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs eventArgs) + { + INotifyParentMapChanged notifyChanged = obj as INotifyParentMapChanged; + + if (notifyChanged != null) + { + notifyChanged.ParentMapChanged(eventArgs.OldValue as Map, eventArgs.NewValue as Map); + } + } + + private static void LocationPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs eventArgs) + { + UIElement element = (UIElement)obj; + Point? location = (Point?)eventArgs.NewValue; + Map parentMap; + + if (location.HasValue && (parentMap = Map.GetParentMap(element)) != null) + { + SetViewPosition(element, parentMap, location); + } + else + { + element.ClearValue(ViewPositionPropertyKey); + element.ClearValue(ViewPositionTransformPropertyKey); + element.Arrange(new Rect()); + } + } + + private static void SetViewPosition(UIElement element, Map parentMap, Point? location) + { + Point viewPosition = parentMap.MapViewTransform.Transform(location.Value); + + element.SetValue(ViewPositionPropertyKey, viewPosition); + + Matrix matrix = new Matrix(1d, 0d, 0d, 1d, viewPosition.X, viewPosition.Y); + MatrixTransform viewTransform = element.GetValue(ViewPositionTransformProperty) as MatrixTransform; + + if (viewTransform != null) + { + viewTransform.Matrix = matrix; + } + else + { + element.SetValue(ViewPositionTransformPropertyKey, new MatrixTransform(matrix)); + } + + ArrangeElement(element, viewPosition); + } + + private static bool ArrangeElement(UIElement element, Point position) + { + Rect rect = new Rect(position, element.DesiredSize); + FrameworkElement frameworkElement = element as FrameworkElement; + + if (frameworkElement != null) + { + if (frameworkElement.HorizontalAlignment == HorizontalAlignment.Stretch && + frameworkElement.VerticalAlignment == VerticalAlignment.Stretch) + { + return false; // do not arrange at position + } + + switch (frameworkElement.HorizontalAlignment) + { + case HorizontalAlignment.Center: + rect.X -= rect.Width / 2d; + break; + case HorizontalAlignment.Right: + rect.X -= rect.Width; + break; + default: + break; + } + + switch (frameworkElement.VerticalAlignment) + { + case VerticalAlignment.Center: + rect.Y -= rect.Height / 2d; + break; + case VerticalAlignment.Bottom: + rect.Y -= rect.Height; + break; + default: + break; + } + } + + element.Arrange(rect); + return true; + } + + private static void ArrangeElement(UIElement element, Size panelSize) + { + Rect rect = new Rect(element.DesiredSize); + FrameworkElement frameworkElement = element as FrameworkElement; + + if (frameworkElement != null) + { + switch (frameworkElement.HorizontalAlignment) + { + case HorizontalAlignment.Center: + rect.X = (panelSize.Width - rect.Width) / 2d; + break; + case HorizontalAlignment.Right: + rect.X = panelSize.Width - rect.Width; + break; + case HorizontalAlignment.Stretch: + rect.Width = panelSize.Width; + break; + default: + break; + } + + switch (frameworkElement.VerticalAlignment) + { + case VerticalAlignment.Center: + rect.Y = (panelSize.Height - rect.Height) / 2d; + break; + case VerticalAlignment.Bottom: + rect.Y = panelSize.Height - rect.Height; + break; + case VerticalAlignment.Stretch: + rect.Height = panelSize.Height; + break; + default: + break; + } + } + + element.Arrange(rect); + } + } +} diff --git a/MapControl/MapPath.cs b/MapControl/MapPath.cs new file mode 100644 index 00000000..6b430c8b --- /dev/null +++ b/MapControl/MapPath.cs @@ -0,0 +1,200 @@ +using System; +using System.Windows; +using System.Windows.Media; +using System.Windows.Shapes; + +namespace MapControl +{ + public class MapPath : MapElement + { + public static readonly DependencyProperty DataProperty = Path.DataProperty.AddOwner( + typeof(MapPath), new FrameworkPropertyMetadata((o, e) => ((MapPath)o).UpdateGeometry())); + + public static readonly DependencyProperty FillProperty = Shape.FillProperty.AddOwner( + typeof(MapPath), new FrameworkPropertyMetadata((o, e) => ((MapPath)o).drawing.Brush = (Brush)e.NewValue)); + + public static readonly DependencyProperty StrokeProperty = Shape.StrokeProperty.AddOwner( + typeof(MapPath), new FrameworkPropertyMetadata(Brushes.Black, (o, e) => ((MapPath)o).drawing.Pen.Brush = (Brush)e.NewValue)); + + public static readonly DependencyProperty StrokeDashArrayProperty = Shape.StrokeDashArrayProperty.AddOwner( + typeof(MapPath), new FrameworkPropertyMetadata((o, e) => ((MapPath)o).drawing.Pen.DashStyle = new DashStyle((DoubleCollection)e.NewValue, ((MapPath)o).StrokeDashOffset))); + + public static readonly DependencyProperty StrokeDashOffsetProperty = Shape.StrokeDashOffsetProperty.AddOwner( + typeof(MapPath), new FrameworkPropertyMetadata((o, e) => ((MapPath)o).drawing.Pen.DashStyle = new DashStyle(((MapPath)o).StrokeDashArray, (double)e.NewValue))); + + public static readonly DependencyProperty StrokeDashCapProperty = Shape.StrokeDashCapProperty.AddOwner( + typeof(MapPath), new FrameworkPropertyMetadata((o, e) => ((MapPath)o).drawing.Pen.DashCap = (PenLineCap)e.NewValue)); + + public static readonly DependencyProperty StrokeStartLineCapProperty = Shape.StrokeStartLineCapProperty.AddOwner( + typeof(MapPath), new FrameworkPropertyMetadata((o, e) => ((MapPath)o).drawing.Pen.StartLineCap = (PenLineCap)e.NewValue)); + + public static readonly DependencyProperty StrokeEndLineCapProperty = Shape.StrokeEndLineCapProperty.AddOwner( + typeof(MapPath), new FrameworkPropertyMetadata((o, e) => ((MapPath)o).drawing.Pen.EndLineCap = (PenLineCap)e.NewValue)); + + public static readonly DependencyProperty StrokeLineJoinProperty = Shape.StrokeLineJoinProperty.AddOwner( + typeof(MapPath), new FrameworkPropertyMetadata((o, e) => ((MapPath)o).drawing.Pen.LineJoin = (PenLineJoin)e.NewValue)); + + public static readonly DependencyProperty StrokeMiterLimitProperty = Shape.StrokeMiterLimitProperty.AddOwner( + typeof(MapPath), new FrameworkPropertyMetadata((o, e) => ((MapPath)o).drawing.Pen.MiterLimit = (double)e.NewValue)); + + public static readonly DependencyProperty StrokeThicknessProperty = Shape.StrokeThicknessProperty.AddOwner( + typeof(MapPath), new FrameworkPropertyMetadata((o, e) => ((MapPath)o).UpdatePenThickness())); + + public static readonly DependencyProperty TransformStrokeProperty = DependencyProperty.Register( + "TransformStroke", typeof(bool), typeof(MapPath), new FrameworkPropertyMetadata((o, e) => ((MapPath)o).UpdatePenThickness())); + + private readonly DrawingVisual visual = new DrawingVisual(); + private readonly GeometryDrawing drawing = new GeometryDrawing(); + + public MapPath() + { + drawing.Brush = Fill; + drawing.Pen = new Pen(Stroke, StrokeThickness); + + using (DrawingContext drawingContext = visual.RenderOpen()) + { + drawingContext.DrawDrawing(drawing); + } + + Loaded += (o, e) => UpdateGeometry(); + } + + public Geometry Data + { + get { return (Geometry)GetValue(DataProperty); } + set { SetValue(DataProperty, value); } + } + + public Brush Fill + { + get { return (Brush)GetValue(FillProperty); } + set { SetValue(FillProperty, value); } + } + + public Brush Stroke + { + get { return (Brush)GetValue(StrokeProperty); } + set { SetValue(StrokeProperty, value); } + } + + public DoubleCollection StrokeDashArray + { + get { return (DoubleCollection)GetValue(StrokeDashArrayProperty); } + set { SetValue(StrokeDashArrayProperty, value); } + } + + public double StrokeDashOffset + { + get { return (double)GetValue(StrokeDashOffsetProperty); } + set { SetValue(StrokeDashOffsetProperty, value); } + } + + public PenLineCap StrokeDashCap + { + get { return (PenLineCap)GetValue(StrokeDashCapProperty); } + set { SetValue(StrokeDashCapProperty, value); } + } + + public PenLineCap StrokeStartLineCap + { + get { return (PenLineCap)GetValue(StrokeStartLineCapProperty); } + set { SetValue(StrokeStartLineCapProperty, value); } + } + + public PenLineCap StrokeEndLineCap + { + get { return (PenLineCap)GetValue(StrokeEndLineCapProperty); } + set { SetValue(StrokeEndLineCapProperty, value); } + } + + public PenLineJoin StrokeLineJoin + { + get { return (PenLineJoin)GetValue(StrokeLineJoinProperty); } + set { SetValue(StrokeLineJoinProperty, value); } + } + + public double StrokeMiterLimit + { + get { return (double)GetValue(StrokeMiterLimitProperty); } + set { SetValue(StrokeMiterLimitProperty, value); } + } + + public double StrokeThickness + { + get { return (double)GetValue(StrokeThicknessProperty); } + set { SetValue(StrokeThicknessProperty, value); } + } + + public bool TransformStroke + { + get { return (bool)GetValue(TransformStrokeProperty); } + set { SetValue(TransformStrokeProperty, value); } + } + + public double TransformedStrokeThickness + { + get { return drawing.Pen.Thickness; } + } + + public PathGeometry TransformedGeometry + { + get { return drawing.Geometry as PathGeometry; } + } + + protected override int VisualChildrenCount + { + get { return 1; } + } + + protected override Visual GetVisualChild(int index) + { + return visual; + } + + protected override void OnInitialized(EventArgs eventArgs) + { + base.OnInitialized(eventArgs); + + AddVisualChild(visual); + } + + protected override void OnViewTransformChanged(Map parentMap) + { + double scale = 1d; + + if (TransformStroke && Data != null) + { + Point center = Data.Bounds.Location + (Vector)Data.Bounds.Size / 2d; + scale = parentMap.GetMapScale(center) * Map.MeterPerDegree; + } + + drawing.Pen.Thickness = scale * StrokeThickness; + } + + private void UpdateGeometry() + { + Map parentMap = MapPanel.GetParentMap(this); + + if (parentMap != null && Data != null) + { + drawing.Geometry = parentMap.MapTransform.Transform(Data); + drawing.Geometry.Transform = parentMap.ViewTransform; + OnViewTransformChanged(parentMap); + } + else + { + drawing.Geometry = null; + } + } + + private void UpdatePenThickness() + { + Map parentMap = MapPanel.GetParentMap(this); + + if (parentMap != null) + { + OnViewTransformChanged(parentMap); + } + } + } +} diff --git a/MapControl/MapPathGeometry.cs b/MapControl/MapPathGeometry.cs new file mode 100644 index 00000000..6694db4b --- /dev/null +++ b/MapControl/MapPathGeometry.cs @@ -0,0 +1,155 @@ +using System; +using System.Linq; +using System.Windows; +using System.Windows.Media; + +namespace MapControl +{ + public static class MapPathGeometry + { + public static PathGeometry Transform(this GeneralTransform transform, Geometry geometry) + { + PathGeometry pathGeometry = geometry as PathGeometry; + + if (pathGeometry == null) + { + pathGeometry = PathGeometry.CreateFromGeometry(geometry); + } + + if (geometry.Transform != null && geometry.Transform != System.Windows.Media.Transform.Identity) + { + GeneralTransformGroup transformGroup = new GeneralTransformGroup(); + transformGroup.Children.Add(geometry.Transform); + transformGroup.Children.Add(transform); + transform = transformGroup; + } + + return new PathGeometry(Transform(transform, pathGeometry.Figures), + pathGeometry.FillRule, System.Windows.Media.Transform.Identity); + } + + public static PathFigureCollection Transform(this GeneralTransform transform, PathFigureCollection figures) + { + PathFigureCollection transformedFigures = new PathFigureCollection(); + + foreach (PathFigure figure in figures) + { + transformedFigures.Add(Transform(transform, figure)); + } + + transformedFigures.Freeze(); + + return transformedFigures; + } + + public static PathFigure Transform(this GeneralTransform transform, PathFigure figure) + { + PathSegmentCollection transformedSegments = new PathSegmentCollection(figure.Segments.Count); + + foreach (PathSegment segment in figure.Segments) + { + PathSegment transformedSegment = null; + + if (segment is LineSegment) + { + LineSegment lineSegment = (LineSegment)segment; + + transformedSegment = new LineSegment( + transform.Transform(lineSegment.Point), + lineSegment.IsStroked); + } + else if (segment is PolyLineSegment) + { + PolyLineSegment polyLineSegment = (PolyLineSegment)segment; + + transformedSegment = new PolyLineSegment( + polyLineSegment.Points.Select(transform.Transform), + polyLineSegment.IsStroked); + } + else if (segment is ArcSegment) + { + ArcSegment arcSegment = (ArcSegment)segment; + Size size = arcSegment.Size; + MapTransform mapTransform = transform as MapTransform; + + if (mapTransform != null) + { + double yScale = mapTransform.RelativeScale(arcSegment.Point); + + if (arcSegment.RotationAngle == 0d) + { + size.Height *= yScale; + } + else + { + double sinR = Math.Sin(arcSegment.RotationAngle * Math.PI / 180d); + double cosR = Math.Cos(arcSegment.RotationAngle * Math.PI / 180d); + + size.Width *= Math.Sqrt(yScale * yScale * sinR * sinR + cosR * cosR); + size.Height *= Math.Sqrt(yScale * yScale * cosR * cosR + sinR * sinR); + } + } + + transformedSegment = new ArcSegment( + transform.Transform(arcSegment.Point), + size, + arcSegment.RotationAngle, + arcSegment.IsLargeArc, + arcSegment.SweepDirection, + arcSegment.IsStroked); + } + else if (segment is BezierSegment) + { + BezierSegment bezierSegment = (BezierSegment)segment; + + transformedSegment = new BezierSegment( + transform.Transform(bezierSegment.Point1), + transform.Transform(bezierSegment.Point2), + transform.Transform(bezierSegment.Point3), + bezierSegment.IsStroked); + } + else if (segment is PolyBezierSegment) + { + PolyBezierSegment polyBezierSegment = (PolyBezierSegment)segment; + + transformedSegment = new PolyBezierSegment( + polyBezierSegment.Points.Select(transform.Transform), + polyBezierSegment.IsStroked); + } + else if (segment is QuadraticBezierSegment) + { + QuadraticBezierSegment quadraticBezierSegment = (QuadraticBezierSegment)segment; + + transformedSegment = new QuadraticBezierSegment( + transform.Transform(quadraticBezierSegment.Point1), + transform.Transform(quadraticBezierSegment.Point2), + quadraticBezierSegment.IsStroked); + } + else if (segment is PolyQuadraticBezierSegment) + { + PolyQuadraticBezierSegment polyQuadraticBezierSegment = (PolyQuadraticBezierSegment)segment; + + transformedSegment = new PolyQuadraticBezierSegment( + polyQuadraticBezierSegment.Points.Select(transform.Transform), + polyQuadraticBezierSegment.IsStroked); + } + + if (transformedSegment != null) + { + transformedSegment.IsSmoothJoin = segment.IsSmoothJoin; + transformedSegments.Add(transformedSegment); + } + } + + PathFigure transformedFigure = new PathFigure( + transform.Transform(figure.StartPoint), + transformedSegments, + figure.IsClosed); + + transformedFigure.IsFilled = figure.IsFilled; + transformedFigure.Freeze(); + + return transformedFigure; + } + } +} diff --git a/MapControl/MapStreamGeometry.cs b/MapControl/MapStreamGeometry.cs new file mode 100644 index 00000000..5866c84e --- /dev/null +++ b/MapControl/MapStreamGeometry.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Windows; +using System.Windows.Media; + +namespace MapControl +{ + public static class MapStreamGeometry + { + public static MapStreamGeometryContext Open(this StreamGeometry mapGeometry, MapTransform transform) + { + return new MapStreamGeometryContext(mapGeometry.Open(), transform); + } + } + + public class MapStreamGeometryContext : IDisposable + { + StreamGeometryContext context; + MapTransform transform; + + public MapStreamGeometryContext(StreamGeometryContext context, MapTransform transform) + { + this.context = context; + this.transform = transform; + } + + void IDisposable.Dispose() + { + context.Close(); + } + + public void Close() + { + context.Close(); + } + + public void BeginFigure(Point startPoint, bool isFilled, bool isClosed) + { + context.BeginFigure(transform.Transform(startPoint), isFilled, isClosed); + } + + public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection, bool isStroked, bool isSmoothJoin) + { + double yScale = transform.RelativeScale(point); + + if (rotationAngle == 0d) + { + size.Height *= yScale; + } + else + { + double sinR = Math.Sin(rotationAngle * Math.PI / 180d); + double cosR = Math.Cos(rotationAngle * Math.PI / 180d); + + size.Width *= Math.Sqrt(yScale * yScale * sinR * sinR + cosR * cosR); + size.Height *= Math.Sqrt(yScale * yScale * cosR * cosR + sinR * sinR); + } + + context.ArcTo(transform.Transform(point), size, rotationAngle, isLargeArc, sweepDirection, isStroked, isSmoothJoin); + } + + public void LineTo(Point point, bool isStroked, bool isSmoothJoin) + { + context.LineTo(transform.Transform(point), isStroked, isSmoothJoin); + } + + public void QuadraticBezierTo(Point point1, Point point2, bool isStroked, bool isSmoothJoin) + { + context.QuadraticBezierTo(transform.Transform(point1), transform.Transform(point2), isStroked, isSmoothJoin); + } + + public void BezierTo(Point point1, Point point2, Point point3, bool isStroked, bool isSmoothJoin) + { + context.BezierTo(transform.Transform(point1), transform.Transform(point2), transform.Transform(point3), isStroked, isSmoothJoin); + } + + public void PolyLineTo(IList points, bool isStroked, bool isSmoothJoin) + { + context.PolyLineTo(TransformPoints(points), isStroked, isSmoothJoin); + } + + public void PolyQuadraticBezierTo(IList points, bool isStroked, bool isSmoothJoin) + { + context.PolyQuadraticBezierTo(TransformPoints(points), isStroked, isSmoothJoin); + } + + public void PolyBezierTo(IList points, bool isStroked, bool isSmoothJoin) + { + context.PolyBezierTo(TransformPoints(points), isStroked, isSmoothJoin); + } + + private IList TransformPoints(IList points) + { + Point[] transformedPoints = new Point[points.Count]; + + for (int i = 0; i < transformedPoints.Length; i++) + { + transformedPoints[i] = transform.Transform(points[i]); + } + + return transformedPoints; + } + } +} diff --git a/MapControl/MapTransform.cs b/MapControl/MapTransform.cs new file mode 100644 index 00000000..bb0d7680 --- /dev/null +++ b/MapControl/MapTransform.cs @@ -0,0 +1,25 @@ +using System; +using System.Windows; +using System.Windows.Media; + +namespace MapControl +{ + /// + /// Defines a normal cylindrical projection. Latitude and longitude values in degrees + /// are transformed to cartesian coordinates with origin at latitude = 0 and longitude = 0. + /// Longitude values are transformed identically to x values in the interval [-180 .. 180]. + /// + public abstract class MapTransform : GeneralTransform + { + /// + /// Gets the absolute value of the minimum and maximum latitude that can be transformed. + /// + public abstract double MaxLatitude { get; } + + /// + /// Gets the point scale factor of the map projection at the specified point + /// relative to the point (0, 0). + /// + public abstract double RelativeScale(Point point); + } +} diff --git a/MapControl/MapViewTransform.cs b/MapControl/MapViewTransform.cs new file mode 100644 index 00000000..1323fd2a --- /dev/null +++ b/MapControl/MapViewTransform.cs @@ -0,0 +1,72 @@ +using System; +using System.Windows; +using System.Windows.Media; + +namespace MapControl +{ + public class MapViewTransform : GeneralTransform + { + private readonly GeneralTransform inverse; + + public MapViewTransform() + { + MapTransform = new MercatorTransform(); + inverse = new InverseMapViewTransform(this); + } + + public MapTransform MapTransform { get; set; } + public Transform ViewTransform { get; set; } + + public override GeneralTransform Inverse + { + get { return inverse; } + } + + public override bool TryTransform(Point point, out Point result) + { + result = ViewTransform.Transform(MapTransform.Transform(point)); + return true; + } + + public override Rect TransformBounds(Rect rect) + { + return ViewTransform.TransformBounds(MapTransform.TransformBounds(rect)); + } + + protected override Freezable CreateInstanceCore() + { + return new MapViewTransform(); + } + } + + internal class InverseMapViewTransform : GeneralTransform + { + private readonly MapViewTransform inverse; + + public InverseMapViewTransform(MapViewTransform inverse) + { + this.inverse = inverse; + } + + public override GeneralTransform Inverse + { + get { return inverse; } + } + + public override bool TryTransform(Point point, out Point result) + { + result = inverse.MapTransform.Inverse.Transform(inverse.ViewTransform.Inverse.Transform(point)); + return true; + } + + public override Rect TransformBounds(Rect rect) + { + return inverse.MapTransform.Inverse.TransformBounds(inverse.ViewTransform.Inverse.TransformBounds(rect)); + } + + protected override Freezable CreateInstanceCore() + { + return new InverseMapViewTransform(inverse); + } + } +} diff --git a/MapControl/MercatorTransform.cs b/MapControl/MercatorTransform.cs new file mode 100644 index 00000000..089358ce --- /dev/null +++ b/MapControl/MercatorTransform.cs @@ -0,0 +1,110 @@ +using System; +using System.Windows; +using System.Windows.Media; + +namespace MapControl +{ + /// + /// Transforms latitude and longitude values in degrees to cartesian coordinates + /// according to the Mercator transform. + /// + public class MercatorTransform : MapTransform + { + private GeneralTransform inverse = new InverseMercatorTransform(); + + public MercatorTransform() + { + Freeze(); + } + + public override GeneralTransform Inverse + { + get { return inverse; } + } + + public override double MaxLatitude + { + get { return 85.0511; } + } + + public override double RelativeScale(Point point) + { + if (point.Y <= -90d) + { + return double.NegativeInfinity; + } + + if (point.Y >= 90d) + { + return double.PositiveInfinity; + } + + return 1d / Math.Cos(point.Y * Math.PI / 180d); + } + + public override bool TryTransform(Point point, out Point result) + { + result = point; + + if (point.Y <= -90d) + { + result.Y = double.NegativeInfinity; + } + else if (point.Y >= 90d) + { + result.Y = double.PositiveInfinity; + } + else + { + double lat = point.Y * Math.PI / 180d; + result.Y = (Math.Log(Math.Tan(lat) + 1d / Math.Cos(lat))) / Math.PI * 180d; + } + + return true; + } + + public override Rect TransformBounds(Rect rect) + { + return new Rect(Transform(rect.TopLeft), Transform(rect.BottomRight)); + } + + protected override Freezable CreateInstanceCore() + { + return new MercatorTransform(); + } + } + + /// + /// Transforms cartesian coordinates to latitude and longitude values in degrees + /// according to the inverse Mercator transform. + /// + public class InverseMercatorTransform : GeneralTransform + { + public InverseMercatorTransform() + { + Freeze(); + } + + public override GeneralTransform Inverse + { + get { return new MercatorTransform(); } + } + + public override bool TryTransform(Point point, out Point result) + { + result = point; + result.Y = Math.Atan(Math.Sinh(point.Y * Math.PI / 180d)) / Math.PI * 180d; + return true; + } + + public override Rect TransformBounds(Rect rect) + { + return new Rect(Transform(rect.TopLeft), Transform(rect.BottomRight)); + } + + protected override Freezable CreateInstanceCore() + { + return new InverseMercatorTransform(); + } + } +} diff --git a/MapControl/Properties/AssemblyInfo.cs b/MapControl/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..3da8f27b --- /dev/null +++ b/MapControl/Properties/AssemblyInfo.cs @@ -0,0 +1,40 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Windows; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("MapControl")] +[assembly: AssemblyDescription("MapControl")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Clemens Fischer")] +[assembly: AssemblyProduct("MapControl")] +[assembly: AssemblyCopyright("Copyright © Clemens Fischer 2012")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("81bb1cbe-eb23-47d4-a79d-71f425876d02")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] + +// ResourceDictionary locations for default control styles +[assembly: ThemeInfo(ResourceDictionaryLocation.None, ResourceDictionaryLocation.SourceAssembly)] diff --git a/MapControl/Themes/Generic.xaml b/MapControl/Themes/Generic.xaml new file mode 100644 index 00000000..eb352ed3 --- /dev/null +++ b/MapControl/Themes/Generic.xaml @@ -0,0 +1,60 @@ + + + + \ No newline at end of file diff --git a/MapControl/Tile.cs b/MapControl/Tile.cs new file mode 100644 index 00000000..683eb49f --- /dev/null +++ b/MapControl/Tile.cs @@ -0,0 +1,59 @@ +using System; +using System.Windows; +using System.Windows.Media; +using System.Windows.Media.Animation; + +namespace MapControl +{ + public enum TileLoadState { NotLoaded, Loading, Loaded }; + + internal class Tile + { + private static readonly DoubleAnimation opacityAnimation = new DoubleAnimation(0d, 1d, TimeSpan.FromSeconds(0.5), FillBehavior.Stop); + + public readonly int ZoomLevel; + public readonly int X; + public readonly int Y; + public readonly Uri Uri; + public readonly ImageBrush Brush = new ImageBrush(); + + public Tile(TileSource tileSource, int zoomLevel, int x, int y) + { + ZoomLevel = zoomLevel; + X = x; + Y = y; + Uri = tileSource.GetUri(XIndex, Y, ZoomLevel); + } + + public TileLoadState LoadState { get; set; } + + public int XIndex + { + get + { + int numTiles = 1 << ZoomLevel; + return ((X % numTiles) + numTiles) % numTiles; + } + } + + public ImageSource Image + { + get { return Brush.ImageSource; } + set + { + if (Brush.ImageSource == null) + { + Brush.BeginAnimation(ImageBrush.OpacityProperty, opacityAnimation); + } + + Brush.ImageSource = value; + LoadState = TileLoadState.Loaded; + } + } + + public override string ToString() + { + return string.Format("{0}.{1}.{2}", ZoomLevel, X, Y); + } + } +} diff --git a/MapControl/TileContainer.cs b/MapControl/TileContainer.cs new file mode 100644 index 00000000..0dfc583e --- /dev/null +++ b/MapControl/TileContainer.cs @@ -0,0 +1,215 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Windows; +using System.Windows.Media; +using System.Windows.Threading; +using System.Collections; + +namespace MapControl +{ + internal class TileContainer : ContainerVisual + { + private const double maxScaledTileSize = 400d; // scaled tile size 200..400 units + private static double zoomLevelSwitchOffset = Math.Log(maxScaledTileSize / 256d, 2d); + + private Size size; + private Point origin; + private Vector offset; + private double rotation; + private double zoomLevel; + private int tileZoomLevel; + private Int32Rect tileGrid; + private TileLayerCollection tileLayers; + private readonly DispatcherTimer updateTimer; + private readonly MatrixTransform viewTransform = new MatrixTransform(); + + public TileContainer() + { + updateTimer = new DispatcherTimer(TimeSpan.FromSeconds(0.5), DispatcherPriority.Background, UpdateTiles, Dispatcher); + } + + public TileLayerCollection TileLayers + { + get { return tileLayers; } + set + { + if (tileLayers != null) + { + tileLayers.CollectionChanged -= TileLayersChanged; + } + + tileLayers = value; + ClearChildren(); + + if (tileLayers != null) + { + tileLayers.CollectionChanged += TileLayersChanged; + AddChildren(0, tileLayers); + } + + ((Map)VisualParent).OnTileLayersChanged(); + } + } + + public Transform ViewTransform + { + get { return viewTransform; } + } + + public double SetTransform(double mapZoomLevel, double mapRotation, Point mapOrigin, Point viewOrigin, Size viewSize) + { + zoomLevel = mapZoomLevel; + rotation = mapRotation; + size = viewSize; + origin = viewOrigin; + + double scale = Math.Pow(2d, zoomLevel) * 256d / 360d; + offset.X = origin.X - (180d + mapOrigin.X) * scale; + offset.Y = origin.Y - (180d - mapOrigin.Y) * scale; + + Matrix transform = new Matrix(1d, 0d, 0d, -1d, 180d, 180d); + transform.Scale(scale, scale); + transform.Translate(offset.X, offset.Y); + transform.RotateAt(rotation, origin.X, origin.Y); + viewTransform.Matrix = transform; + + transform = GetVisualTransform(); + + if (tileLayers != null) + { + foreach (TileLayer tileLayer in tileLayers) + { + tileLayer.TransformMatrix = transform; + } + } + + updateTimer.IsEnabled = true; + + return scale; + } + + private Matrix GetVisualTransform() + { + // Calculates the transform matrix that enables rendering of 256x256 tile rectangles in + // TileLayer.UpdateTiles with origin at tileGrid.X and tileGrid.Y to minimize rounding errors. + + double scale = Math.Pow(2d, zoomLevel - tileZoomLevel); + Matrix transform = new Matrix(1d, 0d, 0d, 1d, tileGrid.X * 256d, tileGrid.Y * 256d); + transform.Scale(scale, scale); + transform.Translate(offset.X, offset.Y); + transform.RotateAt(rotation, origin.X, origin.Y); + + return transform; + } + + private void UpdateTiles(object sender, EventArgs eventArgs) + { + updateTimer.IsEnabled = false; + + int zoom = (int)Math.Floor(zoomLevel + 1d - zoomLevelSwitchOffset); + int numTiles = 1 << zoom; + double mapToTileScale = (double)numTiles / 360d; + Matrix transform = viewTransform.Matrix; + transform.Invert(); // view to map coordinates + transform.Translate(180d, -180d); + transform.Scale(mapToTileScale, -mapToTileScale); // map coordinates to tile indices + + // tile indices of visible rectangle + Point p1 = transform.Transform(new Point(0d, 0d)); + Point p2 = transform.Transform(new Point(size.Width, 0d)); + Point p3 = transform.Transform(new Point(0d, size.Height)); + Point p4 = transform.Transform(new Point(size.Width, size.Height)); + + double left = Math.Min(p1.X, Math.Min(p2.X, Math.Min(p3.X, p4.X))); + double right = Math.Max(p1.X, Math.Max(p2.X, Math.Max(p3.X, p4.X))); + double top = Math.Min(p1.Y, Math.Min(p2.Y, Math.Min(p3.Y, p4.Y))); + double bottom = Math.Max(p1.Y, Math.Max(p2.Y, Math.Max(p3.Y, p4.Y))); + + // index ranges of visible tiles + int x1 = (int)Math.Floor(left); + int x2 = (int)Math.Floor(right); + int y1 = Math.Max((int)Math.Floor(top), 0); + int y2 = Math.Min((int)Math.Floor(bottom), numTiles - 1); + Int32Rect grid = new Int32Rect(x1, y1, x2 - x1 + 1, y2 - y1 + 1); + + if (tileZoomLevel != zoom || tileGrid != grid) + { + tileZoomLevel = zoom; + tileGrid = grid; + transform = GetVisualTransform(); + + if (tileLayers != null) + { + foreach (TileLayer tileLayer in tileLayers) + { + tileLayer.TransformMatrix = transform; + tileLayer.UpdateTiles(tileZoomLevel, tileGrid); + } + } + } + } + + private void TileLayersChanged(object sender, NotifyCollectionChangedEventArgs eventArgs) + { + switch (eventArgs.Action) + { + case NotifyCollectionChangedAction.Add: + AddChildren(eventArgs.NewStartingIndex, eventArgs.NewItems); + break; + + case NotifyCollectionChangedAction.Remove: + RemoveChildren(eventArgs.OldStartingIndex, eventArgs.OldItems); + break; + + case NotifyCollectionChangedAction.Move: + case NotifyCollectionChangedAction.Replace: + RemoveChildren(eventArgs.OldStartingIndex, eventArgs.OldItems); + AddChildren(eventArgs.NewStartingIndex, eventArgs.NewItems); + break; + + case NotifyCollectionChangedAction.Reset: + ClearChildren(); + if (eventArgs.NewItems != null) + { + AddChildren(0, eventArgs.NewItems); + } + break; + } + + ((Map)VisualParent).OnTileLayersChanged(); + } + + private void AddChildren(int index, IList layers) + { + Matrix transform = GetVisualTransform(); + + foreach (TileLayer tileLayer in layers) + { + Children.Insert(index++, tileLayer); + tileLayer.TransformMatrix = transform; + tileLayer.UpdateTiles(tileZoomLevel, tileGrid); + } + } + + private void RemoveChildren(int index, IList layers) + { + foreach (TileLayer tileLayer in layers) + { + tileLayer.ClearTiles(); + } + + Children.RemoveRange(index, layers.Count); + } + + private void ClearChildren() + { + foreach (TileLayer tileLayer in Children) + { + tileLayer.ClearTiles(); + } + + Children.Clear(); + } + } +} diff --git a/MapControl/TileImageLoader.cs b/MapControl/TileImageLoader.cs new file mode 100644 index 00000000..8f7cc61a --- /dev/null +++ b/MapControl/TileImageLoader.cs @@ -0,0 +1,263 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Threading; + +namespace MapControl +{ + public class TileImageLoader : DispatcherObject + { + public static string TileCacheDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "MapCache"); + public static TimeSpan TileCacheExpiryAge = TimeSpan.FromDays(1); + + private readonly Queue pendingTiles = new Queue(); + private int numDownloads; + + internal int MaxDownloads; + internal string TileLayerName; + + internal int TilesPending + { + get { return pendingTiles.Count; } + } + + internal void BeginDownloadTiles(ICollection tiles) + { + ThreadPool.QueueUserWorkItem(BeginDownloadTiles, new List(tiles.Reverse().Where(t => t.LoadState == TileLoadState.NotLoaded))); + } + + internal void EndDownloadTiles() + { + lock (pendingTiles) + { + pendingTiles.Clear(); + } + } + + private void BeginDownloadTiles(object newTilesList) + { + List newTiles = (List)newTilesList; + + lock (pendingTiles) + { + if (!string.IsNullOrEmpty(TileCacheDirectory) && + !string.IsNullOrEmpty(TileLayerName)) + { + List expiredTiles = new List(newTiles.Count); + + newTiles.ForEach(tile => + { + bool cacheExpired; + ImageSource image = GetCachedImage(tile, out cacheExpired); + + if (image != null) + { + Dispatcher.BeginInvoke((Action)(() => tile.Image = image)); + + if (cacheExpired) + { + expiredTiles.Add(tile); // enqueue later + } + } + else + { + pendingTiles.Enqueue(tile); + } + }); + + expiredTiles.ForEach(tile => pendingTiles.Enqueue(tile)); + } + else + { + newTiles.ForEach(tile => pendingTiles.Enqueue(tile)); + } + + DownloadNextTiles(null); + } + } + + private void DownloadNextTiles(object o) + { + while (pendingTiles.Count > 0 && numDownloads < MaxDownloads) + { + Tile tile = pendingTiles.Dequeue(); + tile.LoadState = TileLoadState.Loading; + numDownloads++; + + ThreadPool.QueueUserWorkItem(DownloadTile, tile); + } + } + + private void DownloadTile(object t) + { + Tile tile = (Tile)t; + ImageSource image = DownloadImage(tile); + + Dispatcher.BeginInvoke((Action)(() => tile.Image = image)); + + lock (pendingTiles) + { + numDownloads--; + DownloadNextTiles(null); + } + } + + private ImageSource GetCachedImage(Tile tile, out bool expired) + { + string tileDir = TileDirectory(tile); + ImageSource image = null; + expired = false; + + try + { + if (Directory.Exists(tileDir)) + { + string[] tilePath = Directory.GetFiles(tileDir, string.Format("{0}.*", tile.Y)); + + if (tilePath.Length > 0) + { + try + { + using (Stream fileStream = File.OpenRead(tilePath[0])) + { + image = BitmapFrame.Create(fileStream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad); + } + + expired = File.GetLastWriteTime(tilePath[0]) + TileCacheExpiryAge <= DateTime.Now; + + TraceInformation(expired ? "{0} - Cache Expired" : "{0} - Cached", tile.Uri); + } + catch (Exception exc) + { + TraceWarning("{0} - {1}", tilePath[0], exc.Message); + File.Delete(tilePath[0]); + } + } + } + } + catch (Exception exc) + { + TraceWarning("{0} - {1}", tileDir, exc.Message); + } + + return image; + } + + private ImageSource DownloadImage(Tile tile) + { + ImageSource image = null; + + try + { + TraceInformation("{0} - Requesting", tile.Uri); + + HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create(tile.Uri); + webRequest.UserAgent = typeof(TileImageLoader).ToString(); + webRequest.KeepAlive = true; + + using (HttpWebResponse response = (HttpWebResponse)webRequest.GetResponse()) + { + using (Stream responseStream = response.GetResponseStream()) + { + using (Stream memoryStream = new MemoryStream((int)response.ContentLength)) + { + responseStream.CopyTo(memoryStream); + memoryStream.Position = 0; + + BitmapDecoder decoder = BitmapDecoder.Create(memoryStream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad); + image = decoder.Frames[0]; + + string tilePath; + + if (!string.IsNullOrEmpty(TileCacheDirectory) && + !string.IsNullOrEmpty(TileLayerName) && + (tilePath = TilePath(tile, decoder)) != null) + { + Directory.CreateDirectory(Path.GetDirectoryName(tilePath)); + + using (Stream fileStream = File.OpenWrite(tilePath)) + { + memoryStream.Position = 0; + memoryStream.CopyTo(fileStream); + } + } + } + } + } + + TraceInformation("{0} - Completed", tile.Uri); + } + catch (WebException exc) + { + if (exc.Status == WebExceptionStatus.ProtocolError) + { + TraceInformation("{0} - {1}", tile.Uri, ((HttpWebResponse)exc.Response).StatusCode); + } + else + { + TraceWarning("{0} - {1}", tile.Uri, exc.Status); + } + } + catch (Exception exc) + { + TraceWarning("{0} - {1}", tile.Uri, exc.Message); + } + + return image; + } + + private string TileDirectory(Tile tile) + { + return Path.Combine(TileCacheDirectory, TileLayerName, tile.ZoomLevel.ToString(), tile.XIndex.ToString()); + } + + private string TilePath(Tile tile, BitmapDecoder decoder) + { + string extension; + + if (decoder is PngBitmapDecoder) + { + extension = "png"; + } + else if (decoder is JpegBitmapDecoder) + { + extension = "jpg"; + } + else if (decoder is BmpBitmapDecoder) + { + extension = "bmp"; + } + else if (decoder is GifBitmapDecoder) + { + extension = "gif"; + } + else if (decoder is TiffBitmapDecoder) + { + extension = "tif"; + } + else + { + return null; + } + + return Path.Combine(TileDirectory(tile), string.Format("{0}.{1}", tile.Y, extension)); + } + + private static void TraceWarning(string format, params object[] args) + { + System.Diagnostics.Trace.TraceWarning("[{0:00}] {1}", Thread.CurrentThread.ManagedThreadId, string.Format(format, args)); + } + + private static void TraceInformation(string format, params object[] args) + { +#if DEBUG + System.Diagnostics.Trace.TraceInformation("[{0:00}] {1}", Thread.CurrentThread.ManagedThreadId, string.Format(format, args)); +#endif + } + } +} diff --git a/MapControl/TileLayer.cs b/MapControl/TileLayer.cs new file mode 100644 index 00000000..bb7f03dd --- /dev/null +++ b/MapControl/TileLayer.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows; +using System.Windows.Markup; +using System.Windows.Media; + +namespace MapControl +{ + [ContentProperty("TileSource")] + public class TileLayer : DrawingVisual + { + private readonly TileImageLoader tileImageLoader = new TileImageLoader(); + private readonly List tiles = new List(); + private string description = string.Empty; + private Int32Rect grid; + private int zoomLevel; + + public TileLayer() + { + VisualEdgeMode = EdgeMode.Aliased; + VisualTransform = new MatrixTransform(); + MinZoomLevel = 1; + MaxZoomLevel = 18; + MaxDownloads = 8; + } + + public TileSource TileSource { get; set; } + public bool HasDarkBackground { get; set; } + public int MinZoomLevel { get; set; } + public int MaxZoomLevel { get; set; } + + public int MaxDownloads + { + get { return tileImageLoader.MaxDownloads; } + set { tileImageLoader.MaxDownloads = value; } + } + + public string Name + { + get { return tileImageLoader.TileLayerName; } + set { tileImageLoader.TileLayerName = value; } + } + + public string Description + { + get { return description; } + set { description = value.Replace("{y}", DateTime.Now.Year.ToString()); } + } + + public Matrix TransformMatrix + { + get { return ((MatrixTransform)VisualTransform).Matrix; } + set { ((MatrixTransform)VisualTransform).Matrix = value; } + } + + public void UpdateTiles(int zoomLevel, Int32Rect grid) + { + this.grid = grid; + this.zoomLevel = zoomLevel; + + tileImageLoader.EndDownloadTiles(); + + if (VisualParent != null && TileSource != null) + { + SelectTiles(); + RenderTiles(); + + tileImageLoader.BeginDownloadTiles(tiles); + } + } + + public void ClearTiles() + { + tiles.Clear(); + tileImageLoader.EndDownloadTiles(); + } + + private Int32Rect GetTileGrid(int tileZoomLevel) + { + int tileSize = 1 << (zoomLevel - tileZoomLevel); + int max = (1 << tileZoomLevel) - 1; + int x1 = grid.X / tileSize - 1; + int x2 = (grid.X + grid.Width - 1) / tileSize + 1; + int y1 = Math.Max(0, grid.Y / tileSize - 1); + int y2 = Math.Min(max, (grid.Y + grid.Height - 1) / tileSize + 1); + + return new Int32Rect(x1, y1, x2 - x1 + 1, y2 - y1 + 1); + } + + private void SelectTiles() + { + TileContainer tileContainer = VisualParent as TileContainer; + int maxZoom = Math.Min(zoomLevel, MaxZoomLevel); + int minZoom = maxZoom; + + if (tileContainer != null && tileContainer.TileLayers.IndexOf(this) == 0) + { + minZoom = MinZoomLevel; + } + + tiles.RemoveAll(t => + { + if (t.ZoomLevel > maxZoom || t.ZoomLevel < minZoom) + { + return true; + } + + Int32Rect tileGrid = GetTileGrid(t.ZoomLevel); + return t.X < tileGrid.X || t.X >= tileGrid.X + tileGrid.Width || t.Y < tileGrid.Y || t.Y >= tileGrid.Y + tileGrid.Height; + }); + + for (int tileZoomLevel = minZoom; tileZoomLevel <= maxZoom; tileZoomLevel++) + { + Int32Rect tileGrid = GetTileGrid(tileZoomLevel); + + for (int y = tileGrid.Y; y < tileGrid.Y + tileGrid.Height; y++) + { + for (int x = tileGrid.X; x < tileGrid.X + tileGrid.Width; x++) + { + if (tiles.Find(t => t.ZoomLevel == tileZoomLevel && t.X == x && t.Y == y) == null) + { + Tile tile = new Tile(TileSource, tileZoomLevel, x, y); + Tile equivalent = tiles.Find(t => t.Image != null && t.ZoomLevel == tile.ZoomLevel && t.XIndex == tile.XIndex && t.Y == tile.Y); + + if (equivalent != null) + { + tile.Image = equivalent.Image; + } + + tiles.Add(tile); + } + } + } + } + + tiles.Sort((t1, t2) => t1.ZoomLevel - t2.ZoomLevel); + + System.Diagnostics.Trace.TraceInformation("{0} Tiles: {1}", tiles.Count, string.Join(", ", tiles.Select(t => t.ZoomLevel.ToString()))); + } + + private void RenderTiles() + { + using (DrawingContext drawingContext = RenderOpen()) + { + tiles.ForEach(tile => + { + int tileSize = 256 << (zoomLevel - tile.ZoomLevel); + Rect tileRect = new Rect(tileSize * tile.X - 256 * grid.X, tileSize * tile.Y - 256 * grid.Y, tileSize, tileSize); + + drawingContext.DrawRectangle(tile.Brush, null, tileRect); + + //if (tile.ZoomLevel == zoomLevel) + // drawingContext.DrawText(new FormattedText(tile.ToString(), System.Globalization.CultureInfo.InvariantCulture, FlowDirection.LeftToRight, new Typeface("Segoe UI"), 14, Brushes.Black), tileRect.TopLeft); + }); + } + } + } +} \ No newline at end of file diff --git a/MapControl/TileLayerCollection.cs b/MapControl/TileLayerCollection.cs new file mode 100644 index 00000000..b3ea04b1 --- /dev/null +++ b/MapControl/TileLayerCollection.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.ObjectModel; +using System.Windows.Media; + +namespace MapControl +{ + public class TileLayerCollection : ObservableCollection + { + private string name; + + public TileLayerCollection() + { + } + + public TileLayerCollection(TileLayer tileLayer) + { + Add(tileLayer); + } + + public string Name + { + get { return !string.IsNullOrEmpty(name) ? name : (Count > 0 ? this[0].Name : string.Empty); } + set { name = value; } + } + } +} diff --git a/MapControl/TileSource.cs b/MapControl/TileSource.cs new file mode 100644 index 00000000..e420a33d --- /dev/null +++ b/MapControl/TileSource.cs @@ -0,0 +1,163 @@ +using System; +using System.ComponentModel; +using System.Globalization; +using System.Text; +using System.Windows; + +namespace MapControl +{ + [TypeConverter(typeof(TileSourceTypeConverter))] + public class TileSource + { + public string UriFormat { get; set; } + + public virtual Uri GetUri(int x, int y, int zoomLevel) + { + return new Uri(UriFormat. + Replace("{x}", x.ToString()). + Replace("{y}", y.ToString()). + Replace("{z}", zoomLevel.ToString())); + } + } + + public class OpenStreetMapTileSource : TileSource + { + private static string[] hostChars = { "a", "b", "c" }; + private int hostChar = -1; + + public override Uri GetUri(int x, int y, int zoomLevel) + { + hostChar = (hostChar + 1) % 3; + + return new Uri(UriFormat. + Replace("{c}", hostChars[hostChar]). + Replace("{x}", x.ToString()). + Replace("{y}", y.ToString()). + Replace("{z}", zoomLevel.ToString())); + } + } + + public class GoogleMapsTileSource : TileSource + { + private int hostIndex = -1; + + public override Uri GetUri(int x, int y, int zoomLevel) + { + hostIndex = (hostIndex + 1) % 4; + + return new Uri(UriFormat. + Replace("{i}", hostIndex.ToString()). + Replace("{x}", x.ToString()). + Replace("{y}", y.ToString()). + Replace("{z}", zoomLevel.ToString())); + } + } + + public class MapQuestTileSource : TileSource + { + private int hostNumber; + + public override Uri GetUri(int x, int y, int zoomLevel) + { + hostNumber = (hostNumber % 4) + 1; + + return new Uri(UriFormat. + Replace("{n}", hostNumber.ToString()). + Replace("{x}", x.ToString()). + Replace("{y}", y.ToString()). + Replace("{z}", zoomLevel.ToString())); + } + } + + public class QuadKeyTileSource : TileSource + { + public override Uri GetUri(int x, int y, int zoomLevel) + { + StringBuilder key = new StringBuilder { Length = zoomLevel }; + + for (int z = zoomLevel - 1; z >= 0; z--, x /= 2, y /= 2) + { + key[z] = (char)('0' + 2 * (y % 2) + (x % 2)); + } + + return new Uri(UriFormat. + Replace("{i}", key.ToString(key.Length - 1, 1)). + Replace("{q}", key.ToString())); + } + } + + public class BoundingBoxTileSource : TileSource + { + public override Uri GetUri(int x, int y, int zoomLevel) + { + InverseMercatorTransform t = new InverseMercatorTransform(); + double n = 1 << zoomLevel; + double x1 = (double)x * 360d / n - 180d; + double x2 = (double)(x + 1) * 360d / n - 180d; + double y1 = 180d - (double)(y + 1) * 360d / n; + double y2 = 180d - (double)y * 360d / n; + Point p1 = t.Transform(new Point(x1, y1)); + Point p2 = t.Transform(new Point(x2, y2)); + + return new Uri(UriFormat. + Replace("{w}", p1.X.ToString(CultureInfo.InvariantCulture)). + Replace("{s}", p1.Y.ToString(CultureInfo.InvariantCulture)). + Replace("{e}", p2.X.ToString(CultureInfo.InvariantCulture)). + Replace("{n}", p2.Y.ToString(CultureInfo.InvariantCulture))); + } + } + + public class TileSourceTypeConverter : TypeConverter + { + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { + return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType); + } + + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + { + string uriFormat = value as string; + + if (uriFormat != null) + { + TileSource tileSource = null; + + if (uriFormat.Contains("{x}") && uriFormat.Contains("{y}") && uriFormat.Contains("{z}")) + { + if (uriFormat.Contains("{c}")) + { + tileSource = new OpenStreetMapTileSource(); + } + else if (uriFormat.Contains("{i}")) + { + tileSource = new GoogleMapsTileSource(); + } + else if (uriFormat.Contains("{n}")) + { + tileSource = new MapQuestTileSource(); + } + else + { + tileSource = new TileSource(); + } + } + else if (uriFormat.Contains("{q}")) + { + tileSource = new QuadKeyTileSource(); + } + else if (uriFormat.Contains("{w}") && uriFormat.Contains("{s}") && uriFormat.Contains("{e}") && uriFormat.Contains("{n}")) + { + tileSource = new BoundingBoxTileSource(); + } + + if (tileSource != null) + { + tileSource.UriFormat = uriFormat; + return tileSource; + } + } + + return base.ConvertFrom(context, culture, value); + } + } +}