How I create the daily GPS map
How I create the daily GPS map
November 10, 2025
Steven Watson Buehler
stevenbuehler.us
Introduction
Introduction
Generating each day’s GPS map (or the same for any period) is a combination of compiling the GPS data from my iPhone using an app by Aaron Parecki called Overland and then using Wolfram Language to process that data into the daily map.
You’ll want to study up a bit on the following applications, which I use in this workflow. The nitty-gritty info on these items are outside the scope of this article:
◼
Wolfram Language, either using the full Wolfram App, Wolfram|ONE, or the free-for-noncommercial-use Wolfram Engine (this article works with the Engine and doesn’t require the UI provided by the full suite)
◼
Apache CouchDB—https://couchdb.apache.org
◼
The Instagram API for Professional/Creator accounts—https://developers.facebook.com/products/instagram/
◼
Tailscale, if you want to keep it all behind a VPN without exposing your CouchDB or other database ports to the world—https://tailscale.com
Using Overland
Using Overland
Overland generates JSON packets that look similar to the following (depending on how you set it up). I’ve changed the coordinates value to 0,0 for this purpose, which is a packet with a pair of locations (you can send up to 1,000 location fixes in a packet). Overland is set up on my iPhone 17 Pro Max to capture my location once per second and then to send a packet to my database every five minutes with what it has collected. If for some reason the URL is offline/down, no worries—Overland will hang on to the data until it successfully transmits (by default, it expects a JSON response of { “result”: “OK” } from your server, but there is also a setting to accept any 2xx response as a successful transmission, which allows it to work with CouchDB).
Keep in mind, this is in GeoJSON. Coordinates are listed in x, y order (meaning longitude is shown first, then latitude; that’s the reverse of what one may be used to everywhere else (including the use of GeoPosition on the Wolfram side) and can easily confuse).
"locations": [ { "type": "Feature", "geometry": { "type": "Point", "coordinates": [ -00.00000, 00.00000 ] }, "properties": { "speed": -1, "battery_state": "unplugged", "motion": [ "stationary" ], "timestamp": "2025-11-04T02:41:24Z", "battery_level": 1, "speed_accuracy": -1, "horizontal_accuracy": 6, "vertical_accuracy": 3, "wifi": "SPACEBALLS THE WI-FI", "pauses": false, "unique_id": "7D23E0EE-A5DC-43C2-889C-B9997AD8A647", "locations_in_payload": 1, "course": -1, "activity": "automotive_navigation", "desired_accuracy": -1, "altitude": 43, "course_accuracy": -1, "tracking_mode": 1 } }, { "type": "Feature", "geometry": { "type": "Point", "coordinates": [ -0.0, 0.0 ] }, "properties": { "speed": -1, "battery_state": "unplugged", "motion": [ "stationary" ], "timestamp": "2025-11-04T02:41:42Z", "battery_level": 1, "speed_accuracy": -1, "horizontal_accuracy": 9, "vertical_accuracy": 3, "wifi": "SPACEBALLS THE WI-FI", "pauses": false, "unique_id": "7D23E0EE-A5DC-43C2-889C-B9997AD8A647", "locations_in_payload": 1, "course": -1, "activity": "automotive_navigation", "desired_accuracy": -1, "altitude": 43, "course_accuracy": -1, "tracking_mode": 1 } } ]
Receiving the data: CouchDB
Receiving the data: CouchDB
Overland sends data packets to an HTTP(S) address of your choosing; and on that end you’ll need some way to process the JSON that’s being sent. You can write up a script to process it into the database of your choice like MySQL or MariaDB if you want, but for simplicity’s sake I just set up Apache CouchDB (which can receive the JSON directly) on a Raspberry Pi.
To keep things private, I use TailScale’s Wireguard-based VPN on all my devices, and I don’t have to leave CouchDB’s port 5984 (which isn’t secured) or 6984 (which is, but CouchDB’s way of doing accounts is still a security hole) open to the world.
With the data in CouchDB, I can either let Wolfram Language pull it as-is (it is GeoJSON by design), or create a design view in CouchDB to reduce it down to the bare minimum in a format that Wolfram can handle quickly:
function (doc) { doc.locations.forEach( function (location){ emit( new Date(location.properties.timestamp).valueOf(), `GeoPosition[{${location.geometry.coordinates[1]},${location.geometry.coordinates[0]}}]` ); } )}
The above view takes each record, extracts the locations therein, and then outputs simply the timestamp in UNIX millisecond format (easy enough in JavaScript) as a key value and a GeoPosition expression for the location.
1
Creating the map: Wolfram Language
Creating the map: Wolfram Language
I’m a Wolfram junkie, so I use it to create my daily map. I’ve also done this with Python (using the py-staticmaps module), but I’ve always liked the way Wolfram does maps and charting. Admittedly, Wolfram Language has a rather steep learning curve compared to Python, JavaScript, or PHP.
The first half of my script pulls the data out of CouchDB on the Raspberry Pi. Since I’m using the UNIX timestamp of each fix as the key, it’s easy enough to limit the pull using those keys to the time period I want (in this case, yesterday). Note since Wolfram does UNIX time in seconds and JavaScript in milliseconds, Wolfram’s side has to multiply its timestamp by 1000 to match.
There are some enhancements that I could (and probably should) include, such as not including any points that are within, say, a hundred meters of my apartment (or easier, since Overland includes the Wi-Fi access point in the data packet, any point while I’m using my home Wi-Fi). For such a large-range map like this one though, that’s not so much a concern.
data = Import[ HTTPRequest[ "http://raspberrypi.local:5984/overland/_design/queries/_view/wolframview", <| "Query" -> "inclusive_end" -> False, "start_key" -> UnixTime[DateObject[Yesterday, TimeZone -> "America/New_York"]]*1000, "end_key" -> UnixTime[DateObject[Today, TimeZone -> "America/New_York"]]*1000 |> ] , "RawJSON" ][["rows"]];image = Overlay[ GeoListPlot[ Table[ ToExpression[data[[x, "value"]]] , {x, Length[data]} ] , GeoBackground -> "StreetMap" , ImageSize -> {720, 1280} ], Text[ Framed[ Column[ Style["Daily GPS Trace", "Subtitle"], Style[DateString[Yesterday, "LocaleDateFull"], "Text"], Style["Data captured using Overland app on iPhone 17 Pro Max", "Text"], Style["Plot generated using Wolfram Language", "Text"] , Alignment -> Left ] , FrameMargins -> 10 , Background -> White ] ] , Alignment -> {Top, Left} ]
And here’s the resulting image from my data on November 9, 2025—I was driving back from Walt Disney World after midnight, and then I spent the afternoon out at Kennedy Space Center Visitor Complex to see the launch of Blue Origin’s New Glenn (NG-2) mission, which ultimately got scrubbed due to weather.
The second part of my Wolfram Language script saves this image to a publicly-accessible URL in the Wolfram Cloud so Instagram can grab it (the Meta apps—Instagram, Facebook, Threads—all use cURL to retrieve an image or video, so it has to be publicly accessible), and then invokes the Instagram API to pull the image into a “media container” and then publish the container. For this, you’ll need to set up an application on Meta’s developer platform and obtain an Instagram API token to put in the place of “NUNYA” (as in “nun o’ ya business”).
Also, this posts just a single image or video. A post containing multiple images/videos is called a “carousel” in the Instagram API and requires a different method (one would do a loop to submit each image/video into the carousel, and then use the IDs from each item together as an array to publish the carousel).
Lastly, the Meta APIs only accept JPEG format for images—no PNGs, GIFs, etc. If you try anything other than a JPEG or MP4 video you’ll get an error back.
If you have any questions about some of the detail, feel free to drop a line: steven@stevenbuehler.us.