diff --git a/CHANGELOG.md b/CHANGELOG.md
index 97e28c9..8a551b8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,19 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
+## [0.3.0] 2023-05-12
+
+### Added
+Map Clustering for IOS and Android
+
+## [0.2.0] 2022-12-08
+
+### Added
+Source link support
+
+### update
+bump uno to 4.4.20
+
## [0.1.1] 2022-04-10
### Added
diff --git a/Samples/Samples/Samples.Shared/DynamicMapMenuPage.xaml b/Samples/Samples/Samples.Shared/DynamicMapMenuPage.xaml
index 5864683..c976e85 100644
--- a/Samples/Samples/Samples.Shared/DynamicMapMenuPage.xaml
+++ b/Samples/Samples/Samples.Shared/DynamicMapMenuPage.xaml
@@ -31,6 +31,13 @@
+
+
+
diff --git a/Samples/Samples/Samples.Shared/DynamicMap_FeaturesPage.xaml b/Samples/Samples/Samples.Shared/DynamicMap_FeaturesPage.xaml
index ad639ec..a3ae3c4 100644
--- a/Samples/Samples/Samples.Shared/DynamicMap_FeaturesPage.xaml
+++ b/Samples/Samples/Samples.Shared/DynamicMap_FeaturesPage.xaml
@@ -22,7 +22,8 @@
+ VisibilityIfTrue="Collapsed" />
+
@@ -112,6 +113,7 @@
new DynamicMap_FeaturesPageViewModel());
});
+ public IDynamicCommand GotoDynamicMap_WithClustering_FeaturesPage => this.GetCommandFromTask(async ct =>
+ {
+ await _sectionsNavigator.Navigate(ct, () => new DynamicMap_FeaturesPageViewModel(isClusterEnabled: true));
+ });
+
public IDynamicCommand GotoDynamicMap_MoveSearchPage => this.GetCommandFromTask(async ct =>
{
await _sectionsNavigator.Navigate(ct, () => new DynamicMap_MoveSearchPageViewModel());
diff --git a/Samples/Samples/Samples.Shared/Presentation/DynamicMap_FeaturesPageViewModel.cs b/Samples/Samples/Samples.Shared/Presentation/DynamicMap_FeaturesPageViewModel.cs
index 2e86770..8093815 100644
--- a/Samples/Samples/Samples.Shared/Presentation/DynamicMap_FeaturesPageViewModel.cs
+++ b/Samples/Samples/Samples.Shared/Presentation/DynamicMap_FeaturesPageViewModel.cs
@@ -1,21 +1,18 @@
-using Chinook.DynamicMvvm;
-using Uno.Extensions;
+using Cartography.DynamicMap;
+using Chinook.DynamicMvvm;
+using Chinook.SectionsNavigation;
+using GeolocatorService;
+using Samples.Entities;
using System;
+using System.Collections.Generic;
using System.Linq;
-using System.Reactive;
+using System.Reactive.Concurrency;
using System.Reactive.Linq;
-using System.Reactive.Threading.Tasks;
using System.Threading;
using System.Threading.Tasks;
-using Cartography.DynamicMap;
-using GeolocatorService;
+using Uno.Extensions;
using Uno.Logging;
-using Samples.Entities;
-using System.Collections.Generic;
-using Uno;
using Windows.Devices.Geolocation;
-using Chinook.SectionsNavigation;
-using System.Reactive.Concurrency;
namespace Samples.Presentation
{
@@ -32,8 +29,9 @@ public class DynamicMap_FeaturesPageViewModel : ViewModel, IDynamicMapComponent
private ISectionsNavigator _sectionsNavigator;
private readonly IDispatcherScheduler _dispatcherScheduler;
- public DynamicMap_FeaturesPageViewModel()
+ public DynamicMap_FeaturesPageViewModel(bool isClusterEnabled = false)
{
+ IsClusterEnabled = isClusterEnabled;
_geolocatorService = this.GetService();
_sectionsNavigator = this.GetService();
_dispatcherScheduler = this.GetService();
@@ -179,6 +177,12 @@ public bool IsUserDragging
set => this.Set(value);
}
+ public bool IsClusterEnabled
+ {
+ get => this.Get(initialValue: false);
+ set => this.Set(value);
+ }
+
public LocationResult UserLocation
{
get => this.Get();
@@ -292,17 +296,57 @@ private PushpinEntity[] GetInitialPushpins()
new PushpinEntity
{
Name = "Pushpin 1",
- Coordinates = new Geopoint(new BasicGeoposition{Latitude = 46.3938717, Longitude = -72.0921769})
+ Coordinates = new Geopoint(new BasicGeoposition { Latitude = 45.5040713, Longitude = -73.5587092 })
},
new PushpinEntity
{
Name = "Pushpin 2",
- Coordinates = new Geopoint(new BasicGeoposition { Latitude = 45.5502838, Longitude = -73.2801901 })
+ Coordinates = new Geopoint(new BasicGeoposition { Latitude = 45.5041113, Longitude = -73.5584092 })
},
new PushpinEntity
{
Name = "Pushpin 3",
- Coordinates = new Geopoint(new BasicGeoposition { Latitude = 45.5502838, Longitude = -72.0921769 })
+ Coordinates = new Geopoint(new BasicGeoposition { Latitude = 45.5035613, Longitude = -73.5587392 })
+ },
+ new PushpinEntity
+ {
+ Name = "Pushpin 4",
+ Coordinates = new Geopoint(new BasicGeoposition { Latitude = 45.5043413, Longitude = -73.5567092 })
+ },
+ new PushpinEntity
+ {
+ Name = "Pushpin 5",
+ Coordinates = new Geopoint(new BasicGeoposition { Latitude = 45.5040713, Longitude = -73.5547092 })
+ },
+ new PushpinEntity
+ {
+ Name = "Pushpin 6",
+ Coordinates = new Geopoint(new BasicGeoposition { Latitude = 45.5042713, Longitude = -73.5583092 })
+ },
+ new PushpinEntity
+ {
+ Name = "Pushpin 7",
+ Coordinates = new Geopoint(new BasicGeoposition { Latitude = 45.5046513, Longitude = -73.5587492 })
+ },
+ new PushpinEntity
+ {
+ Name = "Pushpin 8",
+ Coordinates = new Geopoint(new BasicGeoposition { Latitude = 45.5042313, Longitude = -73.5583562 })
+ },
+ new PushpinEntity
+ {
+ Name = "Pushpin 9",
+ Coordinates = new Geopoint(new BasicGeoposition { Latitude = 45.5040313, Longitude = -73.5584322 })
+ },
+ new PushpinEntity
+ {
+ Name = "Pushpin 10",
+ Coordinates = new Geopoint(new BasicGeoposition { Latitude = 45.5502338, Longitude = -73.2801901 })
+ },
+ new PushpinEntity
+ {
+ Name = "Pushpin 11",
+ Coordinates = new Geopoint(new BasicGeoposition { Latitude = 45.5501838, Longitude = -72.0921769 })
},
};
}
@@ -387,7 +431,7 @@ private async Task GetUserCoordinates(CancellationToken ct)
var newPushpin = await CreatePushpinAtCenter(ct);
var list = pushpins.ToList();
- list.Add((IGeoLocated)newPushpin);
+ list.Add(newPushpin);
Pushpins = list.ToArray();
});
diff --git a/Samples/Samples/Samples.Shared/Presentation/DynamicMap_MoveSearchPageViewModel.cs b/Samples/Samples/Samples.Shared/Presentation/DynamicMap_MoveSearchPageViewModel.cs
index 8366cc0..0d03109 100644
--- a/Samples/Samples/Samples.Shared/Presentation/DynamicMap_MoveSearchPageViewModel.cs
+++ b/Samples/Samples/Samples.Shared/Presentation/DynamicMap_MoveSearchPageViewModel.cs
@@ -103,6 +103,12 @@ public int? AnimationDurationSeconds
await _sectionsNavigator.Navigate(ct, () => new DynamicMapMenuViewModel());
});
+ public bool IsClusterEnabled
+ {
+ get => this.Get(initialValue: false);
+ set => this.Set(value);
+ }
+
private MapViewPort GetStartingCoordinates()
{
var mapViewPort = new MapViewPort(new Geopoint(new BasicGeoposition { Latitude = 45.503343, Longitude = -73.571695 }));
diff --git a/build/gitversion.yml b/build/gitversion.yml
index 09f4ede..4701131 100644
--- a/build/gitversion.yml
+++ b/build/gitversion.yml
@@ -1,6 +1,6 @@
assembly-versioning-scheme: MajorMinorPatch
mode: ContinuousDeployment
-next-version: 0.2.0
+next-version: 0.3.0
continuous-delivery-fallback-tag: ""
increment: none # Disabled as it is not used. Saves time on the GitVersion step
branches:
@@ -32,4 +32,4 @@ branches:
source-branches: ['master']
increment: none
ignore:
- sha: []
\ No newline at end of file
+ sha: []
diff --git a/src/Cartography/Cartography.DynamicMap/Cartography.DynamicMap.csproj b/src/Cartography/Cartography.DynamicMap/Cartography.DynamicMap.csproj
index 51076af..8c71f20 100644
--- a/src/Cartography/Cartography.DynamicMap/Cartography.DynamicMap.csproj
+++ b/src/Cartography/Cartography.DynamicMap/Cartography.DynamicMap.csproj
@@ -38,6 +38,7 @@
+
@@ -55,12 +56,9 @@
-
- 117.6.0.5
-
-
- 117.0.1.5
-
+
+
+
diff --git a/src/Cartography/Cartography.DynamicMap/IDynamicMapComponent.cs b/src/Cartography/Cartography.DynamicMap/IDynamicMapComponent.cs
index 80c9692..aa1d886 100644
--- a/src/Cartography/Cartography.DynamicMap/IDynamicMapComponent.cs
+++ b/src/Cartography/Cartography.DynamicMap/IDynamicMapComponent.cs
@@ -11,27 +11,27 @@ namespace Cartography.DynamicMap
public interface IDynamicMapComponent
{
///
- /// Pushpins to display on map
+ /// Pushpins to display on map.
///
IGeoLocated[] Pushpins { get; set; }
///
- /// Selected pushpin
+ /// Selected pushpin.
///
IGeoLocated[] SelectedPushpins { get; set; }
///
- /// Groups to display on map
+ /// Groups to display on map.
///
IGeoLocatedGrouping Groups { get; set; }
///
- /// Min delay to wait between two map viewport update
+ /// Min delay to wait between two map viewport update.
///
TimeSpan ViewPortUpdateMinDelay { get; set; }
///
- /// Equality comparer to use to filter the map viewport updates
+ /// Equality comparer to use to filter the map viewport updates.
///
IEqualityComparer ViewPortUpdateFilter { get; set; }
@@ -41,48 +41,53 @@ public interface IDynamicMapComponent
Action OnMapTapped { get; set; }
///
- /// Defines whether user tracking is enabled - READ / WRITE
+ /// Defines whether user tracking is enabled - READ / WRITE.
///
bool IsUserTrackingCurrentlyEnabled { get; set; }
///
- /// Whether or not the user is currently dragging the map
+ /// Whether or not the user is currently dragging the map.
///
bool IsUserDragging { get; set; }
- ///
- /// User location if any and if display requested, else empty value
- ///
- LocationResult UserLocation { get; set; }
+ ///
+ /// Whether or not the pushpin clustering is enabled.
+ ///
+ bool IsClusterEnabled { get; set; }
+
+ ///
+ /// User location if any and if display requested, else empty value.
+ ///
+ LocationResult UserLocation { get; set; }
///
- /// VisibleRegion of the map - READ ONLY
+ /// VisibleRegion of the map - READ ONLY.
///
MapViewPortCoordinates ViewPortCoordinates { get; set; }
///
- /// ViewPort of the map - READ / WRITE - First viewport MUST be provided by VM
+ /// ViewPort of the map - READ / WRITE - First viewport MUST be provided by VM.
///
MapViewPort ViewPort { get; set; }
///
- /// Sets the desired animation duration
+ /// Sets the desired animation duration.
///
int? AnimationDurationSeconds { get; set; }
}
///
- /// The MapComponent default value for some field
+ /// The MapComponent default value for some field.
///
public static class MapComponentDefaultValue
{
///
- /// Get the Default ViewPortUpdateMinDelay
+ /// Get the Default ViewPortUpdateMinDelay.
///
public static TimeSpan DefaultViewPortUpdateMinDelay = TimeSpan.FromMilliseconds(250);
///
- /// Get the Default ViewPortUpdateFilter
+ /// Get the Default ViewPortUpdateFilter.
///
public static IEqualityComparer DefaultViewPortUpdateFilter =
new PrettyMapViewPortEqualityComparer();
diff --git a/src/Cartography/Cartography.DynamicMap/MapControlBase.cs b/src/Cartography/Cartography.DynamicMap/MapControlBase.cs
index e276116..e29680f 100644
--- a/src/Cartography/Cartography.DynamicMap/MapControlBase.cs
+++ b/src/Cartography/Cartography.DynamicMap/MapControlBase.cs
@@ -231,9 +231,20 @@ public Point PushpinIconsPositionOrigin
get { return (Point)this.GetValue(PushpinsIconsPositionOriginProperty); }
set { this.SetValue(PushpinsIconsPositionOriginProperty, value); }
}
+ #endregion
+
+#region IsClusterEnabled (dp)
+ public static readonly DependencyProperty IsClusterEnabledProperty = DependencyProperty.Register(
+ "IsClusterEnabled", typeof(bool), typeof(MapControlBase), new PropertyMetadata(false));
+
+ public bool IsClusterEnabled
+ {
+ get { return (bool)this.GetValue(IsClusterEnabledProperty); }
+ set { this.SetValue(IsClusterEnabledProperty, value); }
+ }
#endregion
- private readonly SerialDisposable _configuredSourceSubscriptions = new SerialDisposable();
+ private readonly SerialDisposable _configuredSourceSubscriptions = new SerialDisposable();
private ViewModelBase _configuredViewModel;
private Action _onMapTapped;
@@ -511,7 +522,7 @@ private IDisposable SyncPushpinsFrom(ViewModelBase vm)
vm.GetAndObservePushpins(filterOutChangesFromSource: this),
vm.GetAndObserveGroups(filterOutChangesFromSource: this),
vm.GetAndObserveSelectedPushpins(filterOutChangesFromSource: null),
- (pushpins, groups, selected) => new
+ (pushpins, groups, selected) => new
{
items = pushpins
.Concat(groups)
@@ -524,7 +535,7 @@ private IDisposable SyncPushpinsFrom(ViewModelBase vm)
.Do(x =>
{
UpdateMapPushpins(x.items, x.selected);
- })
+ })
.ObserveOn(GetBackgroundScheduler())
.CombineLatest(
// In order to prevent the pushpins being updated when the source changes, but still making sure the pins are updated before the selection
diff --git a/src/Cartography/Cartography.DynamicMap/Platforms/Android/Clustering/CustomClusterManager.Android.cs b/src/Cartography/Cartography.DynamicMap/Platforms/Android/Clustering/CustomClusterManager.Android.cs
new file mode 100644
index 0000000..c8f1238
--- /dev/null
+++ b/src/Cartography/Cartography.DynamicMap/Platforms/Android/Clustering/CustomClusterManager.Android.cs
@@ -0,0 +1,38 @@
+#if __ANDROID__
+using Android.Content;
+using Android.Gms.Maps;
+using Android.Gms.Maps.Utils.Clustering;
+using Android.Gms.Maps.Utils.Collections;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Cartography.DynamicMap;
+
+public class CustomClusterManager : ClusterManager, ClusterManager.IOnClusterClickListener, ClusterManager.IOnClusterInfoWindowClickListener, ClusterManager.IOnClusterItemInfoWindowClickListener
+{
+ private ClusterManager _clusterManager;
+
+ public CustomClusterManager(Context context, GoogleMap map) : base(context, map)
+ {
+ }
+
+ public CustomClusterManager(Context context, GoogleMap map, MarkerManager makerManager) : base(context, map, makerManager)
+ {
+ }
+
+ public bool OnClusterClick(ICluster cluster)
+ {
+ return false;
+ }
+
+ public void OnClusterInfoWindowClick(ICluster cluster)
+ {
+ }
+
+ public void OnClusterItemInfoWindowClick(Java.Lang.Object item)
+ {
+ }
+}
+#endif
\ No newline at end of file
diff --git a/src/Cartography/Cartography.DynamicMap/Platforms/Android/Clustering/MapClusterItem.Android.cs b/src/Cartography/Cartography.DynamicMap/Platforms/Android/Clustering/MapClusterItem.Android.cs
new file mode 100644
index 0000000..9a03906
--- /dev/null
+++ b/src/Cartography/Cartography.DynamicMap/Platforms/Android/Clustering/MapClusterItem.Android.cs
@@ -0,0 +1,31 @@
+#if __ANDROID__
+using Android.Gms.Maps.Model;
+using Android.Gms.Maps.Utils.Clustering;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Cartography.DynamicMap;
+
+public class MapClusterItem : Java.Lang.Object, IMapControlItem, IClusterItem
+{
+ public MapClusterItem(IGeoLocated pushpin, bool isSelected = false)
+ {
+ Item = pushpin;
+ Position = new LatLng(pushpin.Coordinates.Position.Latitude, pushpin.Coordinates.Position.Longitude);
+ Snippet = null;
+ Title = null;
+ IsSelected= isSelected;
+ }
+
+ public IGeoLocated Item { get; set; }
+
+ public LatLng Position { get; set; }
+
+ public string Snippet { get; set; }
+
+ public string Title { get; set; }
+
+ public bool IsSelected { get; set; }
+}
+#endif
\ No newline at end of file
diff --git a/src/Cartography/Cartography.DynamicMap/Platforms/Android/Clustering/MapClusterItems.Android.cs b/src/Cartography/Cartography.DynamicMap/Platforms/Android/Clustering/MapClusterItems.Android.cs
new file mode 100644
index 0000000..1fa0fa0
--- /dev/null
+++ b/src/Cartography/Cartography.DynamicMap/Platforms/Android/Clustering/MapClusterItems.Android.cs
@@ -0,0 +1,109 @@
+#if __ANDROID__
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Cartography.DynamicMap;
+
+///
+/// Represent an array of clusterItems.
+///
+public class MapClusterItems
+{
+ ///
+ /// Initiate the array of cluster Items.
+ ///
+ /// .
+ public MapClusterItems(MapClusterItem[] mapClusterItems)
+ {
+ ClusterItems = mapClusterItems;
+ }
+
+ ///
+ /// Array of Map Cluster Item.
+ ///
+ public MapClusterItem[] ClusterItems { get ; set; }
+
+ ///
+ /// Add a array of items to existing array of MapClusterItems.
+ ///
+ /// .
+ public void AddItems(MapClusterItem[] items)
+ {
+ var listItems = ClusterItems.ToList();
+
+ foreach (var item in items)
+ {
+ listItems.Add(item);
+ }
+
+ ClusterItems = listItems.ToArray();
+ }
+
+ ///
+ /// Add a single item to MapClusterItems.
+ ///
+ /// .
+ public void AddItem(MapClusterItem item)
+ {
+ var listItems = ClusterItems.ToList();
+
+ listItems.Add(item);
+
+ ClusterItems = listItems.ToArray();
+ }
+
+ ///
+ /// Remove a array of items to existing array of MapClusterItems.
+ ///
+ /// .
+ public void RemoveItems(MapClusterItem[] items)
+ {
+ var listItems = ClusterItems.ToList();
+
+ foreach (var item in items)
+ {
+ listItems.Remove(item);
+ }
+
+ ClusterItems = listItems.ToArray();
+ }
+
+ ///
+ /// Remove a single item to MapClusterItems.
+ ///
+ /// .
+ public void RemoveItem(MapClusterItem item)
+ {
+ var listItems = ClusterItems.ToList();
+
+ listItems.Remove(item);
+
+ ClusterItems = listItems.ToArray();
+ }
+
+ ///
+ /// Send one item to another clusterItems and remove it from intial clusterItems.
+ ///
+ /// .
+ /// .
+ public void TransferTo(MapClusterItem item, MapClusterItems otherClusterItems)
+ {
+ RemoveItem(item);
+
+ otherClusterItems.AddItem(item);
+ }
+
+ ///
+ /// Add and remove MapClusterItem from MapClusterItems.
+ ///
+ /// .
+ /// .
+ public void UpdateItems(MapClusterItem[] itemToAdd, MapClusterItem[] itemToRemove)
+ {
+ RemoveItems(itemToRemove);
+ AddItems(itemToAdd);
+ }
+}
+#endif
\ No newline at end of file
diff --git a/src/Cartography/Cartography.DynamicMap/Platforms/Android/Clustering/MapControlBase.Cluster.Android.cs b/src/Cartography/Cartography.DynamicMap/Platforms/Android/Clustering/MapControlBase.Cluster.Android.cs
new file mode 100644
index 0000000..a8c2494
--- /dev/null
+++ b/src/Cartography/Cartography.DynamicMap/Platforms/Android/Clustering/MapControlBase.Cluster.Android.cs
@@ -0,0 +1,129 @@
+#if __ANDROID__
+using Android.Gms.Maps;
+using Android.Gms.Maps.Utils.Clustering;
+using Chinook.DynamicMvvm;
+using GeolocatorService;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reactive.Linq;
+using System.Text;
+using Uno.Extensions;
+using Uno.Logging;
+using Windows.UI.Xaml;
+
+namespace Cartography.DynamicMap;
+
+public partial class MapControlBase
+{
+ private CustomClusterManager _clusterManager;
+ private MapClusterItems _clusterPins;
+
+ private void OnMapClusterReady()
+ {
+ // Zoomlevel does not need to be manage here. ClusterManager will handle how and when a cluster is shown.
+ // A cluster can show a single pin. The diffrent styles will be manage in ClusterManager.
+
+ _clusterPins = new MapClusterItems(new MapClusterItem[0]);
+ _clusterManager = new CustomClusterManager(Context, _map);
+ SetupCustomClusterManager(_clusterManager);
+
+ UpdateMapPushpinOnCameraIdle();
+
+ _isReady = true;
+
+ UpdateAutolocateButtonVisibility(AutolocateButtonVisibility);
+ UpdateCompassButtonVisibility(CompassButtonVisibility);
+ UpdateIsRotateGestureEnabled(IsRotateGestureEnabled);
+ UpdateMapStyleJson(MapStyleJson);
+
+ UpdateIcon(PushpinIcon);
+ UpdateSelectedIcon(SelectedPushpinIcon);
+
+ TryStart();
+ }
+
+ private void SetupCustomClusterManager(CustomClusterManager cluster)
+ {
+ _map.SetOnMarkerClickListener(cluster);
+ _map.SetOnInfoWindowClickListener(cluster);
+ cluster.SetOnClusterClickListener(cluster);
+ cluster.SetOnClusterInfoWindowClickListener(cluster);
+ cluster.SetOnClusterItemClickListener(new ClusterItemClick(this));
+ cluster.SetOnClusterItemInfoWindowClickListener(cluster);
+ }
+
+ private void UpdateAndroidClusteringPushpins(IGeoLocated[] items, IGeoLocated[] selectedItems)
+ {
+ if (_clusterPins != null)
+ {
+ var pinsToAdd = ClusterItemsToAdd(items, false);
+ var pinsToRemove = ClusterItemsToRemove(items, false);
+ _clusterPins.UpdateItems(pinsToAdd, pinsToRemove);
+
+ var selectedPinsToAdd = ClusterItemsToAdd(selectedItems, true);
+ var selectedPinsToRemove = ClusterItemsToRemove(selectedItems, true);
+ _clusterPins.UpdateItems(selectedPinsToAdd, selectedPinsToRemove);
+
+ _clusterManager.RemoveItems(pinsToRemove);
+ _clusterManager.RemoveItems(selectedPinsToRemove);
+ _clusterManager.AddItems(pinsToAdd);
+ _clusterManager.AddItems(selectedPinsToAdd);
+ _clusterManager.Cluster();
+ }
+ }
+
+ private MapClusterItem[] ClusterItemsToAdd(IGeoLocated[] items, bool isSelected)
+ {
+ var mapClusterItemsLookup = _clusterPins.ClusterItems.ToDictionary(mc => mc.Item);
+
+ return items
+ .Where(item => !mapClusterItemsLookup.ContainsKey(item))
+ .Select(item => new MapClusterItem(item, isSelected))
+ .ToArray();
+ }
+
+ private MapClusterItem[] ClusterItemsToRemove(IGeoLocated[] items, bool isSelected)
+ {
+ var ItemLookup = items.ToDictionary(mc => mc);
+
+ return _clusterPins.ClusterItems
+ .Where(mapItem => mapItem.IsSelected == isSelected && !ItemLookup.ContainsKey(mapItem.Item))
+ .ToArray();
+ }
+
+ private class ClusterItemClick : Java.Lang.Object, CustomClusterManager.IOnClusterItemClickListener
+ {
+ private MapControlBase _control;
+
+ public ClusterItemClick(MapControlBase mapControl)
+ {
+ _control = mapControl;
+ }
+
+ public bool OnClusterItemClick(Java.Lang.Object item)
+ {
+ var _clusterItems = _control._clusterPins.ClusterItems;
+ var clusterItem = (MapClusterItem)item;
+ clusterItem.IsSelected = !clusterItem.IsSelected;
+
+ if (!_control.AllowMultipleSelection)
+ {
+ _clusterItems
+ .Where(item => item != clusterItem)
+ .ForEach(item => item.IsSelected = false);
+ }
+
+ var eventSelectedPins = _clusterItems
+ .Where(item => item.IsSelected)
+ .Select(item => item.Item)
+ .ToArray();
+
+ _control._selectedPushpins.OnNext(eventSelectedPins);
+ _control._clusterManager.Cluster();
+
+ return true;
+ }
+ }
+}
+#endif
\ No newline at end of file
diff --git a/src/Cartography/Cartography.DynamicMap/Platforms/Android/MapControlBase.Android.cs b/src/Cartography/Cartography.DynamicMap/Platforms/Android/MapControlBase.Android.cs
index e152368..4521d61 100644
--- a/src/Cartography/Cartography.DynamicMap/Platforms/Android/MapControlBase.Android.cs
+++ b/src/Cartography/Cartography.DynamicMap/Platforms/Android/MapControlBase.Android.cs
@@ -12,10 +12,13 @@
using Android.Content;
using Android.Gms.Maps;
using Android.Gms.Maps.Model;
+using Android.Gms.Maps.Utils.Clustering;
+using Android.Gms.Maps.Utils.Data;
using Android.Graphics;
using Android.Views;
using Android.Widget;
using GeolocatorService;
+using Java.Util;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Uno.Extensions;
@@ -24,692 +27,723 @@
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
-namespace Cartography.DynamicMap
+namespace Cartography.DynamicMap;
+
+public partial class MapControlBase
{
- public partial class MapControlBase
- {
- private const string COMPASS_TAG = "GoogleMapCompass";
-
- private GoogleMapView _internalMapView;
-
- private Thickness _padding;
- private GoogleMapLayer _pushpins;
- private MapLifeCycleCallBacks _callbacks;
- private Android.App.Application _application;
- private BitmapDescriptor _icon;
- private BitmapDescriptor _selectedIcon;
- private MapReadyCallback _callback;
- private View _compass;
-
- ///
- /// Sets the selector that will be used to update the marker. This will get
- /// called when the DataContext changes, the position changes and the selected
- /// state changes for a marker.
- ///
- ///
- /// The call chain for the update are the following:
- /// (UseIcons && PushpinIconsMarkerUpdater)? -> UpdateMarker -> MarkerUpdater?
- ///
- public Action MarkerUpdater { get; set; }
-
- ///
- /// Handles margin for the compass icon
- ///
- public Thickness CompassMargin
- {
- get
- {
- if (_compass == null)
- {
- _compass = _internalMapView.FindViewWithTag(COMPASS_TAG);
- }
-
- RelativeLayout.LayoutParams rlp = (RelativeLayout.LayoutParams)_compass.LayoutParameters;
-
- return new Thickness(rlp.LeftMargin, rlp.TopMargin, rlp.RightMargin, rlp.BottomMargin);
- }
-
- set
- {
- if (_compass == null)
- {
- _compass = _internalMapView.FindViewWithTag(COMPASS_TAG);
- }
-
- RelativeLayout.LayoutParams rlp = (RelativeLayout.LayoutParams)_compass.LayoutParameters;
- rlp.TopMargin = (int)value.Top;
- rlp.RightMargin = (int)value.Right;
- rlp.LeftMargin = (int)value.Left;
- rlp.BottomMargin = (int)value.Bottom;
- }
- }
-
-
- ///
- /// Enables multiple selected pins
- ///
- private bool AllowMultipleSelection { get { return SelectionMode == MapSelectionMode.Multiple; } }
- partial void PartialConstructor(ILogger logger = null)
- {
- Loaded += (sender, args) => OnLoaded();
- Unloaded += (sender, args) => OnUnloaded();
-
- _internalMapView = new GoogleMapView(Android.App.Application.Context, new GoogleMapOptions());
-
- Template = new ControlTemplate(() => _internalMapView);//TODO support templateing
-
- MapsInitializer.Initialize(Android.App.Application.Context);
-
- _internalMapView.GetMapAsync(_callback = new MapReadyCallback(OnMapReady));
-
- _internalMapView.OnCreate(null); // This otherwise the map does not appear
-
- _logger = logger ?? NullLogger.Instance;
-
- }
-
- private void OnLoaded()
- {
- _internalMapView.OnResume(); // This otherwise the map stay empty
-
- HandleActivityLifeCycle();
-
- _internalMapView.TouchOccurred += MapTouchOccurred;
- }
-
- private void OnUnloaded()
- {
- // These line is required for the control to
- // stop actively monitoring the user's location.
- _internalMapView.OnPause();
-
- _application.UnregisterActivityLifecycleCallbacks(_callbacks);
-
- if (_internalMapView != null)
- {
- _internalMapView.TouchOccurred -= MapTouchOccurred;
- }
- }
-
- private void MapTouchOccurred(object sender, MotionEvent e)
- {
- _isUserDragging.OnNext(e.Action == MotionEventActions.Move);
- }
-
- private GoogleMap _map;
- private void OnMapReady(GoogleMap map)
- {
- _map = map;
-
- _padding = Thickness.Empty;
- _pushpins = new GoogleMapLayer(map);
- _isReady = true;
-
- map.MarkerClick += Map_MarkerClick;
- map.MapClick += Map_MapClick;
-
- UpdateMapPushpinOnCameraIdle();
-
- UpdateAutolocateButtonVisibility(AutolocateButtonVisibility);
- UpdateCompassButtonVisibility(CompassButtonVisibility);
- UpdateIsRotateGestureEnabled(IsRotateGestureEnabled);
- UpdateMapStyleJson(MapStyleJson);
-
- UpdateIcon(PushpinIcon);
- UpdateSelectedIcon(SelectedPushpinIcon);
-
- TryStart();
- }
-
- ///
- /// Idea is to register to the LifeCycleCallbacks and properly call the OnResume and OnPause methods when needed.
- /// This will release the GPS while the application is in the background
- ///
- private void HandleActivityLifeCycle()
- {
- _callbacks = new MapLifeCycleCallBacks(onPause: _internalMapView.OnPause, onResume: _internalMapView.OnResume);
-
- _application = Context.ApplicationContext as Android.App.Application;
- if (_application != null)
- {
- _application.RegisterActivityLifecycleCallbacks(_callbacks);
- }
- else
- {
- _logger.Error("ApplicationContext is invalid, could not RegisterActivityLifecycleCallbacks to release GPS when application is paused.");
- }
- }
-
-#region UserLocation
- private void UpdateMapUserLocation(LocationResult locationAndStatus)
- {
- if (locationAndStatus != null)
- _map.MyLocationEnabled = locationAndStatus.IsSuccessful;
- }
-#endregion
-
-#region ViewPort
-
- private IEnumerable> GetViewPortChangedTriggers()
- {
- var map = _map;
-
- yield return System.Reactive.Linq.Observable
- .FromEventPattern(
- h => map.CameraChange += h,
- h => map.CameraChange -= h)
- .Where(_ => !_isAnimating)
- .Select(_ => Unit.Default);
- }
-
- private MapViewPort GetViewPort()
- {
- var position = _map.CameraPosition;
- var point = new BasicGeoposition();
- point.Longitude = position.Target.Longitude;
- point.Latitude = position.Target.Latitude;
-
- return new MapViewPort
- {
- Center = new Geopoint(point),
- Heading = position.Bearing,
- Pitch = position.Tilt,
- ZoomLevel = (ZoomLevel)position.Zoom,
- };
- }
-
- private bool GetInitializationStatus() => true;
-
- private async Task SetViewPort(CancellationToken ct, MapViewPort viewPort)
- {
- await _viewLayedOut.Task;
-
- var animation = new MapCancellableCallback(ct);
-
- if (viewPort.PointsOfInterest.Safe().Any())
- {
- //Rubber-band bounds to PointsOfInterest
- var bounds = viewPort
- .PointsOfInterest
- .Aggregate(
- new LatLngBounds.Builder(),
- (builder, poi) => builder.Include(new LatLng(poi.Position.Latitude, poi.Position.Longitude)))
- .Build();
-
- if (viewPort.Center != default(Geopoint))
- {
- bounds = AddPushpinPaddingToBounds(viewPort);
- }
-
- var cameraBounds = CameraUpdateFactory.NewLatLngBounds(bounds, 0);
-
- if (viewPort.IsAnimationDisabled)
- {
- _map.MoveCamera(cameraBounds);
- }
- else
- {
- _map.AnimateCamera(cameraBounds, animation);
- }
- }
- else
- {
- var builder = new CameraPosition.Builder(_map.CameraPosition)
- .Target(new LatLng(viewPort.Center.Position.Latitude, viewPort.Center.Position.Longitude));
-
- if (viewPort.Heading.HasValue)
- {
- builder.Bearing((float)viewPort.Heading.Value);
- }
-
- if (viewPort.Pitch.HasValue)
- {
- builder.Tilt((float)viewPort.Pitch.Value);
- }
-
- if (viewPort.ZoomLevel.HasValue)
- {
- builder.Zoom((float)viewPort.ZoomLevel);
- }
-
- var cameraUpdate = CameraUpdateFactory.NewCameraPosition(builder.Build());
-
- if (viewPort.IsAnimationDisabled)
- {
- _map.MoveCamera(cameraUpdate);
- }
- else
- {
- _map.AnimateCamera(cameraUpdate, animation);
- }
- }
-
- await animation;
- }
-
- private LatLngBounds AddPushpinPaddingToBounds(MapViewPort viewPort)
- {
- var frontiers = viewPort.GetBounds();
-
- // create ViewPort with calculated dimensions
- var northEastCorner = new LatLng(frontiers.EastFrontier, frontiers.NorthFrontier);
- var southWestCorner = new LatLng(frontiers.WestFrontier, frontiers.SouthFrontier);
-
- return new LatLngBounds(southWestCorner, northEastCorner);
- }
-#endregion
-
-#region ViewPortCoordinates
- private MapViewPortCoordinates GetViewPortCoordinates()
- {
- var visibleRegion = _map.Projection.VisibleRegion;
- return new MapViewPortCoordinates(
- northWest: new BasicGeoposition { Latitude = visibleRegion.FarLeft.Latitude, Longitude = visibleRegion.FarLeft.Longitude },
- northEast: new BasicGeoposition { Latitude = visibleRegion.FarRight.Latitude, Longitude = visibleRegion.FarRight.Longitude },
- southWest: new BasicGeoposition { Latitude = visibleRegion.NearLeft.Latitude, Longitude = visibleRegion.NearLeft.Longitude },
- southEast: new BasicGeoposition { Latitude = visibleRegion.NearRight.Latitude, Longitude = visibleRegion.NearRight.Longitude }
- );
- }
-#endregion
-
-#region Pushpins
- private void UpdateMapPushpins(IGeoLocated[] items, IGeoLocated[] selectedItems)
- {
- _pushpins.Update(
- items: items,
- selectedItems: selectedItems,
- containerFactory: _ => new Pushpin
- {
- Map = this,
- // call chain: PushpinIconsMarkerUpdater? -> UpdateMarker -> MarkerUpdater?
- MarkerUpdater = UseIcons
- ? PushpinIconsMarkerUpdater
- : (Action)UpdateMarker
- },
-
- // Pin instances cannot be recycled in Google Maps.
- canRecycle: false
- );
- }
-
- private void UpdateMarker(Pushpin pushpin, Marker marker)
- {
- // update z-index
- marker.ZIndex = pushpin.ZIndex;
-
- // call injected updater
- MarkerUpdater?.Invoke(pushpin, marker);
- }
-#endregion
-
-#region Pushpin ICONS
- private bool UseIcons => _icon != null;
-
- private void UpdateIcon(object icon)
- {
- if (_icon != null)
- {
- throw new InvalidOperationException("Pushpins icons cannot be changed.");
- }
-
- UpdateIcon(ref _icon, icon);
- }
-
- private void UpdateSelectedIcon(object icon)
- {
- if (_selectedIcon != null)
- {
- throw new InvalidOperationException("Pushpins icons cannot be changed.");
- }
-
- UpdateIcon(ref _selectedIcon, icon);
- }
-
- private void UpdateIcon(ref BitmapDescriptor icon, object value)
- {
- if (!_isReady)
- {
- // Deferring the update, the map control is not available yet.
- return;
- }
-
- if (value == null)
- {
- icon = null;
- return;
- }
-
- icon = ToImageSource(value);
- if (icon == null)
- {
- throw new InvalidOperationException("Failed to convert '{0}' to a PushpinIcon".InvariantCultureFormat(value));
- }
- }
-
- private static BitmapDescriptor ToImageSource(object value)
- {
- var uriStr = value as string;
- if (uriStr.HasValueTrimmed()
- && Uri.IsWellFormedUriString(uriStr, UriKind.RelativeOrAbsolute))
- {
- value = new Uri(uriStr, UriKind.RelativeOrAbsolute);
- }
-
- var uri = value as Uri;
- if (uri != null)
- {
- if (!uri.IsAbsoluteUri)
- {
- //return BitmapDescriptorFactory.FromAsset(uri.OriginalString);
- return BitmapDescriptorFactory_FromAsset(uri.OriginalString);
- }
-
- switch (uri.Scheme.ToUpperInvariant())
- {
- case "RES":
- case "RESOURCE":
- return BitmapDescriptorFactory.FromResource(int.Parse(uri.LocalPath.Trim(new[] { '/' })));
-
- case "FILE":
- return BitmapDescriptorFactory.FromFile(uri.LocalPath);
-
- case "ASSET":
- //return BitmapDescriptorFactory.FromAsset(uri.LocalPath);
- return BitmapDescriptorFactory_FromAsset(uri.LocalPath);
-
- case "HTTP":
- default:
- throw new NotSupportedException("Scheme '{0}' not supported as source of a pushpin icon".InvariantCultureFormat(uri.Scheme));
- }
- }
-
- var bitmap = value as Bitmap;
- if (bitmap != null)
- {
- return BitmapDescriptorFactory.FromBitmap(bitmap);
- }
-
- return null;
- }
-
- private static BitmapDescriptor BitmapDescriptorFactory_FromAsset(string assetName)
- {
- // A known bug with the Google Play services (fixed in 7.3) is that we cannot use assets as icon of pushpins
- // cf. https://code.google.com/p/gmaps-api-issues/issues/detail?id=7696
-
- // As v7.3 binding is not yet released (only private RC for now), we cannot update, so we will use work arround
- // proposed in message #21 of link upper.
-
- var assetManager = Android.App.Application.Context.Assets;
-
- try
- {
- var inputStream = assetManager.Open(assetName.TrimStart("Assets", StringComparison.OrdinalIgnoreCase).TrimStart('/', '\\'));
- var image = BitmapFactory.DecodeStream(inputStream);
-
- return BitmapDescriptorFactory.FromBitmap(image);
- }
- catch
- {
- return null;
- }
- }
-
- private void PushpinIconsMarkerUpdater(Pushpin pushpin, Marker marker)
- {
- var icon = pushpin.IsSelected
- ? _selectedIcon
- : _icon;
-
- marker.SetIcon(icon ?? _icon ?? BitmapDescriptorFactory.DefaultMarker());
-
- UpdateMarker(pushpin, marker);
- }
-#endregion
-
-#region Selected pushpins
- private void Map_MarkerClick(object sender, GoogleMap.MarkerClickEventArgs e)
- {
- _logger.Debug("Clicking on a pin.");
-
- var pushPin = _pushpins.FindPushPin(e.Marker);
-
- if (pushPin != null)
- {
- pushPin.IsSelected = !pushPin.IsSelected;
-
- if (!AllowMultipleSelection)
- {
- _pushpins
- .Items
- .Where(i => i != pushPin)
- .ForEach(p => p.IsSelected = false);
- }
-
- var selectedContent = GetSelectedAnnotationsContent();
-
- _logger.Info($"Clicked on '{selectedContent.Length}' pins.");
-
- _selectedPushpins.OnNext(selectedContent);
- }
- }
-
- void Map_MapClick(object sender, GoogleMap.MapClickEventArgs e)
- {
- _logger.Debug("Clicking on the map.");
-
- if (!AllowMultipleSelection)
- {
- _pushpins
- .Items
- .ForEach(p => p.IsSelected = false);
-
- _selectedPushpins.OnNext(new IGeoLocated[0]);
- }
-
- OnMapTapped(new Geocoordinate(e.Point.Latitude, e.Point.Longitude, 0, DateTime.Now, null, null, null, null, null, default));
-
- _logger.Info("Clicked on the map.");
- }
-
-
- private IGeoLocated[] GetSelectedAnnotationsContent()
- {
- return _pushpins
- .Items
- .Where(p => p.IsSelected)
- .Select(p => p.Content)
- .ToArray();
- }
-
- private void UpdateMapSelectedPushpins(IGeoLocated[] newlySelected)
- {
- _pushpins.UpdateSelection(newlySelected);
- }
-#endregion
-
- private void UpdateMapPushpinOnCameraIdle()
- {
- _map.SetOnCameraIdleListener(new MapOnCameraIdleListener(this));
- }
-
- private class MapOnCameraIdleListener : Java.Lang.Object, GoogleMap.IOnCameraIdleListener
- {
- private readonly MapControlBase _parent;
-
- public MapOnCameraIdleListener(MapControlBase parent)
- {
- _parent = parent;
- }
-
- public void OnCameraIdle()
- {
- var selectedContent = _parent.GetSelectedAnnotationsContent();
- _parent._selectedPushpins.OnNext(selectedContent);
- }
- }
-
- private class MapReadyCallback : Java.Lang.Object, IOnMapReadyCallback
- {
- private readonly Action _mapAvailable;
-
- public MapReadyCallback(Action mapAvailable)
- {
- _mapAvailable = mapAvailable;
- }
-
- public void OnMapReady(GoogleMap googleMap)
- {
- _mapAvailable(googleMap);
- }
- }
-
- private class MapCancellableCallback : Java.Lang.Object, GoogleMap.ICancelableCallback
- {
- private readonly TaskCompletionSource _source = new TaskCompletionSource();
- private readonly IDisposable _cancelSubscription;
-
- public MapCancellableCallback(CancellationToken ct)
- {
- _cancelSubscription = ct.Register(() => _source.TrySetCanceled());
- }
-
- void GoogleMap.ICancelableCallback.OnCancel()
- {
- _source.TrySetCanceled();
- }
-
- void GoogleMap.ICancelableCallback.OnFinish()
- {
- _source.TrySetResult(Unit.Default);
- }
-
- public TaskAwaiter GetAwaiter()
- {
- return _source.Task.GetAwaiter();
- }
-
- protected override void Dispose(bool disposing)
- {
- _cancelSubscription.Dispose();
-
- base.Dispose(disposing);
- }
- }
-
- private class MapLifeCycleCallBacks : Java.Lang.Object, global::Android.App.Application.IActivityLifecycleCallbacks
- {
- private readonly Action _onPause;
- private readonly Action _onResume;
-
- public MapLifeCycleCallBacks(Action onPause, Action onResume)
- {
- _onResume = onResume;
- _onPause = onPause;
- }
-
- public void OnActivityResumed(Activity activity)
- {
- _onResume();
- }
-
- public void OnActivityPaused(Activity activity)
- {
- _onPause();
- }
-
-#region Not implemented
-
- public void OnActivityCreated(Activity activity, global::Android.OS.Bundle savedInstanceState)
- {
- }
-
- public void OnActivityDestroyed(Activity activity)
- {
- }
-
- public void OnActivitySaveInstanceState(Activity activity, global::Android.OS.Bundle outState)
- {
- }
-
- public void OnActivityStarted(Activity activity)
- {
- }
-
- public void OnActivityStopped(Activity activity)
- {
- }
-
-#endregion
- }
-
-
- TaskCompletionSource _viewLayedOut = new TaskCompletionSource();
-
- protected override void OnLayoutCore(bool changed, int left, int top, int right, int bottom)
- {
- base.OnLayoutCore(changed, left, top, right, bottom);
-
- _viewLayedOut.TrySetResult(true);
- }
-
- partial void UpdateAutolocateButtonVisibility(Visibility visibility)
- {
- _logger.Debug("Updating the autolocate button's visibility.");
-
- if (_map != null)
- {
- _map.UiSettings.MyLocationButtonEnabled = visibility == Visibility.Visible;
-
- _logger.Info("Updated the autolocate button's visibility.");
- }
- else
- {
- _logger.Error("Could not update the autolocate button's visibility .");
- }
- }
-
- partial void UpdateCompassButtonVisibility(Visibility visibility)
- {
- _logger.Debug("Updating the compass button's visibility.");
-
- if (_map != null)
- {
- _map.UiSettings.CompassEnabled = visibility == Visibility.Visible;
-
- _logger.Info("Updated the autolocate button's visibility.");
- }
- else
- {
- _logger.Error("Could not update the compass button's visibility.");
- }
- }
-
- partial void UpdateIsRotateGestureEnabled(bool isRotateGestureEnabled)
- {
- _logger.Debug($"{(isRotateGestureEnabled ? "Enabling" : "Disabling")} the gesture rotation.");
-
- if (_map != null)
- {
- _map.UiSettings.RotateGesturesEnabled = isRotateGestureEnabled;
-
- _logger.Debug($"{(isRotateGestureEnabled ? "Enabled" : "Disabled")} the gesture rotation.");
- }
- else
- {
- _logger.Error($" Could not {(isRotateGestureEnabled ? "enable" : "disable")} the gesture rotation.");
- }
- }
-
- partial void UpdateMapStyleJson(string mapStyleJson)
- {
- if (_map != null)
- {
- var newStyle = mapStyleJson.HasValueTrimmed() ? mapStyleJson : "[]";
-
- _map.SetMapStyle(new MapStyleOptions(newStyle));
- }
- }
- }
+ private const string COMPASS_TAG = "GoogleMapCompass";
+
+ private GoogleMapView _internalMapView;
+
+ private Thickness _padding;
+ private GoogleMapLayer _pushpins;
+ private MapLifeCycleCallBacks _callbacks;
+ private Android.App.Application _application;
+ private BitmapDescriptor _icon;
+ private BitmapDescriptor _selectedIcon;
+ private MapReadyCallback _callback;
+ private View _compass;
+
+ ///
+ /// Sets the selector that will be used to update the marker. This will get
+ /// called when the DataContext changes, the position changes and the selected
+ /// state changes for a marker.
+ ///
+ ///
+ /// The call chain for the update are the following:
+ /// (UseIcons && PushpinIconsMarkerUpdater)? -> UpdateMarker -> MarkerUpdater?
+ ///
+ public Action MarkerUpdater { get; set; }
+
+ ///
+ /// Handles margin for the compass icon.
+ ///
+ public Thickness CompassMargin
+ {
+ get
+ {
+ if (_compass == null)
+ {
+ _compass = _internalMapView.FindViewWithTag(COMPASS_TAG);
+ }
+
+ RelativeLayout.LayoutParams rlp = (RelativeLayout.LayoutParams)_compass.LayoutParameters;
+
+ return new Thickness(rlp.LeftMargin, rlp.TopMargin, rlp.RightMargin, rlp.BottomMargin);
+ }
+
+ set
+ {
+ if (_compass == null)
+ {
+ _compass = _internalMapView.FindViewWithTag(COMPASS_TAG);
+ }
+
+ RelativeLayout.LayoutParams rlp = (RelativeLayout.LayoutParams)_compass.LayoutParameters;
+ rlp.TopMargin = (int)value.Top;
+ rlp.RightMargin = (int)value.Right;
+ rlp.LeftMargin = (int)value.Left;
+ rlp.BottomMargin = (int)value.Bottom;
+ }
+ }
+
+ ///
+ /// Enables multiple selected pins.
+ ///
+ private bool AllowMultipleSelection { get { return SelectionMode == MapSelectionMode.Multiple; } }
+
+ partial void PartialConstructor(ILogger logger = null)
+ {
+ Loaded += (sender, args) => OnLoaded();
+ Unloaded += (sender, args) => OnUnloaded();
+
+ _internalMapView = new GoogleMapView(Android.App.Application.Context, new GoogleMapOptions());
+
+ Template = new ControlTemplate(() => _internalMapView);//TODO support templateing
+
+ MapsInitializer.Initialize(Android.App.Application.Context);
+
+ _internalMapView.GetMapAsync(_callback = new MapReadyCallback(OnMapReady));
+
+ _internalMapView.OnCreate(null); // This otherwise the map does not appear
+
+ _logger = logger ?? NullLogger.Instance;
+ }
+
+ private void OnLoaded()
+ {
+ _internalMapView.OnResume(); // This otherwise the map stay empty
+
+ HandleActivityLifeCycle();
+
+ _internalMapView.TouchOccurred += MapTouchOccurred;
+ }
+
+ private void OnUnloaded()
+ {
+ // These line is required for the control to
+ // stop actively monitoring the user's location.
+ _internalMapView.OnPause();
+
+ _application.UnregisterActivityLifecycleCallbacks(_callbacks);
+
+ if (_internalMapView != null)
+ {
+ _internalMapView.TouchOccurred -= MapTouchOccurred;
+ }
+ }
+
+ private void MapTouchOccurred(object sender, MotionEvent e)
+ {
+ _isUserDragging.OnNext(e.Action == MotionEventActions.Move);
+ }
+
+ private GoogleMap _map;
+
+ private void OnMapReady(GoogleMap map)
+ {
+ _map = map;
+
+ _padding = Thickness.Empty;
+
+ // Cannot put this before because DependencyProperty isn't ready in partial constructor.
+ if (IsClusterEnabled)
+ {
+ OnMapClusterReady();
+ return;
+ }
+
+ _pushpins = new GoogleMapLayer(map);
+
+ _isReady = true;
+
+ map.MarkerClick += Map_MarkerClick;
+ map.MapClick += Map_MapClick;
+
+ UpdateMapPushpinOnCameraIdle();
+
+ UpdateAutolocateButtonVisibility(AutolocateButtonVisibility);
+ UpdateCompassButtonVisibility(CompassButtonVisibility);
+ UpdateIsRotateGestureEnabled(IsRotateGestureEnabled);
+ UpdateMapStyleJson(MapStyleJson);
+
+ UpdateIcon(PushpinIcon);
+ UpdateSelectedIcon(SelectedPushpinIcon);
+
+ TryStart();
+ }
+
+ ///
+ /// Idea is to register to the LifeCycleCallbacks and properly call the OnResume and OnPause methods when needed.
+ /// This will release the GPS while the application is in the background.
+ ///
+ private void HandleActivityLifeCycle()
+ {
+ _callbacks = new MapLifeCycleCallBacks(onPause: _internalMapView.OnPause, onResume: _internalMapView.OnResume);
+
+ _application = Context.ApplicationContext as Android.App.Application;
+ if (_application != null)
+ {
+ _application.RegisterActivityLifecycleCallbacks(_callbacks);
+ }
+ else
+ {
+ _logger.Error("ApplicationContext is invalid, could not RegisterActivityLifecycleCallbacks to release GPS when application is paused.");
+ }
+ }
+
+ #region UserLocation
+ private void UpdateMapUserLocation(LocationResult locationAndStatus)
+ {
+ if (locationAndStatus != null)
+ _map.MyLocationEnabled = locationAndStatus.IsSuccessful;
+ }
+ #endregion
+
+ #region ViewPort
+ private IEnumerable> GetViewPortChangedTriggers()
+ {
+ var map = _map;
+
+ yield return System.Reactive.Linq.Observable
+ .FromEventPattern(
+ h => map.CameraChange += h,
+ h => map.CameraChange -= h)
+ .Where(_ => !_isAnimating)
+ .Select(_ => Unit.Default);
+ }
+
+ private MapViewPort GetViewPort()
+ {
+ var position = _map.CameraPosition;
+ var point = new BasicGeoposition();
+ point.Longitude = position.Target.Longitude;
+ point.Latitude = position.Target.Latitude;
+
+ return new MapViewPort
+ {
+ Center = new Geopoint(point),
+ Heading = position.Bearing,
+ Pitch = position.Tilt,
+ ZoomLevel = (ZoomLevel)position.Zoom,
+ };
+ }
+
+ private bool GetInitializationStatus() => true;
+
+ private async Task SetViewPort(CancellationToken ct, MapViewPort viewPort)
+ {
+ await _viewLayedOut.Task;
+
+ var animation = new MapCancellableCallback(ct);
+
+ if (viewPort.PointsOfInterest.Safe().Any())
+ {
+ //Rubber-band bounds to PointsOfInterest
+ var bounds = viewPort
+ .PointsOfInterest
+ .Aggregate(
+ new LatLngBounds.Builder(),
+ (builder, poi) => builder.Include(new LatLng(poi.Position.Latitude, poi.Position.Longitude)))
+ .Build();
+
+ if (viewPort.Center != default(Geopoint))
+ {
+ bounds = AddPushpinPaddingToBounds(viewPort);
+ }
+
+ var cameraBounds = CameraUpdateFactory.NewLatLngBounds(bounds, 0);
+
+ if (viewPort.IsAnimationDisabled)
+ {
+ _map.MoveCamera(cameraBounds);
+ }
+ else
+ {
+ _map.AnimateCamera(cameraBounds, animation);
+ }
+ }
+ else
+ {
+ var builder = new CameraPosition.Builder(_map.CameraPosition)
+ .Target(new LatLng(viewPort.Center.Position.Latitude, viewPort.Center.Position.Longitude));
+
+ if (viewPort.Heading.HasValue)
+ {
+ builder.Bearing((float)viewPort.Heading.Value);
+ }
+
+ if (viewPort.Pitch.HasValue)
+ {
+ builder.Tilt((float)viewPort.Pitch.Value);
+ }
+
+ if (viewPort.ZoomLevel.HasValue)
+ {
+ builder.Zoom((float)viewPort.ZoomLevel);
+ }
+
+ var cameraUpdate = CameraUpdateFactory.NewCameraPosition(builder.Build());
+
+ if (viewPort.IsAnimationDisabled)
+ {
+ _map.MoveCamera(cameraUpdate);
+ }
+ else
+ {
+ _map.AnimateCamera(cameraUpdate, animation);
+ }
+ }
+
+ await animation;
+ }
+
+ private LatLngBounds AddPushpinPaddingToBounds(MapViewPort viewPort)
+ {
+ var frontiers = viewPort.GetBounds();
+
+ // create ViewPort with calculated dimensions
+ var northEastCorner = new LatLng(frontiers.EastFrontier, frontiers.NorthFrontier);
+ var southWestCorner = new LatLng(frontiers.WestFrontier, frontiers.SouthFrontier);
+
+ return new LatLngBounds(southWestCorner, northEastCorner);
+ }
+ #endregion
+
+ #region ViewPortCoordinates
+ private MapViewPortCoordinates GetViewPortCoordinates()
+ {
+ var visibleRegion = _map.Projection.VisibleRegion;
+ return new MapViewPortCoordinates(
+ northWest: new BasicGeoposition { Latitude = visibleRegion.FarLeft.Latitude, Longitude = visibleRegion.FarLeft.Longitude },
+ northEast: new BasicGeoposition { Latitude = visibleRegion.FarRight.Latitude, Longitude = visibleRegion.FarRight.Longitude },
+ southWest: new BasicGeoposition { Latitude = visibleRegion.NearLeft.Latitude, Longitude = visibleRegion.NearLeft.Longitude },
+ southEast: new BasicGeoposition { Latitude = visibleRegion.NearRight.Latitude, Longitude = visibleRegion.NearRight.Longitude }
+ );
+ }
+ #endregion
+
+ #region Pushpins
+ private void UpdateMapPushpins(IGeoLocated[] items, IGeoLocated[] selectedItems)
+ {
+ if (IsClusterEnabled)
+ {
+ UpdateAndroidClusteringPushpins(items, selectedItems);
+ }
+ else
+ {
+ UpdateStandardPushpins(items, selectedItems);
+ }
+ }
+
+ private void UpdateStandardPushpins(IGeoLocated[] items, IGeoLocated[] selectedItems)
+ {
+ _pushpins.Update(
+ items: items,
+ selectedItems: selectedItems,
+ containerFactory: _ => new Pushpin
+ {
+ Map = this,
+ // call chain: PushpinIconsMarkerUpdater? -> UpdateMarker -> MarkerUpdater?
+ MarkerUpdater = UseIcons
+ ? PushpinIconsMarkerUpdater
+ : (Action)UpdateMarker
+ },
+
+ // Pin instances cannot be recycled in Google Maps.
+ canRecycle: false
+ );
+ }
+
+ private void UpdateMarker(Pushpin pushpin, Marker marker)
+ {
+ // update z-index
+ marker.ZIndex = pushpin.ZIndex;
+
+ // call injected updater
+ MarkerUpdater?.Invoke(pushpin, marker);
+ }
+ #endregion
+
+ #region Pushpin ICONS
+ private bool UseIcons => _icon != null;
+
+ private void UpdateIcon(object icon)
+ {
+ if (_icon != null)
+ {
+ throw new InvalidOperationException("Pushpins icons cannot be changed.");
+ }
+
+ UpdateIcon(ref _icon, icon);
+ }
+
+ private void UpdateSelectedIcon(object icon)
+ {
+ if (_selectedIcon != null)
+ {
+ throw new InvalidOperationException("Pushpins icons cannot be changed.");
+ }
+
+ UpdateIcon(ref _selectedIcon, icon);
+ }
+
+ private void UpdateIcon(ref BitmapDescriptor icon, object value)
+ {
+ if (!_isReady)
+ {
+ // Deferring the update, the map control is not available yet.
+ return;
+ }
+
+ if (value == null)
+ {
+ icon = null;
+ return;
+ }
+
+ icon = ToImageSource(value);
+ if (icon == null)
+ {
+ throw new InvalidOperationException("Failed to convert '{0}' to a PushpinIcon".InvariantCultureFormat(value));
+ }
+ }
+
+ private static BitmapDescriptor ToImageSource(object value)
+ {
+ var uriStr = value as string;
+ if (uriStr.HasValueTrimmed()
+ && Uri.IsWellFormedUriString(uriStr, UriKind.RelativeOrAbsolute))
+ {
+ value = new Uri(uriStr, UriKind.RelativeOrAbsolute);
+ }
+
+ var uri = value as Uri;
+ if (uri != null)
+ {
+ if (!uri.IsAbsoluteUri)
+ {
+ //return BitmapDescriptorFactory.FromAsset(uri.OriginalString);
+ return BitmapDescriptorFactory_FromAsset(uri.OriginalString);
+ }
+
+ switch (uri.Scheme.ToUpperInvariant())
+ {
+ case "RES":
+ case "RESOURCE":
+ return BitmapDescriptorFactory.FromResource(int.Parse(uri.LocalPath.Trim(new[] { '/' })));
+
+ case "FILE":
+ return BitmapDescriptorFactory.FromFile(uri.LocalPath);
+
+ case "ASSET":
+ //return BitmapDescriptorFactory.FromAsset(uri.LocalPath);
+ return BitmapDescriptorFactory_FromAsset(uri.LocalPath);
+
+ case "HTTP":
+ default:
+ throw new NotSupportedException("Scheme '{0}' not supported as source of a pushpin icon".InvariantCultureFormat(uri.Scheme));
+ }
+ }
+
+ var bitmap = value as Bitmap;
+ if (bitmap != null)
+ {
+ return BitmapDescriptorFactory.FromBitmap(bitmap);
+ }
+
+ return null;
+ }
+
+ private static BitmapDescriptor BitmapDescriptorFactory_FromAsset(string assetName)
+ {
+ // A known bug with the Google Play services (fixed in 7.3) is that we cannot use assets as icon of pushpins
+ // cf. https://code.google.com/p/gmaps-api-issues/issues/detail?id=7696
+
+ // As v7.3 binding is not yet released (only private RC for now), we cannot update, so we will use work arround
+ // proposed in message #21 of link upper.
+
+ var assetManager = Android.App.Application.Context.Assets;
+
+ try
+ {
+ var inputStream = assetManager.Open(assetName.TrimStart("Assets", StringComparison.OrdinalIgnoreCase).TrimStart('/', '\\'));
+ var image = BitmapFactory.DecodeStream(inputStream);
+
+ return BitmapDescriptorFactory.FromBitmap(image);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ private void PushpinIconsMarkerUpdater(Pushpin pushpin, Marker marker)
+ {
+ var icon = pushpin.IsSelected
+ ? _selectedIcon
+ : _icon;
+
+ marker.SetIcon(icon ?? _icon ?? BitmapDescriptorFactory.DefaultMarker());
+
+ UpdateMarker(pushpin, marker);
+ }
+ #endregion
+
+ #region Selected pushpins
+ private void Map_MarkerClick(object sender, GoogleMap.MarkerClickEventArgs e)
+ {
+ _logger.Debug("Clicking on a pin.");
+
+ var pushPin = _pushpins.FindPushPin(e.Marker);
+
+ if (pushPin != null)
+ {
+ pushPin.IsSelected = !pushPin.IsSelected;
+
+ if (!AllowMultipleSelection)
+ {
+ _pushpins
+ .Items
+ .Where(i => i != pushPin)
+ .ForEach(p => p.IsSelected = false);
+ }
+
+ var selectedContent = GetSelectedAnnotationsContent();
+
+ _logger.Info($"Clicked on '{selectedContent.Length}' pins.");
+
+ _selectedPushpins.OnNext(selectedContent);
+ }
+ }
+
+ void Map_MapClick(object sender, GoogleMap.MapClickEventArgs e)
+ {
+ _logger.Debug("Clicking on the map.");
+
+ if (!AllowMultipleSelection)
+ {
+ _pushpins
+ .Items
+ .ForEach(p => p.IsSelected = false);
+
+ _selectedPushpins.OnNext(new IGeoLocated[0]);
+ }
+
+ OnMapTapped(new Geocoordinate(e.Point.Latitude, e.Point.Longitude, 0, DateTime.Now, null, null, null, null, null, default));
+
+ _logger.Info("Clicked on the map.");
+ }
+
+ private IGeoLocated[] GetSelectedAnnotationsContent()
+ {
+ return _pushpins
+ .Items
+ .Where(p => p.IsSelected)
+ .Select(p => p.Content)
+ .ToArray();
+ }
+
+ private void UpdateMapSelectedPushpins(IGeoLocated[] newlySelected)
+ {
+ if (IsClusterEnabled)
+ {
+ // not needed for clustering at the moment.
+ }
+ else
+ {
+ _pushpins.UpdateSelection(newlySelected);
+ }
+ }
+ #endregion
+
+ private void UpdateMapPushpinOnCameraIdle()
+ {
+ if (IsClusterEnabled)
+ {
+ _map.SetOnCameraIdleListener(_clusterManager);
+ }
+ else
+ {
+ _map.SetOnCameraIdleListener(new MapOnCameraIdleListener(this));
+ }
+ }
+
+ private class MapOnCameraIdleListener : Java.Lang.Object, GoogleMap.IOnCameraIdleListener
+ {
+ private readonly MapControlBase _parent;
+
+ public MapOnCameraIdleListener(MapControlBase parent)
+ {
+ _parent = parent;
+ }
+
+ public void OnCameraIdle()
+ {
+ var selectedContent = _parent.GetSelectedAnnotationsContent();
+ _parent._selectedPushpins.OnNext(selectedContent);
+ }
+ }
+
+ private class MapReadyCallback : Java.Lang.Object, IOnMapReadyCallback
+ {
+ private readonly Action _mapAvailable;
+
+ public MapReadyCallback(Action mapAvailable)
+ {
+ _mapAvailable = mapAvailable;
+ }
+
+ public void OnMapReady(GoogleMap googleMap)
+ {
+ _mapAvailable(googleMap);
+ }
+ }
+
+ private class MapCancellableCallback : Java.Lang.Object, GoogleMap.ICancelableCallback
+ {
+ private readonly TaskCompletionSource _source = new TaskCompletionSource();
+ private readonly IDisposable _cancelSubscription;
+
+ public MapCancellableCallback(CancellationToken ct)
+ {
+ _cancelSubscription = ct.Register(() => _source.TrySetCanceled());
+ }
+
+ void GoogleMap.ICancelableCallback.OnCancel()
+ {
+ _source.TrySetCanceled();
+ }
+
+ void GoogleMap.ICancelableCallback.OnFinish()
+ {
+ _source.TrySetResult(Unit.Default);
+ }
+
+ public TaskAwaiter GetAwaiter()
+ {
+ return _source.Task.GetAwaiter();
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ _cancelSubscription.Dispose();
+
+ base.Dispose(disposing);
+ }
+ }
+
+ private class MapLifeCycleCallBacks : Java.Lang.Object, global::Android.App.Application.IActivityLifecycleCallbacks
+ {
+ private readonly Action _onPause;
+ private readonly Action _onResume;
+
+ public MapLifeCycleCallBacks(Action onPause, Action onResume)
+ {
+ _onResume = onResume;
+ _onPause = onPause;
+ }
+
+ public void OnActivityResumed(Activity activity)
+ {
+ _onResume();
+ }
+
+ public void OnActivityPaused(Activity activity)
+ {
+ _onPause();
+ }
+
+ #region Not implemented
+
+ public void OnActivityCreated(Activity activity, global::Android.OS.Bundle savedInstanceState)
+ {
+ }
+
+ public void OnActivityDestroyed(Activity activity)
+ {
+ }
+
+ public void OnActivitySaveInstanceState(Activity activity, global::Android.OS.Bundle outState)
+ {
+ }
+
+ public void OnActivityStarted(Activity activity)
+ {
+ }
+
+ public void OnActivityStopped(Activity activity)
+ {
+ }
+
+ #endregion
+ }
+
+ TaskCompletionSource _viewLayedOut = new TaskCompletionSource();
+
+ protected override void OnLayoutCore(bool changed, int left, int top, int right, int bottom)
+ {
+ base.OnLayoutCore(changed, left, top, right, bottom);
+
+ _viewLayedOut.TrySetResult(true);
+ }
+
+ partial void UpdateAutolocateButtonVisibility(Visibility visibility)
+ {
+ _logger.Debug("Updating the autolocate button's visibility.");
+
+ if (_map != null)
+ {
+ _map.UiSettings.MyLocationButtonEnabled = visibility == Visibility.Visible;
+
+ _logger.Info("Updated the autolocate button's visibility.");
+ }
+ else
+ {
+ _logger.Error("Could not update the autolocate button's visibility .");
+ }
+ }
+
+ partial void UpdateCompassButtonVisibility(Visibility visibility)
+ {
+ _logger.Debug("Updating the compass button's visibility.");
+
+ if (_map != null)
+ {
+ _map.UiSettings.CompassEnabled = visibility == Visibility.Visible;
+
+ _logger.Info("Updated the autolocate button's visibility.");
+ }
+ else
+ {
+ _logger.Error("Could not update the compass button's visibility.");
+ }
+ }
+
+ partial void UpdateIsRotateGestureEnabled(bool isRotateGestureEnabled)
+ {
+ _logger.Debug($"{(isRotateGestureEnabled ? "Enabling" : "Disabling")} the gesture rotation.");
+
+ if (_map != null)
+ {
+ _map.UiSettings.RotateGesturesEnabled = isRotateGestureEnabled;
+
+ _logger.Debug($"{(isRotateGestureEnabled ? "Enabled" : "Disabled")} the gesture rotation.");
+ }
+ else
+ {
+ _logger.Error($" Could not {(isRotateGestureEnabled ? "enable" : "disable")} the gesture rotation.");
+ }
+ }
+
+ partial void UpdateMapStyleJson(string mapStyleJson)
+ {
+ if (_map != null)
+ {
+ var newStyle = mapStyleJson.HasValueTrimmed() ? mapStyleJson : "[]";
+
+ _map.SetMapStyle(new MapStyleOptions(newStyle));
+ }
+ }
}
#endif
diff --git a/src/Cartography/Cartography.DynamicMap/Platforms/iOS/ClusterView.iOS.cs b/src/Cartography/Cartography.DynamicMap/Platforms/iOS/ClusterView.iOS.cs
new file mode 100644
index 0000000..9aef4d2
--- /dev/null
+++ b/src/Cartography/Cartography.DynamicMap/Platforms/iOS/ClusterView.iOS.cs
@@ -0,0 +1,90 @@
+#if __IOS__
+using System;
+using CoreGraphics;
+using Foundation;
+using MapKit;
+using UIKit;
+
+namespace Cartography.DynamicMap;
+
+[Register("ClusterView")]
+public class ClusterView : MKAnnotationView
+{
+ ///
+ /// Sets the selector that will be used to create Pin group view templates. The first parameter
+ /// is the instance of the native annotation.
+ ///
+ public static Func ClusterPinTemplate { get; set; }
+
+ public override IMKAnnotation Annotation
+ {
+ get
+ {
+ return base.Annotation;
+ }
+ set
+ {
+ base.Annotation = value;
+ var cluster = MKAnnotationWrapperExtensions.UnwrapClusterAnnotation(value);
+ if (cluster != null)
+ {
+ var renderer = new UIGraphicsImageRenderer(new CGSize(40, 40));
+ var count = cluster.MemberAnnotations.Length;
+
+ Image = renderer.CreateImage((context) =>
+ {
+ var backgroundColor = MapClusterProperties.BackgroundColor;
+
+ var foregroundColor = MapClusterProperties.ForegroundColor;
+
+ backgroundColor.SetFill();
+ UIBezierPath.FromOval(MapClusterProperties.ClusterSize).Fill();
+
+ var attributes = new UIStringAttributes()
+ {
+ ForegroundColor = foregroundColor,
+ Font = MapClusterProperties.ClusterFontSize
+ };
+ var text = new NSString($"{count}");
+ var size = text.GetSizeUsingAttributes(attributes);
+ var rect = new CGRect(MapClusterProperties.ClusterSize.GetMidX() - size.Width / 2, MapClusterProperties.ClusterSize.GetMidY() - size.Height / 2, size.Width, size.Height);
+ text.DrawString(rect, attributes);
+ });
+ }
+ }
+ }
+ #region Constructors
+ public ClusterView()
+ {
+ }
+
+ public ClusterView(NSCoder coder) : base(coder)
+ {
+ }
+
+ public ClusterView(IntPtr handle) : base(handle)
+ {
+ }
+
+ public ClusterView(IMKAnnotation annotation, string reuseIdentifier) : base(annotation, reuseIdentifier)
+ {
+ // Initialize
+ DisplayPriority = MKFeatureDisplayPriority.DefaultHigh;
+ CollisionMode = MKAnnotationViewCollisionMode.Circle;
+
+ // Offset center point to animate better with marker annotations
+ CenterOffset = new CoreGraphics.CGPoint(0, -10);
+ }
+ #endregion
+}
+
+public static class MKAnnotationWrapperExtensions
+{
+ public static MKClusterAnnotation UnwrapClusterAnnotation(IMKAnnotation annotation)
+ {
+ if (annotation == null) return null;
+ return ObjCRuntime.Runtime.GetNSObject(annotation.Handle) as MKClusterAnnotation;
+ }
+}
+
+#endif
\ No newline at end of file
diff --git a/src/Cartography/Cartography.DynamicMap/Platforms/iOS/MapClusterConstants.IOS.cs b/src/Cartography/Cartography.DynamicMap/Platforms/iOS/MapClusterConstants.IOS.cs
new file mode 100644
index 0000000..1b28a31
--- /dev/null
+++ b/src/Cartography/Cartography.DynamicMap/Platforms/iOS/MapClusterConstants.IOS.cs
@@ -0,0 +1,21 @@
+#if __IOS__
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Cartography.DynamicMap;
+
+public static class MapClusterConstants
+{
+ ///
+ /// approximately 1 miles (1 degree of arc ~= 69 miles)
+ ///
+ public const double MINIMUM_ZOOM_ARC = 0.014;
+ public const double MAX_DEGREES_ARC = 360.0;
+ public const double MAX_GOOGLE_LEVELS = 20;
+ ///
+ /// Used to align apple map zoom scale to google map scale
+ ///
+ public const double ZOOM_LEVEL_COEFFICIENT = 1.15;
+}
+#endif
\ No newline at end of file
diff --git a/src/Cartography/Cartography.DynamicMap/Platforms/iOS/MapClusterProperties.IOS.cs b/src/Cartography/Cartography.DynamicMap/Platforms/iOS/MapClusterProperties.IOS.cs
new file mode 100644
index 0000000..f208661
--- /dev/null
+++ b/src/Cartography/Cartography.DynamicMap/Platforms/iOS/MapClusterProperties.IOS.cs
@@ -0,0 +1,17 @@
+#if __IOS__
+using CoreGraphics;
+using System;
+using System.Collections.Generic;
+using System.Text;
+using UIKit;
+
+namespace Cartography.DynamicMap;
+
+public static class MapClusterProperties
+{
+ public static UIColor BackgroundColor { get; set; } = UIColor.Red;
+ public static UIColor ForegroundColor { get; set; } = UIColor.Black;
+ public static CGRect ClusterSize { get; set; } = new CGRect(0, 0, 40, 40);
+ public static UIFont ClusterFontSize { get; set; } = UIFont.BoldSystemFontOfSize(20);
+}
+#endif
\ No newline at end of file
diff --git a/src/Cartography/Cartography.DynamicMap/Platforms/iOS/MapControl.iOS.cs b/src/Cartography/Cartography.DynamicMap/Platforms/iOS/MapControl.iOS.cs
index 352f595..398ecdc 100644
--- a/src/Cartography/Cartography.DynamicMap/Platforms/iOS/MapControl.iOS.cs
+++ b/src/Cartography/Cartography.DynamicMap/Platforms/iOS/MapControl.iOS.cs
@@ -26,686 +26,686 @@
namespace Cartography.DynamicMap
{
- // see: http://developer.xamarin.com/guides/ios/platform_features/ios_maps_walkthrough/
-
- partial class MapControl
- {
- private MKMapView _internalMapView;
-
- private const double MINIMUM_ZOOM_ARC = 0.014; // approximately 1 miles (1 degree of arc ~= 69 miles)
- private const double MAX_DEGREES_ARC = 360.0;
- private const double MAX_GOOGLE_LEVELS = 20;
- private const double ZOOM_LEVEL_COEFFICIENT = 1.15; // Used to align apple map zoom scale to google map scale
-
- private readonly List _pushPins = new List();
- private IMapLayer _pushpinsLayer;
- private readonly ILogger _logger = NullLogger.Instance;
-
- private readonly Dictionary _overlayRenderers = new Dictionary();
-
- private double? _animationDurationSeconds;
- private bool _isViewPortInitialized;
-
- ///
- /// Defines the amount of space that is created around the items on which the map is zooming.
- ///
- public double AutoZoomModifyer { get; set; }
-
- ///
- /// Sets the selector that will be used to create Pin view templates. The first parameter
- /// is the instance of the native annotation.
- ///
- public Func PinTemplate { get; set; }
-
- ///
- /// Sets the selector that will be used to create Pin group view templates. The first parameter
- /// is the instance of the native annotation.
- ///
- public Func PinGroupTemplate { get; set; }
-
- ///
- /// Enables or disables the zoom animations globally.
- ///
- public bool EnableZoomAnimations { get; set; }
-
- partial void Initialize()
- {
- Loaded += (sender, args) => OnLoaded();
- Unloaded += (sender, args) => OnUnloaded();
-
- _internalMapView = new MKMapView();
- AddDragGestureRecognizer(_internalMapView);
- _pushpinsLayer = new IosMapLayer(_internalMapView);
- Padding = Thickness.Empty;
-
- Template = new ControlTemplate(() => _internalMapView);//TODO use templates
-
- // Set so that the pins are not too close to the edges.
- AutoZoomModifyer = 1.15f;
-
- EnableZoomAnimations = true;
-
- _internalMapView.GetViewForAnnotation = OnGetViewForAnnotation;
- _internalMapView.DidDeselectAnnotationView += MapControl_DidDeselectAnnotationView;
- _internalMapView.DidSelectAnnotationView += MapControl_DidSelectAnnotationView;
- }
-
- [EditorBrowsable(EditorBrowsableState.Never)]
- [Obsolete("Deprecated, please use the IsRotateGestureEnabled property instead.")]
- public bool RotateEnabled
- {
- get
- {
- return _internalMapView.RotateEnabled;
- }
- set
- {
- _internalMapView.RotateEnabled = value;
- }
- }
-
- public bool ShowPointsOfInterest
- {
- get
- {
- return _internalMapView.ShowsPointsOfInterest;
- }
- set
- {
- _internalMapView.ShowsPointsOfInterest = value;
- }
- }
-
- private bool _lastShowLocation;
- private bool _isLoaded;
- private void OnLoaded()
- {
- MonitorTapped();
-
- _internalMapView.ShowsUserLocation = _lastShowLocation;
- _isLoaded = true;
- }
-
- private void OnUnloaded()
- {
- _lastShowLocation = _internalMapView.ShowsUserLocation;
- _internalMapView.ShowsUserLocation = false;//Disable location tracking while unloaded
- _isLoaded = false;
- }
-
- ///
- /// Allows the map to be user interaction able.
- ///
- public override bool UserInteractionEnabled
- {
- get
- {
- return base.UserInteractionEnabled;
- }
- set
- {
- _internalMapView.ZoomEnabled = value;
- _internalMapView.ScrollEnabled = value;
- _internalMapView.PitchEnabled = value;
- _internalMapView.RotateEnabled = value;
- base.UserInteractionEnabled = value;
- }
- }
-
-#region UserLocation
- protected override void UpdateMapUserLocation(LocationResult locationAndStatus)
- {
- if (locationAndStatus == null)
- return;
-
- _logger.Debug($"Updating the user's location on the map (status: '{locationAndStatus?.IsSuccessful}').");
-
- if (_isLoaded)
- {
- //TODO: Use a custom pin for current location ?
- _internalMapView.ShowsUserLocation = locationAndStatus.IsSuccessful;
- }
- else
- {
- _lastShowLocation = locationAndStatus.IsSuccessful;
- }
-
- _logger.Info($"Updated the user's location on the map (status: '{locationAndStatus?.IsSuccessful}').");
- }
-#endregion
-
-#region ViewPort
-
-
- protected override IEnumerable> GetViewPortChangedTriggers()
- {
- yield return Observable
- .FromEventPattern(
- h => _internalMapView.RegionChanged += h,
- h => _internalMapView.RegionChanged -= h
- )
- .Where(ep => (!ep.EventArgs.Animated && !IsAnimating))
- .Select(_ => Unit.Default)
- .Do(_ =>
- {
- _logger.Info("The view port changed.");
- });
-
- yield return Observable.Return(Unit.Default);
- }
-
- protected override ZoomLevel GetZoomLevel()
- {
- // https://github.com/jdp-global/MKMapViewZoom/blob/master/MKMapView%2BZoomLevel.m
-
- MKCoordinateRegion region = _internalMapView.Region;
-
- double centerPixelX = MapHelper.LongitudeToPixelSpaceX(region.Center.Longitude);
- double topLeftPixelX = MapHelper.LongitudeToPixelSpaceX(region.Center.Longitude - region.Span.LongitudeDelta / 2);
-
- double scaledMapWidth = (centerPixelX - topLeftPixelX) * 2;
- var mapSizeInPixels = Bounds.Size;
- double zoomScale = scaledMapWidth / mapSizeInPixels.Width;
- double zoomExponent = Math.Log(zoomScale) / Math.Log(2);
- double zoomLevel = 20 - zoomExponent;
-
- return new ZoomLevel(zoomLevel);
- }
-
- protected override async Task SetViewPort(CancellationToken ct, MapViewPort viewPort)
- {
- _logger.Debug($"Setting viewport with '{viewPort?.PointsOfInterest}' POIs and a zoom level of '{viewPort?.ZoomLevel}'.");
-
- if (viewPort.PointsOfInterest != null && viewPort.PointsOfInterest.Length > 0)
- {
- SetViewport(viewPort, preventAnimations: viewPort.IsAnimationDisabled);
- }
- else
- {
- //TODO
- // SetRegion(new MKCoordinateRegion(viewPort.Center, Region.Span), true);
- SetViewport(viewPort.Center.Position, viewPort.Heading, viewPort.Pitch, viewPort.ZoomLevel ?? ZoomLevel, preventAnimations: viewPort.IsAnimationDisabled);
- }
-
- _logger.Info($"Viewport set with '{viewPort?.PointsOfInterest}' and a zoom level of '{viewPort?.ZoomLevel}'.");
-
- _isViewPortInitialized = true;
- }
-
- protected override bool GetInitializationStatus()
- {
- return _isViewPortInitialized;
- }
-
- private void SetViewport(MapViewPort viewPort, bool preventAnimations = false)
- {
- var padding = ApplyPaddingToCoordinate(new BasicGeoposition { Latitude = _internalMapView.CenterCoordinate.Latitude, Longitude = _internalMapView.CenterCoordinate.Longitude }, this.Padding, this.ZoomLevel);
-
- _internalMapView.CenterCoordinate = new CLLocationCoordinate2D(padding.Latitude, padding.Longitude);
-
- var locRect = ComputeBoundingRectangle(viewPort);
-
- bool animate = EnableZoomAnimations && !preventAnimations;
-
- if (animate && _animationDurationSeconds.HasValue)
- {
- var uiViewPropertyAnimator = new UIViewPropertyAnimator(_animationDurationSeconds.Value,
- UIViewAnimationCurve.EaseOut,
- () => _internalMapView.SetRegion(locRect, true));
-
- uiViewPropertyAnimator.StartAnimation();
- }
- else
- {
- _internalMapView.SetRegion(locRect, animate);
- }
- }
-
- private void SetViewport(BasicGeoposition centerCoordinate, double? heading, double? pitch, ZoomLevel zoomLevel, bool preventAnimations = false)
- {
- centerCoordinate = ApplyPaddingToCoordinate(centerCoordinate, this.Padding, zoomLevel);
-
- var region = MapHelper.CreateRegion(centerCoordinate, zoomLevel, Bounds.Size);
-
- bool animate = EnableZoomAnimations && !preventAnimations;
-
- if (animate && _animationDurationSeconds.HasValue)
- {
-
- var uiViewPropertyAnimator = new UIViewPropertyAnimator(_animationDurationSeconds.Value,
- UIViewAnimationCurve.EaseOut,
- () => _internalMapView.SetRegion(region, true));
+ // see: http://developer.xamarin.com/guides/ios/platform_features/ios_maps_walkthrough/
+
+ partial class MapControl
+ {
+ private MKMapView _internalMapView;
+
+ private readonly List _pushPins = new List();
+ private IMapLayer _pushpinsLayer;
+ private readonly ILogger _logger = NullLogger.Instance;
+
+ private readonly Dictionary _overlayRenderers = new Dictionary();
+
+ private double? _animationDurationSeconds;
+ private bool _isViewPortInitialized;
+
+ ///
+ /// Defines the amount of space that is created around the items on which the map is zooming.
+ ///
+ public double AutoZoomModifyer { get; set; }
+
+ ///
+ /// Sets the selector that will be used to create Pin view templates. The first parameter
+ /// is the instance of the native annotation.
+ ///
+ public Func PinTemplate { get; set; }
+
+ ///
+ /// Sets the selector that will be used to create Pin group view templates. The first parameter
+ /// is the instance of the native annotation.
+ ///
+ public Func PinGroupTemplate { get; set; }
+
+ ///
+ /// Enables or disables the zoom animations globally.
+ ///
+ public bool EnableZoomAnimations { get; set; }
+
+ partial void Initialize()
+ {
+ Loaded += (sender, args) => OnLoaded();
+ Unloaded += (sender, args) => OnUnloaded();
+
+ _internalMapView = new MKMapView();
+ AddDragGestureRecognizer(_internalMapView);
+ _pushpinsLayer = new IosMapLayer(_internalMapView);
+ Padding = Thickness.Empty;
+
+ Template = new ControlTemplate(() => _internalMapView);//TODO use templates
+
+ // Set so that the pins are not too close to the edges.
+ AutoZoomModifyer = 1.15f;
+
+ EnableZoomAnimations = true;
+ _internalMapView.Register(typeof(ClusterView), MKMapViewDefault.ClusterAnnotationViewReuseIdentifier);
+ _internalMapView.GetViewForAnnotation = OnGetViewForAnnotation;
+ _internalMapView.DidDeselectAnnotationView += MapControl_DidDeselectAnnotationView;
+ _internalMapView.DidSelectAnnotationView += MapControl_DidSelectAnnotationView;
+ }
+
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ [Obsolete("Deprecated, please use the IsRotateGestureEnabled property instead.")]
+ public bool RotateEnabled
+ {
+ get
+ {
+ return _internalMapView.RotateEnabled;
+ }
+ set
+ {
+ _internalMapView.RotateEnabled = value;
+ }
+ }
+
+ public bool ShowPointsOfInterest
+ {
+ get
+ {
+ return _internalMapView.ShowsPointsOfInterest;
+ }
+ set
+ {
+ _internalMapView.ShowsPointsOfInterest = value;
+ }
+ }
+
+ private bool _lastShowLocation;
+ private bool _isLoaded;
+ private void OnLoaded()
+ {
+ MonitorTapped();
+
+ _internalMapView.ShowsUserLocation = _lastShowLocation;
+ _isLoaded = true;
+ }
+
+ private void OnUnloaded()
+ {
+ _lastShowLocation = _internalMapView.ShowsUserLocation;
+ _internalMapView.ShowsUserLocation = false;//Disable location tracking while unloaded
+ _isLoaded = false;
+ }
+
+ ///
+ /// Allows the map to be user interaction able.
+ ///
+ public override bool UserInteractionEnabled
+ {
+ get
+ {
+ return base.UserInteractionEnabled;
+ }
+ set
+ {
+ _internalMapView.ZoomEnabled = value;
+ _internalMapView.ScrollEnabled = value;
+ _internalMapView.PitchEnabled = value;
+ _internalMapView.RotateEnabled = value;
+ base.UserInteractionEnabled = value;
+ }
+ }
+
+ #region UserLocation
+ protected override void UpdateMapUserLocation(LocationResult locationAndStatus)
+ {
+ if (locationAndStatus == null)
+ return;
+
+ _logger.Debug($"Updating the user's location on the map (status: '{locationAndStatus?.IsSuccessful}').");
+
+ if (_isLoaded)
+ {
+ //TODO: Use a custom pin for current location ?
+ _internalMapView.ShowsUserLocation = locationAndStatus.IsSuccessful;
+ }
+ else
+ {
+ _lastShowLocation = locationAndStatus.IsSuccessful;
+ }
+
+ _logger.Info($"Updated the user's location on the map (status: '{locationAndStatus?.IsSuccessful}').");
+ }
+ #endregion
+
+ #region ViewPort
+
+
+ protected override IEnumerable> GetViewPortChangedTriggers()
+ {
+ yield return Observable
+ .FromEventPattern(
+ h => _internalMapView.RegionChanged += h,
+ h => _internalMapView.RegionChanged -= h
+ )
+ .Where(ep => (!ep.EventArgs.Animated && !IsAnimating))
+ .Select(_ => Unit.Default)
+ .Do(_ =>
+ {
+ _logger.Info("The view port changed.");
+ });
+
+ yield return Observable.Return(Unit.Default);
+ }
+
+ protected override ZoomLevel GetZoomLevel()
+ {
+ // https://github.com/jdp-global/MKMapViewZoom/blob/master/MKMapView%2BZoomLevel.m
+
+ MKCoordinateRegion region = _internalMapView.Region;
+
+ double centerPixelX = MapHelper.LongitudeToPixelSpaceX(region.Center.Longitude);
+ double topLeftPixelX = MapHelper.LongitudeToPixelSpaceX(region.Center.Longitude - region.Span.LongitudeDelta / 2);
+
+ double scaledMapWidth = (centerPixelX - topLeftPixelX) * 2;
+ var mapSizeInPixels = Bounds.Size;
+ double zoomScale = scaledMapWidth / mapSizeInPixels.Width;
+ double zoomExponent = Math.Log(zoomScale) / Math.Log(2);
+ double zoomLevel = 20 - zoomExponent;
+
+ return new ZoomLevel(zoomLevel);
+ }
+
+ protected override async Task SetViewPort(CancellationToken ct, MapViewPort viewPort)
+ {
+ _logger.Debug($"Setting viewport with '{viewPort?.PointsOfInterest}' POIs and a zoom level of '{viewPort?.ZoomLevel}'.");
+
+ if (viewPort.PointsOfInterest != null && viewPort.PointsOfInterest.Length > 0)
+ {
+ SetViewport(viewPort, preventAnimations: viewPort.IsAnimationDisabled);
+ }
+ else
+ {
+ //TODO
+ // SetRegion(new MKCoordinateRegion(viewPort.Center, Region.Span), true);
+ SetViewport(viewPort.Center.Position, viewPort.Heading, viewPort.Pitch, viewPort.ZoomLevel ?? ZoomLevel, preventAnimations: viewPort.IsAnimationDisabled);
+ }
+
+ _logger.Info($"Viewport set with '{viewPort?.PointsOfInterest}' and a zoom level of '{viewPort?.ZoomLevel}'.");
+
+ _isViewPortInitialized = true;
+ }
+
+ protected override bool GetInitializationStatus()
+ {
+ return _isViewPortInitialized;
+ }
+
+ private void SetViewport(MapViewPort viewPort, bool preventAnimations = false)
+ {
+ var padding = ApplyPaddingToCoordinate(new BasicGeoposition { Latitude = _internalMapView.CenterCoordinate.Latitude, Longitude = _internalMapView.CenterCoordinate.Longitude }, this.Padding, this.ZoomLevel);
+
+ _internalMapView.CenterCoordinate = new CLLocationCoordinate2D(padding.Latitude, padding.Longitude);
+
+ var locRect = ComputeBoundingRectangle(viewPort);
+
+ bool animate = EnableZoomAnimations && !preventAnimations;
+
+ if (animate && _animationDurationSeconds.HasValue)
+ {
+ var uiViewPropertyAnimator = new UIViewPropertyAnimator(_animationDurationSeconds.Value,
+ UIViewAnimationCurve.EaseOut,
+ () => _internalMapView.SetRegion(locRect, true));
+
+ uiViewPropertyAnimator.StartAnimation();
+ }
+ else
+ {
+ _internalMapView.SetRegion(locRect, animate);
+ }
+ }
+
+ private void SetViewport(BasicGeoposition centerCoordinate, double? heading, double? pitch, ZoomLevel zoomLevel, bool preventAnimations = false)
+ {
+ centerCoordinate = ApplyPaddingToCoordinate(centerCoordinate, this.Padding, zoomLevel);
+
+ var region = MapHelper.CreateRegion(centerCoordinate, zoomLevel, Bounds.Size);
+
+ bool animate = EnableZoomAnimations && !preventAnimations;
- uiViewPropertyAnimator.StartAnimation();
- }
- else
- {
- _internalMapView.SetRegion(region, animate);
- }
- }
-#endregion
-
-#region Pushpins
- protected override void UpdateMapPushpins(IGeoLocated[] items, IGeoLocated[] selectedItems)
- {
- _logger.Debug($"Updating the '{items.Safe().Count()}' map pushpins (number of selected items: '{selectedItems?.Length}').");
+ if (animate && _animationDurationSeconds.HasValue)
+ {
+
+ var uiViewPropertyAnimator = new UIViewPropertyAnimator(_animationDurationSeconds.Value,
+ UIViewAnimationCurve.EaseOut,
+ () => _internalMapView.SetRegion(region, true));
+
+ uiViewPropertyAnimator.StartAnimation();
+ }
+ else
+ {
+ _internalMapView.SetRegion(region, animate);
+ }
+ }
+ #endregion
+
+ #region Pushpins
+ protected override void UpdateMapPushpins(IGeoLocated[] items, IGeoLocated[] selectedItems)
+ {
+ _logger.Debug($"Updating the '{items.Safe().Count()}' map pushpins (number of selected items: '{selectedItems?.Length}').");
- _pushpinsLayer.Update(items, selectedItems, CreatePushpin);
-
- _logger.Info($"Updated the '{items.Safe().Count()}' map pushpins (number of selected items: '{selectedItems?.Length}').");
- }
-
- private Pushpin CreatePushpin(IGeoLocated item)
- {
- return item.IsGrouping()
- ? new MapGroupAnnotation { Map = this }
- : new Pushpin { Map = this };
- }
-
- private MKAnnotationView OnGetViewForAnnotation(MKMapView mapView, IMKAnnotation annotation)
- {
- if (annotation is MKUserLocation || PinTemplate == null)
- return null;
-
- var mapAnnotation = annotation as Pushpin;
-
- if (mapAnnotation != null)
- {
- // Determine the selector, fallback on standard template for groups if not set.
- var templateId = Pushpin.AnnotationId;
- var selector = annotation is MapGroupAnnotation ? PinGroupTemplate ?? PinTemplate : PinTemplate;
-
- var annotationView = mapView.DequeueReusableAnnotation(templateId);
-
- if (annotationView == null)
- {
- annotationView = new MKAnnotationView(mapAnnotation, templateId);
- annotationView.Add(selector(annotationView));
-
- _pushPins.Add(annotationView);
-
- // Set the frame size, or the pin will not be selectable.
- annotationView.Frame = annotationView.Subviews[0].Frame;
-
- if (_icon != null)
- {
- annotationView.CenterOffset = new CGPoint(
- PushpinIconsPositionOrigin.X * annotationView.Frame.Width,
- PushpinIconsPositionOrigin.Y * annotationView.Frame.Height);
- }
-
- if (annotationView.Frame.Width == 0 || annotationView.Frame.Height == 0)
- {
- _logger.Debug($"The frame for '{annotationView.Subviews[0]}' is '{annotationView.Frame}', which is too narrow. Set the frame for the Pin UIView.");
- }
- }
-
- var dataContextProvider = annotationView.Subviews.FirstOrDefault() as IDataContextProvider;
-
- if (dataContextProvider != null)
- {
- dataContextProvider.DataContext = mapAnnotation;
- }
-
- // We don't need callouts for this implementation.
- annotationView.CanShowCallout = false;
-
- //Refresh Pushin when view refresh
- var selectedContent = GetSelectedAnnotationsContent();
-
- OnPushpinsSelected(selectedContent);
-
- return annotationView;
- }
-
- return null;
- }
-#endregion
-
-#region Pushpins ICONS
-
- protected override void UpdateIcon(object icon)
- {
- if (_icon != null)
- {
- _logger.Error($"Pushpins icons cannot be changed (_icon: '{_icon}')");
-
- throw new InvalidOperationException($"Pushpins icons cannot be changed (_icon: '{_icon}').");
- }
-
- UpdateIcon(ref _icon, icon);
-
- if (_icon != null)
- {
- PinTemplate = annotationView => new UIKit.UIImageView(_icon)
- {
- Frame = new CGRect(CGPoint.Empty, _icon.Size)
- };
- }
- }
-
- protected override void UpdateSelectedIcon(object icon)
- {
- if (_selectedIcon != null)
- {
- _logger.Error($"Pushpins icons cannot be changed (_selectedIcon: '{_selectedIcon}')");
-
- throw new InvalidOperationException("Pushpins icons cannot be changed.");
- }
-
- UpdateIcon(ref _selectedIcon, icon);
-
- if (_selectedIcon != null)
- {
- _internalMapView.DidSelectAnnotationView += (snd, e) =>
- {
- var imageView = e.View.Subviews.FirstOrDefault() as UIImageView;
- if (imageView != null)
- {
- imageView.Image = _selectedIcon;
- }
- };
-
- _internalMapView.DidDeselectAnnotationView += (snd, e) =>
- {
- var imageView = e.View.Subviews.FirstOrDefault() as UIImageView;
- if (imageView != null)
- {
- imageView.Image = _icon;
- }
- };
- }
- }
-#endregion
-
-#region SelectedPushpins
- private void MapControl_DidSelectAnnotationView(object sender, MKAnnotationViewEventArgs e)
- {
- _logger.Debug("Selecting the pushpins on the map.");
-
- GetDispatcherScheduler().Schedule(() =>
- {
- var mapAnnotation = e.View.Annotation as Pushpin;
-
- if (!AllowMultipleSelection)
- {
- _internalMapView.SelectedAnnotations
- .Safe()
- .Where(a => a.Handle != e.View.Annotation.Handle)
- .ForEach(a => _internalMapView.DeselectAnnotation(a, true));
- }
-
- if (mapAnnotation != null && !mapAnnotation.IsSelected && !mapAnnotation.IsSelectionChangeAlreadyHandled)
- {
- // Avoid infinite loops caused by this selection causing the native control to re-select
- mapAnnotation.IsSelectionChangeAlreadyHandled = true;
-
- mapAnnotation.IsSelected = true;
-
- mapAnnotation.IsSelectionChangeAlreadyHandled = false;
-
- var selectedContent = GetSelectedAnnotationsContent();
-
- OnPushpinsSelected(selectedContent);
-
- _logger.Info("Selected the pushpins on the map.");
- }
- });
- }
-
- private void MapControl_DidDeselectAnnotationView(object sender, MKAnnotationViewEventArgs e)
- {
- _logger.Debug("Deselecting the pushpins on the map.");
-
- GetDispatcherScheduler().Schedule(() =>
- {
- var mapAnnotation = e.View.Annotation as Pushpin;
-
- if (mapAnnotation != null && mapAnnotation.IsSelected && !mapAnnotation.IsSelectionChangeAlreadyHandled)
- {
- // Avoid infinite loops caused by this selection causing the native control to deselect again
- mapAnnotation.IsSelectionChangeAlreadyHandled = true;
-
- mapAnnotation.IsSelected = false;
- _internalMapView.DeselectAnnotation(mapAnnotation, true);
-
- mapAnnotation.IsSelectionChangeAlreadyHandled = false;
-
- var selectedContent = GetSelectedAnnotationsContent();
-
- OnPushpinsSelected(selectedContent);
-
- _logger.Info("Deselected the pushpins on the map.");
- }
- });
- }
-
- private IGeoLocated[] GetSelectedAnnotationsContent()
- {
- return _internalMapView.SelectedAnnotations
- .Safe()
- .OfType()
- .Select(a => a.Item)
- .ToArray();
- }
-
- ///
- /// Update the whole list of select items.
- ///
- protected override void UpdateMapSelectedPushpins(IGeoLocated[] items)
- {
- _pushpinsLayer.UpdateSelection(items);
- }
-#endregion
-
- private void MonitorTapped()
- {
- var tapRecognizer = new UITapGestureRecognizer(
- recognizer =>
- {
- var point = recognizer.LocationInView(_internalMapView);
- var coordinate = _internalMapView.ConvertPoint(point, _internalMapView);
-
- OnMapTapped(new Geocoordinate(coordinate.Latitude, coordinate.Longitude, 0, DateTime.Now, null, null, null, null, null, default));
- });
-
- _internalMapView.AddGestureRecognizer(tapRecognizer);
- }
-
- private IObservable ObserveRegionChanged()
- {
- return Observable
- .FromEventPattern(
- h => _internalMapView.RegionChanged += h,
- h => _internalMapView.RegionChanged -= h
- )
- .Select(_ => Unit.Default)
- .Do(_ =>
- {
- //OnPushpinsSelected(GetSelectedAnnotationsContent());
-
- _logger.Info("The region changed.");
- });
- }
-
-#region Helpers
- private MKCoordinateRegion ComputeBoundingRectangle(MapViewPort viewPort)
- {
- MKCoordinateRegion region;
- if (viewPort.Center == default(Geopoint))
- {
- var coordinates = viewPort.PointsOfInterest
- .Select(p => MKMapPoint.FromCoordinate(new CLLocationCoordinate2D { Latitude = p.Position.Latitude, Longitude = p.Position.Longitude }));
-
- var poly = MKPolygon.FromPoints(coordinates.ToArray());
- var mapRect = poly.BoundingMapRect;
-
- //convert MKCoordinateRegion from MKMapRect
- region = MKCoordinateRegion.FromMapRect(mapRect);
- }
- else //if the center is defined
- {
- //Set center
- region.Center.Latitude = viewPort.Center.Position.Latitude;
- region.Center.Longitude = viewPort.Center.Position.Longitude;
-
- var centerCoordinates = new BasicGeoposition { Latitude = region.Center.Latitude, Longitude = region.Center.Longitude };
-
- //Get the farthest pushpin
- var farthestHorizontalPushpin = viewPort.PointsOfInterest
- .OrderBy(pushpin => centerCoordinates.GetHorizontalDistanceTo(pushpin.Position))
- .LastOrDefault();
- var farthestVerticalPushpin = viewPort.PointsOfInterest
- .OrderBy(pushpin => centerCoordinates.GetVerticalDistanceTo(pushpin.Position))
- .LastOrDefault();
-
- //Set the width and the height required to show the farthest pushpin while keeping the center centered.
- region.Span.LatitudeDelta = Math.Abs(viewPort.Center.Position.Latitude - farthestHorizontalPushpin.Position.Latitude) * 2;
- region.Span.LongitudeDelta = Math.Abs(viewPort.Center.Position.Longitude - farthestVerticalPushpin.Position.Longitude) * 2;
- }
-
- //apply padding to allow user to see the entire pushpin and not just a part of it.
- region.Span.LatitudeDelta *= viewPort.PointsOfInterestPadding.HorizontalPadding.Value;
- region.Span.LongitudeDelta *= viewPort.PointsOfInterestPadding.VerticalPadding.Value;
-
- //but padding can’t be bigger than the world
- if (region.Span.LatitudeDelta > MAX_DEGREES_ARC) { region.Span.LatitudeDelta = MAX_DEGREES_ARC; }
- if (region.Span.LongitudeDelta > MAX_DEGREES_ARC) { region.Span.LongitudeDelta = MAX_DEGREES_ARC; }
-
- //and don’t zoom in stupid-close on small samples
- if (region.Span.LatitudeDelta < MINIMUM_ZOOM_ARC) { region.Span.LatitudeDelta = MINIMUM_ZOOM_ARC; }
- if (region.Span.LongitudeDelta < MINIMUM_ZOOM_ARC) { region.Span.LongitudeDelta = MINIMUM_ZOOM_ARC; }
-
- return region;
- }
-
- private static double ZoomLevelToZoomScale(ZoomLevel zoomLevel)
- {
- var zoomExponent = 20 - zoomLevel.Value;
- double zoomScale = Math.Pow(2, zoomExponent);
- return zoomScale;
- }
-
- private static BasicGeoposition ApplyPaddingToCoordinate(BasicGeoposition coordinate, Thickness padding, ZoomLevel zoomLevel)
- {
- if (padding == Thickness.Empty)
- {
- return coordinate;
- }
-
- // determine the scale value from the zoom level
- double zoomScale = ZoomLevelToZoomScale(zoomLevel);
-
- // convert center coordiate to pixel space
- double centerPixelX = MapHelper.LongitudeToPixelSpaceX(coordinate.Longitude);
- double centerPixelY = MapHelper.LatitudeToPixelSpaceY(coordinate.Latitude);
-
- // offset center with padding
- centerPixelX += (((padding.Right - padding.Left) / 2) * zoomScale);
- centerPixelY += (((padding.Bottom - padding.Top) / 2) * zoomScale);
-
- return new BasicGeoposition
- {
- Longitude = MapHelper.PixelSpaceXToLongitude(centerPixelX),
- Latitude = MapHelper.PixelSpaceYToLatitude(centerPixelY)
- };
- }
-
- protected override Geopoint GetCenter()
- {
- var center = new BasicGeoposition();
- center.Latitude = _internalMapView.Region.Center.Latitude;
- center.Longitude = _internalMapView.Region.Center.Longitude;
- return new Geopoint(center);
- }
-
- protected override MapViewPortCoordinates GetViewPortCoordinates()
- {
- var center = _internalMapView.Region.Center;
- var latitudeDelta = _internalMapView.Region.Span.LatitudeDelta;
- var longitudeDelta = _internalMapView.Region.Span.LongitudeDelta;
-
- return new MapViewPortCoordinates(
- northWest: new BasicGeoposition
- {
- Latitude = GetLatitude(center.Latitude, latitudeDelta, isNorth: true),
- Longitude = GetLongitude(center.Longitude, longitudeDelta, isEast: false)
- },
- northEast: new BasicGeoposition
- {
- Latitude = GetLatitude(center.Latitude, latitudeDelta, isNorth: true),
- Longitude = GetLongitude(center.Longitude, longitudeDelta, isEast: true)
- },
- southWest: new BasicGeoposition
- {
- Latitude = GetLatitude(center.Latitude, latitudeDelta, isNorth: false),
- Longitude = GetLongitude(center.Longitude, longitudeDelta, isEast: false)
- },
- southEast: new BasicGeoposition
- {
- Latitude = GetLatitude(center.Latitude, latitudeDelta, isNorth: false),
- Longitude = GetLongitude(center.Longitude, longitudeDelta, isEast: true)
- }
- );
- }
-
- private double GetLatitude(double centerLatitude, double latitudeDelta, bool isNorth)
- {
- var factor = isNorth ? 1 : -1;
-
- // Need to bound the values because sometimes the delta exceeds the possible range
- return Math.Max(
- -90,
- Math.Min(
- 90,
- centerLatitude + (latitudeDelta * factor / 2)
- ));
- }
-
- private double GetLongitude(double centerLongitude, double longitudeDelta, bool isEast)
- {
- var factor = isEast ? 1 : -1;
-
- // Need to bound the values because sometimes the delta exceeds the possible range
- return Math.Max(
- -180,
- Math.Min(
- 180,
- centerLongitude + (longitudeDelta * factor / 2)
- ));
- }
-#endregion
-
- internal void SelectAnnotation(Pushpin pushpin, bool animated)
- {
- _internalMapView.SelectAnnotation(pushpin, animated);
- }
-
- internal void DeselectAnnotation(Pushpin pushpin, bool animated)
- {
- _internalMapView.DeselectAnnotation(pushpin, animated);
- }
-
- protected override void UpdateCompassButtonVisibilityInner(Visibility visibility)
- {
- if (_internalMapView != null)
- {
- _internalMapView.ShowsCompass = visibility == Visibility.Visible;
- }
- }
-
- protected override void UpdateIsRotateGestureEnabledInner(bool isRotateGestureEnabled)
- {
- if (_internalMapView != null)
- {
- _internalMapView.RotateEnabled = isRotateGestureEnabled;
- }
- }
-
- protected override void SetAnimationDuration(double? animationDurationSeconds)
- {
- _animationDurationSeconds = animationDurationSeconds;
- }
- }
+ _pushpinsLayer.Update(items, selectedItems, CreatePushpin);
+
+ _logger.Info($"Updated the '{items.Safe().Count()}' map pushpins (number of selected items: '{selectedItems?.Length}').");
+ }
+
+ private Pushpin CreatePushpin(IGeoLocated item)
+ {
+ return item.IsGrouping()
+ ? new MapGroupAnnotation { Map = this }
+ : new Pushpin { Map = this };
+ }
+
+ private MKAnnotationView OnGetViewForAnnotation(MKMapView mapView, IMKAnnotation annotation)
+ {
+ if (annotation is MKUserLocation || PinTemplate == null)
+ return null;
+
+ var mapAnnotation = annotation as Pushpin;
+
+ if (mapAnnotation != null)
+ {
+ // Determine the selector, fallback on standard template for groups if not set.
+ var templateId = Pushpin.AnnotationId;
+ var selector = annotation is MapGroupAnnotation ? PinGroupTemplate ?? PinTemplate : PinTemplate;
+
+ var annotationView = mapView.DequeueReusableAnnotation(templateId);
+
+ if (annotationView == null)
+ {
+ annotationView = new MKAnnotationView(mapAnnotation, templateId);
+ annotationView.Add(selector(annotationView));
+
+ _pushPins.Add(annotationView);
+
+ // Set the frame size, or the pin will not be selectable.
+ annotationView.Frame = annotationView.Subviews[0].Frame;
+
+ if (_icon != null)
+ {
+ annotationView.CenterOffset = new CGPoint(
+ PushpinIconsPositionOrigin.X * annotationView.Frame.Width,
+ PushpinIconsPositionOrigin.Y * annotationView.Frame.Height);
+ }
+
+ if (annotationView.Frame.Width == 0 || annotationView.Frame.Height == 0)
+ {
+ _logger.Debug($"The frame for '{annotationView.Subviews[0]}' is '{annotationView.Frame}', which is too narrow. Set the frame for the Pin UIView.");
+ }
+ }
+
+ if (IsClusterEnabled)
+ {
+ annotationView.ClusteringIdentifier = Pushpin.AnnotationId;
+ }
+
+ var dataContextProvider = annotationView.Subviews.FirstOrDefault() as IDataContextProvider;
+
+ if (dataContextProvider != null)
+ {
+ dataContextProvider.DataContext = mapAnnotation;
+ }
+
+ // We don't need callouts for this implementation.
+ annotationView.CanShowCallout = false;
+
+ //Refresh Pushin when view refresh
+ var selectedContent = GetSelectedAnnotationsContent();
+
+ OnPushpinsSelected(selectedContent);
+
+ return annotationView;
+ }
+
+ return null;
+ }
+ #endregion
+
+ #region Pushpins ICONS
+
+ protected override void UpdateIcon(object icon)
+ {
+ if (_icon != null)
+ {
+ _logger.Error($"Pushpins icons cannot be changed (_icon: '{_icon}')");
+
+ throw new InvalidOperationException($"Pushpins icons cannot be changed (_icon: '{_icon}').");
+ }
+
+ UpdateIcon(ref _icon, icon);
+
+ if (_icon != null)
+ {
+ PinTemplate = annotationView => new UIKit.UIImageView(_icon)
+ {
+ Frame = new CGRect(CGPoint.Empty, _icon.Size)
+ };
+ }
+ }
+
+ protected override void UpdateSelectedIcon(object icon)
+ {
+ if (_selectedIcon != null)
+ {
+ _logger.Error($"Pushpins icons cannot be changed (_selectedIcon: '{_selectedIcon}')");
+
+ throw new InvalidOperationException("Pushpins icons cannot be changed.");
+ }
+
+ UpdateIcon(ref _selectedIcon, icon);
+
+ if (_selectedIcon != null)
+ {
+ _internalMapView.DidSelectAnnotationView += (snd, e) =>
+ {
+ var imageView = e.View.Subviews.FirstOrDefault() as UIImageView;
+ if (imageView != null)
+ {
+ imageView.Image = _selectedIcon;
+ }
+ };
+
+ _internalMapView.DidDeselectAnnotationView += (snd, e) =>
+ {
+ var imageView = e.View.Subviews.FirstOrDefault() as UIImageView;
+ if (imageView != null)
+ {
+ imageView.Image = _icon;
+ }
+ };
+ }
+ }
+ #endregion
+
+ #region SelectedPushpins
+ private void MapControl_DidSelectAnnotationView(object sender, MKAnnotationViewEventArgs e)
+ {
+ _logger.Debug("Selecting the pushpins on the map.");
+
+ GetDispatcherScheduler().Schedule(() =>
+ {
+ var mapAnnotation = e.View.Annotation as Pushpin;
+
+ if (!AllowMultipleSelection)
+ {
+ _internalMapView.SelectedAnnotations
+ .Safe()
+ .Where(a => a.Handle != e.View.Annotation.Handle)
+ .ForEach(a => _internalMapView.DeselectAnnotation(a, true));
+ }
+
+ if (mapAnnotation != null && !mapAnnotation.IsSelected && !mapAnnotation.IsSelectionChangeAlreadyHandled)
+ {
+ // Avoid infinite loops caused by this selection causing the native control to re-select
+ mapAnnotation.IsSelectionChangeAlreadyHandled = true;
+
+ mapAnnotation.IsSelected = true;
+
+ mapAnnotation.IsSelectionChangeAlreadyHandled = false;
+
+ var selectedContent = GetSelectedAnnotationsContent();
+
+ OnPushpinsSelected(selectedContent);
+
+ _logger.Info("Selected the pushpins on the map.");
+ }
+ });
+ }
+
+ private void MapControl_DidDeselectAnnotationView(object sender, MKAnnotationViewEventArgs e)
+ {
+ _logger.Debug("Deselecting the pushpins on the map.");
+
+ GetDispatcherScheduler().Schedule(() =>
+ {
+ var mapAnnotation = e.View.Annotation as Pushpin;
+
+ if (mapAnnotation != null && mapAnnotation.IsSelected && !mapAnnotation.IsSelectionChangeAlreadyHandled)
+ {
+ // Avoid infinite loops caused by this selection causing the native control to deselect again
+ mapAnnotation.IsSelectionChangeAlreadyHandled = true;
+
+ mapAnnotation.IsSelected = false;
+ _internalMapView.DeselectAnnotation(mapAnnotation, true);
+
+ mapAnnotation.IsSelectionChangeAlreadyHandled = false;
+
+ var selectedContent = GetSelectedAnnotationsContent();
+
+ OnPushpinsSelected(selectedContent);
+
+ _logger.Info("Deselected the pushpins on the map.");
+ }
+ });
+ }
+
+ private IGeoLocated[] GetSelectedAnnotationsContent()
+ {
+ return _internalMapView.SelectedAnnotations
+ .Safe()
+ .OfType()
+ .Select(a => a.Item)
+ .ToArray();
+ }
+
+ ///
+ /// Update the whole list of select items.
+ ///
+ protected override void UpdateMapSelectedPushpins(IGeoLocated[] items)
+ {
+ _pushpinsLayer.UpdateSelection(items);
+ }
+ #endregion
+
+ private void MonitorTapped()
+ {
+ var tapRecognizer = new UITapGestureRecognizer(
+ recognizer =>
+ {
+ var point = recognizer.LocationInView(_internalMapView);
+ var coordinate = _internalMapView.ConvertPoint(point, _internalMapView);
+
+ OnMapTapped(new Geocoordinate(coordinate.Latitude, coordinate.Longitude, 0, DateTime.Now, null, null, null, null, null, default));
+ });
+
+ _internalMapView.AddGestureRecognizer(tapRecognizer);
+ }
+
+ private IObservable ObserveRegionChanged()
+ {
+ return Observable
+ .FromEventPattern(
+ h => _internalMapView.RegionChanged += h,
+ h => _internalMapView.RegionChanged -= h
+ )
+ .Select(_ => Unit.Default)
+ .Do(_ =>
+ {
+ //OnPushpinsSelected(GetSelectedAnnotationsContent());
+
+ _logger.Info("The region changed.");
+ });
+ }
+
+ #region Helpers
+ private MKCoordinateRegion ComputeBoundingRectangle(MapViewPort viewPort)
+ {
+ MKCoordinateRegion region;
+ if (viewPort.Center == default(Geopoint))
+ {
+ var coordinates = viewPort.PointsOfInterest
+ .Select(p => MKMapPoint.FromCoordinate(new CLLocationCoordinate2D { Latitude = p.Position.Latitude, Longitude = p.Position.Longitude }));
+
+ var poly = MKPolygon.FromPoints(coordinates.ToArray());
+ var mapRect = poly.BoundingMapRect;
+
+ //convert MKCoordinateRegion from MKMapRect
+ region = MKCoordinateRegion.FromMapRect(mapRect);
+ }
+ else //if the center is defined
+ {
+ //Set center
+ region.Center.Latitude = viewPort.Center.Position.Latitude;
+ region.Center.Longitude = viewPort.Center.Position.Longitude;
+
+ var centerCoordinates = new BasicGeoposition { Latitude = region.Center.Latitude, Longitude = region.Center.Longitude };
+
+ //Get the farthest pushpin
+ var farthestHorizontalPushpin = viewPort.PointsOfInterest
+ .OrderBy(pushpin => centerCoordinates.GetHorizontalDistanceTo(pushpin.Position))
+ .LastOrDefault();
+ var farthestVerticalPushpin = viewPort.PointsOfInterest
+ .OrderBy(pushpin => centerCoordinates.GetVerticalDistanceTo(pushpin.Position))
+ .LastOrDefault();
+
+ //Set the width and the height required to show the farthest pushpin while keeping the center centered.
+ region.Span.LatitudeDelta = Math.Abs(viewPort.Center.Position.Latitude - farthestHorizontalPushpin.Position.Latitude) * 2;
+ region.Span.LongitudeDelta = Math.Abs(viewPort.Center.Position.Longitude - farthestVerticalPushpin.Position.Longitude) * 2;
+ }
+
+ //apply padding to allow user to see the entire pushpin and not just a part of it.
+ region.Span.LatitudeDelta *= viewPort.PointsOfInterestPadding.HorizontalPadding.Value;
+ region.Span.LongitudeDelta *= viewPort.PointsOfInterestPadding.VerticalPadding.Value;
+
+ //but padding can’t be bigger than the world
+ if (region.Span.LatitudeDelta > MapClusterConstants.MAX_DEGREES_ARC) { region.Span.LatitudeDelta = MapClusterConstants.MAX_DEGREES_ARC; }
+ if (region.Span.LongitudeDelta > MapClusterConstants.MAX_DEGREES_ARC) { region.Span.LongitudeDelta = MapClusterConstants.MAX_DEGREES_ARC; }
+
+ //and don’t zoom in stupid-close on small samples
+ if (region.Span.LatitudeDelta < MapClusterConstants.MINIMUM_ZOOM_ARC) { region.Span.LatitudeDelta = MapClusterConstants.MINIMUM_ZOOM_ARC; }
+ if (region.Span.LongitudeDelta < MapClusterConstants.MINIMUM_ZOOM_ARC) { region.Span.LongitudeDelta = MapClusterConstants.MINIMUM_ZOOM_ARC; }
+
+ return region;
+ }
+
+ private static double ZoomLevelToZoomScale(ZoomLevel zoomLevel)
+ {
+ var zoomExponent = 20 - zoomLevel.Value;
+ double zoomScale = Math.Pow(2, zoomExponent);
+ return zoomScale;
+ }
+
+ private static BasicGeoposition ApplyPaddingToCoordinate(BasicGeoposition coordinate, Thickness padding, ZoomLevel zoomLevel)
+ {
+ if (padding == Thickness.Empty)
+ {
+ return coordinate;
+ }
+
+ // determine the scale value from the zoom level
+ double zoomScale = ZoomLevelToZoomScale(zoomLevel);
+
+ // convert center coordiate to pixel space
+ double centerPixelX = MapHelper.LongitudeToPixelSpaceX(coordinate.Longitude);
+ double centerPixelY = MapHelper.LatitudeToPixelSpaceY(coordinate.Latitude);
+
+ // offset center with padding
+ centerPixelX += (((padding.Right - padding.Left) / 2) * zoomScale);
+ centerPixelY += (((padding.Bottom - padding.Top) / 2) * zoomScale);
+
+ return new BasicGeoposition
+ {
+ Longitude = MapHelper.PixelSpaceXToLongitude(centerPixelX),
+ Latitude = MapHelper.PixelSpaceYToLatitude(centerPixelY)
+ };
+ }
+
+ protected override Geopoint GetCenter()
+ {
+ var center = new BasicGeoposition();
+ center.Latitude = _internalMapView.Region.Center.Latitude;
+ center.Longitude = _internalMapView.Region.Center.Longitude;
+ return new Geopoint(center);
+ }
+
+ protected override MapViewPortCoordinates GetViewPortCoordinates()
+ {
+ var center = _internalMapView.Region.Center;
+ var latitudeDelta = _internalMapView.Region.Span.LatitudeDelta;
+ var longitudeDelta = _internalMapView.Region.Span.LongitudeDelta;
+
+ return new MapViewPortCoordinates(
+ northWest: new BasicGeoposition
+ {
+ Latitude = GetLatitude(center.Latitude, latitudeDelta, isNorth: true),
+ Longitude = GetLongitude(center.Longitude, longitudeDelta, isEast: false)
+ },
+ northEast: new BasicGeoposition
+ {
+ Latitude = GetLatitude(center.Latitude, latitudeDelta, isNorth: true),
+ Longitude = GetLongitude(center.Longitude, longitudeDelta, isEast: true)
+ },
+ southWest: new BasicGeoposition
+ {
+ Latitude = GetLatitude(center.Latitude, latitudeDelta, isNorth: false),
+ Longitude = GetLongitude(center.Longitude, longitudeDelta, isEast: false)
+ },
+ southEast: new BasicGeoposition
+ {
+ Latitude = GetLatitude(center.Latitude, latitudeDelta, isNorth: false),
+ Longitude = GetLongitude(center.Longitude, longitudeDelta, isEast: true)
+ }
+ );
+ }
+
+ private double GetLatitude(double centerLatitude, double latitudeDelta, bool isNorth)
+ {
+ var factor = isNorth ? 1 : -1;
+
+ // Need to bound the values because sometimes the delta exceeds the possible range
+ return Math.Max(
+ -90,
+ Math.Min(
+ 90,
+ centerLatitude + (latitudeDelta * factor / 2)
+ ));
+ }
+
+ private double GetLongitude(double centerLongitude, double longitudeDelta, bool isEast)
+ {
+ var factor = isEast ? 1 : -1;
+
+ // Need to bound the values because sometimes the delta exceeds the possible range
+ return Math.Max(
+ -180,
+ Math.Min(
+ 180,
+ centerLongitude + (longitudeDelta * factor / 2)
+ ));
+ }
+ #endregion
+
+ internal void SelectAnnotation(Pushpin pushpin, bool animated)
+ {
+ _internalMapView.SelectAnnotation(pushpin, animated);
+ }
+
+ internal void DeselectAnnotation(Pushpin pushpin, bool animated)
+ {
+ _internalMapView.DeselectAnnotation(pushpin, animated);
+ }
+
+ protected override void UpdateCompassButtonVisibilityInner(Visibility visibility)
+ {
+ if (_internalMapView != null)
+ {
+ _internalMapView.ShowsCompass = visibility == Visibility.Visible;
+ }
+ }
+
+ protected override void UpdateIsRotateGestureEnabledInner(bool isRotateGestureEnabled)
+ {
+ if (_internalMapView != null)
+ {
+ _internalMapView.RotateEnabled = isRotateGestureEnabled;
+ }
+ }
+
+ protected override void SetAnimationDuration(double? animationDurationSeconds)
+ {
+ _animationDurationSeconds = animationDurationSeconds;
+ }
+ }
}
#endif