Eigene Karte mit Mapsui – auch in Xamarin.Forms

Auch wenn, zumindest derzeit, noch keine direkte Xamarin.Forms Implementierung von Mapsui existiert, kann dieses dennoch genutzt werden. Dies ist sogar mit erstaunlich wenig Aufwand bei guter Performance möglich. Aber was ist Mapsui? Nun Mapsui ist ein OpenSource Projekt von Paul den Dulk und ist über NuGet sowie Github verfügbar. Aktuell unterstützt das Projekt WPF sowie Xamarin Nativ Android, iOS und UWP. Mit Mapsui ist es möglich neben OpenStreetMap auch eigenes Kartenmaterial zu verwenden und zwar offline sowie online. Das Kartenmaterial liegt hier in der Regel im *.mbtiles-Format vor. Weiterhin ist es natürlich möglich allerlei Markierungen, Label und andere geometrische Formen auf der Karte zu platzieren. Die Performance ist ebenfalls sehr gut. In meinem ersten Projekt mit dieser Bibliothek (siehe unten) kann ich problemlos knapp 300MB Kartenmaterial und 1500 Marker darstellen ohne Performanceprobleme zu bekommen.

Die Verwendung der Bibliothek ist sehr einfach gestaltet und es gibt auf Github einige Samples zu Mapsui. Hierbei wird in der WPF Anwendung so gut wie jede Facette von Mapsui gezeigt. Die nativen Xamarin Anwendungen laden jeweils einen einzelnen Bereich, der im Sample-Code umgestellt werden kann (z.B. Custom-Tiles zu OpenStreetMap usw.).

Um diesen Beitrag in englisch zu sehen hier klicken: View this post in english

 

Share-Online 

Vorbereitung

Um die Mapsui Bibliothek verwenden zu können, muss diese zuerst referenziert werden. Ich würde hierbei den Weg über NuGet bevorzugen, aber natürlich ist es auch möglich direkt die Projekte aus dem Github Repository zu verwenden. Es sollte also, falls nicht ohnehin schon geschehen, zunächst das Projekt anlegt werden. Da es in diesem Post um Xamarin.Forms geht, ist natürlich ein Projekt von diesem Typ von Vorteil, allerdings unterstützt die Bibliothek auch die Xamarin Native Plattformen. Die Auswahl zwischen Shared und PCL ist hierbei fast egal. Es ist beides problemlos möglich. Der Einfachheit halber, werde ich hier allerdings ein Shared Projekt nutzen. Somit nutze ich ein Xamarin.Forms/Shared Projekt. Um nun die Bibliothek zu referenzieren, wird der NuGet Packet-Manager geöffnet und dort nach „Mapsui“ gesucht. Derzeit nutze ich Version 1.0.7, welche stabil und zuverlässig läuft. Sollte – aus welchem Grund auch immer -eine andere Version verwendet werden, bitte den Changelog von Mapsui beachten, ob relevante Details geändert haben. Vor Version 1.0.7 funktioniert die Anleitung dieses Beitrages im Übrigen nicht (Android gar nicht und iOS eingeschränkt). Außerdem empfiehlt es sich, die aktuellste Version von Xamarin zu verwenden – unabhängig von dem, was man nutzen möchte.

Custom Renderer

Da zum jetzigen Zeitpunkt noch keine direkte Xamarin.Forms Unterstützung vorhanden ist, muss ein Custom Renderer gebaut werden, der die notwendig Funktionalität bereitstellt. Hierfür ist eine Wrapper-Klasse um die Mapsui Komponente notwendig, die anschließend in XAML oder im Code verwendet werden kann. Bei den Namespaces in dem hier gezeigten Code beziehe ich mich übrigens auf das Xamarin.Forms Sample, welches am Ende des Beitrages zu finden ist.

using Mapsui.Styles;

namespace MapsuiFormsSample
{
    public class MapsUIView : Xamarin.Forms.View
    {
        public Mapsui.Map NativeMap { get; }

        protected internal MapsUIView()
        {
            NativeMap = new Mapsui.Map();
            NativeMap.BackColor = Color.Black; //This Color should match the map - I prefer Black over White here
        }
    }
}

Da nun ein Xamarin.Forms kompatibles Control existiert, das die Mapsui Karte als Eigenschaft beinhaltet, können wir uns nun daran machen die Renderer der einzelnen Plattformen zu implementieren.

Android Renderer

Der plattformspezifische Renderer ist eigentlich nicht mehr als ein simples 1:1 Mapping auf die Nativen Renderer aus der Mapsui Bibliothek. Dazu wird die „OnElementChanged“-Methode unseres ViewRenderer’s überschrieben. Weitere Informationen zu Custom Renderer kann der Xamarin-Dokumentation entnommen werden.

using MapsuiFormsSample;
using MapsuiFormsSample.Droid;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;

[assembly: ExportRenderer(typeof(MapsUIView), typeof(MapViewRenderer))]
namespace MapsuiFormsSample.Droid
{
    public class MapViewRenderer : ViewRenderer<MapsUIView, Mapsui.UI.Android.MapControl>
    {
        Mapsui.UI.Android.MapControl mapNativeControl;
        MapsUIView mapViewControl;

        protected override void OnElementChanged(ElementChangedEventArgs<MapsUIView> e)
        {
            base.OnElementChanged(e);

            if (mapViewControl == null && e.NewElement != null)
                mapViewControl = e.NewElement;

            if (mapNativeControl == null && mapViewControl != null)
            {
                mapNativeControl = new Mapsui.UI.Android.MapControl(Context, null);
                mapNativeControl.Map = mapViewControl.NativeMap;

                SetNativeControl(mapNativeControl);
            }
        }
    }
}

iOS Renderer

Ebenso wie bei Android ist der iOS Renderer nur ein 1:1 Mapping. Auch hier gilt, weitere Informationen zu Custom Renderer können der Xamarin-Dokumentation entnommen werden.

using CoreGraphics;
using Foundation;
using MapsuiFormsSample;
using MapsuiFormsSample.iOS;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;

[assembly: ExportRenderer(typeof(MapsUIView), typeof(MapViewRenderer))]
namespace MapsuiFormsSample.iOS
{
    [Preserve(AllMembers = true)]
    public class MapViewRenderer : ViewRenderer<MapsUIView, Mapsui.UI.iOS.MapControl>
    {
        Mapsui.UI.iOS.MapControl mapNativeControl;
        MapsUIView mapViewControl;

        protected override void OnElementChanged(ElementChangedEventArgs<MapsUIView> e)
        {
            base.OnElementChanged(e);

            if (mapViewControl == null && e.NewElement != null)
                mapViewControl = e.NewElement;

            if (mapNativeControl == null && mapViewControl != null)
            {
                var rectangle = mapViewControl.Bounds;
                var rect = new CGRect(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height);

                mapNativeControl = new Mapsui.UI.iOS.MapControl(rect);
                mapNativeControl.Map = mapViewControl.NativeMap;
                mapNativeControl.Frame = rect;

                SetNativeControl(mapNativeControl);
            }
        }
    }
}

UWP Renderer

Und zu guter Letzt noch der Renderer für UWP. Diesen habe ich noch nicht so ausführlich getestet wie den unter iOS und Android.

using MapsuiFormsSample;
using MapsuiFormsSample.UWP;
using Xamarin.Forms.Platform.UWP;

[assembly: ExportRenderer(typeof(MapsUIView), typeof(MapViewRenderer))]
namespace MapsuiFormsSample.UWP
{
    public class MapViewRenderer : ViewRenderer<MapsUIView, Mapsui.UI.Uwp.MapControl>
    {
        Mapsui.UI.Uwp.MapControl mapNativeControl;
        MapsUIView mapViewControl;

        protected override void OnElementChanged(ElementChangedEventArgs<MapsUIView> e)
        {
            base.OnElementChanged(e);
            
            if (mapViewControl == null && e.NewElement != null)
                mapViewControl = e.NewElement as MapsUIView;

            if (mapNativeControl == null && mapViewControl != null)
            {
                mapNativeControl = new Mapsui.UI.Uwp.MapControl();
                mapNativeControl.Map = mapViewControl.NativeMap;
                
                SetNativeControl(mapNativeControl);
            }
        }
    }
}

Map-Daten, Tiles und Marker

Nun geht es darum die Map zu „generieren“. Hierfür existieren mehrere Möglichkeiten. Wird das Control nur in XAML eingebunden ist es am Besten, wenn die Erzeugung der Daten, Tiles und Marker im „MapsUIView“-Control erfolgt. Dies wäre durchaus möglich und wohl auch die sauberste Lösung (zumindest meiner Meinung nach). Alternativ wäre es auch möglich, im XAML einen Container zu definieren und über dessen Namen im CodeBehind das „MapsUIView“-Control als Children hinzuzufügen. Hier kann dann auch die betreffende Seite der App die gesamte Erzeugung übernehmen. Methode 1 bietet hierbei Vorteile, wenn die identische Map an mehreren Punkten der Anwendung verwendet werden soll. Methode 2 bietet Vorteile, wenn verschiedene Maps auf unterschiedlichen Seiten der App dargestellt werden sollen. Für welchen Weg sich letzt endlich entschieden wird, ist eher eine persönliches Angelegenheit. In diesem Beitrag werde ich mich auf Methode 2 konzentrieren, da diese mehr Code (wenn auch nur wenige Zeilen) erfordert. Für Methode 1 müsste lediglich das setzen vom CodeBehind entfernt werden und das Erzeugen der einzelnen Schichten im „MapsUIView“-Control erfolgen. Anschließend muss nur noch das Control direkt im XAML verwendet werden.

Zunächst einmal der XAML Code:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MapsuiFormsSample.MainPage">

    <ContentPage.Content>
        <Grid x:Name="ContentGrid" />
    </ContentPage.Content>
</ContentPage>

Und jetzt können wir die Karte vom CodeBehind dem „ContentGrid“ hinzufügen. So sieht meine Mainpage.xaml.cs Datei aus:

using System.Diagnostics;
using Mapsui.Geometries;
using Mapsui.Layers;
using Mapsui.Providers;
using Mapsui.Styles;
using Mapsui.Utilities;

namespace MapsuiFormsSample
{
	public partial class MainPage
	{
		public MainPage()
		{
			InitializeComponent();

			var mapControl = new MapsUIView();
			mapControl.NativeMap.Layers.Add(OpenStreetMap.CreateTileLayer());

			var layer = GenerateIconLayer();
			mapControl.NativeMap.Layers.Add(layer);
			mapControl.NativeMap.InfoLayers.Add(layer);

			mapControl.NativeMap.Info += (sender, args) =>
				{
	                		var layername = args.Layer?.Name;
	                		var featureLabel = args.Feature?["Label"]?.ToString();
	                		var featureType = args.Feature?["Type"]?.ToString();

	                		Debug.WriteLine("Info Event was invoked.");
	                		Debug.WriteLine("Layername: " + layername);
	                		Debug.WriteLine("Feature Label: " + featureLabel);
	                		Debug.WriteLine("Feature Type: " + featureType);

	                		Debug.WriteLine("World Postion: {0:F4} , {1:F4}", args.WorldPosition?.X, args.WorldPosition?.Y);
	                		Debug.WriteLine("Screen Postion: {0:F4} , {1:F4}", args.ScreenPosition?.X, args.ScreenPosition?.Y);
	            		};

	        	ContentGrid.Children.Add(mapControl);
	    	}

	    	private ILayer GenerateIconLayer()
	    	{
	        	var layername = "My Local Layer";
	        	return new Layer(layername)
	            		{
	                		Name = layername,
	                		DataSource = new MemoryProvider(GetIconFeatures()),
                   			Style = new SymbolStyle
	                    			{
	                        			SymbolScale = 0.8,
	                        			Fill = new Brush(Color.Red),
	                        			Outline = { Color = Color.Black, Width = 1 }
	                    			}
                		};
	    	}

	    	private Features GetIconFeatures()
	    	{
	        	var features = new Features();
	        	var feature = new Feature
	        		{
	            			Geometry = new Polygon(new LinearRing(new[]
	            				{
	                				new Point(1066689.6851, 6892508.8652),
	                				new Point(1005540.0624, 6987290.7802),
	                				new Point(1107659.9322, 7056389.8538),
	                				new Point(1066689.6851, 6892508.8652)
	            				})),
	            			["Label"] = "My Feature Label",
	            			["Type"] = "My Feature Type"
	        		};

	        	features.Add(feature);
	        	return features;
		}
	}
}

Aber was genau passiert hier? Nun, offensichtlich dürfte die Erzeugung einer neuen Instanz in Zeile 15 sein.  Aber bereits in Zeile 16 passiert etwas Mapsui spezifisches. Hier wird das Karten-Bildmaterial (die Map-Tiles) unserer Mapsui-Map-Eigenschaft (hier NativeMap) in der ersten Schicht hinzugefügt. Ist hier die Verwendung von eigenem Bildmaterial erwünscht, kann man dies mittels einer *.mbtiles Datenbank erfolgen. Hierzu gibt es Beispiele im Mapsui Github Repository. Desweiteren wird im Code eine „Icon“-Schicht erzeugt. Diese wird den ganz normalen Schichten sowie den Information-Schichten hinzugefügt. Letztere sind notwendig, um auf Touch-Events z.B. für Icons zu reagieren. Diese Reaktion wird direkt im Anschluss genutzt. Hier gibt es als Parameter eigentlich alle benötigten Informationen inklusive der selbst gesetzten wie im Beispiel mit „Label“ und „Type“.

Das Ergebnis

Nun ist es Zeit für Ergebnisse. Im Ergebnis sehen wir ein rotes Dreieck zwischen Hannover, Bremen und Hamburg, welches wir im Code erzeugt haben:

Sollten Fragen auftauchen, können diese gerne in den Kommentaren gestellt werden. Sollten Fehler mit Mapsui auftreten, so gehören diese in den „Issue“-Bereich von Github.

Meine Apps mit Mapsui

Breath Companion
Download im iOS AppStore
Download im Google Play Store
Breath Companion
Download im iOS AppStore
Download im Google Play Store

Beispielcode

Anbei der Quellcode für diesen Beitrag: Quellcode herunterladen.

Werbung

Der Beitrag hat euch gefallen, geholfen und Ihr wollt mich unterstützen? Dann macht dies doch einfach direkt hier. Teilt die Seite über Social Media Netzwerke, nutzt die Amazon Links oder spendet einfach einen kleinen Betrag via PayPal. Jede Art der Unterstützung ist eine Hilfe für Blogs wie diesen.