{"id":24940,"date":"2026-09-27T09:58:51","date_gmt":"2026-09-27T13:58:51","guid":{"rendered":"https:\/\/nuxx.net\/blog\/?p=24940"},"modified":"2026-09-27T10:07:12","modified_gmt":"2026-09-27T14:07:12","slug":"maplibre-gl-lanes-v1-0-0","status":"publish","type":"post","link":"https:\/\/nuxx.net\/blog\/2026\/09\/27\/maplibre-gl-lanes-v1-0-0\/","title":{"rendered":"maplibre-gl-lanes v1.0.0"},"content":{"rendered":"<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-large\"><a href=\"https:\/\/nuxx.net\/blog\/wp-content\/uploads\/2026\/09\/junction.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"576\" src=\"https:\/\/nuxx.net\/blog\/wp-content\/uploads\/2026\/09\/junction-1024x576.png\" alt=\"\" class=\"wp-image-24941\" srcset=\"https:\/\/nuxx.net\/blog\/wp-content\/uploads\/2026\/09\/junction-1024x576.png 1024w, https:\/\/nuxx.net\/blog\/wp-content\/uploads\/2026\/09\/junction-300x169.png 300w, https:\/\/nuxx.net\/blog\/wp-content\/uploads\/2026\/09\/junction-768x432.png 768w, https:\/\/nuxx.net\/blog\/wp-content\/uploads\/2026\/09\/junction-1536x864.png 1536w, https:\/\/nuxx.net\/blog\/wp-content\/uploads\/2026\/09\/junction.png 1600w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><figcaption class=\"wp-element-caption\">maplibre-gl-lanes v1.0.0 Lanes Rendering Example<\/figcaption><\/figure>\n<\/div>\n\n\n<p class=\"wp-block-paragraph\">The <a href=\"http:\/\/www.singletrackmapping.com\/uploads\/4\/7\/3\/1\/47318895\/ramba-trail-map-singletrack-maps_1_orig.jpg\">old (outdated) print RAMBA map<\/a> was the inspiration for the parallel lines\/lanes (public transit-style) maps on <a href=\"https:\/\/trailmaps.app\">trailmaps.app<\/a>. They functioned, but I wasn&#8217;t super happy with how they looked. It worked by using the built-in <a href=\"https:\/\/maplibre.org\/maplibre-style-spec\/layers\/#line-offset\">line-offset<\/a> and bolting on a bunch of fixes at map generation time. This looked&#8230; mostly okay&#8230; but fell apart at complex intersections and had lots of back-and-forth transitions.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I realized that a better way to do this would be via a plugin for <a href=\"https:\/\/maplibre.org\/maplibre-gl-js\/docs\/\">MapLibre GL JS<\/a> which calculates and draws these routes as zoom changes. It turns out there are papers from 2018-2021 about software called <a href=\"https:\/\/github.com\/ad-freiburg\/loom\">LOOM<\/a> (Line-Ordering Optimized Maps: <a href=\"https:\/\/ad-publications.informatik.uni-freiburg.de\/ACM_efficient%20Generation%20of%20%20Geographically%20Accurate%20Transit%20Maps_extended%20version.pdf\">1<\/a>, <a href=\"https:\/\/ad-publications.informatik.uni-freiburg.de\/EuroVis%20octi-maps.pdf\">2<\/a>, <a href=\"https:\/\/ad-publications.informatik.uni-freiburg.de\/SSTD_Metro%20Maps%20on%20Flexible%20Base%20Grids.pdf\">3<\/a>, <a href=\"https:\/\/ad-publications.informatik.uni-freiburg.de\/SIGSPATIAL_transitmaps_2018.pdf\">4<\/a>) and a reference implementation or and it did basically this, but with the intention of transit maps themselves.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">So, a few weeks ago I fired up the clanker\u2020 and set it to work implementing this, via a plugin, without using any existing code, which <a href=\"https:\/\/github.com\/c0nsumer\/trailmaps.app-map-generator\">c0nsumer\/trailmaps.app-map-generator<\/a> would be able to use for drawing the lines on my maps. After a bunch of back and forth, some really interesting learning about having multiple agents (on different packages) coordinate their work and testing (a plugin + its consumer), and a late-night session to tie things up (interestingly, after <a href=\"https:\/\/www.strava.com\/activities\/20343308914\">riding a bunch of RAMBA trails that afternoon<\/a>), it&#8217;s now live at v1.0.0: <a href=\"https:\/\/github.com\/c0nsumer\/maplibre-gl-lanes\">c0nsumer\/maplibre-gl-lanes<\/a> (and <a href=\"https:\/\/www.npmjs.com\/package\/maplibre-gl-lanes\">via npm<\/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\/09\/Screenshot-2026-09-27-at-09-49-54-De-Ronde-van-Grampian-Map-scaled.png\"><img loading=\"lazy\" decoding=\"async\" width=\"300\" height=\"147\" src=\"https:\/\/nuxx.net\/blog\/wp-content\/uploads\/2026\/09\/Screenshot-2026-09-27-at-09-49-54-De-Ronde-van-Grampian-Map-300x147.png\" alt=\"\" class=\"wp-image-24942\" srcset=\"https:\/\/nuxx.net\/blog\/wp-content\/uploads\/2026\/09\/Screenshot-2026-09-27-at-09-49-54-De-Ronde-van-Grampian-Map-300x147.png 300w, https:\/\/nuxx.net\/blog\/wp-content\/uploads\/2026\/09\/Screenshot-2026-09-27-at-09-49-54-De-Ronde-van-Grampian-Map-1024x501.png 1024w, https:\/\/nuxx.net\/blog\/wp-content\/uploads\/2026\/09\/Screenshot-2026-09-27-at-09-49-54-De-Ronde-van-Grampian-Map-768x376.png 768w, https:\/\/nuxx.net\/blog\/wp-content\/uploads\/2026\/09\/Screenshot-2026-09-27-at-09-49-54-De-Ronde-van-Grampian-Map-1536x751.png 1536w, https:\/\/nuxx.net\/blog\/wp-content\/uploads\/2026\/09\/Screenshot-2026-09-27-at-09-49-54-De-Ronde-van-Grampian-Map-2048x1002.png 2048w\" sizes=\"auto, (max-width: 300px) 100vw, 300px\" \/><\/a><figcaption class=\"wp-element-caption\">DRvG route on App v507 w\/ maplibre-gl-lanes<\/figcaption><\/figure>\n<\/div>\n\n\n<p class=\"wp-block-paragraph\">I&#8217;m really, really happy with this. <a href=\"https:\/\/c0nsumer.github.io\/maplibre-gl-lanes\/\">Here is the demo from GitHub pages that allows you to A\/B the simple line-offset and \/ plugin drawing styles<\/a>, or if you look at my current maps for <a href=\"https:\/\/trailmaps.app\/ramba\/\">RAMBA<\/a> or <a href=\"https:\/\/trailmaps.app\/glacialhills\/\">Glacial Hills<\/a> you can see it in action. While not as key, it really makes <a href=\"https:\/\/trailmaps.app\/burchfield\/\">Burchfield<\/a> pop.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">It looks particularly good on maps of routes like those for <a href=\"https:\/\/cramba.org\/events\/massive-fallout\/\">Massive Fallout<\/a> and <a href=\"https:\/\/www.drvg-gravel.com\/\">De Ronde van Grampian<\/a> where there&#8217;s just 3-4 routes of varying lengths with some overlap. I can&#8217;t share those maps publicly yet, but this image shows a bit of it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">There&#8217;s also been some further tweaking to the map generator, such as not drawing the thin white lines under the trail routes, better coloring (heavily inspired by <a href=\"https:\/\/www.nps.gov\/maps\/tools\/park-tiles\/view\/index.html#4\/39.06\/-95.98\">NPS Park Tiles<\/a>), showing swamps and railroads, which have helped make things even better, but the bulk of the improvement is from this plugin.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">More to come on map improvements, including possibly adding a color-by-difficulty option for trail systems that only color\/designate their trails this way (eg: <a href=\"https:\/\/www.copperharbortrails.org\/\">Copper Harbor<\/a>, <a href=\"https:\/\/www.noquetrails.org\/\">NTN<\/a>), but that&#8217;s all going to take some planning and thinking.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u2020 A laptop that I use for running Claude, accessed via SSH. I should probably write something up about this later&#8230;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>The old (outdated) print RAMBA map was the inspiration for the parallel lines\/lanes (public transit-style) maps on trailmaps.app. They functioned, but I wasn&#8217;t super happy with how they looked. It worked by using the built-in line-offset and bolting on a bunch of fixes at map generation time. This looked&#8230; mostly okay&#8230; but fell apart at\u2026<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"closed","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[11,57],"tags":[],"class_list":["post-24940","post","type-post","status-publish","format-standard","hentry","category-making-things","category-mapping"],"_links":{"self":[{"href":"https:\/\/nuxx.net\/blog\/wp-json\/wp\/v2\/posts\/24940","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=24940"}],"version-history":[{"count":3,"href":"https:\/\/nuxx.net\/blog\/wp-json\/wp\/v2\/posts\/24940\/revisions"}],"predecessor-version":[{"id":24945,"href":"https:\/\/nuxx.net\/blog\/wp-json\/wp\/v2\/posts\/24940\/revisions\/24945"}],"wp:attachment":[{"href":"https:\/\/nuxx.net\/blog\/wp-json\/wp\/v2\/media?parent=24940"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/nuxx.net\/blog\/wp-json\/wp\/v2\/categories?post=24940"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/nuxx.net\/blog\/wp-json\/wp\/v2\/tags?post=24940"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}