High performant real-time tracking on web using Google Map
Ability to track an entity on Google Map in real time is one of the most important features of ride-hailing and delivery centric applications. So, while building for Ola PWA, we knew, track-ride page is going to be one of the most used screens.
Like any other live tracking page, Ola PWA track-ride page also enables its users to track exact location of the cars on a specific route in real time. But what was challenging, was not only to match up to the native experience on web, but to better it on a low-end mobile device.
As they say, “Rome wasn’t built in a day”. We also didn’t achieve the desired result in first try. In fact, it took us multiple iterations and user surveys and brainstorming sessions with product team to slowly build towards the ultimate intended experience. In the process, we did a lot of R&D, made quite a few POCs, tried different tricks & techniques & algorithms and more over, learnt a lot.
Car Data from a polling API call
Before we dive deeper into different development phases of our track-ride screen, let us be clear on what needs to be tracked on the map and how to get that data.
So, we had to move a car image on Google map from one coordinate to another in every 5 secs (configurable). We might also need to rotate the image to certain degrees every time. We need an API which we need to poll from front-end to get all the data. A sample response of this API could be in the following format.
We used Google Map’s V3 apis for all our map related operations.
All the gifs below represent a simulation of the actual animations run on a Mac book pro with 6x slow CPU setting (using Chrome Developer toolbar). There could be data loss while creating the gifs and the actual animations could be smoother. All the FPS reports truly depict the real performance of the animations.
Phase 0 : Finding an existing example
Before we started coding for this page, we wanted to check if we could find an existing page with similar functionality. We found one.
In this page, the map was getting loaded with the new lat-lng as it’s centre every 5 sec. The car is a Marker on the map placed at the centre . Every 5 sec the map gets panned to a new centre based on the API data and a code runs to place the marker again at the centre, giving an impression that the car is moving (while the reality is that the map pans and the car remains at the centre). But there is a time lag between the old marker getting removed and a new marker getting rendered at the centre, causing a shaky experience.
FPS was really poor (20 FPS) as we expected.
Also if you have noticed, new map tiles are getting loaded on every 5 sec as the map pans. Not much bandwidth friendly I guess.
It was good enough for a start as we got to know the exact issues we needed to solve to give a better experience.
- smoother animation
- Save data and battery
Phase 1 : We started.
Car as a DOM element
We knew we could smoothly animate the car by using hardware accelerated CSS3 animations if we could convert the car marker into a DOM element as Google Marker object does not support CSS3 animations. So, first we converted the car into an HTML img object. Now we needed to move the car from one lat-lng to the next. But DOM elements operate on pixels and not geo coordinates. So we needed a projection library to convert the lat-lng into corresponding pixels.
We started with one open source Mercator Projection library. But finally we used
getProjection() method of Google Map’s Overlay class (V3 apis) to get projection Object and then implemented the
onAdd() method and accessed the projection inside
onAdd(). Then used
getLatLngToContainerPixel()method of the projection object to get the pixels for a given lat-lng. Sample code as follows.
For a smoother effect we used bezier as CSS3 transition-timing function for the transform animation and played around with it’s configuration. Sample code as follows.
Now that we solved the smooth animation problem, we shifted our focus to address the data saving part. We realised that, in a standard zoom level (like 14 or 15), user can clearly track his moving car for around 1 km area without any necessity to pan the map.
We used this finding and wrote our own algorithm to pan the map to the new coordinates only when the car has reached closer to any of the four borders of the map container and then centre the car image. We also initialised the map with zoom level 14.
With this change, for every 1 km tracking, we brought down the required panning to only once, in comparison to 24 times in the earlier approach saving more data and battery. (assuming an average car speed of 30km/hr and polling interval of 5 sec)
We still had a problem to solve, specially on mobile devices. “What happens to the car when user pans or pinch-zooms the map?” Had the car image been a Google marker, it would have moved seamlessly along with the panning or zooming map. But remember ? Our car image was a DOM element running on a different layer. We needed to catch every map events and accordingly re-render our car to new positions. For events like pan and pinch zoom (which consists of a series of drag, drag-start, drag-end, mouse-move events) the experience won’t be seamless at all with so many repaints. Also there could be a lag between users’ touches and the re-positioning of the car on a low end device.
So, we took a shortcut. We disabled the default zoom and pan features of the google map. We created our own HTML zoom plus-minus buttons within the parent div to allow users to change the zoom level by clicking on them. We only re-rendered the car image when there is a user initiated zoom level change. Not very convenient but still neat.
No panning was allowed but we thought user could probably live with this.
You can see the final experience of the moving car in Phase1 here.
Here is the animation report run on Mac book Pro with 6x slowdown CPU.
Phase 2 : Some quick fix.
Default panning and zoom enabled
We found out that our approach on Phase1 was not quite user friendly after-all and most of our users were more inclined towards panning and zooming the map to adjust to their viewport. Very few people were actually clicking on the Custom Zoom buttons. So we re-enabled the default panning and pinch-zooming functionality on Google Map.
Hide car on pan and zoom
But then we had to solve for the problem for which we had to disable them. That is to seamlessly sync DOM car element with default map events. We started exploring all google map events and listed out the ones that occur while panning or zooming (like drag, dragstart, dragend, zoom_changed, center_changed, mousemove etc) and hid the HTML car on those events. We made it visible again on ‘idle’ event (when map is stable again). You can check all the Map events here.
Not perfect. But still better than the Phase1. You can see the final zoom/pan experience of Phase2 here.
Phase 3 : R&D. More R&D.
We knew we were NOT yet there and we still needed a solution that addresses both smooth animation of car and panning or zooming of the map simultaneously. We started doing different POCs.
Re-rendering the car on panning & zooming
We tried to re-position the HTML car on every panning and zooming events. While we thought we might be getting shaky effect, to our surprise, the entire experience was really seamless on desktop. But our “hearts broke” when we found a non-documented behaviour of Google Map because of which our solution became ineffective. We could not reposition the car while panning as the map’s centre was not getting changed while panning. You can read about the issue here.
Back to Car as a Marker.
We took a step back and started exploring the marker object once more, with a hope that we could get a way to rotate and animate the marker smoothly on canvas. With marker we didn’t have to reposition it on different events as markers are part of the map itself. We wrote our own code to move the marker on an imaginary line between 2 sets of coordinates. We played with different values of the interval time duration and the no. of divisions within the imaginary line. We could achieve a satisfactory experience on desktop. But on a medium-end mobile, we could clearly see the lag in animation.
Animation report with markers.
Car as a Symbol Object ? Eureka !!!!
We had only two options left.
- Mimic the exact pan and zoom gestures on a transparent div on top of the Map and move the car and the map beneath accordingly. But it was lot of coding and it could be really difficult to match the default elastic effect that you get while panning.
- Explore map overlays again.
We took the 2nd approach and eureka!!! We found Symbol object of map which can smoothly animate on a polyline connecting two coordinates and at the same time can rotate & move seamlessly while panning and zooming. We still needed the projection library to detect when to load new map in order to minimise map tile loading. We tested our POC on a low end mobile and it worked like magic.
“Symbol animations on polyline are much smoother as they are part of the polyline itself.”.
For smooth animations, we created an invisible polyline, with end point as the new position of the car and start point as the current point of the car (current position of the symbol in the map).
For an invisible polyline, the stroke opacity and stroke weight were set to a minimum.
Now our, icon moves on the polyline, using offset(from 0 to 100%) and rotation from car’s current rotation to final rotation value.
Example code for animating symbol on a line can be found here.
There was still one small glitch. Unlike markers, Symbol object does not take an icon url. It has to be a vector with single “path” attribute. But the car image that needed to be shown was moderately complex with shadows and different shades.
You can see the Symbol experience here.
Phase 4 : Finally !!!!
R&D with design team. Multiple Symbols for one car.
We started doing R&D along with the design team to try out different vectors and found out that creating a multi layered vector image with single path is extremely difficult. So we came up with a solution to move and rotate multiple smaller single path vector images together on the polyline. We placed them in layers with same anchor so that for end-users they looked like one image only.
One more limitation solved by RequestAnimationFrame
The animations were smooth but then we ran into the setInterval DOM problem.
Modern browsers like chrome tend to suppress DOM animations when the tab in inactive. So when, the user comes back to the screen, the car would have travelled a good distance, and all the DOM animations which were getting queued till now, would start to execute, causing the car to “fly” from source to destination.
Comes to our rescue, requestAnimationFrame.
This API is present in all the major browsers and it eliminates the possibility of unnecessary draws and can bunch together multiple animations into a single reflow and repaint cycle, thus helping us achieve 60fps animations.
In our case, the method to animate the car is passed as a parameter to window.requestAnimationFrame. For every new position of the car, we cancel the previous request (using cancelAnimationFrame) if there is one pending. Thus, when the tab becomes active again, the latest request for animation gets executed which is between the car’s location(when the tab went inactive) to the newest location. Also, since our algorithm detects that the distance to travel is big(and unreal), it just place the car at the centre of the map without any animation (no flying around ! ).
Sample code below showing use of requestAnimationFrame.
moveCab determines if the animation needs to be performed or not
As mentioned, for each new position, we clear previous intervals and animation frame request,
You can see the final experience here.
The animation was also very smooth (57FPS).
So that was it. Work is still in progress. There are so many things that we would love to try. Like exploring other Google Map components, optimising the polling API, new animation algorithms etc. Will keep this thread alive with all the updates !!!
Meanwhile you can book an Ola cab @ https://book.olacabs.com and check the post-booking page to see the above code in production.
Ratul Roy. Principal Engineer, Ola
Ravi Patwal. Software Engineer II, Ola