Version 2.4.0.

- Added ImageFileCache and FileDbCache for WinRT
- Improved TileImageLoader
- Removed TileContainer, TileLayer can be added as MapBase child
- Removed attached property MapPanel.ViewportPosition
This commit is contained in:
ClemensF 2014-11-19 21:11:14 +01:00
parent f04025067c
commit 3b6545e738
84 changed files with 5504 additions and 1940 deletions

View file

@ -57,8 +57,7 @@ namespace MapControl
var request = (HttpWebRequest)asyncResult.AsyncState;
using (var response = request.EndGetResponse(asyncResult))
using (var responseStream = response.GetResponseStream())
using (var xmlReader = XmlReader.Create(responseStream))
using (var xmlReader = XmlReader.Create(response.GetResponseStream()))
{
ReadImageryMetadataResponse(xmlReader);
}

View file

@ -21,7 +21,7 @@ namespace MapControl
/// Converts text containing hyperlinks in markdown syntax [text](url)
/// to a collection of Run and Hyperlink inlines.
/// </summary>
public static ICollection<Inline> ToInlines(this string text)
public static List<Inline> ToInlines(this string text)
{
var inlines = new List<Inline>();

View file

@ -6,8 +6,6 @@ namespace MapControl
{
public interface IMapElement
{
MapBase ParentMap { get; }
void SetParentMap(MapBase parentMap);
MapBase ParentMap { get; set; }
}
}

View file

@ -0,0 +1,14 @@
// XAML Map Control - http://xamlmapcontrol.codeplex.com/
// Copyright © 2014 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
using System.Collections.Generic;
namespace MapControl
{
public interface ITileImageLoader
{
void BeginLoadTiles(TileLayer tileLayer, IEnumerable<Tile> tiles);
void CancelLoadTiles(TileLayer tileLayer);
}
}

View file

@ -0,0 +1,22 @@
// XAML Map Control - http://xamlmapcontrol.codeplex.com/
// Copyright © 2014 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
using System;
using System.Threading.Tasks;
using Windows.Storage.Streams;
namespace MapControl.Caching
{
public class ImageCacheItem
{
public IBuffer Buffer { get; set; }
public DateTime Expires { get; set; }
}
public interface IImageCache
{
Task<ImageCacheItem> GetAsync(string key);
Task SetAsync(string key, IBuffer buffer, DateTime expires);
}
}

View file

@ -1,67 +0,0 @@
// XAML Map Control - http://xamlmapcontrol.codeplex.com/
// Copyright © 2014 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using Windows.Storage;
using Windows.Storage.Streams;
namespace MapControl
{
public class ImageFileCache : IObjectCache
{
private readonly IStorageFolder rootFolder;
public ImageFileCache()
{
rootFolder = ApplicationData.Current.TemporaryFolder;
}
public ImageFileCache(IStorageFolder folder)
{
if (folder == null)
{
throw new ArgumentNullException("The parameter folder must not be null.");
}
rootFolder = folder;
}
public async Task<object> GetAsync(string key)
{
try
{
return await PathIO.ReadBufferAsync(Path.Combine(rootFolder.Path, key));
}
catch
{
return null;
}
}
public async Task SetAsync(string key, object value)
{
try
{
var buffer = (IBuffer)value;
var names = key.Split('\\');
var folder = rootFolder;
for (int i = 0; i < names.Length - 1; i++)
{
folder = await folder.CreateFolderAsync(names[i], CreationCollisionOption.OpenIfExists);
}
var file = await folder.CreateFileAsync(names[names.Length - 1], CreationCollisionOption.ReplaceExisting);
await FileIO.WriteBufferAsync(file, buffer);
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
}
}
}

View file

@ -30,15 +30,16 @@ namespace MapControl
{
if (IsAsync)
{
var buffer = new WebClient().DownloadData(uri);
var request = HttpWebRequest.CreateHttp(uri);
request.UserAgent = TileImageLoader.HttpUserAgent;
if (buffer != null)
using (var response = (HttpWebResponse)request.GetResponse())
using (var responseStream = response.GetResponseStream())
using (var memoryStream = new MemoryStream())
{
using (var stream = new MemoryStream(buffer))
{
image = BitmapFrame.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
image.Freeze();
}
responseStream.CopyTo(memoryStream);
memoryStream.Position = 0;
image = BitmapFrame.Create(memoryStream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
}
}
else

View file

@ -4,7 +4,7 @@
namespace MapControl
{
internal struct Int32Rect
public struct Int32Rect
{
public Int32Rect(int x, int y, int width, int height)
: this()

View file

@ -2,14 +2,17 @@
// Copyright © 2014 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
using System;
#if WINDOWS_RUNTIME
using Windows.Foundation;
using Windows.UI;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Controls;
#else
using System.Windows;
using System.Windows.Media;
using System.Windows.Controls;
#endif
namespace MapControl
@ -45,7 +48,11 @@ namespace MapControl
partial void Initialize()
{
Background = new SolidColorBrush(Colors.Transparent);
// set Background by Style to enable resetting by ClearValue in RemoveTileLayers
var style = new Style(typeof(MapBase));
style.Setters.Add(new Setter(Panel.BackgroundProperty, new SolidColorBrush(Colors.Transparent)));
Style = style;
Clip = new RectangleGeometry();
SizeChanged += OnRenderSizeChanged;
@ -58,11 +65,39 @@ namespace MapControl
UpdateTransform();
}
private void SetViewportTransform(Point mapOrigin)
{
viewportTransform.Matrix = Matrix.Identity
.Translate(-mapOrigin.X, -mapOrigin.Y)
.Scale(ViewportScale, -ViewportScale)
.Rotate(Heading)
.Translate(viewportOrigin.X, viewportOrigin.Y);
}
private void SetTileLayerTransform()
{
var scale = Math.Pow(2d, ZoomLevel - TileZoomLevel);
tileLayerTransform.Matrix = Matrix.Identity
.Translate(TileGrid.X * TileSource.TileSize, TileGrid.Y * TileSource.TileSize)
.Scale(scale, scale)
.Translate(tileLayerOffset.X, tileLayerOffset.Y)
.RotateAt(Heading, viewportOrigin.X, viewportOrigin.Y); ;
}
private void SetTransformMatrixes()
{
scaleTransform.Matrix = new Matrix(CenterScale, 0d, 0d, CenterScale, 0d, 0d);
scaleTransform.Matrix = Matrix.Identity.Scale(CenterScale, CenterScale);
rotateTransform.Matrix = Matrix.Identity.Rotate(Heading);
scaleRotateTransform.Matrix = scaleTransform.Matrix.Multiply(rotateTransform.Matrix);
}
private Matrix GetTileIndexMatrix(double scale)
{
return viewportTransform.Matrix
.Invert() // view to map coordinates
.Translate(180d, -180d)
.Scale(scale, -scale); // map coordinates to tile indices
}
}
}

View file

@ -2,6 +2,7 @@
// Copyright © 2014 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
@ -11,7 +12,7 @@ namespace MapControl
public partial class MapBase
{
public static readonly DependencyProperty ForegroundProperty =
System.Windows.Controls.Control.ForegroundProperty.AddOwner(typeof(MapBase));
Control.ForegroundProperty.AddOwner(typeof(MapBase));
public static readonly DependencyProperty CenterProperty = DependencyProperty.Register(
"Center", typeof(Location), typeof(MapBase), new FrameworkPropertyMetadata(
@ -64,13 +65,50 @@ namespace MapControl
UpdateTransform();
}
private void SetViewportTransform(Point mapOrigin)
{
var transform = Matrix.Identity;
transform.Translate(-mapOrigin.X, -mapOrigin.Y);
transform.Scale(ViewportScale, -ViewportScale);
transform.Rotate(Heading);
transform.Translate(viewportOrigin.X, viewportOrigin.Y);
viewportTransform.Matrix = transform;
}
private void SetTileLayerTransform()
{
var scale = Math.Pow(2d, ZoomLevel - TileZoomLevel);
var transform = Matrix.Identity;
transform.Translate(TileGrid.X * TileSource.TileSize, TileGrid.Y * TileSource.TileSize);
transform.Scale(scale, scale);
transform.Translate(tileLayerOffset.X, tileLayerOffset.Y);
transform.RotateAt(Heading, viewportOrigin.X, viewportOrigin.Y);
tileLayerTransform.Matrix = transform;
}
private void SetTransformMatrixes()
{
Matrix rotateMatrix = Matrix.Identity;
var rotateMatrix = Matrix.Identity;
rotateMatrix.Rotate(Heading);
rotateTransform.Matrix = rotateMatrix;
scaleTransform.Matrix = new Matrix(CenterScale, 0d, 0d, CenterScale, 0d, 0d);
scaleRotateTransform.Matrix = scaleTransform.Matrix * rotateMatrix;
var scaleMatrix = Matrix.Identity;
scaleMatrix.Scale(CenterScale, CenterScale);
scaleTransform.Matrix = scaleMatrix;
scaleRotateTransform.Matrix = scaleMatrix * rotateMatrix;
}
private Matrix GetTileIndexMatrix(double scale)
{
var transform = viewportTransform.Matrix;
transform.Invert(); // view to map coordinates
transform.Translate(180d, -180d);
transform.Scale(scale, -scale); // map coordinates to tile indices
return transform;
}
}
}

View file

@ -3,6 +3,7 @@
// Licensed under the Microsoft Public License (Ms-PL)
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
#if WINDOWS_RUNTIME
@ -14,6 +15,7 @@ using Windows.UI.Xaml.Media.Animation;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Threading;
#endif
namespace MapControl
@ -28,17 +30,17 @@ namespace MapControl
{
private const double MaximumZoomLevel = 22d;
public static readonly DependencyProperty TileLayersProperty = DependencyProperty.Register(
"TileLayers", typeof(TileLayerCollection), typeof(MapBase), new PropertyMetadata(null,
(o, e) => ((MapBase)o).TileLayersPropertyChanged((TileLayerCollection)e.OldValue, (TileLayerCollection)e.NewValue)));
public static TimeSpan TileUpdateInterval = TimeSpan.FromSeconds(0.5);
public static TimeSpan AnimationDuration = TimeSpan.FromSeconds(0.3);
public static EasingFunctionBase AnimationEasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut };
public static readonly DependencyProperty TileLayerProperty = DependencyProperty.Register(
"TileLayer", typeof(TileLayer), typeof(MapBase), new PropertyMetadata(null,
(o, e) => ((MapBase)o).TileLayerPropertyChanged((TileLayer)e.NewValue)));
public static readonly DependencyProperty TileOpacityProperty = DependencyProperty.Register(
"TileOpacity", typeof(double), typeof(MapBase), new PropertyMetadata(1d,
(o, e) => ((MapBase)o).tileContainer.Opacity = (double)e.NewValue));
public static readonly DependencyProperty TileLayersProperty = DependencyProperty.Register(
"TileLayers", typeof(TileLayerCollection), typeof(MapBase), new PropertyMetadata(null,
(o, e) => ((MapBase)o).TileLayersPropertyChanged((TileLayerCollection)e.OldValue, (TileLayerCollection)e.NewValue)));
public static readonly DependencyProperty MinZoomLevelProperty = DependencyProperty.Register(
"MinZoomLevel", typeof(double), typeof(MapBase), new PropertyMetadata(1d,
@ -52,29 +54,32 @@ namespace MapControl
"CenterPoint", typeof(Point), typeof(MapBase), new PropertyMetadata(new Point(),
(o, e) => ((MapBase)o).CenterPointPropertyChanged((Point)e.NewValue)));
private readonly TileContainer tileContainer = new TileContainer();
private readonly PanelBase tileLayerPanel = new PanelBase();
private readonly DispatcherTimer tileUpdateTimer = new DispatcherTimer { Interval = TileUpdateInterval };
private readonly MapTransform mapTransform = new MercatorTransform();
private readonly MatrixTransform viewportTransform = new MatrixTransform();
private readonly MatrixTransform tileLayerTransform = new MatrixTransform();
private readonly MatrixTransform scaleTransform = new MatrixTransform();
private readonly MatrixTransform rotateTransform = new MatrixTransform();
private readonly MatrixTransform scaleRotateTransform = new MatrixTransform();
private Location transformOrigin;
private Point viewportOrigin;
private Point tileLayerOffset;
private PointAnimation centerAnimation;
private DoubleAnimation zoomLevelAnimation;
private DoubleAnimation headingAnimation;
private Brush storedBackground;
private Brush storedForeground;
private bool internalPropertyChange;
public MapBase()
{
SetParentMap();
Children.Add(tileLayerPanel);
TileLayers = new TileLayerCollection();
Children.Add(tileContainer);
Initialize();
tileUpdateTimer.Tick += UpdateTiles;
Loaded += OnLoaded;
Initialize();
}
partial void Initialize();
@ -85,6 +90,11 @@ namespace MapControl
/// </summary>
public event EventHandler ViewportChanged;
/// <summary>
/// Raised when the TileZoomLevel or TileGrid properties have changed.
/// </summary>
public event EventHandler TileGridChanged;
/// <summary>
/// Gets or sets the map foreground Brush.
/// </summary>
@ -95,16 +105,7 @@ namespace MapControl
}
/// <summary>
/// Gets or sets the TileLayers used by this Map.
/// </summary>
public TileLayerCollection TileLayers
{
get { return (TileLayerCollection)GetValue(TileLayersProperty); }
set { SetValue(TileLayersProperty, value); }
}
/// <summary>
/// Gets or sets the base TileLayer used by this Map, i.e. TileLayers[0].
/// Gets or sets the base TileLayer used by the Map control.
/// </summary>
public TileLayer TileLayer
{
@ -113,12 +114,15 @@ namespace MapControl
}
/// <summary>
/// Gets or sets the opacity of the tile layers.
/// Gets or sets optional multiple TileLayers that are used simultaneously.
/// The first element in the collection is equal to the value of the TileLayer property.
/// The additional TileLayers usually have transparent backgrounds and their IsOverlay
/// property is set to true.
/// </summary>
public double TileOpacity
public TileLayerCollection TileLayers
{
get { return (double)GetValue(TileOpacityProperty); }
set { SetValue(TileOpacityProperty, value); }
get { return (TileLayerCollection)GetValue(TileLayersProperty); }
set { SetValue(TileLayersProperty, value); }
}
/// <summary>
@ -208,7 +212,15 @@ namespace MapControl
/// </summary>
public Transform ViewportTransform
{
get { return tileContainer.ViewportTransform; }
get { return viewportTransform; }
}
/// <summary>
/// Gets the RenderTransform to be used by TileLayers, with origin at TileGrid.X and TileGrid.Y.
/// </summary>
public Transform TileLayerTransform
{
get { return tileLayerTransform; }
}
/// <summary>
@ -245,6 +257,16 @@ namespace MapControl
/// </summary>
public double CenterScale { get; private set; }
/// <summary>
/// Gets the zoom level to be used by TileLayers.
/// </summary>
public int TileZoomLevel { get; private set; }
/// <summary>
/// Gets the tile grid to be used by TileLayers.
/// </summary>
public Int32Rect TileGrid { get; private set; }
/// <summary>
/// Gets the map scale at the specified location as viewport coordinate units (pixels) per meter.
/// </summary>
@ -258,7 +280,7 @@ namespace MapControl
/// </summary>
public Point LocationToViewportPoint(Location location)
{
return ViewportTransform.Transform(mapTransform.Transform(location));
return viewportTransform.Transform(mapTransform.Transform(location));
}
/// <summary>
@ -266,7 +288,7 @@ namespace MapControl
/// </summary>
public Location ViewportPointToLocation(Point point)
{
return mapTransform.Transform(ViewportTransform.Inverse.Transform(point));
return mapTransform.Transform(viewportTransform.Inverse.Transform(point));
}
/// <summary>
@ -365,105 +387,27 @@ namespace MapControl
{
if (southWest.Latitude < northEast.Latitude && southWest.Longitude < northEast.Longitude)
{
var p1 = MapTransform.Transform(southWest);
var p2 = MapTransform.Transform(northEast);
var p1 = mapTransform.Transform(southWest);
var p2 = mapTransform.Transform(northEast);
var lonScale = RenderSize.Width / (p2.X - p1.X) * 360d / TileSource.TileSize;
var latScale = RenderSize.Height / (p2.Y - p1.Y) * 360d / TileSource.TileSize;
var lonZoom = Math.Log(lonScale, 2d);
var latZoom = Math.Log(latScale, 2d);
TargetZoomLevel = Math.Min(lonZoom, latZoom);
TargetCenter = MapTransform.Transform(new Point((p1.X + p2.X) / 2d, (p1.Y + p2.Y) / 2d));
TargetCenter = mapTransform.Transform(new Point((p1.X + p2.X) / 2d, (p1.Y + p2.Y) / 2d));
TargetHeading = 0d;
}
}
protected override void OnViewportChanged()
{
base.OnViewportChanged();
if (ViewportChanged != null)
{
ViewportChanged(this, EventArgs.Empty);
}
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
Loaded -= OnLoaded;
if (TileLayer == null)
if (tileLayerPanel.Children.Count == 0 && !Children.OfType<TileLayer>().Any())
{
TileLayer = TileLayer.Default;
}
UpdateTransform();
}
private void TileLayerCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
tileContainer.AddTileLayers(e.NewStartingIndex, e.NewItems.Cast<TileLayer>());
break;
case NotifyCollectionChangedAction.Remove:
tileContainer.RemoveTileLayers(e.OldStartingIndex, e.OldItems.Count);
break;
#if !SILVERLIGHT
case NotifyCollectionChangedAction.Move:
#endif
case NotifyCollectionChangedAction.Replace:
tileContainer.RemoveTileLayers(e.NewStartingIndex, e.OldItems.Count);
tileContainer.AddTileLayers(e.NewStartingIndex, e.NewItems.Cast<TileLayer>());
break;
case NotifyCollectionChangedAction.Reset:
tileContainer.ClearTileLayers();
if (e.NewItems != null)
{
tileContainer.AddTileLayers(0, e.NewItems.Cast<TileLayer>());
}
break;
default:
break;
}
var firstTileLayer = TileLayers.FirstOrDefault();
if (TileLayer != firstTileLayer)
{
TileLayer = firstTileLayer;
}
}
private void TileLayersPropertyChanged(TileLayerCollection oldTileLayers, TileLayerCollection newTileLayers)
{
tileContainer.ClearTileLayers();
if (oldTileLayers != null)
{
oldTileLayers.CollectionChanged -= TileLayerCollectionChanged;
}
if (newTileLayers != null)
{
newTileLayers.CollectionChanged += TileLayerCollectionChanged;
tileContainer.AddTileLayers(0, newTileLayers);
var firstTileLayer = TileLayers.FirstOrDefault();
if (TileLayer != firstTileLayer)
{
TileLayer = firstTileLayer;
}
}
else
{
TileLayer = null;
}
}
private void TileLayerPropertyChanged(TileLayer tileLayer)
@ -484,35 +428,94 @@ namespace MapControl
TileLayers[0] = tileLayer;
}
}
}
if (tileLayer != null && tileLayer.Background != null)
private void TileLayersPropertyChanged(TileLayerCollection oldTileLayers, TileLayerCollection newTileLayers)
{
if (oldTileLayers != null)
{
if (storedBackground == null)
oldTileLayers.CollectionChanged -= TileLayerCollectionChanged;
RemoveTileLayers(0, oldTileLayers.Count);
}
if (newTileLayers != null)
{
AddTileLayers(0, newTileLayers);
newTileLayers.CollectionChanged += TileLayerCollectionChanged;
}
TileLayer = TileLayers.FirstOrDefault();
}
private void TileLayerCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
AddTileLayers(e.NewStartingIndex, e.NewItems.Cast<TileLayer>());
break;
case NotifyCollectionChangedAction.Remove:
RemoveTileLayers(e.OldStartingIndex, e.OldItems.Count);
break;
#if !SILVERLIGHT
case NotifyCollectionChangedAction.Move:
#endif
case NotifyCollectionChangedAction.Replace:
RemoveTileLayers(e.NewStartingIndex, e.OldItems.Count);
AddTileLayers(e.NewStartingIndex, e.NewItems.Cast<TileLayer>());
break;
case NotifyCollectionChangedAction.Reset:
if (e.OldItems != null)
{
RemoveTileLayers(0, e.OldItems.Count);
}
if (e.NewItems != null)
{
AddTileLayers(0, e.NewItems.Cast<TileLayer>());
}
break;
default:
break;
}
TileLayer = TileLayers.FirstOrDefault();
}
private void AddTileLayers(int index, IEnumerable<TileLayer> tileLayers)
{
foreach (var tileLayer in tileLayers)
{
if (index == 0)
{
storedBackground = Background;
if (tileLayer.Background != null)
{
Background = tileLayer.Background;
}
if (tileLayer.Foreground != null)
{
Foreground = tileLayer.Foreground;
}
}
Background = tileLayer.Background;
tileLayerPanel.Children.Insert(index++, tileLayer);
}
else if (storedBackground != null)
}
private void RemoveTileLayers(int index, int count)
{
while (count-- > 0)
{
Background = storedBackground;
storedBackground = null;
tileLayerPanel.Children.RemoveAt(index + count);
}
if (tileLayer != null && tileLayer.Foreground != null)
if (index == 0)
{
if (storedForeground == null)
{
storedForeground = Foreground;
}
Foreground = tileLayer.Foreground;
}
else if (storedForeground != null)
{
Foreground = storedForeground;
storedForeground = null;
ClearValue(BackgroundProperty);
ClearValue(ForegroundProperty);
}
}
@ -552,7 +555,7 @@ namespace MapControl
if (centerAnimation == null)
{
InternalSetValue(TargetCenterProperty, center);
InternalSetValue(CenterPointProperty, MapTransform.Transform(center));
InternalSetValue(CenterPointProperty, mapTransform.Transform(center));
}
}
}
@ -573,10 +576,10 @@ namespace MapControl
// animate private CenterPoint property by PointAnimation
centerAnimation = new PointAnimation
{
From = MapTransform.Transform(Center),
To = MapTransform.Transform(targetCenter, Center.Longitude),
Duration = Settings.MapAnimationDuration,
EasingFunction = Settings.MapAnimationEasingFunction,
From = mapTransform.Transform(Center),
To = mapTransform.Transform(targetCenter, Center.Longitude),
Duration = AnimationDuration,
EasingFunction = AnimationEasingFunction,
FillBehavior = FillBehavior.HoldEnd
};
@ -594,7 +597,7 @@ namespace MapControl
centerAnimation = null;
InternalSetValue(CenterProperty, TargetCenter);
InternalSetValue(CenterPointProperty, MapTransform.Transform(TargetCenter));
InternalSetValue(CenterPointProperty, mapTransform.Transform(TargetCenter));
RemoveAnimation(CenterPointProperty); // remove holding animation in WPF
ResetTransformOrigin();
@ -607,7 +610,7 @@ namespace MapControl
if (!internalPropertyChange)
{
centerPoint.X = Location.NormalizeLongitude(centerPoint.X);
InternalSetValue(CenterProperty, MapTransform.Transform(centerPoint));
InternalSetValue(CenterProperty, mapTransform.Transform(centerPoint));
ResetTransformOrigin();
UpdateTransform();
}
@ -680,8 +683,8 @@ namespace MapControl
zoomLevelAnimation = new DoubleAnimation
{
To = targetZoomLevel,
Duration = Settings.MapAnimationDuration,
EasingFunction = Settings.MapAnimationEasingFunction,
Duration = AnimationDuration,
EasingFunction = AnimationEasingFunction,
FillBehavior = FillBehavior.HoldEnd
};
@ -755,8 +758,8 @@ namespace MapControl
headingAnimation = new DoubleAnimation
{
By = delta,
Duration = Settings.MapAnimationDuration,
EasingFunction = Settings.MapAnimationEasingFunction,
Duration = AnimationDuration,
EasingFunction = AnimationEasingFunction,
FillBehavior = FillBehavior.HoldEnd
};
@ -784,7 +787,12 @@ namespace MapControl
{
Location center;
if (transformOrigin != null)
if (transformOrigin == null)
{
center = Center;
SetViewportTransform(center);
}
else
{
SetViewportTransform(transformOrigin);
@ -802,7 +810,7 @@ namespace MapControl
if (centerAnimation == null)
{
InternalSetValue(TargetCenterProperty, center);
InternalSetValue(CenterPointProperty, MapTransform.Transform(center));
InternalSetValue(CenterPointProperty, mapTransform.Transform(center));
}
if (resetTransformOrigin)
@ -811,11 +819,6 @@ namespace MapControl
SetViewportTransform(center);
}
}
else
{
center = Center;
SetViewportTransform(center);
}
CenterScale = ViewportScale * mapTransform.RelativeScale(center) / TileSource.MetersPerDegree; // Pixels per meter at center latitude
@ -823,9 +826,77 @@ namespace MapControl
OnViewportChanged();
}
protected override void OnViewportChanged()
{
base.OnViewportChanged();
var viewportChanged = ViewportChanged;
if (viewportChanged != null)
{
viewportChanged(this, EventArgs.Empty);
}
}
private void SetViewportTransform(Location origin)
{
ViewportScale = tileContainer.SetViewportTransform(ZoomLevel, Heading, mapTransform.Transform(origin), viewportOrigin);
var oldMapOriginX = (viewportOrigin.X - tileLayerOffset.X) / ViewportScale - 180d;
var mapOrigin = mapTransform.Transform(origin);
ViewportScale = Math.Pow(2d, ZoomLevel) * TileSource.TileSize / 360d;
SetViewportTransform(mapOrigin);
tileLayerOffset.X = viewportOrigin.X - (180d + mapOrigin.X) * ViewportScale;
tileLayerOffset.Y = viewportOrigin.Y - (180d - mapOrigin.Y) * ViewportScale;
if (Math.Abs(mapOrigin.X - oldMapOriginX) > 180d)
{
// immediately handle map origin leap when map center moves across 180° longitude
UpdateTiles(this, EventArgs.Empty);
}
else
{
SetTileLayerTransform();
tileUpdateTimer.Start();
}
}
private void UpdateTiles(object sender, object e)
{
tileUpdateTimer.Stop();
// relative size of scaled tile ranges from 0.75 to 1.5 (192 to 384 pixels)
var zoomLevelSwitchDelta = Math.Log(0.75, 2d);
var zoomLevel = (int)Math.Floor(ZoomLevel - zoomLevelSwitchDelta);
var transform = GetTileIndexMatrix((double)(1 << zoomLevel) / 360d);
// tile indices of visible rectangle
var p1 = transform.Transform(new Point(0d, 0d));
var p2 = transform.Transform(new Point(RenderSize.Width, 0d));
var p3 = transform.Transform(new Point(0d, RenderSize.Height));
var p4 = transform.Transform(new Point(RenderSize.Width, 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 grid = new Int32Rect(x1, y1, x2 - x1 + 1, y2 - y1 + 1);
if (TileZoomLevel != zoomLevel || TileGrid != grid)
{
TileZoomLevel = zoomLevel;
TileGrid = grid;
SetTileLayerTransform();
var tileGridChanged = TileGridChanged;
if (tileGridChanged != null)
{
tileGridChanged(this, EventArgs.Empty);
}
}
}
}
}

View file

@ -23,7 +23,7 @@
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>Bin\Debug</OutputPath>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE;SILVERLIGHT;WINDOWS_PHONE</DefineConstants>
<NoStdLib>true</NoStdLib>
<NoConfig>true</NoConfig>
@ -33,7 +33,7 @@
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>none</DebugType>
<Optimize>true</Optimize>
<OutputPath>Bin\Release</OutputPath>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE;SILVERLIGHT;WINDOWS_PHONE</DefineConstants>
<NoStdLib>true</NoStdLib>
<NoConfig>true</NoConfig>
@ -48,6 +48,7 @@
<Compile Include="ImageTileSource.Silverlight.WinRT.cs" />
<Compile Include="IMapElement.cs" />
<Compile Include="Int32Rect.cs" />
<Compile Include="ITileImageLoader.cs" />
<Compile Include="Location.cs" />
<Compile Include="LocationCollection.cs" />
<Compile Include="LocationCollectionConverter.cs" />
@ -78,13 +79,11 @@
<Compile Include="PanelBase.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Pushpin.Silverlight.WinRT.cs" />
<Compile Include="Settings.cs" />
<Compile Include="Tile.cs" />
<Compile Include="Tile.Silverlight.WinRT.cs" />
<Compile Include="TileContainer.cs" />
<Compile Include="TileContainer.Silverlight.WinRT.cs" />
<Compile Include="TileImageLoader.Silverlight.cs" />
<Compile Include="TileLayer.cs" />
<Compile Include="TileLayer.Silverlight.WinRT.cs" />
<Compile Include="TileLayerCollection.cs" />
<Compile Include="TileSource.cs" />
<Compile Include="TileSourceConverter.cs" />

View file

@ -75,6 +75,7 @@
<Compile Include="ImageTileSource.Silverlight.WinRT.cs" />
<Compile Include="IMapElement.cs" />
<Compile Include="Int32Rect.cs" />
<Compile Include="ITileImageLoader.cs" />
<Compile Include="Location.cs" />
<Compile Include="LocationCollection.cs" />
<Compile Include="LocationCollectionConverter.cs" />
@ -105,13 +106,11 @@
<Compile Include="PanelBase.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Pushpin.Silverlight.WinRT.cs" />
<Compile Include="Settings.cs" />
<Compile Include="Tile.cs" />
<Compile Include="Tile.Silverlight.WinRT.cs" />
<Compile Include="TileContainer.cs" />
<Compile Include="TileContainer.Silverlight.WinRT.cs" />
<Compile Include="TileImageLoader.Silverlight.cs" />
<Compile Include="TileLayer.cs" />
<Compile Include="TileLayer.Silverlight.WinRT.cs" />
<Compile Include="TileLayerCollection.cs" />
<Compile Include="TileSource.cs" />
<Compile Include="TileSourceConverter.cs" />

View file

@ -58,6 +58,7 @@
<Compile Include="HyperlinkText.cs" />
<Compile Include="ImageTileSource.WPF.cs" />
<Compile Include="IMapElement.cs" />
<Compile Include="ITileImageLoader.cs" />
<Compile Include="Location.cs" />
<Compile Include="LocationCollection.cs" />
<Compile Include="LocationCollectionConverter.cs" />
@ -77,7 +78,6 @@
<Compile Include="MapPanel.cs" />
<Compile Include="MapPanel.WPF.cs" />
<Compile Include="MapRectangle.WPF.cs" />
<Compile Include="PanelBase.cs" />
<Compile Include="MapPath.cs" />
<Compile Include="MapPath.WPF.cs" />
<Compile Include="MapPolyline.cs" />
@ -86,15 +86,14 @@
<Compile Include="MapScale.cs" />
<Compile Include="MapTransform.cs" />
<Compile Include="MercatorTransform.cs" />
<Compile Include="PanelBase.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Pushpin.WPF.cs" />
<Compile Include="Settings.cs" />
<Compile Include="Tile.cs" />
<Compile Include="Tile.WPF.cs" />
<Compile Include="TileContainer.cs" />
<Compile Include="TileContainer.WPF.cs" />
<Compile Include="TileImageLoader.WPF.cs" />
<Compile Include="TileLayer.cs" />
<Compile Include="TileLayer.WPF.cs" />
<Compile Include="TileLayerCollection.cs" />
<Compile Include="TileSource.cs" />
<Compile Include="TileSourceConverter.cs" />

View file

@ -19,7 +19,7 @@ namespace MapControl
{
public static readonly DependencyProperty SourceProperty = DependencyProperty.Register(
"Source", typeof(ImageSource), typeof(MapImage),
new PropertyMetadata(null, (o, e) => ((MapImage)o).SourceChanged((ImageSource)e.NewValue)));
new PropertyMetadata(null, (o, e) => ((MapImage)o).SourcePropertyChanged((ImageSource)e.NewValue)));
public ImageSource Source
{
@ -27,7 +27,7 @@ namespace MapControl
set { SetValue(SourceProperty, value); }
}
private void SourceChanged(ImageSource image)
private void SourcePropertyChanged(ImageSource image)
{
Fill = new ImageBrush
{

View file

@ -40,7 +40,7 @@ namespace MapControl
Children.Add(new MapImage { Opacity = 0d });
Children.Add(new MapImage { Opacity = 0d });
updateTimer = new DispatcherTimer { Interval = Settings.TileUpdateInterval };
updateTimer = new DispatcherTimer { Interval = MapBase.TileUpdateInterval };
updateTimer.Tick += (s, e) => UpdateImage();
}
@ -59,7 +59,7 @@ namespace MapControl
/// <summary>
/// Relative size of the map images in relation to the current viewport size.
/// Setting a value greater than one will let MapImageLayer request images that
/// are larger than the viewport, in order to support smooth panning.
/// are larger than the viewport, in order to support smooth panning.
/// </summary>
public double RelativeImageSize
{
@ -175,16 +175,16 @@ namespace MapControl
if (topImage.Source != null)
{
topImage.BeginAnimation(UIElement.OpacityProperty,
new DoubleAnimation { To = 1d, Duration = Settings.TileAnimationDuration });
new DoubleAnimation { To = 1d, Duration = Tile.OpacityAnimationDuration });
}
if (bottomImage.Source != null)
{
var fadeOutAnimation = new DoubleAnimation { To = 0d, Duration = Settings.TileAnimationDuration };
var fadeOutAnimation = new DoubleAnimation { To = 0d, Duration = Tile.OpacityAnimationDuration };
if (topImage.Source != null)
{
fadeOutAnimation.BeginTime = Settings.TileAnimationDuration;
fadeOutAnimation.BeginTime = Tile.OpacityAnimationDuration;
}
bottomImage.BeginAnimation(UIElement.OpacityProperty, fadeOutAnimation);

View file

@ -3,12 +3,9 @@
// Licensed under the Microsoft Public License (Ms-PL)
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace MapControl
{
@ -17,10 +14,6 @@ namespace MapControl
/// </summary>
public class MapItemsControl : ListBox
{
public static readonly DependencyProperty SelectionGeometryProperty = DependencyProperty.Register(
"SelectionGeometry", typeof(Geometry), typeof(MapItemsControl),
new PropertyMetadata((o, e) => ((MapItemsControl)o).SelectionGeometryPropertyChanged((Geometry)e.NewValue)));
static MapItemsControl()
{
DefaultStyleKeyProperty.OverrideMetadata(
@ -43,58 +36,6 @@ namespace MapControl
return item is MapItem;
}
/// <summary>
/// Gets or sets a Geometry that selects all items inside its fill area, i.e.
/// where Geometry.FillContains returns true for the item's viewport position.
/// </summary>
public Geometry SelectionGeometry
{
get { return (Geometry)GetValue(SelectionGeometryProperty); }
set { SetValue(SelectionGeometryProperty, value); }
}
public object GetFirstItemInGeometry(Geometry geometry)
{
if (geometry == null || geometry.IsEmpty())
{
return null;
}
return Items.Cast<object>().FirstOrDefault(i => IsItemInGeometry(i, geometry));
}
public IList<object> GetItemsInGeometry(Geometry geometry)
{
if (geometry == null || geometry.IsEmpty())
{
return null;
}
return Items.Cast<object>().Where(i => IsItemInGeometry(i, geometry)).ToList();
}
private bool IsItemInGeometry(object item, Geometry geometry)
{
var container = ItemContainerGenerator.ContainerFromItem(item) as UIElement;
Point? viewportPosition;
return container != null &&
(viewportPosition = MapPanel.GetViewportPosition(container)).HasValue &&
geometry.FillContains(viewportPosition.Value);
}
private void SelectionGeometryPropertyChanged(Geometry geometry)
{
if (SelectionMode == SelectionMode.Single)
{
SelectedItem = GetFirstItemInGeometry(geometry);
}
else
{
SetSelectedItems(GetItemsInGeometry(geometry));
}
}
private void CurrentItemChanging(object sender, CurrentChangingEventArgs e)
{
var container = ItemContainerGenerator.ContainerFromItem(Items.CurrentItem) as UIElement;

View file

@ -4,11 +4,9 @@
#if WINDOWS_RUNTIME
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;
#else
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
#endif
@ -21,7 +19,11 @@ namespace MapControl
public MapPanel()
{
if (!(this is MapBase))
if (this is MapBase)
{
SetValue(ParentMapProperty, this);
}
else
{
AddParentMapHandlers(this);
}
@ -67,10 +69,5 @@ namespace MapControl
return parentMap;
}
internal void SetParentMap()
{
SetValue(ParentMapProperty, this);
}
}
}

View file

@ -14,14 +14,17 @@ namespace MapControl
public static readonly DependencyProperty ParentMapProperty = ParentMapPropertyKey.DependencyProperty;
public MapPanel()
{
if (this is MapBase)
{
SetValue(ParentMapPropertyKey, this);
}
}
public static MapBase GetParentMap(UIElement element)
{
return (MapBase)element.GetValue(ParentMapProperty);
}
internal void SetParentMap()
{
SetValue(ParentMapPropertyKey, this);
}
}
}

View file

@ -24,9 +24,6 @@ namespace MapControl
public static readonly DependencyProperty LocationProperty = DependencyProperty.RegisterAttached(
"Location", typeof(Location), typeof(MapPanel), new PropertyMetadata(null, LocationPropertyChanged));
public static readonly DependencyProperty ViewportPositionProperty = DependencyProperty.RegisterAttached(
"ViewportPosition", typeof(Point?), typeof(MapPanel), null);
public static Location GetLocation(UIElement element)
{
return (Location)element.GetValue(LocationProperty);
@ -37,21 +34,12 @@ namespace MapControl
element.SetValue(LocationProperty, value);
}
public static Point? GetViewportPosition(UIElement element)
{
return (Point?)element.GetValue(ViewportPositionProperty);
}
private MapBase parentMap;
public MapBase ParentMap
{
get { return parentMap; }
}
void IMapElement.SetParentMap(MapBase map)
{
SetParentMapOverride(map);
set { SetParentMapOverride(value); }
}
protected virtual void SetParentMapOverride(MapBase map)
@ -114,7 +102,7 @@ namespace MapControl
if (mapElement != null)
{
mapElement.SetParentMap(e.NewValue as MapBase);
mapElement.ParentMap = e.NewValue as MapBase;
}
}
@ -149,12 +137,10 @@ namespace MapControl
{
var mapPosition = parentMap.MapTransform.Transform(location, parentMap.Center.Longitude); // nearest to center longitude
viewportPosition = parentMap.ViewportTransform.Transform(mapPosition);
element.SetValue(ViewportPositionProperty, viewportPosition);
}
else
{
viewportPosition = new Point();
element.ClearValue(ViewportPositionProperty);
}
var translateTransform = element.RenderTransform as TranslateTransform;
@ -189,7 +175,7 @@ namespace MapControl
private static void ArrangeElementWithLocation(UIElement element)
{
var rect = new Rect(0d, 0d, element.DesiredSize.Width, element.DesiredSize.Height);
var rect = new Rect(new Point(), element.DesiredSize);
var frameworkElement = element as FrameworkElement;
if (frameworkElement != null)
@ -228,7 +214,7 @@ namespace MapControl
private static void ArrangeElementWithoutLocation(UIElement element, Size parentSize)
{
var rect = new Rect(0d, 0d, element.DesiredSize.Width, element.DesiredSize.Height);
var rect = new Rect(new Point(), element.DesiredSize);
var frameworkElement = element as FrameworkElement;
if (frameworkElement != null)

View file

@ -20,12 +20,11 @@ namespace MapControl
public MapBase ParentMap
{
get { return parentMap; }
}
void IMapElement.SetParentMap(MapBase map)
{
parentMap = map;
UpdateData();
set
{
parentMap = value;
UpdateData();
}
}
protected virtual void UpdateData()

View file

@ -7,7 +7,6 @@ using System.Linq;
using Windows.UI.Xaml.Media;
#else
using System.Windows.Media;
#endif
namespace MapControl

View file

@ -7,7 +7,6 @@ using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Media;
#else
using System;
using System.Windows;
using System.Windows.Media;
#endif
@ -82,10 +81,9 @@ namespace MapControl
!double.IsNaN(West) && !double.IsNaN(East) &&
South < North && West < East)
{
var p1 = ParentMap.MapTransform.Transform(new Location(South, West));
var p2 = ParentMap.MapTransform.Transform(new Location(North, East));
SetRect(new Rect(p1.X, p1.Y, p2.X - p1.X, p2.Y - p1.Y));
SetRect(new Rect(
ParentMap.MapTransform.Transform(new Location(South, West)),
ParentMap.MapTransform.Transform(new Location(North, East))));
}
else
{

View file

@ -14,7 +14,7 @@ using System.Windows.Controls;
namespace MapControl
{
/// <summary>
/// Common base class for MapPanel, TileLayer and TileContainer.
/// Common base class for MapPanel and TileLayer.
/// </summary>
public class PanelBase : Panel
{
@ -32,9 +32,9 @@ namespace MapControl
protected override Size ArrangeOverride(Size finalSize)
{
foreach (UIElement child in Children)
foreach (UIElement element in Children)
{
child.Arrange(new Rect(new Point(), finalSize));
element.Arrange(new Rect(new Point(), finalSize));
}
return finalSize;

View file

@ -17,8 +17,8 @@ using System.Windows;
[assembly: AssemblyCompany("Clemens Fischer")]
[assembly: AssemblyCopyright("Copyright © 2014 Clemens Fischer")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyVersion("2.3.1")]
[assembly: AssemblyFileVersion("2.3.1")]
[assembly: AssemblyVersion("2.4.0")]
[assembly: AssemblyFileVersion("2.4.0")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]

View file

@ -1,32 +0,0 @@
// XAML Map Control - http://xamlmapcontrol.codeplex.com/
// Copyright © 2014 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
using System;
#if WINDOWS_RUNTIME
using Windows.UI.Xaml.Media.Animation;
#else
using System.Windows.Media.Animation;
#endif
namespace MapControl
{
/// <summary>
/// Stores global static properties that control the behaviour of the map control.
/// </summary>
public static class Settings
{
public static TimeSpan TileUpdateInterval { get; set; }
public static TimeSpan TileAnimationDuration { get; set; }
public static TimeSpan MapAnimationDuration { get; set; }
public static EasingFunctionBase MapAnimationEasingFunction { get; set; }
static Settings()
{
TileUpdateInterval = TimeSpan.FromSeconds(0.5);
TileAnimationDuration = TimeSpan.FromSeconds(0.3);
MapAnimationDuration = TimeSpan.FromSeconds(0.3);
MapAnimationEasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut };
}
}
}

View file

@ -1,4 +1,4 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:map="clr-namespace:MapControl">
<Style TargetType="map:MapItemsControl">

View file

@ -2,6 +2,7 @@
// Copyright © 2014 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
using System;
#if WINDOWS_RUNTIME
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
@ -20,15 +21,15 @@ namespace MapControl
{
public partial class Tile
{
public void SetImageSource(ImageSource image, bool animateOpacity)
public void SetImage(ImageSource image, bool animateOpacity = true, bool isDownloading = true)
{
if (image != null && Image.Source == null)
{
if (animateOpacity)
if (animateOpacity && OpacityAnimationDuration > TimeSpan.Zero)
{
var bitmap = image as BitmapImage;
BitmapImage bitmap;
if (bitmap != null)
if (isDownloading && (bitmap = image as BitmapImage) != null)
{
bitmap.ImageOpened += BitmapImageOpened;
bitmap.ImageFailed += BitmapImageFailed;
@ -36,17 +37,18 @@ namespace MapControl
else
{
Image.BeginAnimation(Image.OpacityProperty,
new DoubleAnimation { To = 1d, Duration = Settings.TileAnimationDuration });
new DoubleAnimation { From = 0d, To = 1d, Duration = OpacityAnimationDuration });
}
}
else
{
Image.Opacity = 1d;
}
Image.Source = image;
}
Image.Source = image;
HasImageSource = true;
Pending = false;
}
private void BitmapImageOpened(object sender, RoutedEventArgs e)
@ -57,7 +59,7 @@ namespace MapControl
bitmap.ImageFailed -= BitmapImageFailed;
Image.BeginAnimation(Image.OpacityProperty,
new DoubleAnimation { To = 1d, Duration = Settings.TileAnimationDuration });
new DoubleAnimation { From = 0d, To = 1d, Duration = OpacityAnimationDuration });
}
private void BitmapImageFailed(object sender, ExceptionRoutedEventArgs e)

View file

@ -12,11 +12,11 @@ namespace MapControl
{
public partial class Tile
{
public void SetImageSource(ImageSource image, bool animateOpacity)
public void SetImage(ImageSource image, bool animateOpacity = true)
{
if (image != null && Image.Source == null)
{
if (animateOpacity)
if (animateOpacity && OpacityAnimationDuration > TimeSpan.Zero)
{
var bitmap = image as BitmapSource;
@ -28,17 +28,18 @@ namespace MapControl
else
{
Image.BeginAnimation(Image.OpacityProperty,
new DoubleAnimation(1d, Settings.TileAnimationDuration));
new DoubleAnimation(0d, 1d, OpacityAnimationDuration));
}
}
else
{
Image.Opacity = 1d;
}
Image.Source = image;
}
Image.Source = image;
HasImageSource = true;
Pending = false;
}
private void BitmapDownloadCompleted(object sender, EventArgs e)
@ -49,7 +50,7 @@ namespace MapControl
bitmap.DownloadFailed -= BitmapDownloadFailed;
Image.BeginAnimation(Image.OpacityProperty,
new DoubleAnimation(1d, Settings.TileAnimationDuration));
new DoubleAnimation(0d, 1d, OpacityAnimationDuration));
}
private void BitmapDownloadFailed(object sender, ExceptionEventArgs e)

View file

@ -13,6 +13,8 @@ namespace MapControl
{
public partial class Tile
{
public static TimeSpan OpacityAnimationDuration = TimeSpan.FromSeconds(0.3);
public readonly int ZoomLevel;
public readonly int X;
public readonly int Y;
@ -23,9 +25,10 @@ namespace MapControl
ZoomLevel = zoomLevel;
X = x;
Y = y;
Pending = true;
}
public bool HasImageSource { get; private set; }
public bool Pending { get; private set; }
public int XIndex
{

View file

@ -1,49 +0,0 @@
// XAML Map Control - http://xamlmapcontrol.codeplex.com/
// Copyright © 2014 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
using System;
#if WINDOWS_RUNTIME
using Windows.Foundation;
using Windows.UI.Xaml.Media;
#else
using System.Windows;
using System.Windows.Media;
#endif
namespace MapControl
{
internal partial class TileContainer
{
private Matrix GetTileIndexMatrix(double scale)
{
return ViewportTransform.Matrix
.Invert() // view to map coordinates
.Translate(180d, -180d)
.Scale(scale, -scale); // map coordinates to tile indices
}
private void UpdateViewportTransform(double scale, Point mapOrigin)
{
ViewportTransform.Matrix = Matrix.Identity
.Translate(-mapOrigin.X, -mapOrigin.Y)
.Scale(scale, -scale)
.Rotate(rotation)
.Translate(viewportOrigin.X, viewportOrigin.Y);
}
/// <summary>
/// Sets a RenderTransform with origin at tileGrid.X and tileGrid.Y to minimize rounding errors.
/// </summary>
private void UpdateRenderTransform()
{
var scale = Math.Pow(2d, zoomLevel - tileZoomLevel);
((MatrixTransform)RenderTransform).Matrix = Matrix.Identity
.Translate(tileGrid.X * TileSource.TileSize, tileGrid.Y * TileSource.TileSize)
.Scale(scale, scale)
.Translate(tileLayerOffset.X, tileLayerOffset.Y)
.RotateAt(rotation, viewportOrigin.X, viewportOrigin.Y);
}
}
}

View file

@ -1,49 +0,0 @@
// XAML Map Control - http://xamlmapcontrol.codeplex.com/
// Copyright © 2014 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
using System;
using System.Windows;
using System.Windows.Media;
namespace MapControl
{
internal partial class TileContainer
{
private Matrix GetTileIndexMatrix(double scale)
{
var transform = ViewportTransform.Matrix;
transform.Invert(); // view to map coordinates
transform.Translate(180d, -180d);
transform.Scale(scale, -scale); // map coordinates to tile indices
return transform;
}
private void UpdateViewportTransform(double scale, Point mapOrigin)
{
var transform = Matrix.Identity;
transform.Translate(-mapOrigin.X, -mapOrigin.Y);
transform.Scale(scale, -scale);
transform.Rotate(rotation);
transform.Translate(viewportOrigin.X, viewportOrigin.Y);
ViewportTransform.Matrix = transform;
}
/// <summary>
/// Sets a RenderTransform with origin at tileGrid.X and tileGrid.Y to minimize rounding errors.
/// </summary>
private void UpdateRenderTransform()
{
var scale = Math.Pow(2d, zoomLevel - tileZoomLevel);
var transform = Matrix.Identity;
transform.Translate(tileGrid.X * TileSource.TileSize, tileGrid.Y * TileSource.TileSize);
transform.Scale(scale, scale);
transform.Translate(tileLayerOffset.X, tileLayerOffset.Y);
transform.RotateAt(rotation, viewportOrigin.X, viewportOrigin.Y);
((MatrixTransform)RenderTransform).Matrix = transform;
}
}
}

View file

@ -1,151 +0,0 @@
// XAML Map Control - http://xamlmapcontrol.codeplex.com/
// Copyright © 2014 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
using System;
using System.Collections.Generic;
using System.Linq;
#if WINDOWS_RUNTIME
using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Media;
#else
using System.Windows;
using System.Windows.Media;
using System.Windows.Threading;
#endif
namespace MapControl
{
internal partial class TileContainer : PanelBase
{
// relative size of scaled tile ranges from 0.75 to 1.5 (192 to 384 pixels)
private static double zoomLevelSwitchDelta = -Math.Log(0.75, 2d);
private readonly DispatcherTimer updateTimer;
private Point viewportOrigin;
private Point tileLayerOffset;
private double rotation;
private double zoomLevel;
private int tileZoomLevel;
private Int32Rect tileGrid;
public readonly MatrixTransform ViewportTransform = new MatrixTransform();
public TileContainer()
{
RenderTransform = new MatrixTransform();
updateTimer = new DispatcherTimer { Interval = Settings.TileUpdateInterval };
updateTimer.Tick += UpdateTiles;
}
public IEnumerable<TileLayer> TileLayers
{
get { return Children.Cast<TileLayer>(); }
}
public void AddTileLayers(int index, IEnumerable<TileLayer> tileLayers)
{
foreach (var tileLayer in tileLayers)
{
if (index < Children.Count)
{
Children.Insert(index, tileLayer);
}
else
{
Children.Add(tileLayer);
}
index++;
tileLayer.UpdateTiles(tileZoomLevel, tileGrid);
}
}
public void RemoveTileLayers(int index, int count)
{
while (count-- > 0)
{
((TileLayer)Children[index]).ClearTiles();
Children.RemoveAt(index);
}
}
public void ClearTileLayers()
{
foreach (TileLayer tileLayer in Children)
{
tileLayer.ClearTiles();
}
Children.Clear();
}
public double SetViewportTransform(double mapZoomLevel, double mapRotation, Point mapOrigin, Point viewOrigin)
{
var scale = Math.Pow(2d, zoomLevel) * TileSource.TileSize / 360d;
var oldMapOriginX = (viewportOrigin.X - tileLayerOffset.X) / scale - 180d;
if (zoomLevel != mapZoomLevel)
{
zoomLevel = mapZoomLevel;
scale = Math.Pow(2d, zoomLevel) * TileSource.TileSize / 360d;
}
rotation = mapRotation;
viewportOrigin = viewOrigin;
tileLayerOffset.X = viewportOrigin.X - (180d + mapOrigin.X) * scale;
tileLayerOffset.Y = viewportOrigin.Y - (180d - mapOrigin.Y) * scale;
UpdateViewportTransform(scale, mapOrigin);
UpdateRenderTransform();
if (Math.Abs(mapOrigin.X - oldMapOriginX) > 180d)
{
// immediately handle map origin leap when map center moves across 180° longitude
UpdateTiles(this, EventArgs.Empty);
}
else
{
updateTimer.Start();
}
return scale;
}
private void UpdateTiles(object sender, object e)
{
updateTimer.Stop();
var zoom = (int)Math.Floor(zoomLevel + zoomLevelSwitchDelta);
var scale = (double)(1 << zoom) / 360d;
var transform = GetTileIndexMatrix(scale);
// tile indices of visible rectangle
var p1 = transform.Transform(new Point(0d, 0d));
var p2 = transform.Transform(new Point(RenderSize.Width, 0d));
var p3 = transform.Transform(new Point(0d, RenderSize.Height));
var p4 = transform.Transform(new Point(RenderSize.Width, 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 grid = new Int32Rect(x1, y1, x2 - x1 + 1, y2 - y1 + 1);
if (tileZoomLevel != zoom || tileGrid != grid)
{
tileZoomLevel = zoom;
tileGrid = grid;
UpdateRenderTransform();
foreach (TileLayer tileLayer in Children)
{
tileLayer.UpdateTiles(tileZoomLevel, tileGrid);
}
}
}
}
}

View file

@ -39,7 +39,7 @@ namespace MapControl
}
}
tile.SetImageSource(image, tileLayer.AnimateTileOpacity);
tile.SetImage(image);
}
catch (Exception ex)
{

View file

@ -12,6 +12,7 @@ using System.Linq;
using System.Net;
using System.Runtime.Caching;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
@ -24,53 +25,62 @@ namespace MapControl
public class TileImageLoader : ITileImageLoader
{
/// <summary>
/// Default Name of an ObjectCache instance that is assigned to the Cache property.
/// Default name of an ObjectCache instance that is assigned to the Cache property.
/// </summary>
public const string DefaultCacheName = "TileCache";
/// <summary>
/// Default value for the directory where an ObjectCache instance may save cached data.
/// Default folder path where an ObjectCache instance may save cached data.
/// </summary>
public static readonly string DefaultCacheDirectory =
public static readonly string DefaultCacheFolder =
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "MapControl");
/// <summary>
/// Default expiration time span for cached images. Used when no expiration date
/// was transmitted on download. The default value is seven days.
/// Default expiration time for cached tile images. Used when no expiration time
/// was transmitted on download. The default and recommended minimum value is seven days.
/// See OpenStreetMap tile usage policy: http://wiki.openstreetmap.org/wiki/Tile_usage_policy
/// </summary>
public static TimeSpan DefaultCacheExpiration { get; set; }
public static TimeSpan DefaultCacheExpiration = TimeSpan.FromDays(7);
/// <summary>
/// The ObjectCache used to cache tile images. The default is MemoryCache.Default.
/// </summary>
public static ObjectCache Cache { get; set; }
public static ObjectCache Cache = MemoryCache.Default;
static TileImageLoader()
/// <summary>
/// Optional value to be used for the HttpWebRequest.UserAgent property. The default is null.
/// </summary>
public static string HttpUserAgent;
private class PendingTile
{
DefaultCacheExpiration = TimeSpan.FromDays(7);
Cache = MemoryCache.Default;
public readonly Tile Tile;
public readonly ImageSource CachedImage;
public PendingTile(Tile tile, ImageSource cachedImage = null)
{
Tile = tile;
CachedImage = cachedImage;
}
}
private readonly ConcurrentQueue<Tile> pendingTiles = new ConcurrentQueue<Tile>();
private int threadCount;
private readonly ConcurrentQueue<PendingTile> pendingTiles = new ConcurrentQueue<PendingTile>();
private int taskCount;
public void BeginLoadTiles(TileLayer tileLayer, IEnumerable<Tile> tiles)
{
if (tiles.Any())
{
// get current TileLayer property values in UI thread
var dispatcher = tileLayer.Dispatcher;
var tileSource = tileLayer.TileSource;
var imageTileSource = tileSource as ImageTileSource;
var animateOpacity = tileLayer.AnimateTileOpacity;
var dispatcher = tileLayer.Dispatcher;
if (imageTileSource != null && !imageTileSource.IsAsync) // call LoadImage in UI thread
if (imageTileSource != null && !imageTileSource.IsAsync) // call LoadImage in UI thread with low priority
{
var setImageAction = new Action<Tile>(t => t.SetImageSource(LoadImage(imageTileSource, t), animateOpacity));
foreach (var tile in tiles)
{
dispatcher.BeginInvoke(setImageAction, DispatcherPriority.Background, tile); // with low priority
dispatcher.BeginInvoke(new Action<Tile>(t => t.SetImage(LoadImage(imageTileSource, t))), DispatcherPriority.Background, tile);
}
}
else
@ -79,70 +89,65 @@ namespace MapControl
var sourceName = tileLayer.SourceName;
var maxDownloads = tileLayer.MaxParallelDownloads;
ThreadPool.QueueUserWorkItem(o =>
GetTiles(tileList, dispatcher, tileSource, sourceName, animateOpacity, maxDownloads));
Task.Run(() => GetTiles(tileList, dispatcher, tileSource, sourceName, maxDownloads));
}
}
}
public void CancelLoadTiles(TileLayer tileLayer)
{
Tile tile;
while (pendingTiles.TryDequeue(out tile)) ; // no Clear method
PendingTile pendingTile;
while (pendingTiles.TryDequeue(out pendingTile)) ; // no Clear method
}
private void GetTiles(List<Tile> tiles, Dispatcher dispatcher, TileSource tileSource, string sourceName, bool animateOpacity, int maxDownloads)
private void GetTiles(List<Tile> tiles, Dispatcher dispatcher, TileSource tileSource, string sourceName, int maxDownloads)
{
if (Cache != null && !string.IsNullOrWhiteSpace(sourceName) &&
!(tileSource is ImageTileSource) && !tileSource.UriFormat.StartsWith("file:"))
if (Cache != null &&
!string.IsNullOrWhiteSpace(sourceName) &&
!(tileSource is ImageTileSource) &&
!tileSource.UriFormat.StartsWith("file:"))
{
var setImageAction = new Action<Tile, ImageSource>((t, i) => t.SetImageSource(i, animateOpacity));
var outdatedTiles = new List<Tile>(tiles.Count);
foreach (var tile in tiles)
{
var buffer = Cache.Get(TileCache.Key(sourceName, tile)) as byte[];
var image = CreateImage(buffer);
BitmapSource image;
if (image == null)
if (GetCachedImage(GetCacheKey(sourceName, tile), out image))
{
pendingTiles.Enqueue(tile); // not yet cached
}
else if (TileCache.IsExpired(buffer))
{
dispatcher.Invoke(setImageAction, tile, image); // synchronously before enqueuing
outdatedTiles.Add(tile); // update outdated cache
dispatcher.BeginInvoke(new Action<Tile, ImageSource>((t, i) => t.SetImage(i)), tile, image);
}
else
{
dispatcher.BeginInvoke(setImageAction, tile, image);
pendingTiles.Enqueue(new PendingTile(tile, image));
}
}
tiles = outdatedTiles; // enqueue outdated tiles after current tiles
}
else
{
foreach (var tile in tiles)
{
pendingTiles.Enqueue(new PendingTile(tile));
}
}
foreach (var tile in tiles)
{
pendingTiles.Enqueue(tile);
}
var newTaskCount = Math.Min(pendingTiles.Count, maxDownloads) - taskCount;
while (threadCount < Math.Min(pendingTiles.Count, maxDownloads))
while (newTaskCount-- > 0)
{
Interlocked.Increment(ref threadCount);
Interlocked.Increment(ref taskCount);
ThreadPool.QueueUserWorkItem(o => LoadPendingTiles(dispatcher, tileSource, sourceName, animateOpacity));
Task.Run(() => LoadPendingTiles(dispatcher, tileSource, sourceName));
}
}
private void LoadPendingTiles(Dispatcher dispatcher, TileSource tileSource, string sourceName, bool animateOpacity)
private void LoadPendingTiles(Dispatcher dispatcher, TileSource tileSource, string sourceName)
{
var setImageAction = new Action<Tile, ImageSource>((t, i) => t.SetImageSource(i, animateOpacity));
var imageTileSource = tileSource as ImageTileSource;
Tile tile;
PendingTile pendingTile;
while (pendingTiles.TryDequeue(out tile))
while (pendingTiles.TryDequeue(out pendingTile))
{
var tile = pendingTile.Tile;
ImageSource image = null;
if (imageTileSource != null)
@ -161,29 +166,33 @@ namespace MapControl
}
else
{
DateTime expirationTime;
var buffer = DownloadImage(uri, out expirationTime);
HttpStatusCode statusCode;
image = CreateImage(buffer);
image = DownloadImage(uri, GetCacheKey(sourceName, tile), out statusCode);
if (image != null &&
Cache != null &&
!string.IsNullOrWhiteSpace(sourceName) &&
expirationTime > DateTime.UtcNow)
if (statusCode == HttpStatusCode.NotFound)
{
Cache.Set(TileCache.Key(sourceName, tile), buffer, new CacheItemPolicy { AbsoluteExpiration = expirationTime });
tileSource.IgnoreTile(tile.XIndex, tile.Y, tile.ZoomLevel); // do not request again
}
else if (image == null) // download failed, use cached image if available
{
image = pendingTile.CachedImage;
}
}
}
}
if (image != null || !tile.HasImageSource) // set null image if tile does not yet have an ImageSource
if (image != null)
{
dispatcher.BeginInvoke(setImageAction, tile, image);
dispatcher.BeginInvoke(new Action<Tile, ImageSource>((t, i) => t.SetImage(i)), tile, image);
}
else
{
tile.SetImage(null);
}
}
Interlocked.Decrement(ref threadCount);
Interlocked.Decrement(ref taskCount);
}
private static ImageSource LoadImage(ImageTileSource tileSource, Tile tile)
@ -196,7 +205,7 @@ namespace MapControl
}
catch (Exception ex)
{
Trace.TraceWarning("Loading tile image failed: {0}", ex.Message);
Debug.WriteLine("Loading tile image failed: {0}", (object)ex.Message);
}
return image;
@ -210,122 +219,138 @@ namespace MapControl
{
try
{
using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
using (var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
{
image = BitmapFrame.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
image = BitmapFrame.Create(fileStream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
}
}
catch (Exception ex)
{
Trace.TraceWarning("Creating tile image failed: {0}", ex.Message);
Debug.WriteLine("Creating tile image failed: {0}", (object)ex.Message);
}
}
return image;
}
private static ImageSource CreateImage(byte[] buffer)
private static ImageSource DownloadImage(Uri uri, string cacheKey, out HttpStatusCode statusCode)
{
ImageSource image = null;
if (buffer != null)
{
try
{
using (var stream = TileCache.ImageStream(buffer))
{
image = BitmapFrame.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
}
}
catch (Exception ex)
{
Trace.TraceWarning("Creating tile image failed: {0}", ex.Message);
}
}
return image;
}
private static byte[] DownloadImage(Uri uri, out DateTime expirationTime)
{
expirationTime = DateTime.UtcNow + DefaultCacheExpiration;
byte[] buffer = null;
BitmapSource image = null;
statusCode = HttpStatusCode.Unused;
try
{
var request = HttpWebRequest.CreateHttp(uri);
request.UserAgent = HttpUserAgent;
using (var response = (HttpWebResponse)request.GetResponse())
using (var responseStream = response.GetResponseStream())
{
var expiresHeader = response.Headers["Expires"];
DateTime expires;
statusCode = response.StatusCode;
if (expiresHeader != null &&
DateTime.TryParse(expiresHeader, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out expires) &&
expirationTime > expires)
using (var responseStream = response.GetResponseStream())
using (var memoryStream = new MemoryStream())
{
expirationTime = expires;
responseStream.CopyTo(memoryStream);
memoryStream.Position = 0;
image = BitmapFrame.Create(memoryStream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
}
buffer = TileCache.CreateBuffer(responseStream, (int)response.ContentLength, expirationTime);
if (cacheKey != null)
{
SetCachedImage(cacheKey, image, GetExpiration(response.Headers));
}
}
//Trace.TraceInformation("Downloaded {0}, expires {1}", uri, expirationTime);
}
catch (WebException ex)
{
if (ex.Status == WebExceptionStatus.ProtocolError)
var response = ex.Response as HttpWebResponse;
if (response != null)
{
var statusCode = ((HttpWebResponse)ex.Response).StatusCode;
if (statusCode != HttpStatusCode.NotFound)
{
Trace.TraceWarning("Downloading {0} failed: {1}", uri, ex.Message);
}
}
else
{
Trace.TraceWarning("Downloading {0} failed with {1}: {2}", uri, ex.Status, ex.Message);
statusCode = response.StatusCode;
}
Debug.WriteLine("Downloading {0} failed: {1}: {2}", uri, ex.Status, ex.Message);
}
catch (Exception ex)
{
Trace.TraceWarning("Downloading {0} failed: {1}", uri, ex.Message);
Debug.WriteLine("Downloading {0} failed: {1}", uri, ex.Message);
}
return buffer;
return image;
}
private static class TileCache
private static string GetCacheKey(string sourceName, Tile tile)
{
private const int imageBufferOffset = sizeof(Int64);
public static string Key(string sourceName, Tile tile)
if (Cache == null || string.IsNullOrWhiteSpace(sourceName))
{
return string.Format("{0}/{1}/{2}/{3}", sourceName, tile.ZoomLevel, tile.XIndex, tile.Y);
return null;
}
public static MemoryStream ImageStream(byte[] cacheBuffer)
return string.Format("{0}/{1}/{2}/{3}", sourceName, tile.ZoomLevel, tile.XIndex, tile.Y);
}
private static bool GetCachedImage(string cacheKey, out BitmapSource image)
{
image = Cache.Get(cacheKey) as BitmapSource;
if (image == null)
{
return new MemoryStream(cacheBuffer, imageBufferOffset, cacheBuffer.Length - imageBufferOffset, false);
return false;
}
public static bool IsExpired(byte[] cacheBuffer)
{
return DateTime.FromBinary(BitConverter.ToInt64(cacheBuffer, 0)) < DateTime.UtcNow;
}
var metadata = (BitmapMetadata)image.Metadata;
DateTime expiration;
public static byte[] CreateBuffer(Stream imageStream, int length, DateTime expirationTime)
// get cache expiration date from BitmapMetadata.DateTaken, must be parsed with CurrentCulture
return metadata == null
|| metadata.DateTaken == null
|| !DateTime.TryParse(metadata.DateTaken, CultureInfo.CurrentCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out expiration)
|| expiration > DateTime.UtcNow;
}
private static void SetCachedImage(string cacheKey, BitmapSource image, DateTime expiration)
{
var bitmap = BitmapFrame.Create(image);
var metadata = (BitmapMetadata)bitmap.Metadata;
// store cache expiration date in BitmapMetadata.DateTaken
metadata.DateTaken = expiration.ToString(CultureInfo.InvariantCulture);
metadata.Freeze();
bitmap.Freeze();
Cache.Set(cacheKey, bitmap, new CacheItemPolicy { AbsoluteExpiration = expiration });
//Debug.WriteLine("Cached {0}, Expires {1}", cacheKey, expiration);
}
private static DateTime GetExpiration(WebHeaderCollection headers)
{
var cacheControl = headers["Cache-Control"];
int maxAge;
DateTime expiration;
if (cacheControl != null &&
cacheControl.StartsWith("max-age=") &&
int.TryParse(cacheControl.Substring(8), out maxAge))
{
using (var memoryStream = length > 0 ? new MemoryStream(length + imageBufferOffset) : new MemoryStream())
maxAge = Math.Min(maxAge, (int)DefaultCacheExpiration.TotalSeconds);
expiration = DateTime.UtcNow.AddSeconds(maxAge);
}
else
{
var expires = headers["Expires"];
var maxExpiration = DateTime.UtcNow.Add(DefaultCacheExpiration);
if (expires == null ||
!DateTime.TryParse(expires, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out expiration) ||
expiration > maxExpiration)
{
memoryStream.Write(BitConverter.GetBytes(expirationTime.ToBinary()), 0, imageBufferOffset);
imageStream.CopyTo(memoryStream);
return length > 0 ? memoryStream.GetBuffer() : memoryStream.ToArray();
expiration = maxExpiration;
}
}
return expiration;
}
}
}

View file

@ -3,14 +3,15 @@
// Licensed under the Microsoft Public License (Ms-PL)
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Windows.Foundation;
using Windows.Storage;
using Windows.Storage.Streams;
using Windows.UI.Core;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Media.Imaging;
using Windows.Web.Http;
using Windows.Web.Http.Filters;
@ -18,142 +19,180 @@ using Windows.Web.Http.Filters;
namespace MapControl
{
/// <summary>
/// Loads map tile images.
/// Loads map tile images and optionally caches them in a IImageCache.
/// </summary>
public class TileImageLoader : ITileImageLoader
{
public static IObjectCache Cache { get; set; }
/// <summary>
/// Default name of an IImageCache instance that is assigned to the Cache property.
/// </summary>
public const string DefaultCacheName = "TileCache";
private HttpClient httpClient;
/// <summary>
/// Default StorageFolder where an IImageCache instance may save cached data.
/// </summary>
public static readonly StorageFolder DefaultCacheFolder = ApplicationData.Current.TemporaryFolder;
/// <summary>
/// Default expiration time for cached tile images. Used when no expiration time
/// was transmitted on download. The default and recommended minimum value is seven days.
/// See OpenStreetMap tile usage policy: http://wiki.openstreetmap.org/wiki/Tile_usage_policy
/// </summary>
public static TimeSpan DefaultCacheExpiration = TimeSpan.FromDays(7);
/// <summary>
/// The IImageCache implementation used to cache tile images. The default is null.
/// </summary>
public static Caching.IImageCache Cache;
private class PendingTile
{
public readonly Tile Tile;
public readonly Uri Uri;
public readonly BitmapSource Image;
public PendingTile(Tile tile, Uri uri)
{
Tile = tile;
Uri = uri;
Image = new BitmapImage();
}
}
private readonly ConcurrentQueue<PendingTile> pendingTiles = new ConcurrentQueue<PendingTile>();
private int taskCount;
public void BeginLoadTiles(TileLayer tileLayer, IEnumerable<Tile> tiles)
{
var imageTileSource = tileLayer.TileSource as ImageTileSource;
var tileSource = tileLayer.TileSource;
var imageTileSource = tileSource as ImageTileSource;
var sourceName = tileLayer.SourceName;
var useCache = Cache != null && !string.IsNullOrEmpty(sourceName);
foreach (var tile in tiles)
{
try
{
ImageSource image = null;
if (imageTileSource != null)
{
image = imageTileSource.LoadImage(tile.XIndex, tile.Y, tile.ZoomLevel);
tile.SetImage(imageTileSource.LoadImage(tile.XIndex, tile.Y, tile.ZoomLevel));
}
else
{
var uri = tileLayer.TileSource.GetUri(tile.XIndex, tile.Y, tile.ZoomLevel);
var uri = tileSource.GetUri(tile.XIndex, tile.Y, tile.ZoomLevel);
if (uri != null)
if (uri == null)
{
if (Cache == null || string.IsNullOrEmpty(tileLayer.SourceName))
{
image = new BitmapImage(uri);
}
else
{
var bitmap = new BitmapImage();
image = bitmap;
Task.Run(async () => await LoadCachedImage(tileLayer, tile, uri, bitmap));
}
tile.SetImage(null);
}
else if (!useCache)
{
tile.SetImage(new BitmapImage(uri));
}
else
{
pendingTiles.Enqueue(new PendingTile(tile, uri));
}
}
tile.SetImageSource(image, tileLayer.AnimateTileOpacity);
}
catch (Exception ex)
{
Debug.WriteLine("Loading tile image failed: {0}", ex.Message);
}
var newTaskCount = Math.Min(pendingTiles.Count, tileLayer.MaxParallelDownloads) - taskCount;
while (newTaskCount-- > 0)
{
Interlocked.Increment(ref taskCount);
Task.Run(async () => await LoadPendingTiles(tileSource, sourceName));
}
}
}
public void CancelLoadTiles(TileLayer tileLayer)
{
PendingTile pendingTile;
while (pendingTiles.TryDequeue(out pendingTile)) ; // no Clear method
}
private async Task LoadCachedImage(TileLayer tileLayer, Tile tile, Uri uri, BitmapImage bitmap)
private async Task LoadPendingTiles(TileSource tileSource, string sourceName)
{
var cacheKey = string.Format(@"{0}\{1}\{2}\{3}{4}",
tileLayer.SourceName, tile.ZoomLevel, tile.XIndex, tile.Y, Path.GetExtension(uri.LocalPath));
PendingTile pendingTile;
var buffer = await Cache.GetAsync(cacheKey) as IBuffer;
if (buffer != null)
while (pendingTiles.TryDequeue(out pendingTile))
{
await LoadImageFromBuffer(buffer, bitmap);
//Debug.WriteLine("Loaded cached image {0}", cacheKey);
}
else
{
DownloadAndCacheImage(uri, bitmap, cacheKey);
}
}
var tile = pendingTile.Tile;
var image = pendingTile.Image;
var uri = pendingTile.Uri;
var extension = Path.GetExtension(uri.LocalPath);
private async Task LoadImageFromBuffer(IBuffer buffer, BitmapImage bitmap)
{
using (var stream = new InMemoryRandomAccessStream())
{
await stream.WriteAsync(buffer);
await stream.FlushAsync();
stream.Seek(0);
await bitmap.Dispatcher.RunAsync(CoreDispatcherPriority.Low, async () =>
if (string.IsNullOrEmpty(extension) || extension == ".jpeg")
{
try
{
await bitmap.SetSourceAsync(stream);
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
});
}
}
private void DownloadAndCacheImage(Uri uri, BitmapImage bitmap, string cacheKey)
{
try
{
if (httpClient == null)
{
var filter = new HttpBaseProtocolFilter();
filter.AllowAutoRedirect = false;
filter.CacheControl.ReadBehavior = HttpCacheReadBehavior.Default;
filter.CacheControl.WriteBehavior = HttpCacheWriteBehavior.NoCache;
httpClient = new HttpClient(filter);
extension = ".jpg";
}
httpClient.GetAsync(uri).Completed = async (request, status) =>
var cacheKey = string.Format(@"{0}\{1}\{2}\{3}{4}", sourceName, tile.ZoomLevel, tile.XIndex, tile.Y, extension);
var cacheItem = await Cache.GetAsync(cacheKey);
var cachedBuffer = cacheItem != null ? cacheItem.Buffer : null;
if (cachedBuffer != null && cacheItem.Expires > DateTime.UtcNow)
{
if (status == AsyncStatus.Completed)
await LoadImageFromBuffer(tile, image, cachedBuffer);
}
else
{
var statusCode = await DownloadImage(tile, image, uri, cacheKey, cachedBuffer);
if (statusCode == HttpStatusCode.NotFound)
{
using (var response = request.GetResults())
{
await LoadImageFromHttpResponse(response, bitmap, cacheKey);
}
tileSource.IgnoreTile(tile.XIndex, tile.Y, tile.ZoomLevel); // do not request again
}
else
}
}
Interlocked.Decrement(ref taskCount);
}
private async Task<HttpStatusCode> DownloadImage(Tile tile, BitmapSource image, Uri uri, string cacheKey, IBuffer cachedBuffer)
{
HttpStatusCode result = HttpStatusCode.None;
try
{
using (var httpClient = new HttpClient(new HttpBaseProtocolFilter { AllowAutoRedirect = false }))
using (var response = await httpClient.GetAsync(uri))
{
result = response.StatusCode;
if (response.IsSuccessStatusCode)
{
Debug.WriteLine("{0}: {1}", uri, request.ErrorCode != null ? request.ErrorCode.Message : status.ToString());
await LoadImageFromHttpResponse(response, tile, image, cacheKey);
return result;
}
};
Debug.WriteLine("{0}: ({1}) {2}", uri, (int)response.StatusCode, response.ReasonPhrase);
}
}
catch (Exception ex)
{
Debug.WriteLine("{0}: {1}", uri, ex.Message);
}
if (cachedBuffer != null)
{
await LoadImageFromBuffer(tile, image, cachedBuffer);
}
return result;
}
private async Task LoadImageFromHttpResponse(HttpResponseMessage response, BitmapImage bitmap, string cacheKey)
private async Task LoadImageFromHttpResponse(HttpResponseMessage response, Tile tile, BitmapSource image, string cacheKey)
{
if (response.IsSuccessStatusCode)
using (var stream = new InMemoryRandomAccessStream())
{
var stream = new InMemoryRandomAccessStream();
using (var content = response.Content)
{
await content.WriteToStreamAsync(stream);
@ -162,35 +201,65 @@ namespace MapControl
await stream.FlushAsync();
stream.Seek(0);
await bitmap.Dispatcher.RunAsync(CoreDispatcherPriority.Low, async () =>
var loaded = await LoadImageFromStream(tile, image, stream);
if (loaded && cacheKey != null)
{
var buffer = new Windows.Storage.Streams.Buffer((uint)stream.Size);
stream.Seek(0);
await stream.ReadAsync(buffer, buffer.Capacity, InputStreamOptions.None);
var maxAge = DefaultCacheExpiration;
if (response.Headers.CacheControl.MaxAge.HasValue &&
response.Headers.CacheControl.MaxAge.Value < maxAge)
{
maxAge = response.Headers.CacheControl.MaxAge.Value;
}
await Cache.SetAsync(cacheKey, buffer, DateTime.UtcNow.Add(maxAge));
}
}
}
private async Task<bool> LoadImageFromBuffer(Tile tile, BitmapSource image, IBuffer buffer)
{
using (var stream = new InMemoryRandomAccessStream())
{
await stream.WriteAsync(buffer);
await stream.FlushAsync();
stream.Seek(0);
return await LoadImageFromStream(tile, image, stream);
}
}
private async Task<bool> LoadImageFromStream(Tile tile, BitmapSource image, IRandomAccessStream stream)
{
var completion = new TaskCompletionSource<bool>();
var action = image.Dispatcher.RunAsync(
CoreDispatcherPriority.Normal,
async () =>
{
try
{
await bitmap.SetSourceAsync(stream);
await image.SetSourceAsync(stream);
tile.SetImage(image, true, false);
// cache image asynchronously, after successful decoding
var task = Task.Run(async () =>
{
var buffer = new Windows.Storage.Streams.Buffer((uint)stream.Size);
stream.Seek(0);
await stream.ReadAsync(buffer, buffer.Capacity, InputStreamOptions.None);
stream.Dispose();
await Cache.SetAsync(cacheKey, buffer);
});
completion.SetResult(true);
}
catch (Exception ex)
{
Debug.WriteLine("{0}: {1}", response.RequestMessage.RequestUri, ex.Message);
stream.Dispose();
Debug.WriteLine(ex.Message);
tile.SetImage(null);
completion.SetResult(false);
}
});
}
else
{
Debug.WriteLine("{0}: {1}", response.RequestMessage.RequestUri, response.StatusCode);
}
return await completion.Task;
}
}
}

View file

@ -2,13 +2,14 @@
// Copyright © 2014 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
using System.Threading.Tasks;
namespace MapControl
{
public interface IObjectCache
public partial class TileLayer
{
Task<object> GetAsync(string key);
Task SetAsync(string key, object value);
partial void Initialize()
{
IsHitTestVisible = false;
MapPanel.AddParentMapHandlers(this);
}
}
}

View file

@ -0,0 +1,17 @@
// XAML Map Control - http://xamlmapcontrol.codeplex.com/
// Copyright © 2014 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
using System.Windows;
namespace MapControl
{
public partial class TileLayer
{
static TileLayer()
{
UIElement.IsHitTestVisibleProperty.OverrideMetadata(
typeof(TileLayer), new FrameworkPropertyMetadata(false));
}
}
}

View file

@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Linq;
#if WINDOWS_RUNTIME
using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Documents;
using Windows.UI.Xaml.Markup;
using Windows.UI.Xaml.Media;
@ -19,12 +20,6 @@ using System.Windows.Media;
namespace MapControl
{
public interface ITileImageLoader
{
void BeginLoadTiles(TileLayer tileLayer, IEnumerable<Tile> tiles);
void CancelLoadTiles(TileLayer tileLayer);
}
/// <summary>
/// Fills a rectangular area with map tiles from a TileSource.
/// </summary>
@ -33,7 +28,7 @@ namespace MapControl
#else
[ContentProperty("TileSource")]
#endif
public class TileLayer : PanelBase
public partial class TileLayer : PanelBase, IMapElement
{
public static TileLayer Default
{
@ -42,17 +37,40 @@ namespace MapControl
return new TileLayer
{
SourceName = "OpenStreetMap",
Description="© [OpenStreetMap Contributors](http://www.openstreetmap.org/copyright)",
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()));
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 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 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 ITileImageLoader tileImageLoader;
private TileSource tileSource;
private List<Tile> tiles = new List<Tile>();
private int zoomLevel;
private Int32Rect grid;
private MapBase parentMap;
public TileLayer()
: this(new TileImageLoader())
@ -62,123 +80,190 @@ namespace MapControl
public TileLayer(ITileImageLoader tileImageLoader)
{
this.tileImageLoader = tileImageLoader;
MinZoomLevel = 0;
MaxZoomLevel = 18;
MaxParallelDownloads = 4;
LoadLowerZoomLevels = true;
AnimateTileOpacity = true;
Initialize();
}
public string SourceName { get; set; }
public string Description { get; set; }
public int MinZoomLevel { get; set; }
public int MaxZoomLevel { get; set; }
public int MaxParallelDownloads { get; set; }
public bool LoadLowerZoomLevels { get; set; }
public bool AnimateTileOpacity { get; set; }
public Brush Foreground { get; set; }
partial void Initialize();
/// <summary>
/// Controls how map tiles are loaded.
/// </summary>
public TileSource TileSource
{
get { return (TileSource)GetValue(TileSourceProperty); }
set { SetValue(TileSourceProperty, value); }
}
/// <summary>
/// Name of the TileSource. Used as key in a TileLayerCollection and to name an optional tile cache.
/// </summary>
public string SourceName
{
get { return (string)GetValue(SourceNameProperty); }
set { SetValue(SourceNameProperty, value); }
}
/// <summary>
/// Description of the TileLayer.
/// </summary>
public string Description
{
get { return (string)GetValue(DescriptionProperty); }
set { SetValue(DescriptionProperty, value); }
}
/// <summary>
/// Minimum zoom level supported by the TileLayer.
/// </summary>
public int MinZoomLevel
{
get { return (int)GetValue(MinZoomLevelProperty); }
set { SetValue(MinZoomLevelProperty, value); }
}
/// <summary>
/// Maximum zoom level supported by the TileLayer.
/// </summary>
public int MaxZoomLevel
{
get { return (int)GetValue(MaxZoomLevelProperty); }
set { SetValue(MaxZoomLevelProperty, value); }
}
/// <summary>
/// Maximum number of parallel downloads that may be performed by the TileLayer's ITileImageLoader.
/// </summary>
public int MaxParallelDownloads
{
get { return (int)GetValue(MaxParallelDownloadsProperty); }
set { SetValue(MaxParallelDownloadsProperty, value); }
}
/// <summary>
/// Sets MapBase.Foreground, if not null.
/// </summary>
public Brush Foreground
{
get { return (Brush)GetValue(ForegroundProperty); }
set { SetValue(ForegroundProperty, value); }
}
/// <summary>
/// Sets MapBase.Background, if not null.
/// New property prevents filling of RenderTransformed TileLayer with Panel.Background.
/// </summary>
public new Brush Background
{
get { return (Brush)GetValue(BackgroundProperty); }
set { SetValue(BackgroundProperty, value); }
}
/// <summary>
/// In case the Description text contains copyright links in markdown syntax [text](url),
/// the DescriptionInlines property may be used to create a collection of Run and Hyperlink
/// inlines to be displayed in e.g. a TextBlock or a Silverlight RichTextBlock.
/// </summary>
public ICollection<Inline> DescriptionInlines
public List<Inline> DescriptionInlines
{
get { return Description.ToInlines(); }
}
public TileSource TileSource
public MapBase ParentMap
{
get { return tileSource; }
get { return parentMap; }
set
{
tileSource = value;
if (grid.Width > 0 && grid.Height > 0)
if (parentMap != null)
{
ClearTiles();
UpdateTiles();
parentMap.TileGridChanged -= UpdateTiles;
ClearValue(RenderTransformProperty);
}
parentMap = value;
if (parentMap != null)
{
parentMap.TileGridChanged += UpdateTiles;
RenderTransform = parentMap.TileLayerTransform;
}
UpdateTiles();
}
}
internal void ClearTiles()
protected virtual void UpdateTiles()
{
tileImageLoader.CancelLoadTiles(this);
tiles.Clear();
Children.Clear();
}
internal void UpdateTiles(int zoomLevel, Int32Rect grid)
{
this.zoomLevel = zoomLevel;
this.grid = grid;
UpdateTiles();
}
private void UpdateTiles()
{
if (tileSource != null)
if (tiles.Count > 0)
{
tileImageLoader.CancelLoadTiles(this);
}
SelectTiles();
Children.Clear();
SelectTiles();
Children.Clear();
if (tiles.Count > 0)
{
foreach (var tile in tiles)
{
Children.Add(tile.Image);
}
tileImageLoader.BeginLoadTiles(this, tiles.Where(t => !t.HasImageSource));
tileImageLoader.BeginLoadTiles(this, tiles.Where(t => t.Pending));
}
}
private void UpdateTiles(object sender, EventArgs e)
{
UpdateTiles();
}
private void SelectTiles()
{
var maxZoomLevel = Math.Min(zoomLevel, MaxZoomLevel);
var minZoomLevel = maxZoomLevel;
if (LoadLowerZoomLevels &&
Parent is TileContainer &&
((TileContainer)Parent).TileLayers.FirstOrDefault() == this)
{
minZoomLevel = MinZoomLevel;
}
var newTiles = new List<Tile>();
for (var z = minZoomLevel; z <= maxZoomLevel; z++)
if (parentMap != null && TileSource != null)
{
var tileSize = 1 << (zoomLevel - z);
var x1 = (int)Math.Floor((double)grid.X / tileSize); // may be negative
var x2 = (grid.X + grid.Width - 1) / tileSize;
var y1 = Math.Max(grid.Y / tileSize, 0);
var y2 = Math.Min((grid.Y + grid.Height - 1) / tileSize, (1 << z) - 1);
var grid = parentMap.TileGrid;
var zoomLevel = parentMap.TileZoomLevel;
var maxZoomLevel = Math.Min(zoomLevel, MaxZoomLevel);
var minZoomLevel = MinZoomLevel;
for (var y = y1; y <= y2; y++)
if (parentMap.TileLayers.FirstOrDefault() != this)
{
for (var x = x1; x <= x2; x++)
// do not load background tiles if this is not the base layer
minZoomLevel = Math.Max(maxZoomLevel, minZoomLevel);
}
for (var z = minZoomLevel; z <= maxZoomLevel; z++)
{
var tileSize = 1 << (zoomLevel - z);
var x1 = (int)Math.Floor((double)grid.X / tileSize); // may be negative
var x2 = (grid.X + grid.Width - 1) / tileSize;
var y1 = Math.Max(grid.Y / tileSize, 0);
var y2 = Math.Min((grid.Y + grid.Height - 1) / tileSize, (1 << z) - 1);
for (var y = y1; y <= y2; y++)
{
var tile = tiles.FirstOrDefault(t => t.ZoomLevel == z && t.X == x && t.Y == y);
if (tile == null)
for (var x = x1; x <= x2; x++)
{
tile = new Tile(z, x, y);
var tile = tiles.FirstOrDefault(t => t.ZoomLevel == z && t.X == x && t.Y == y);
var equivalentTile = tiles.FirstOrDefault(
t => t.Image.Source != null && t.ZoomLevel == z && t.XIndex == tile.XIndex && t.Y == y);
if (equivalentTile != null)
if (tile == null)
{
// do not animate to avoid flicker when crossing date line
tile.SetImageSource(equivalentTile.Image.Source, false);
}
}
tile = new Tile(z, x, y);
newTiles.Add(tile);
var equivalentTile = tiles.FirstOrDefault(
t => t.Image.Source != null && t.ZoomLevel == z && t.XIndex == tile.XIndex && t.Y == y);
if (equivalentTile != null)
{
// do not animate to avoid flicker when crossing 180°
tile.SetImage(equivalentTile.Image.Source, false);
}
}
newTiles.Add(tile);
}
}
}
}
@ -188,12 +273,18 @@ namespace MapControl
protected override Size ArrangeOverride(Size finalSize)
{
foreach (var tile in tiles)
if (parentMap != null)
{
var tileSize = (double)(256 << (zoomLevel - tile.ZoomLevel));
tile.Image.Width = tileSize;
tile.Image.Height = tileSize;
tile.Image.Arrange(new Rect(tileSize * tile.X - 256 * grid.X, tileSize * tile.Y - 256 * grid.Y, tileSize, tileSize));
foreach (var tile in tiles)
{
var tileSize = (double)(256 << (parentMap.TileZoomLevel - tile.ZoomLevel));
var x = tileSize * tile.X - 256 * parentMap.TileGrid.X;
var y = tileSize * tile.Y - 256 * parentMap.TileGrid.Y;
tile.Image.Width = tileSize;
tile.Image.Height = tileSize;
tile.Image.Arrange(new Rect(x, y, tileSize, tileSize));
}
}
return finalSize;

View file

@ -3,6 +3,7 @@
// Licensed under the Microsoft Public License (Ms-PL)
using System;
using System.Collections.Generic;
using System.Globalization;
#if WINDOWS_RUNTIME
using Windows.Foundation;
@ -20,6 +21,8 @@ namespace MapControl
public const int TileSize = 256;
public const double MetersPerDegree = 6378137d * Math.PI / 180d; // WGS 84 semi major axis
protected readonly HashSet<long> IgnoredTiles = new HashSet<long>();
private Func<int, int, int, Uri> getUri;
private string uriFormat = string.Empty;
@ -31,7 +34,7 @@ namespace MapControl
{
this.uriFormat = uriFormat;
}
public string UriFormat
{
get { return uriFormat; }
@ -84,7 +87,38 @@ namespace MapControl
public virtual Uri GetUri(int x, int y, int zoomLevel)
{
return getUri != null ? getUri(x, y, zoomLevel) : null;
if (getUri == null)
{
return null;
}
if (IgnoredTiles.Count > 0)
{
lock (IgnoredTiles)
{
if (IgnoredTiles.Contains(GetHashCode(x, y, zoomLevel)))
{
return null;
}
}
}
return getUri(x, y, zoomLevel);
}
public void IgnoreTile(int x, int y, int zoomLevel)
{
lock (IgnoredTiles)
{
IgnoredTiles.Add(GetHashCode(x, y, zoomLevel));
}
}
protected static long GetHashCode(int x, int y, int zoomLevel)
{
return (long)(x & 0xFFFFFF)
+ ((long)(y & 0xFFFFFF) << 24)
+ ((long)(zoomLevel & 0xFF) << 48);
}
private Uri GetBasicUri(int x, int y, int zoomLevel)

View file

@ -48,8 +48,8 @@
<Compile Include="..\HyperlinkText.cs">
<Link>HyperlinkText.cs</Link>
</Compile>
<Compile Include="..\ImageFileCache.WinRT.cs">
<Link>ImageFileCache.WinRT.cs</Link>
<Compile Include="..\ImageCache.WinRT.cs">
<Link>ImageCache.WinRT.cs</Link>
</Compile>
<Compile Include="..\ImageTileSource.Silverlight.WinRT.cs">
<Link>ImageTileSource.Silverlight.WinRT.cs</Link>
@ -60,8 +60,8 @@
<Compile Include="..\Int32Rect.cs">
<Link>Int32Rect.cs</Link>
</Compile>
<Compile Include="..\IObjectCache.WinRT.cs">
<Link>IObjectCache.WinRT.cs</Link>
<Compile Include="..\ITileImageLoader.cs">
<Link>ITileImageLoader.cs</Link>
</Compile>
<Compile Include="..\Location.cs">
<Link>Location.cs</Link>
@ -144,27 +144,21 @@
<Compile Include="..\Pushpin.Silverlight.WinRT.cs">
<Link>Pushpin.Silverlight.WinRT.cs</Link>
</Compile>
<Compile Include="..\Settings.cs">
<Link>Settings.cs</Link>
</Compile>
<Compile Include="..\Tile.cs">
<Link>Tile.cs</Link>
</Compile>
<Compile Include="..\Tile.Silverlight.WinRT.cs">
<Link>Tile.Silverlight.WinRT.cs</Link>
</Compile>
<Compile Include="..\TileContainer.cs">
<Link>TileContainer.cs</Link>
</Compile>
<Compile Include="..\TileContainer.Silverlight.WinRT.cs">
<Link>TileContainer.Silverlight.WinRT.cs</Link>
</Compile>
<Compile Include="..\TileImageLoader.WinRT.cs">
<Link>TileImageLoader.WinRT.cs</Link>
</Compile>
<Compile Include="..\TileLayer.cs">
<Link>TileLayer.cs</Link>
</Compile>
<Compile Include="..\TileLayer.Silverlight.WinRT.cs">
<Link>TileLayer.Silverlight.WinRT.cs</Link>
</Compile>
<Compile Include="..\TileLayerCollection.cs">
<Link>TileLayerCollection.cs</Link>
</Compile>

View file

@ -7,8 +7,8 @@ using System.Runtime.InteropServices;
[assembly: AssemblyCompany("Clemens Fischer")]
[assembly: AssemblyCopyright("Copyright © 2014 Clemens Fischer")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyVersion("2.3.1")]
[assembly: AssemblyFileVersion("2.3.1")]
[assembly: AssemblyVersion("2.4.0")]
[assembly: AssemblyFileVersion("2.4.0")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]