Intro
Every time I meet someone from the Windows Phone Map team (or get into contact with them in some other way) I talk about the importance of being able to data bind shapes to the maps. Things are definitely improving: XAML elements can displayed using data binding out of the box (we used to need the Windows Phone toolkit for that), but Map Shapes alas not. As the saying goes – ‘if the mountain will not come to Muhammad, then Muhammad must go to the mountain' I took up an old idea that I used for Bing Maps on Windows 8 and later converted to Windows Phone 8.0 – and basically adapted it for use in Windows Phone 8.1. Including some bug fixes.
Map display elements recap
You can display either XAML elements on top of the map or use map shapes. XAML elements can be anything, a can be displayed by either adding them in code to the map’s Children property, or by using a MapItemControl, databind it’s ItemSource property to a list object and them define a template for the display using properties from the object bound to the template. Typically, that looks like this:
<Maps:MapItemsControl ItemsSource="{Binding Activities}"> <Maps:MapItemsControl.ItemTemplate> <DataTemplate > <Image Source="{Binding Image}" Height="25" Maps:MapControl.NormalizedAnchorPoint="{Binding Anchor}" Maps:MapControl.Location="{Binding Position}"> </Image> </DataTemplate> </Maps:MapItemsControl.ItemTemplate> </Maps:MapItemsControl>
Important to know is that these elements are not drawn by the map, but on top of the map, by the UI thread. Typically, they are slow.
Map shapes, on the other hand, are drawn by the map itself, by native code, and therefore fast. You can add them to the map by adding a MapShape child class to the maps’ MapElements properties. You can choose from MapPolyLine, MapPolygon and – new in Windows Phone 8.1 – MapIcon. Data binding is, unfortunately, not something that is supported.
Enter WpWinNlMaps
This time, you won’t have to type or have to look at a lot of code, as I have already published all that’s necessary on NuGet – in the WpWinNlMaps package. You simply add this to your application, and you are ready to go. Mind you, this pulls in a lot of other stuff – the WpWinNl package itself, MVVMLight, and some more stuff. This will enable you to bind view models to the map, have them displayed to and make them ‘tappable’. This is all done with the aid of – you guessed it – a behavior. MapShapeDrawBehavior to be precise.
Concepts
Typically, maps are divided into layers. You can think of this as logical units representing one class of real-world objects (or ‘features’ as they tend to be called in the geospatial word). For instance, “houses”, “gas stations”, “roads”. In Windows Phone, this is clearly not the case: all MapShapes are thrown into one basket – the MapElements property. In WpWinNlMaps, a layer roughly translates to one behavior attached to the map.
A MapShapeDrawBehavior contains a few properties
- ItemsSource – this is where you bind your business objects/view models to
- PathPropertyName – the name of the property in a bound object that contains the Geopath describing the object’s location
- LayerName – the name of the layer. Make sure this is unique within the map
- ShapeDrawer – the name of the class that actually determines how the Geopath in PathPropertyName is actually displayed
- EventToCommandMappers – contains a collection of events of the map that need to be trapped, mapped to a command on the bound object that needs to be called when the map receives this event. Presently, the only events that make sense are “Tapped” and “MapTapped”.
A sample up front
Before going further into detail, let’s do a little sample first, because theory is fine, but code works better, in my experience. So I have a little class containing a viewmodel that has a Geopath, and Name, and a command that can be fired:
public class PointList : ViewModelBase { public PointList() { Points = null; } public string Name { get; set; } public Geopath Points { get; set; } public ICommand SelectCommand { get { return new RelayCommand<MapSelectionParameters>( p => DispatcherHelper.CheckBeginInvokeOnUI(() => Messenger.Default.Send(new MessageDialogMessage( Name, "Selected object", "Ok", "Cancel")))); } } }
Suppose a couple of these things are sitting in my main view model’s property “Lines”, then I can simply display a number of blue violet lines by using the following XAML
<maps:MapControl x:Name="MyMap" > <mapbinding:MapShapeDrawBehavior LayerName="Lines" ItemsSource="{Binding Lines}" PathPropertyName="Points"> <mapbinding:MapShapeDrawBehavior.EventToCommandMappers> <mapbinding:EventToCommandMapper EventName="MapTapped" CommandName="SelectCommand"/> </mapbinding:MapShapeDrawBehavior.EventToCommandMappers> <mapbinding:MapShapeDrawBehavior.ShapeDrawer> <mapbinding:MapPolylineDrawer Color="BlueViolet"/> </mapbinding:MapShapeDrawBehavior.ShapeDrawer> </mapbinding:MapShapeDrawBehavior> </maps:MapControl
- Objects are created from the collection “Lines”
- The name of the layer is “Lines” (this does not need to correspond with the name used in the previous bullet)
- The property containing the Geopath for a single object in the list “Lines” is called “Points”
- When the event “MapTapped” is detected on one of these lines, the command “SelectCommand” is to be fired. Mind you, this command should be on the bound object. Notice this fires a MessageDialogMessage that can be displayed by a MessageDialogBehavior. Basically, if all the parts are in place, it will show a message dialog displaying the name of the object the user tapped on.
- This object is to be drawn as a blue violet line.
The result being something as displayed to the right.
Map shape drawers
These are classes that turn the Geopath into an actual shape. You get three out of the box that can be configured using a few simple properties.
- MapIconDrawer
- MapPolyLineDrawer
- MapPolygonDrawer
To draw an icon, line or polygon (duh). The key thing is – the drawers are very simple. For instance, this is the drawer that makes a line:
public class MapPolylineDrawer : MapLinearShapeDrawer { public MapPolylineDrawer() { Color = Colors.Black; Width = 5; } public override MapElement CreateShape(object viewModel, Geopath path) { return new MapPolyline { Path = path, StrokeThickness = Width, StrokeColor = Color, StrokeDashed = StrokeDashed, ZIndex = ZIndex }; } }
There is only one method CreateShape that you need to override to make your own drawer. You get the viewmodel and the Geopath (as extracted by the MapShapeDrawBehavior and you can simply mess around with it.
The drawer class model is like this:
Trapping events and activating commands with EventToCommandMapper
By adding an EventToCommandMapper to EventToCommandMappers you can make a command get called when an event occurs. You can do that easily in XAML as displayed in the sample. Basically only events that have a MapInputEventArgs or TappedRoutedEventArgs can be trapped. In most real-world cases you will only need to trap MapTapped. See the example above how to do that. Keep in mind, again, that although the event is trapped on the map, the command is executed on the elements found on the location of the event.
There is a special case though where you might want to trap the “Tapped” event too – that is when you mix and match XAML elements and MapShapes. See this article for background information on that particular subject.
Some limitations and caveats
- Even for MapIcons the PathPropertyName needs to point to a Geopath. This is to create a common signature for the CreateShape method in the drawers. Geopaths of one point are valid, so that is no problem. If you provide Geopaths containing more than one point, it will just use the first point.
- Although the properties are read from bound objects, those properties are not bound themselves. Therefore, an object that has already been drawn on the map will not be updated on the map if you change the contents of the Geopath. You will need to make sure the objects are in an ObservableCollection and then replace the whole object in the collection to achieve that result.
- If you use a method like mine to deal with selecting objects (firing messages), your app’s code will need to deal with multiple selected objects – since there can be more than one object on one location. My sample solution does clearly not do that. All objects will fire a message, but only one will show the message dialog. A better way to deal with it would be
- Make a message that contains the MapSelectionParameters that are supplied to the command that is fired on select (see sample code)
- Have the MainViewModel collect those messages, and decide based upon the SelectTime timestamp what messages are the result of one select action and should be displayed together.
- In professional map systems the order of the layers determines the order in which elements are drawn. So the layers that are drawn first, appear at the bottom, and everything that’s drawn later on top of it. In Windows Phone, the Z-index of each element determines that. So be sure to set those in the right order if you want to appear stuff on top of each other.
- Be aware that MapIcons are a bit of an oddball. First, they are drawn using a ‘best effort’. In layman’s terms, this means that some of them won’t a appear if they are too close together. If there are a lot close together, a lot won’t appear. This will change while you zoom and pan, and you have no way to control it. Furthermore, MapIcons don’t always show on top of other shapes even if you specify the Z-index to be higher.
Conclusion
The sample solution shows the drawing of icons, lines and polygons using the binding provided by WpWinNlMaps. You can tap on a map element and the name of the tapped element will display in a message dialog. This should give you a running start in using this library.
Although there are quite some limitations with respect to the binding, mainly caused by the nature of how the map works (you cannot bind to a command that is to be called when you tap the element – you can just provide the name of the command, just like the path property) I think this library makes using the map a lot easier – at least in MVVM apps. I am using this library extensively in my latest app Travalyzer.
Happy mapping!
3 comments:
Another crucial limitation: polygons can't have holes in themselves. If some regions of a polygon overlap, the whole polygon won't be rendered anymore. Sample code: https://github.com/sibbl/Windows-Phone-map-bug
That is correct. I remember now that I was planning to mention that, and in the end I forgot. Polygons with holes, aka "donuts" as they are called in GIS country, are not supported. With some clever mathematical trickery you can emulate it, but as soon as you self-intersect, you are done.
People interested in this article could be interested also to know that now there is also a demo project for UWP in https://github.com/LocalJoost/WpWinNl/tree/uwp/uap10.0/WpWinNl.MapBindingDemo
Thank for your work, Joost!
Post a Comment