Monday, April 21, 2014

Adding Interactivity to a Map with Popovers

On Friday I started my app "GetThereDC". I started by adding the locations of all of the Bikeshare stations in DC to a map. Knowing where the stations are is great, but it's a bummer when you go to a station and there are no bikes, or there are no empty parking spots. Fortunately, that exact information is in the XML feed, so I just need a way to display it.  
The way I decided to do it is to make the POI (the little icons for each station on the map) clickable, and when the user clicks the POI to use the Popover feature in the Ubuntu Components toolkit to display the data.

Make the POI Clickable

When you want to make anyting "clickable" in QML, you just use a MouseArea component. Remember that each POI is constructed as a delegate in the MapItemView as an Image component. So all I have to do is add a MouseArea inside the Image and respond to the Click event. So, not my image looks like this:
           sourceItem: Image  
           {  
             id: poiImage  
             width: units.gu(2)  
             height: units.gu(2)  
             source: "images/bike_poi.png"  
             MouseArea  
             {  
               anchors.fill: parent  
               onClicked:  
               {  
                 print("The POI was clicked! ")  
               }  
             }  
           }  
This can be used anywhere in QML to make an image respond to a click. MouseArea, of course, has other useful events as well, for things like onPressed, onPressAndHold, etc...

Add the Roles to the XmlListModel

I already know that I'll want something to use for a title for each station, the address, as well as the number of bikes and the number of parking slots. Looking at the XML I can see that the "name" property is the address, so that's a bonus. Additionally, I can see the other properties I want are called "nbBikes" and "nbEmptyDocks". So, all I do is add those three new roles to the XmlListModel that I constructed before:
   XmlListModel  
   {  
     id: bikeStationModel  
     source: "https://www.capitalbikeshare.com/data/stations/bikeStations.xml"  
     query: "/stations/station"  
     XmlRole { name: "lat"; query: "lat/string()"; isKey: true }  
     XmlRole { name: "lng"; query: "long/string()"; isKey: true }  
     XmlRole {name: "name"; query: "name/string()"; isKey: true}  
     XmlRole {name: "available"; query: "nbBikes/string()"; isKey: true}  
     XmlRole {name: "freeSlots"; query: "nbEmptyDocks/string()"; isKey: true}  
   }  

Make a Popover Component

The Ubuntu SDK offers some options for displaying additional information. In old school applications these might be dialog boxes, or message boxes. For the purposes of this app, Popover looks like the best bet. I suspect that over time the popover code might get a little complex, so I don't want it to be too deeply nested inside the MapItemView, as the code will become unwieldy. So, instead I decided to add a file called BikeShareStationPopover.qml to the components sub-directory. Then I copy and pasted the sample code in the documentation to get started. 

To make a popover, you start with a Component tag, and then add a popover tag inside that. Then, you can put pretty much whatever you want into that Popover. I am going to go with a Column and use ListItem components because I think it will look nice, and it's the easiest way to get started. Since I already added the XmlRoles I'll just use those roles in the construction of each popover. 

Since I know that I will be adding other kinds of POI, I decided to add a Capital Bike Share logo to the top of the list so users will know what kind of POI they clicked. I also added a close button just to be certain that users don't get confused about how to go back to the map. So, at the end of they day, I just have a column with ListItems:
 import QtQuick 2.0  
 import Ubuntu.Components 0.1  
 import Ubuntu.Components.ListItems 0.1 as ListItem  
 import Ubuntu.Components.Popups 0.1  
 Component  
 {  
   id: popoverComponent  
   Popover  
   {  
     id: popover  
     Column  
     {  
       id: containerLayout  
       anchors  
       {  
         left: parent.left  
         top: parent.top  
         right: parent.right  
       }  
       ListItem.SingleControl  
       {  
         control: Image  
         {  
           source: "../images/CapitalBikeshare_Logo.jpg"  
           height: units.gu(5)  
           width: units.gu(5)  
         }  
       }  
       ListItem.Header { text: name}  
       ListItem.Standard { text: available + " bikes available" }  
       ListItem.Standard { text: freeSlots + " parking spots available"}  
       ListItem.SingleControl  
       {  
         highlightWhenPressed: false  
         control: Button  
         {  
           text: "Close"  
           onClicked: PopupUtils.close(popover)  
         }  
     }  
   }  
 }  

Make the Popover Component Appear on Click

So, now that I made the component code, I just need to add it to the MapItemView and make it appear on click. So, I add the tag and give it an id to the MapQuickItem Delegate, and change the onClicked handler for the MouseArea to open the popover:
 delegate: MapQuickItem  
         {  
           id: poiItem  
           coordinate: QtPositioning.coordinate(lat,lng)  
           anchorPoint.x: poiImage.width * 0.5  
           anchorPoint.y: poiImage.height  
           z: 9  
           sourceItem: Image  
           {  
             id: poiImage  
             width: units.gu(2)  
             height: units.gu(2)  
             source: "images/bike_poi.png"  
             MouseArea  
             {  
               anchors.fill: parent  
               onClicked:  
               {  
                 PopupUtils.open(bikeSharePopover)  
               }  
             }  
           }  
           BikeShareStationPopover  
           {  
             id: bikeSharePopover  
           }  
         }  
And when I run the app, I can click on any POI and see the info I want! Easy!

Code is here

Friday, April 18, 2014

It's Easy and Fun to Write Map Based Apps

Yesterday I started work on an app that I personally want to use. I don't have a car, so I use services like Metro Bus, Metro Rail, Car2Go, and BikeShare around DC all the time. It's annoying to go to each different web page or app to get the information that I want, so I decided to write an app that combines it all for me in one place.

After asking around, I settled on a best practice for Ubuntu map apps, and I was pointed to this excellent code as a basis:
http://bazaar.launchpad.net/~yohanboniface/osmtouch/trunk/view/head:/OSMTouch.qml

It was so easy and fun once I got started, that I decided to show the world. So, here we go.

I started with a "Simple UI" project. Then I deleted the default column that it started with, and I set the title of the Page to an empty string. While I was at it, I changed the height and width to be more like a phone's dimensions to make testing a little easier. So my starter code for an emply Window looks like this:
 import QtQuick 2.0  
 import Ubuntu.Components 0.1  
 import "components"  
 MainView {  
   objectName: "mainView"  
   applicationName: "com.ubuntu.developer.rick-rickspencer3.MapExample"  
   width: units.gu(40)  
   height: units.gu(60)  
   Page  
   {  
     title: i18n.tr("")  
   }  
 }  
So what's missing now is a map. First I need to import the parts of Qt where I get location and map information, so I add these imports:
 import QtPositioning 5.2  
 import QtLocation 5.0  
Then I can use the Map tag to add a Map to the MainView. I do four things in the Map to make it show up. First, I tell it to fill it's parent (normal for any component). Then I set it's center property. I choose to do this using a coordinate. Note that you can't make a coordinate in a declarative way, you have to construct it like below. The center property tells the map the latitude and longitude to be centered on. Then I choose the zoom level, which determines the scale of the map. Finally, I need to specify the plug in. For various reasons, I choose to use the Open Street Maps plugin, though feel free to experiment with others. So, a basic funcitonal map looks like this:
   Page  
   {  
     title: i18n.tr("")  
     Map  
     {  
       anchors.fill: parent  
       center: QtPositioning.coordinate(38.87, -77.045)  
       zoomLevel: 13  
       plugin: Plugin { name: "osm"}  
     }  
    }  
When I run it, I get a lot of functionality for free. On the desktop I can drag the map, and when I run the app on my phone or tablet, I can pinch to zoom in or out. All that functionality comes for free. Of course, you are free to add mapping controls as desired, but I find that map works well out of the box, at least on a device that supports pinch and zoom.


Typically, a map displays little pinpoints. These are often referred to as Points of Interest, or more typically "POI". It's delightfully easy to populate your map with POI using our old friend XmlListModel. First, you will need some XML that has location information. For this exmaple, I am going to use the Bike Share feed for Washington, DC. It's easy to get and to parse, so it makes a nice example. You can see the feed here:
https://www.capitalbikeshare.com/data/stations/bikeStations.xml

So let's use it to set up our XmlListModel. First, of course, we need to import the XmlListModel functionality.


 import QtQuick.XmlListModel 2.0  
Next, we'll make the list model, and use the query and Roles functionality to set up the model with the latitude and longitude of each POI inside the model. This is *exactly* like using the XmlListModel for a typical list view. Very cool.
   XmlListModel  
   {  
     id: bikeStationModel  
     source: "https://www.capitalbikeshare.com/data/stations/bikeStations.xml"  
     query: "/stations/station"  
     XmlRole { name: "lat"; query: "lat/string()"; isKey: true }  
     XmlRole { name: "lng"; query: "long/string()"; isKey: true }  
   }  
Now that I have my list model set up, it's time to display them on the Map. We don't do that with a ListView, but rather wtih a MapItemView. This works exactly the same as a ListView, except it displays items on a map instead of in a list. Just like a ListView I need a delegate that will translate use data from the each item in the XmlListModel to create a UI element. In this case, it's a MapQuickItem instead of a ListItem (or similar). A MapQuickItem needs to know 4 things.

  1. The model where it will get the data. In this case, it's my XmlListModel, but it could be a javascript list or other model as well.
  2. A latitude and longitude for the POI, which I set up as roles in the XmlListModel.
  3. An offset for whatever I am using for POI so that it is positioned properly. In this case I have made a little pushpin image out of the bikeshare logo (I know it's bad I'll make a better one later :) ). The offset is set by anchorPoint, so I make the anchorPoint the bottom and center of of the pushpin. 
  4. Something to use for the POI. In this case, I choose to use an image. Note that it is important to use grid units, or the POI may appear too small on some devices, and too large on others. Grid Units make them "just right" on all devices, and ensure that users can click them on any device. 


So, here is my MapItemView that goes *inside* the Map tag. It's a MapItemView for the map, after all.

       MapItemView  
       {  
         model: bikeStationModel  
         delegate: MapQuickItem  
         {  
          id: poiItem  
          coordinate: QtPositioning.coordinate(lat,lng)  
          anchorPoint.x: poiImage.width * 0.5  
          anchorPoint.y: poiImage.height  
          sourceItem: Image  
          {  
            id: poiImage  
            width: units.gu(2)  
            height: units.gu(2)  
            source: "bike_poi.png"  
          }  
         }  
       }  
Now when I run the app, the POI are displayed. As you would expect, when the user moves the Map, the MapItemView automatically displays the correct POI. It's really that easy.


If you want to add interactivity, that's easy, you can simply add a MouseArea to the Image and then use things like Ubuntu.Components.Popups to popup additional information about the POI. 


This sample code is here: http://bazaar.launchpad.net/~rick-rickspencer3/+junk/MapExample/view/head:/MapExample.qml