Intro
In my previous post in this series I have talked about how to get a map control in a Universal Windows App and how to set it up with a proper key, now it's time to get the thing to actually do something. In this episode I am going to show how to perform basic map manipulations (i.e. control what location it's looking at, and how) and how to draw shapes on the map. I am going to refer to a sample app that has already been published on GitHub and has all the moving parts for the whole series.
Manipulation
When I talk about manipulation of a Windows 10 Universal Windows App map (there's a mouthful), I mean three basic things that can be manipulated - that is, when we consider the map as a traditional 2D map, i.e. a flat surface where you look upon from right above.
- View area
- Heading (aka rotation)
- Pitch
As we will see later on in this series, in Windows 10 this is actually a bit too limiting a statement, but I have to start somewhere. We will also see the view area is actually determined by not one but two factors - the center location and the zoom level.
View area / zoom level
The sample app has a couple of 'button' controls that control location and zoom level. If you tap the "Initial location", it will zoom from the default view (the whole world) to a location around my house. It will not just jump to the location, but zoom out a little, then zoom in all the way to this location - animating all the way. The same goes for the "Zoom" in and "Zoom out button", the map will zoom in, animating from one zoom level to another.
The methods doing this can be found in MainPage.xaml.cs:
private async void InitialLocation(object sender, RoutedEventArgs e)
{
await MyMap.TrySetViewAsync(
new Geopoint(new BasicGeoposition { Latitude = 52.181427,
Longitude = 5.399780 }),
16, RotationSlider.Value, PitchSlider.Value, MapAnimationKind.Bow);
//MyMap.Center =
// new Geopoint(new BasicGeoposition { Latitude = 52.181427,
Longitude = 5.399780});
//MyMap.ZoomLevel = 16;
}
private void ZoomOut(object sender, RoutedEventArgs e)
{
MyMap.TryZoomOutAsync();
// MyMap.ZoomLevel -= MyMap.ZoomLevel > MyMap.MinZoomLevel ? 1 : 0;
}
private void ZoomIn(object sender, RoutedEventArgs e)
{
MyMap.TryZoomInAsync();
//MyMap.ZoomLevel += MyMap.ZoomLevel < MyMap.MaxZoomLevel ? 1 : 0;
}
As you see, there is a lot of commented out code. This is because I want to show there is more than one way to achieve the same result. Personally I find the MyMap.TrySetViewAsync method the most recommendable, as this animates to a location and a zoom level and also (as can be seen from the code) can set a rotation and a pitch (explained below). You can also just set zoom level and center location in one go, but that will make the map 'jump;' to a location, which is a way less desirable user experience IMHO. Remember the old mantra - 'fast and fluid' ;)
The same goes for zoom in and zoom out. The TryZoomOutAsync and TryZoomInAsync methods give a fluid animation going from one zoom level to another, and have the added bonus of not crashing when you try to zoom below 0 or over the maximum zoom level (or the need of creating a complex expression to prevent that).
This is what you need when you use a simple 2D map. As we will see later, when we go to 3D maps other factors come into play.
Aside - coordinates
For some reason that is not clear to me, the map API has two types that depict a location. The first one is a struct BasicGeopositionthat only holds latitude, longitude and altitude, For creating shapes, this is usually enough. Yet, for manipulating the map we need Geopoint. You create a Geopoint from a BasicGeoposition. A Geopoint has some extra capabilities, like SpatialReferenceId (that to my knowledge still has no function yet, and and AltitudeReferenceSystem that I never needed to use as of yet. Geopoint is a class. Maybe this is some optimization thing.The key takeaway is that there are two coordinate types, and sometimes you need the one, and sometimes the other. Intellisense is your friend here.
Heading
Every cartographer or GIS person - and everyone who used to travel before the ubiquitous affordable GPS systems became available (we had paper maps back then, kids, can you believe it) knows one golden rule: "North Is Up". Unfortunately 99.5% of the population does not care about proper GIS rules, nor does it want to use a compass, and it particularly does not want to deal with mentally rotating a map while navigating a complex and busy highway intersection - they follow the mapping school of "I Just Want To Get There". So GPS systems tend to show 'up' as the direction you are travelling in. And the map control supports that too. Of course you can simply apply a rotation transformation to the control, but that will show all labels at an angle too. This is clearly not what we want, and that's what Microsoft knows too.
Here you see the same map as above, but rotated 90° counter-clockwise. Notice how all labels are still readable.
There are quite some ways to accomplish that. I did it by data binding the maps Heading property to the slider's value:
<Slider x:Name="RotationSlider" VerticalAlignment="Top"
Minimum ="-180" Maximum="180"
Value="{Binding ElementName=MyMap, Path=Heading, Mode=TwoWay}" />
This has the advantage slider and map heading stay synchronized when the user rotates the map with a two-finger gesture. Unfortunately, although the map accepts -180° to 180° it really wants to use 0° to 360° so if you rotate the map like that you will see the slider jump to the right. There is some more code necessary to keep that in sync. There are no less than three ways to set the heading, as you can see in this unused method in the code:
// Not used now - replaced by databinding
private void RotationSliderOnValueChanged(object sender,
RangeBaseValueChangedEventArgs e)
{
MyMap.TryRotateToAsync(e.NewValue);
//Same result, sans the cool animation.
MyMap.Heading = e.NewValue;
//Rotates BY a certain value.
MyMap.TryRotateAsync(e.NewValue);
}
TryRotateToAsync rotates the map to a set angle, and do this animated. If you set "Heading" directly, it will have the same effect, but it will just jump to the nieuw display. TryRotateAsync finally does not rotate to an angle, but by an angle, so if you repeatedly call this method with "10" it will rotate your map ten degrees counter clockwise for every call.
Pitch
This is like a fake 3D view, like the effect you get on GPS systems - or when you rotate the top of a paper map backwards from you:
This is the same map a the previous two, but now with a pitch of 60. A pitch may be anywhere from 0 to 75. The is a catch - pitch values is only effective at zoom level 7 and down. Above that, you can set pitch to any value you like but you get the same result as when you set it to 0. Which makes sense - otherwise your viewpoint is so 'high' above the map that pitching it would mean you/d mainly see the empty space 'above' and 'beyond' the actual flat map. Setting the map pitch from the slider is also done by data binding. Note that I need to bind to the DesiredPitch property:
<Slider x:Name="PitchSlider" Grid.Column="1" Grid.Row="1"
Orientation="Vertical" Margin="0,0,0,12" Minimum="0" Maximum="75"
Value="{Binding ElementName=MyMap, Path=DesiredPitch, Mode=TwoWay}" />
as the map might just refuse you to give the pitch you want because to are zoomed out too much. You can also do this from code:
private void PitchSliderOnValueChanged(object sender,
RangeBaseValueChangedEventArgs e)
{
//Only effective on ZoomLevel 7 and higher!
MyMap.DesiredPitch = e.NewValue;
// Does not work - read only
// MyMap.Pitch = e.NewValue;
}
and there is in fact the Pitch property on the map, but that is read only. DesiredPitch is the pitch you request, Pitch holds the pitch you actually get.
Manipulation by canvas controls
A new way of enabling the user control the map zoom, pitch is making use of the new canvas control properties. These are not used in the code sample, but you can make controls visible like this:
<maps:MapControl x:Name="MyMap"
ZoomInteractionMode ="GestureAndControl"
RotateInteractionMode ="GestureAndControl"
TiltInteractionMode = "GestureAndControl" />
This will result on these four controls on the right side of the map control
On a tablet or PC, where there is usually enough room for external toolbars and sliders these are a very nice touch already, but if you want to use the map on a smaller device like a phone or mini tablet they are actually a godsend, for a you can see on the illustration on the right the sliders actually use up a lot of the valuable screen real estate on a phone. In a real application I would probably write an adaptive trigger to do away with the sliders on a phone and/or small screen device.
You can set the value of these three properties actually to five different values:
- Auto
- Disabled
- GestureOnly
- ControlOnly
- GestureAndControl
And setting "Disabled" to all properties has the added bonus of being able to lock down the map, which can be useful on a phone. Who has not accidentally swiped the map way, operating the phone one-handed, looking for a parking spot or something. There is, by the way, a fourth map property to lock down even the panning: PanInteractionMode, but that one has no control option, and only supports the values "Auto" and "Disabled".
Notice the power of Universal Windows Apps by the way: the only thing I had to change for my app was to point Visual Studio to the phone emulator in stead of Windows itself. Deploying it to my phone would require only pointing it to a device and selecting ARM in stead of x86/x64 compile. ZERO code change.
Drawing (and deleting)
If you are coming from Windows Phone 8.1, not much has changed in Windows 10, apart from the fact things work a lot better. If you are coming from Windows 8 store apps - your cheese has moved quite a bit, but that is all for the better.We still draw polygons, lines and points but things have changed a bit.
Filling the map
If you start the sample app, click "initial location, then "draw lines", "draw shapes" and then "draw points" a couple of times, you will end up with this mess :)
Drawing a line
Drawing a couple lines is as simple as this:
private void DrawLines(object sender, RoutedEventArgs e)
{
if (!DeleteShapesFromLevel(2))
{
var strokeColor = Colors.Green;
strokeColor.A = 200;
//Drawing lines, notice the use of Geopath. Consists out of BasicGeopositions
foreach (var dataObject in PointList.GetLines())
{
var shape = new MapPolyline
{
StrokeThickness = 9,
StrokeColor = strokeColor,
StrokeDashed = false,
ZIndex = 2,
Path = new Geopath(dataObject.Points)
};
MyMap.MapElements.Add(shape);
}
}
}
Lines are created as a MapPolyLine object that have the following properties:
- A stroke thickness
- A color (not this is a partially translucent green color)
- A dash (the line is dashed or not, there are no other pattern available)
- A z-index (this determines which objects appear on top of each other - a shape with a higher z-index will appear to be 'on top' of a shape with a lower z-index
- Most importantly - a Geopath that contains a number of points.
Shapes are then simply added to the MapELements collection of the Map.
The PointList.GetLines() I used to create data just gets a list of objects that each contain a list of points:
public static IEnumerable<PointList> GetLines()
{
var result = new List<PointList>
{
new PointList
{
Name = "Line1",
Points = new List<BasicGeoposition>
{
new BasicGeoposition{Latitude = 52.1823604591191, Longitude = 5.3976580593735},
new BasicGeoposition{Latitude = 52.182687940076 , Longitude = 5.39744247682393},
new BasicGeoposition{Latitude = 52.1835449058563, Longitude = 5.40016567334533},
new BasicGeoposition{Latitude = 52.1837400365621, Longitude = 5.40009761229157}
}
},
new PointList
{
Name = "Line2",
Points = new List<BasicGeoposition>
{
new BasicGeoposition{Latitude = 52.181295119226 , Longitude = 5.39748212322593},
new BasicGeoposition{Latitude = 52.1793784294277, Longitude = 5.39909915998578}
}
}
};
return result;
}
Drawing shapes
This is nearly the same as drawing shapes, only this draws a closed shaped. You are not required to close the shape (the last point of the Geopath does not need to be equal to the first one, as in some geographical systems - the shape is implicitly closed. The code is nearly the same.
private void DrawShapes(object sender, RoutedEventArgs e)
{
if (!DeleteShapesFromLevel(1))
{
var strokeColor = Colors.DarkBlue;
strokeColor.A = 100;
var fillColor = Colors.Blue;
fillColor.A = 100;
foreach (var dataObject in PointList.GetAreas())
{
var shape = new MapPolygon
{
StrokeThickness = 3,
StrokeColor = strokeColor,
FillColor = fillColor,
StrokeDashed = true,
ZIndex = 1,
Path = new Geopath(dataObject.Points)
};
MyMap.MapElements.Add(shape);
}
}
}
The only difference is that we not only have a stroke color (which here is the outline of the shape), but we now also have fill color that determines how the inside of the shape is colored. Oh, and I do use a dash for the stroke here, if only to show what it looks like
Drawing icons
Windows Phone 8.1 introduced the MapIcon, a point object type that allows you to show icon type objects - with label! - that were drawn by the map itself. Compared to the 'old' way of displaying XAML elements on top of the map this made things vastly more simple and above all offered an awesome performance, compared to the 'old way'' .Unfortunately it also introduced the concept of 'best effort drawing' - that is, icons close to each other were not displayed in some seemingly randomly way to preserve performance. In practice, everyone used the 'old' way of doing things, i.e. drawing XAML elements on top of the map, which gave a horrible performance, but at least the data was displayed.
Rejoice, now, because this has been fixed. MapIcons are now a fully viable concept, and very much usable. Drawing MapIcons goes like this:
private async void DrawPoints(object sender, RoutedEventArgs e)
{
// How to draw a new MapIcon with a label, anchorpoint and custom icon.
// Icon comes from project assets
var anchorPoint = new Point(0.5, 0.5);
var image = RandomAccessStreamReference.CreateFromUri(
new Uri("ms-appx:///Assets/wplogo.png"));
try
{
// Helper extension method var area = MyMap.GetViewArea();
var area = MyMap.GetViewArea();
// PointList is just a helper class that gives 'some data'
var points = PointList.GetRandomPoints(
new Geopoint(area.NorthwestCorner), new Geopoint(area.SoutheastCorner),
50);
foreach (var dataObject in points)
{
var shape = new MapIcon
{
Title = dataObject.Name,
Location = new Geopoint(dataObject.Points.First()),
NormalizedAnchorPoint = anchorPoint,
Image = image,
CollisionBehaviorDesired =
onCollisionShow? MapElementCollisionBehavior.RemainVisible :
MapElementCollisionBehavior.Hide,
ZIndex = 3,
};
MyMap.MapElements.Add(shape);
}
}
catch (Exception)
{
var dialog = new MessageDialog("GetViewArea error");
await dialog.ShowAsync();
}
}
This may look a bit intimidating, but only the middle part is where the actual drawing takes place in the center of the code - the creation of the MapIcon object. That is distinctly different from the other two shapes:
- A MapIcon has a label, that is set in the Title property. This is optional.
- It also has an anchor point, this describes where the image appears on the map relative to the location. 0,0 is left top, 1,1 is right bottom. So if you set it to (0.5, 0.5) the center of the image will fall exactly op top of the coordinate in Location. And of course you can use everything in between should you desire so.
Tip: make your icons square. If they cannot be, then make the image square and make the superfluous part translucent. I have found non-square icons playing havoc with the anchor point.
- It can be displayed using a custom image. This is also optional, if you don't set this you will get a default blue-ish text balloon like image.
- The CollisionBehaviorDesired property determines whether the MapIcon drawing should use the 'best effort' drawing method of old (Hide), or the new one (RemainVisible).The difference is controlled by this switch on the UI all the way at the bottom
It is default set to hide (just like the default value on the icon), but tor all practical purposes I think it should always be set to RemainVisible. Note - the effect of changing this setting may not be visible immediately, but it will be once you start to zoom in or out.
The method draws 50 icons per click on the button within the current view of the map, which is calculated using the extension method GetViewArea. This is not a standard map method - its actually quite an old extension method I wrote some time ago, and in the new world of Windows 10, it can fail in certain circumstances, hence the try-catch
Deleting
The first two drawing methods toggle shapes, and also feature a call to a DeleteShapesFromLevel. Deleting shapes is a easy as removing them from the MapElements collection :)
private bool DeleteShapesFromLevel(int zIndex)
{
// Delete shapes by z-index
var shapesOnLevel = MyMap.MapElements.Where(p => p.ZIndex == zIndex);
if (shapesOnLevel.Any())
{
foreach (var shape in shapesOnLevel.ToList())
{
MyMap.MapElements.Remove(shape);
}
return true;
}
return false;
}
The only 'fancy' thing here is that I select shapes by z-index, which I keep calling 'level - having worked in GIS for 23 years tends to stick.
Summary and concluding remarks
- Drawing lines, polygons and icons in Universal Windows Apps look remarkably like drawing the same things in Windows Phone 8.1
- There are still two types to deal with when handling coordinates - BasicGeoposition and Geopoint, and you will just have to see by Intellisense when you need the one, and when you need the other.
- There is no such thing as levels or layers as in the Windows 8.x Bing Maps control - there is only Z-index. As shape with a higher Z-index appears on top of a shape with a lower Z-index. In this maps control Z-indexes are properly honored now - at least for MapIcons there where some issues in Windows Phone 8.1
- MapIcons are now a fully viable way of drawing icons that give a very good performance, now the developer has a way of controlling the way the are displayed).
Once again I refer to the sample app to see all the concepts described above in action.