I wanted a real-time map generator to visualize regional property price changes based on chosen time interval. I didn't want to resort to pre-generated tiles, because this would prevent user-customized output and limit configuration options. To get the best performance, I implemented a FastCGI process in C++ with a RESTful interface to generate the required tiles in parallel. The resulting program can generate a customized 1280 × 720 resolution JPG in 30 milliseconds and equivalent PNG in 60 milliseconds.
Heat maps are great way to visualize scalar fields (such as surface temperature, population density or property prices). Unfortunately online maps are usually based on pre-generated tiles, and thus don't allow more user-customized functionality. An other interesting approach is to first load pre-generated map tiles and then use HTML5 and JavaScript to draw a semi-transparent color layer on top. This might be the technically most advanced approach, but it relies heavily on client-side calculations and it might not be well supported in older browsers or inside mobile applications. It might also force the sending sensitive or proprietary information to the browser, which would be a great interest of competitors.
To avoid these issues, I wrote all generation logic on the server side in C++, and it is accessible via HTTP by using Nginx (or any other HTTP server) and FastCGI communication with the image generation process. The technique described here applies to various scenarios, but when I first wrote this I wanted to visualize proprietary regional property price indexes which I developed for PropertyGuru.com.sg. I also included a slider to choose the time period for which the quarterly price index change is calculated. The proof-of-consept UI and the map output can be seen in Figure 1. Green colors indicate a price index change of ±2%, red colors are up-to +15% and blue colors are for -15%.
In this case regional price indexes were calculated at 330 meter intervals forming a hexagonal grid, which totals in approximately 2000 pre-calculated indexes. When the process is spawned it first reads in this price index data, together with the background map file, mask for land/water classification and a configuration file to convert pixel coordinates into latitude and longitude and back. To optimize the code for map generation speed, each pixel's location is pre-processed to check if it is in water. If it is not, then its nearest three price indexes are determined and stored for future use. The final output is a weighted average of the nearest price indexes, the weight being roughly inversely proportional to the squared distance from the pixel. These weights are stored along side with pointers to nearest price indexes.
In addition to price index data, also the underlying map and the water mask is pre-loaded in memory and split into 2 × 4 sub-images. These are later used to generate and store eight tiles in parallel. At the default resolution of 1280 × 720 each sub-image has the resolution of 320 × 360 pixels.
Each request to the API includes the start and end date of the time period for which price index changes are calculated. Price indexes are traversed in eight parallel threads, and the price index's change between the two points in time are stored within its own class member. This avoids the price index change being calculated for each pixel separately, and instead it can be shared by any number of pixels.
After price index changes are computed, each sub-image is rendered in parallel. Within each sub-image each pixel is traversed, and if it contains pointers to nearest price indexes their weighted average value is calculated. This value is converted to RGB color from YCbCr color space and alpha-blended to the underlying map image. The value interpolation is very important to avoid blocking between cell boundaries, as can be seen from Figure 2.
When sub-images are ready, they need to be compressed into a standard image format such as JPEG or PNG. Since alpha-blending has already been done, there is no problem using the JPEG format to produce smaller image files. It is also a lot faster format to compress to, but it will introduce compression artifacts. PNG on the other hand is loss-less and supports the alpha channel, so it has its benefits. Also the compressed images could be temporarily be stored in-memory and discarded soon after, or stored into a more permanent cache folder in the file system. To keep my code simple I only implemented the file system based solution, and then I can also let Nginx to handle this static content.
When the browser makes the GET request to indicate which time span's images it wishes to receive, all the steps above are executed. Once this is done, the browser receives a JSON response which lists URLs of the eight generated sub-images. Then it only needs to iterate through this list and update the URLs of the UI image tile elements. The whole image generation process for 1280 × 720 output takes only 60 milliseconds in PNG format and 30 milliseconds in JPEG format, so clearly the real-time constrains are well met. These timings are from an Ubuntu running inside a virtual machine with six available cores. It seems that the only bottleneck is the network over which the images are transferred.
Related blog posts:
|
|
|
|
|
|
|
|
|