Recently, I rewatched one of my all time favorite movies, Pirates of the Caribbean: The Curse of the Black Pearl. In Pirates of the Caribbean, the one of the main characters, Captain Jack Sparrow, is in possession of a compass that doesn't point north. Although it is the butt of a couple of jokes throughout the franchise, what the compass actually does is far more useful than just pointing north. Sparrow's compass always points instead to what its wielder desires the most. Naturally, this got the gears in my head turning, and being a programmer, I decided that I could program my own magic compass. So I did.
The functionality of the my compass will require three components: the current location of the user, the location of the target, and the bearing between the two. To handle getting the user's location, we use two Flutter packages, Geolocator and Flutter_Compass, in order to obtain GPS and compass data from the user's phone. This gives us both the current position of the user and the direction that they are currently facing, which we are able to use to calculate a heading toward the selected target for our compass. Speaking of...
We are creating a compass that always points toward the nearest liquor store. Entirely because it sounded funny. But in order to do that, we need to find the nearest liquor store. This means that we'll need a mapping API of some sort. There are a bunch of options for this at varying price points. Originally, I created this app utilizing the OpenStreetMap API, as it is the zero dollar-est option. Unfortunately, although my area is mapped out quite thoroughly on OpenStreetMap, business information is severely lacking. This meant that it would always report that the nearest liquor store was over ten miles from me, despite the fact that there is one just down the road.
Because of this, I had to switch to the significantly less free Google Places API. Not a huge deal, let's just hope this app doesn't go viral. If you're curious, the API call for getting all the liquor stores in a given radius looks like this:
It's a pretty generic call. The Places API returns results by pages, not individually, with a maximum of 20 per page, which is why we set the maxResultCount to 20. As the Places API charges by the page, setting this lower than 20 has no real advantage.
With the list of nearby stores from the API, we next need to find the closest one. The Places API returns results sorted by "relevant", not distance, so it's not as simple as taking the first one in the list, but since the Geolocator package has a function to calculate the distance between two sets of coordinates, it's still super straightforward.
With the closest store found, all that is left is to get the compass heading toward the store. To accomplish this, Geolocator has a function that returns a compass bearing between two points, and we can subtract that value from the user's current heading to find the corrected heading that our compass should point towards.
As alluded to earlier, constantly querying the Google Places API would without a doubt cause my credit card provider to put a bounty on my head. To solve this, we want to call out to the API as infrequently as possible, and when we do make a call we want to cache the results. The designed use case for this app is the late night adventure with your friends to restock the party, so for cache invalidation, we will use a combination of a location radius and a rate limit. Assuming we have stores in the cache, we only call out to the API if the user has moved out of a given radius from the last call, and the rate limit has not been exceeded.
In order to achieve full functionality, all that is needed now is a compass UI. Faisal Nasir Al-Ghaithi on Medium made an excellent guide for how to make a Material UI compass app in Flutter, which I stole followed and tweaked in order to design a compass that I was happy with.
This was a super fun project, and while making it I actually wound up learning a lot about how navigational and GPS apps work, which I found super interesting. Also, I made the app start playing Sea Shanties when it is opened, because it felt like the right thing to do.