Mäp

Web and Android app showing routes from GPX files on a map.

Map

Where?

Web app runs at https://map.milanlaslop.dev. Android app downloadable from that web.

Used Technologies

  • ReactJS
  • Kotlin, Anko library

Implementation Details and Challenges

Map

The Pigeon Maps library is used for displaying and manipulating with the map. The library provides a map component with all common features, which works out-of-the-box. Moreover it supports creating any custom overlays - that’s how I could add the routes (lines) to the map.

Web + Android

The web and Android variants run exactly the same JavaScript application. Android application contains a WebView and communicates with the JavaScript application using JavaScript calls (javascript: URL) and JavascriptInterface.

The Android application additionally contains the possibility to download GPX files from a Strava account. This is done by an automated browsing of the Strava web site in a hidden WebView.

Accessing Files

The Android application can load GPX files from the filesystem (storage), without even requiring READ_EXTERNAL_STORAGE permission. This can be done by opening the file chooser provided by the system (Intent.ACTION_OPEN_DOCUMENT_TREE) and reading the result using DocumentsContract. After the user chooses some directory in the provided file chooser, the application automatically obtains the rights to read the corresponding files.

Performance

Performance was the most challenging aspect. I especially dealt with:

Performance of the XML (GPX) Parsing

After discovering that files loading takes long and finding out that the XML parsing is the most time-consuming part, I tried to find a faster replacement of the JavaScript’s DOMParser.

Instead of DOM parsers, I focused on event-emitting (SAX-like) parsers - which I assumed would be faster and less memory-consuming. I tried sax and Saxophone. Saxophone turned out to be faster (yes, their Benchmark tells the same).

I/O Performance of Files Loading

The web application needed to access file data provided by the Android application. At first, I did it the simplest way - through JavascriptInterface. It turned out to be painfully slow because:

  • the call is synchronous - JavaScript code waits for the response (the web application of course completely freezes until the Android code responds)
  • there is some additional time spent by the processing of the call itself, containing some data conversions

After some experimenting, I discovered another way how to provide files - through shouldInterceptRequest (the web application would basically access some URL, and Android code would respond with a stream).

This way it became a proper asynchronous operation in JavaScript. And, additionally, another performance gain could be obtained by doing the necessary file parsing in JavaScript while the next file was being downloaded, conveniently achieving something like a multi-threaded code (without even creating any threads explicitly).

Map Overlays (Lines)

The lines for the routes are SVG images in the HTML DOM, containing a lot of point specifications. When moving the map, only one thing changes in the DOM - SVG transformation specifying the translation (position) of the image. The browsers seem to understand this and do not try to re-draw or rebuild something unnecessarily. Of course some work had to be done around correct usages of React to achieve an optimal behavior (correct decomposition to components, correct design of props and state for React to not re-check or re-render the components unnecessarily).

Zooming is a more complex operation - images are re-built because:

  • different zoom levels require different detail (precision and count of the line segments)
  • the projection (from latitude+longitude to the pixels on the 2D map) has to be re-calculated

Display Shape

The Android app supports switching to full-screen mode. Thus, the UI layout was designed in a way that even displays with notch or round edges can accommodate the UI (at least on some devices).

Map