// XAML Map Control - http://xamlmapcontrol.codeplex.com/ // © 2015 Clemens Fischer // Licensed under the Microsoft Public License (Ms-PL) using System; using System.Collections.Generic; using System.Linq; #if NETFX_CORE using Windows.Foundation; using Windows.UI.Xaml; using Windows.UI.Xaml.Markup; using Windows.UI.Xaml.Media; #else using System.Windows; using System.Windows.Markup; using System.Windows.Media; using System.Windows.Threading; using System.Diagnostics; #endif namespace MapControl { /// /// Fills a rectangular area with map tiles from a TileSource. /// #if NETFX_CORE [ContentProperty(Name = "TileSource")] #else [ContentProperty("TileSource")] #endif public partial class TileLayer : PanelBase, IMapElement { public static TileLayer OpenStreetMapTileLayer { get { return new TileLayer { SourceName = "OpenStreetMap", Description = "© [OpenStreetMap Contributors](http://www.openstreetmap.org/copyright)", TileSource = new TileSource { UriFormat = "http://{c}.tile.openstreetmap.org/{z}/{x}/{y}.png" } }; } } public static readonly DependencyProperty TileSourceProperty = DependencyProperty.Register( "TileSource", typeof(TileSource), typeof(TileLayer), new PropertyMetadata(null, (o, e) => ((TileLayer)o).UpdateTiles(true))); public static readonly DependencyProperty SourceNameProperty = DependencyProperty.Register( "SourceName", typeof(string), typeof(TileLayer), new PropertyMetadata(null)); public static readonly DependencyProperty DescriptionProperty = DependencyProperty.Register( "Description", typeof(string), typeof(TileLayer), new PropertyMetadata(null)); public static readonly DependencyProperty LogoImageProperty = DependencyProperty.Register( "LogoImage", typeof(ImageSource), typeof(TileLayer), new PropertyMetadata(null)); public static readonly DependencyProperty ZoomLevelOffsetProperty = DependencyProperty.Register( "ZoomLevelOffset", typeof(double), typeof(TileLayer), new PropertyMetadata(0d, (o, e) => ((TileLayer)o).UpdateTileRect())); public static readonly DependencyProperty MinZoomLevelProperty = DependencyProperty.Register( "MinZoomLevel", typeof(int), typeof(TileLayer), new PropertyMetadata(0)); public static readonly DependencyProperty MaxZoomLevelProperty = DependencyProperty.Register( "MaxZoomLevel", typeof(int), typeof(TileLayer), new PropertyMetadata(18)); public static readonly DependencyProperty MaxParallelDownloadsProperty = DependencyProperty.Register( "MaxParallelDownloads", typeof(int), typeof(TileLayer), new PropertyMetadata(4)); public static readonly DependencyProperty UpdateIntervalProperty = DependencyProperty.Register( "UpdateInterval", typeof(TimeSpan), typeof(TileLayer), new PropertyMetadata(TimeSpan.FromSeconds(0.5), (o, e) => ((TileLayer)o).updateTimer.Interval = (TimeSpan)e.NewValue)); public static readonly DependencyProperty UpdateWhileViewportChangingProperty = DependencyProperty.Register( "UpdateWhileViewportChanging", typeof(bool), typeof(TileLayer), new PropertyMetadata(true)); public static readonly DependencyProperty ForegroundProperty = DependencyProperty.Register( "Foreground", typeof(Brush), typeof(TileLayer), new PropertyMetadata(null)); public static readonly new DependencyProperty BackgroundProperty = DependencyProperty.Register( "Background", typeof(Brush), typeof(TileLayer), new PropertyMetadata(null)); private readonly DispatcherTimer updateTimer; private MapBase parentMap; private double mapOriginX; public TileLayer() : this(new TileImageLoader()) { } public TileLayer(ITileImageLoader tileImageLoader) { Initialize(); RenderTransform = new MatrixTransform(); TileImageLoader = tileImageLoader; Tiles = new List(); TileZoomLevel = -1; updateTimer = new DispatcherTimer { Interval = UpdateInterval }; updateTimer.Tick += (s, e) => UpdateTileRect(); } partial void Initialize(); // Windows Runtime and Silverlight only public ITileImageLoader TileImageLoader { get; private set; } public ICollection Tiles { get; private set; } public Int32Rect TileRect { get; private set; } public int TileZoomLevel { get; private set; } /// /// Provides map tile URIs or images. /// public TileSource TileSource { get { return (TileSource)GetValue(TileSourceProperty); } set { SetValue(TileSourceProperty, value); } } /// /// Name of the TileSource. Used as key in a TileLayerCollection and as component of a tile cache key. /// public string SourceName { get { return (string)GetValue(SourceNameProperty); } set { SetValue(SourceNameProperty, value); } } /// /// Description of the TileLayer. Used to display copyright information on top of the map. /// public string Description { get { return (string)GetValue(DescriptionProperty); } set { SetValue(DescriptionProperty, value); } } /// /// Logo image. Used to display a provider brand logo on top of the map. /// public ImageSource LogoImage { get { return (ImageSource)GetValue(LogoImageProperty); } set { SetValue(LogoImageProperty, value); } } /// /// Adds an offset to the Map's ZoomLevel for a relative scale between the Map and the TileLayer. /// public double ZoomLevelOffset { get { return (double)GetValue(ZoomLevelOffsetProperty); } set { SetValue(ZoomLevelOffsetProperty, value); } } /// /// Minimum zoom level supported by the TileLayer. /// public int MinZoomLevel { get { return (int)GetValue(MinZoomLevelProperty); } set { SetValue(MinZoomLevelProperty, value); } } /// /// Maximum zoom level supported by the TileLayer. /// public int MaxZoomLevel { get { return (int)GetValue(MaxZoomLevelProperty); } set { SetValue(MaxZoomLevelProperty, value); } } /// /// Maximum number of parallel downloads that may be performed by the TileLayer's ITileImageLoader. /// public int MaxParallelDownloads { get { return (int)GetValue(MaxParallelDownloadsProperty); } set { SetValue(MaxParallelDownloadsProperty, value); } } /// /// Minimum time interval between tile updates. /// public TimeSpan UpdateInterval { get { return (TimeSpan)GetValue(UpdateIntervalProperty); } set { SetValue(UpdateIntervalProperty, value); } } /// /// Controls if tiles are updates while the viewport is still changing. /// public bool UpdateWhileViewportChanging { get { return (bool)GetValue(UpdateWhileViewportChangingProperty); } set { SetValue(UpdateWhileViewportChangingProperty, value); } } /// /// Optional foreground brush. Sets MapBase.Foreground, if not null. /// public Brush Foreground { get { return (Brush)GetValue(ForegroundProperty); } set { SetValue(ForegroundProperty, value); } } /// /// Optional background brush. Sets MapBase.Background, if not null. /// New property prevents filling of RenderTransformed TileLayer with Panel.Background. /// public new Brush Background { get { return (Brush)GetValue(BackgroundProperty); } set { SetValue(BackgroundProperty, value); } } public MapBase ParentMap { get { return parentMap; } set { if (parentMap != null) { parentMap.ViewportChanged -= ViewportChanged; TileZoomLevel = -1; UpdateTiles(true); } parentMap = value; if (parentMap != null) { parentMap.ViewportChanged += ViewportChanged; ViewportChanged(this, EventArgs.Empty); } } } private void ViewportChanged(object sender, EventArgs e) { if (TileZoomLevel < 0 || Math.Abs(parentMap.MapOrigin.X - mapOriginX) > 180d) { // immediately handle map origin leap when map center moves across 180° longitude UpdateTileRect(); } else { SetRenderTransform(); if (!UpdateWhileViewportChanging) { updateTimer.Stop(); } updateTimer.Start(); } mapOriginX = parentMap.MapOrigin.X; } protected void UpdateTileRect() { updateTimer.Stop(); if (parentMap != null) { var zoomLevel = (int)Math.Round(parentMap.ZoomLevel + ZoomLevelOffset); var transform = GetTileIndexMatrix(zoomLevel); // tile indices of visible rectangle var p1 = transform.Transform(new Point(0d, 0d)); var p2 = transform.Transform(new Point(parentMap.RenderSize.Width, 0d)); var p3 = transform.Transform(new Point(0d, parentMap.RenderSize.Height)); var p4 = transform.Transform(new Point(parentMap.RenderSize.Width, parentMap.RenderSize.Height)); // index ranges of visible tiles var x1 = (int)Math.Floor(Math.Min(p1.X, Math.Min(p2.X, Math.Min(p3.X, p4.X)))); var y1 = (int)Math.Floor(Math.Min(p1.Y, Math.Min(p2.Y, Math.Min(p3.Y, p4.Y)))); var x2 = (int)Math.Floor(Math.Max(p1.X, Math.Max(p2.X, Math.Max(p3.X, p4.X)))); var y2 = (int)Math.Floor(Math.Max(p1.Y, Math.Max(p2.Y, Math.Max(p3.Y, p4.Y)))); var rect = new Int32Rect(x1, y1, x2 - x1 + 1, y2 - y1 + 1); if (TileZoomLevel != zoomLevel || TileRect != rect) { TileZoomLevel = zoomLevel; TileRect = rect; SetRenderTransform(); UpdateTiles(false); } } } protected virtual void UpdateTiles(bool clearTiles) { if (Tiles.Count > 0) { TileImageLoader.CancelLoadTiles(this); } if (clearTiles) { Tiles.Clear(); } SelectTiles(); Children.Clear(); if (Tiles.Count > 0) { foreach (var tile in Tiles) { Children.Add(tile.Image); } TileImageLoader.BeginLoadTiles(this, Tiles.Where(t => t.Pending).OrderByDescending(t => t.ZoomLevel)); } } protected void SelectTiles() { var newTiles = new List(); if (TileZoomLevel >= 0 && parentMap != null && TileSource != null) { var maxZoomLevel = Math.Min(TileZoomLevel, MaxZoomLevel); var minZoomLevel = MinZoomLevel; if (minZoomLevel < maxZoomLevel && this != parentMap.TileLayers.FirstOrDefault()) { // do not load background tiles if this is not the base layer minZoomLevel = maxZoomLevel; } for (var z = minZoomLevel; z <= maxZoomLevel; z++) { var tileSize = 1 << (TileZoomLevel - z); var x1 = (int)Math.Floor((double)TileRect.X / tileSize); // may be negative var x2 = (TileRect.X + TileRect.Width - 1) / tileSize; var y1 = Math.Max(TileRect.Y / tileSize, 0); var y2 = Math.Min((TileRect.Y + TileRect.Height - 1) / tileSize, (1 << z) - 1); for (var y = y1; y <= y2; y++) { for (var x = x1; x <= x2; x++) { var tile = Tiles.FirstOrDefault(t => t.ZoomLevel == z && t.X == x && t.Y == y); if (tile == null) { tile = new Tile(z, x, y); var equivalentTile = Tiles.FirstOrDefault( t => t.ZoomLevel == z && t.XIndex == tile.XIndex && t.Y == y && t.Image.Source != null); if (equivalentTile != null) { // do not animate to avoid flicker when crossing 180° tile.SetImage(equivalentTile.Image.Source, false); } } newTiles.Add(tile); } } } } Tiles = newTiles; } protected override Size ArrangeOverride(Size finalSize) { if (TileZoomLevel >= 0) { foreach (var tile in Tiles) { var tileSize = (double)(256 << (TileZoomLevel - tile.ZoomLevel)); var x = tileSize * tile.X - 256 * TileRect.X; var y = tileSize * tile.Y - 256 * TileRect.Y; tile.Image.Width = tileSize; tile.Image.Height = tileSize; tile.Image.Arrange(new Rect(x, y, tileSize, tileSize)); } } return finalSize; } } }