Monday, May 5, 2014

Delaying Queries to a Server from Map Move Events



Last week I added BikeShare info to my map. But I also take the bus a lot. I would like the information I need to choose between taking a bus and grabbing a bike. So I need to show buses on the map. In my first iteration of showing bus data, I downloaded the XML for every bus stop in the metro system and loaded that. Unfortunately, this did not work. 

There are over 11,000 bus stops in the DC Metro System and using that may POI in a MapItemView made the map unusably slow. Even if it wasn't slow, when zoomed out on the map, the POI covered the map so it was just a huge blob of POI. Oh well, looks like I need to do something slightly more elegant. 

What I need to do is query the server every time the user moves the map, through a pan or a zoom and ask the web service for the XML for the bus stops centered on the center of the map, and within a certain radius that is slightly bigger than the map. However, if I do that for each and eveyr pan and zoom, I end up hammering the server when ever the user swipes. The web service doesn't like to be called so much so fast, and it's just a waste of resources and bandwidth. So I decided to add a little logic to only load the data after the map is "at rest" for some period of time. I chose to start with a two second rest, but it could be whatever. I did this by first creating a timer.

       Timer
       {
         id: mapRestTimer  
         running: false  
         interval: 2000  
       
       }  

Then I wrote add some event handlers to the Map, for zoom and pan events.
       onZoomLevelChanged:  
       {  
         if(zoomLevel >= minimumBusDataZoomLevel)  
         {  
           timeMapRest()  
         }  
       }  
       onCenterChanged:  
       {  
         if(zoomLevel >= minimumBusDataZoomLevel)  
         {  
           timeMapRest()  
         }  
       }  
Note that I set it up so that bus stops aren't displayed if the user is panned out too far due to the problems I mentioned above. The timeMapRest function just decides whether it needs to restart the timer, or to start it. If the timer is already running, then the user has moved the map before the rest period was up, so we'll reset the timer. Otherwise, we'll go ahead and start the timer.
   function timeMapRest()  
       {  
         if(mapRestTimer.running)  
         {  
           mapRestTimer.restart()  
         }  
         else  
         {  
           mapRestTimer.start()  
         }  
       }  

To finish up, I just had to write an onTriggered handler and set properties on my subclassed XmlListModel, which then goes ahead an updates itself based on the new latlong or zoom level.
         id: mapRestTimer  
         running: false  
         interval: 2000  
         onTriggered:  
         {  
           //stop the timer and set the new latlong  
           running: false  

           //get the info necessary to calculate the correct radius
           busStopListModel.latlong = [getThereMap.center.latitude,getThereMap.center.longitude]  
           var tr = getThereMap.toCoordinate(Qt.point(0,0))  
           var bl = getThereMap.toCoordinate(Qt.point(getThereMap.width,getThereMap.height))  
           var latdiff = bl.latitude - tr.latitude  
           var londiff = bl.longitude - tr.longitude  
           //TODO: fix this silly calculation for the radius  
           var rad = (latdiff > londiff) ? latdiff * (34.67 * 5280 / 3) : londiff * (34.67 * 5280 / 3 )  
           busStopListModel.mapRadius = rad  
         }  
       }  


The code for the whole project is here.