{"id":20106,"date":"2026-04-07T07:52:47","date_gmt":"2026-04-07T11:52:47","guid":{"rendered":"https:\/\/nuxx.net\/blog\/?p=20106"},"modified":"2026-04-07T07:52:49","modified_gmt":"2026-04-07T11:52:49","slug":"local-heatmap-tile-server-v1","status":"publish","type":"post","link":"https:\/\/nuxx.net\/blog\/2026\/04\/07\/local-heatmap-tile-server-v1\/","title":{"rendered":"local-heatmap-tile-server v1"},"content":{"rendered":"\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/nuxx.net\/blog\/wp-content\/uploads\/2026\/04\/local-heatmap-tile-server_v1_Northern_Michigan-scaled.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"848\" src=\"https:\/\/nuxx.net\/blog\/wp-content\/uploads\/2026\/04\/local-heatmap-tile-server_v1_Northern_Michigan-1024x848.png\" alt=\"\" class=\"wp-image-20120\" srcset=\"https:\/\/nuxx.net\/blog\/wp-content\/uploads\/2026\/04\/local-heatmap-tile-server_v1_Northern_Michigan-1024x848.png 1024w, https:\/\/nuxx.net\/blog\/wp-content\/uploads\/2026\/04\/local-heatmap-tile-server_v1_Northern_Michigan-300x248.png 300w, https:\/\/nuxx.net\/blog\/wp-content\/uploads\/2026\/04\/local-heatmap-tile-server_v1_Northern_Michigan-768x636.png 768w, https:\/\/nuxx.net\/blog\/wp-content\/uploads\/2026\/04\/local-heatmap-tile-server_v1_Northern_Michigan-1536x1272.png 1536w, https:\/\/nuxx.net\/blog\/wp-content\/uploads\/2026\/04\/local-heatmap-tile-server_v1_Northern_Michigan-2048x1696.png 2048w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><figcaption class=\"wp-element-caption\">local-heatmap-tile-server v1 showing Northern Michigan in Warm style and Light appearance.<\/figcaption><\/figure>\n\n\n\n<p>During a long drive to (and from) Florida, and a lot of thinking about maps, I realized something that I really wanted, and something that I could use AI-assisted development to experiment with: generating a heatmap from all my personal, archived activity files. Specifically, generating XYZ tiles, making them available via <a href=\"https:\/\/en.wikipedia.org\/wiki\/Tile_Map_Service\">TMS<\/a> (so they can be used as an imagery layer in JOSM), and also displaying them on a slippy map.<\/p>\n\n\n\n<p>For years I&#8217;ve been using the <a href=\"https:\/\/nuxx.net\/blog\/2020\/05\/24\/high-resolution-strava-global-heatmap-in-josm\/\" data-type=\"post\" data-id=\"18873\">Strava heatmap as a layer in JOSM for OpenStreetMap (OSM) editing<\/a> and this works great, but I&#8217;m finding myself disconnecting from online social networks, including Strava, more and more. And while the <a href=\"https:\/\/nuxx.net\/blog\/2026\/02\/09\/strava-high-res-heatmap-in-josm-w-free-account\/\" data-type=\"post\" data-id=\"20091\">Strava Global heatmap does work as a data layer with a free account<\/a>, I began thinking about other options to use it, and other cloud providers, less and less. And yes, there&#8217;s similar offerings from RideWithGPS and whatnot, but I really wanted to generate my own since it&#8217;d give me a lot more flexibility.<\/p>\n\n\n\n<p>So, for my next project working with Claude, I decided to try building a personal heatmap generation tool. And it worked.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/nuxx.net\/blog\/wp-content\/uploads\/2026\/04\/local-heatmap-tile-server_v1_RAMBA_JOSM-scaled.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"565\" src=\"https:\/\/nuxx.net\/blog\/wp-content\/uploads\/2026\/04\/local-heatmap-tile-server_v1_RAMBA_JOSM-1024x565.png\" alt=\"\" class=\"wp-image-20121\" srcset=\"https:\/\/nuxx.net\/blog\/wp-content\/uploads\/2026\/04\/local-heatmap-tile-server_v1_RAMBA_JOSM-1024x565.png 1024w, https:\/\/nuxx.net\/blog\/wp-content\/uploads\/2026\/04\/local-heatmap-tile-server_v1_RAMBA_JOSM-300x166.png 300w, https:\/\/nuxx.net\/blog\/wp-content\/uploads\/2026\/04\/local-heatmap-tile-server_v1_RAMBA_JOSM-768x424.png 768w, https:\/\/nuxx.net\/blog\/wp-content\/uploads\/2026\/04\/local-heatmap-tile-server_v1_RAMBA_JOSM-1536x848.png 1536w, https:\/\/nuxx.net\/blog\/wp-content\/uploads\/2026\/04\/local-heatmap-tile-server_v1_RAMBA_JOSM-2048x1130.png 2048w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><figcaption class=\"wp-element-caption\">Cool heatmap of my ride and hike data, used as a layer in JOSM. (Ishpeming\/Negaunee area.)<\/figcaption><\/figure>\n\n\n\n<p>Using AI tools to develop software is nothing new, but I&#8217;ve never really been one to jump right on brand-new things, instead waiting for them to bake and show their utility before I dig in and use\/learn them. I also find it very difficult to learn any tool or system unless I have a way to apply it. But when I do, getting my head around it comes pretty quickly.<\/p>\n\n\n\n<p>In making this I&#8217;ve learned \/ found \/ finally-realized is that with a known set of inputs, a desired output, an ability to identify\/recognize bugs, and a task that&#8217;s known-possible, AI-assisted development saves can save incredible amount of time. Within reason it makes it possible for me to be more of a product manager than developer. Since I&#8217;m not really a developer (my career is in systems management and troubleshooting), that work for me is slow&#8230; and I&#8217;m not good at it.<\/p>\n\n\n\n<p>Using <a href=\"https:\/\/claude.com\/download\">Claude<\/a> on the desktop to write the code, <a href=\"https:\/\/code.visualstudio.com\/\">VS Code<\/a> to read and make a few manual edits, and <a href=\"https:\/\/www.docker.com\/products\/docker-desktop\/\">Docker Desktop<\/a> so I could keep an eye on things, after about a week of free-time iterating, this is what I came up with, and I&#8217;m quite pleased:<\/p>\n\n\n\n<p><a href=\"https:\/\/github.com\/c0nsumer\/local-heatmap-tile-server\">c0nsumer\/local-heatmap-tile-server<\/a><\/p>\n\n\n\n<p>This is a single Docker container that uses a bunch of Python to import GPS data files (<code>.FIT<\/code>, <code>.GPX<\/code>, .<code>TCX<\/code>), imports, deduplicates, and renders a complete set of XYZ tiles. It then makes them available via HTTP (for display in a slippy map or something like JOSM) or exports them to a <a href=\"https:\/\/docs.protomaps.com\/pmtiles\/\">PMTiles<\/a> file for simple hosting. And it has a built-in slippy map viewer\/data manager and a couple bundled viewers for completely static hosting (<a href=\"https:\/\/trailmaps.app\/pmtiles-viewer-demo\/pmtiles-viewer.html\">example<\/a>).<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"alignright size-medium\"><a href=\"https:\/\/nuxx.net\/blog\/wp-content\/uploads\/2026\/04\/local-heatmap-tile-server_v1_Dashboard-scaled.png\"><img loading=\"lazy\" decoding=\"async\" width=\"300\" height=\"112\" src=\"https:\/\/nuxx.net\/blog\/wp-content\/uploads\/2026\/04\/local-heatmap-tile-server_v1_Dashboard-300x112.png\" alt=\"\" class=\"wp-image-20117\" srcset=\"https:\/\/nuxx.net\/blog\/wp-content\/uploads\/2026\/04\/local-heatmap-tile-server_v1_Dashboard-300x112.png 300w, https:\/\/nuxx.net\/blog\/wp-content\/uploads\/2026\/04\/local-heatmap-tile-server_v1_Dashboard-1024x384.png 1024w, https:\/\/nuxx.net\/blog\/wp-content\/uploads\/2026\/04\/local-heatmap-tile-server_v1_Dashboard-768x288.png 768w, https:\/\/nuxx.net\/blog\/wp-content\/uploads\/2026\/04\/local-heatmap-tile-server_v1_Dashboard-1536x576.png 1536w, https:\/\/nuxx.net\/blog\/wp-content\/uploads\/2026\/04\/local-heatmap-tile-server_v1_Dashboard-2048x768.png 2048w\" sizes=\"auto, (max-width: 300px) 100vw, 300px\" \/><\/a><figcaption class=\"wp-element-caption\">Dashboard for importing new files, stats, and exporting the heatmap for static use.<\/figcaption><\/figure>\n<\/div>\n\n\n<p>The Python webserver, <a href=\"https:\/\/uvicorn.dev\/\">uvicorn<\/a>, isn&#8217;t the fastest nor great at caching, so the XYZ tiles are fronted with <a href=\"https:\/\/nginx.org\/\">nginx<\/a> to very quickly serve them from disk, only passing the request back to uvicorn and the Python stack for rendering if the tile isn&#8217;t present. Once the tiles are rendered they are cached very quickly served up solely by nginx, to the point where panning and zooming freely is seamless. (And yes, you can pre-render all tiles for optimal performance.)<\/p>\n\n\n\n<p>It&#8217;s been tested on ~4000 track single-GPX files (exported from rubiTrack), ~4000 <code>.FIT<\/code> files directly from Garmin devices, and a bunch of different types of single GPX files. And&#8230; it seems to work!<\/p>\n\n\n\n<p>The file inputs (FIT, TCX, GPX) aren&#8217;t special and parsers have existed for a long time. Nothing about heatmaps is new. Tile rendering isn&#8217;t new. Tile serving isn&#8217;t new. Nor are web-based heatmaps from fitness tracker data. But it needed to be glued together to get something that works this way, and this type of development made it possible. And I learned something new about AI-assisted software development along the way. It&#8217;s sure an interesting new world with these tools.<\/p>\n\n\n\n<p>And yes, beyond thinking about the features I had to do a lot of nudging along the way.<\/p>\n\n\n\n<p>Some major bugs that were encountered were getting cross-tile heatmap brightness correct, missing cross-tile data, tiles not rendering properly when called via different ways, moving to a faster web server so panning the map felt smooth, and a whole lot of tweaking of brightness and line thickness and blur and such at different zoom levels so it&#8217;d feel nice to use, noticing and dealing with malformed XML in GPXs&#8230;<\/p>\n\n\n\n<p>But this was nudging via prompts and having a bit of an idea what it was doing, not coding. Which is what&#8217;s so weird and new to me. It&#8217;s like directing a team of pretty-decent junior devs.<\/p>\n\n\n\n<p>And the end result is something I&#8217;ve wanted for a while. And now it exists. (And no, none of this post was written by any AI tool.)<\/p>\n","protected":false},"excerpt":{"rendered":"<p>During a long drive to (and from) Florida, and a lot of thinking about maps, I realized something that I really wanted, and something that&#8230;<\/p>\n<div class=\"more-link-wrapper\"><a class=\"more-link\" href=\"https:\/\/nuxx.net\/blog\/2026\/04\/07\/local-heatmap-tile-server-v1\/\">Continue reading<span class=\"screen-reader-text\">local-heatmap-tile-server v1<\/span><\/a><\/div>\n","protected":false},"author":2,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[13,11,57],"tags":[],"class_list":["post-20106","post","type-post","status-publish","format-standard","hentry","category-computers","category-making-things","category-mapping","entry"],"amp_enabled":true,"_links":{"self":[{"href":"https:\/\/nuxx.net\/blog\/wp-json\/wp\/v2\/posts\/20106","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/nuxx.net\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/nuxx.net\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/nuxx.net\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/nuxx.net\/blog\/wp-json\/wp\/v2\/comments?post=20106"}],"version-history":[{"count":9,"href":"https:\/\/nuxx.net\/blog\/wp-json\/wp\/v2\/posts\/20106\/revisions"}],"predecessor-version":[{"id":20125,"href":"https:\/\/nuxx.net\/blog\/wp-json\/wp\/v2\/posts\/20106\/revisions\/20125"}],"wp:attachment":[{"href":"https:\/\/nuxx.net\/blog\/wp-json\/wp\/v2\/media?parent=20106"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/nuxx.net\/blog\/wp-json\/wp\/v2\/categories?post=20106"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/nuxx.net\/blog\/wp-json\/wp\/v2\/tags?post=20106"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}