[{"data":1,"prerenderedAt":2532},["ShallowReactive",2],{"navigation":3,"index":47,"blogs":72,"work":388},[4,22],{"title":5,"path":6,"stem":7,"children":8,"page":21},"Blog","\u002Fblog","blog",[9,13,17],{"title":10,"path":11,"stem":12},"Anki Baby Steps","\u002Fblog\u002Fanki-baby-steps","blog\u002Fanki-baby-steps",{"title":14,"path":15,"stem":16},"GSoC 2026: a Hopeful Rejection","\u002Fblog\u002Fgsoc-2026-a-hopeful-rejection","blog\u002Fgsoc-2026-a-hopeful-rejection",{"title":18,"path":19,"stem":20},"Play vs Prove: CFU comparison","\u002Fblog\u002Fplay-vs-prove-cfu-comparison","blog\u002Fplay-vs-prove-cfu-comparison",false,{"title":23,"path":24,"stem":25,"children":26,"page":21},"Work","\u002Fwork","work",[27,31,35,39,43],{"title":28,"path":29,"stem":30},"D-Map Cache","\u002Fwork\u002Fdmap-cache","work\u002Fdmap-cache",{"title":32,"path":33,"stem":34},"Edge Relax","\u002Fwork\u002Fedge-relax","work\u002Fedge-relax",{"title":36,"path":37,"stem":38},"Focal Grid","\u002Fwork\u002Ffocal-grid","work\u002Ffocal-grid",{"title":40,"path":41,"stem":42},"Pico Router","\u002Fwork\u002Fpico-router","work\u002Fpico-router",{"title":44,"path":45,"stem":46},"Valhalla","\u002Fwork\u002Fvalhalla","work\u002Fvalhalla",{"id":48,"title":49,"about":50,"blog":53,"body":53,"description":54,"experience":55,"extension":57,"faq":53,"hero":58,"meta":64,"navigation":65,"path":66,"seo":67,"stem":70,"testimonials":53,"__hash__":71},"index\u002Findex.yml","Victor Yanson",{"title":51,"description":52},"About Me","Systems software engineer focused on embedded systems, computer architecture, and open-source infrastructure. Currently exploring FPGA acceleration and RISC-V architectures for graph-processing workloads.\n",null,"Systems software engineer focused on embedded systems, computer architecture, and open-source infrastructure. Currently exploring FPGA acceleration and RISC-V architectures for graph-processing workloads.",{"title":56},"Selected projects","yml",{"links":59},[60],{"label":61,"to":62,"color":63},"Check out Pico Router","https:\u002F\u002Fgithub.com\u002FPico-Router\u002FPico-Router","neutral",{},true,"\u002F",{"title":68,"description":69},"Home","I'm a systems & embedded software engineer that loves exploring the power of computers. This page is made to get to know me and my work a bit better!","index","nZuPbnfHiYAevuSYTLnVbTeiN8XrFTwLvH9yS29J7os",[73,100,194],{"id":74,"title":18,"author":53,"badge":75,"body":76,"date":92,"description":93,"extension":94,"image":95,"meta":96,"minRead":97,"navigation":65,"path":19,"seo":98,"stem":20,"__hash__":99},"blog\u002Fblog\u002Fplay-vs-prove-cfu-comparison.md","Hardware Acceleration",{"type":77,"value":78,"toc":88},"minimark",[79,84],[80,81,83],"h4",{"id":82},"tldr","TL;DR",[85,86,87],"p",{},"...",{"title":89,"searchDepth":90,"depth":90,"links":91},"",2,[],"2026-08-02","An in-depth head-to-head analysis of two","md","\u002Fcontent\u002Fplay-vs-prove-banner.png",{},7,{"title":18,"description":93},"3aGxfsFpa74yFPfQcoi0ktSyntSZWMXvVrBUlRzcnhM",{"id":101,"title":10,"author":53,"badge":102,"body":103,"date":187,"description":188,"extension":94,"image":189,"meta":190,"minRead":191,"navigation":65,"path":11,"seo":192,"stem":12,"__hash__":193},"blog\u002Fblog\u002Fanki-baby-steps.md","Japanese Study",{"type":77,"value":104,"toc":181},[105,107,110,114,127,132,135,139,142,146,155,162,165,171,174,178],[80,106,83],{"id":82},[85,108,109],{},"I've started using Anki for a month straight to build up my Japanese vocabulary. Being relatively new to Spaced Repetition System (SRS) based learning, I found the experience quite surprising. What originally felt like looking at abstract hieroglyphs quickly turned into a daily habit, allowing me to learn about 300 words from scratch.",[80,111,113],{"id":112},"helpful-definition","Helpful Definition",[115,116,117],"blockquote",{},[85,118,119,120],{},"\"Anki is a free and open-source flashcard program. It uses techniques from cognitive science such as active recall testing and spaced repetition to aid the user in memorization.\" — ",[121,122,126],"a",{"href":123,"rel":124},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FAnki",[125],"nofollow","wikipedia.com",[128,129,131],"h2",{"id":130},"wait-youre-learning-japanese","Wait, You're Learning Japanese?",[85,133,134],{},"You heard correctly. I'm currently taking a stab at the Japanese language. It all has to do with some reflection I had regarding my future progression as a person and an engineer. Being a (mostly) autodidact in programming, I've experienced both upsides and downsides to my unorthodox path. Without getting into too much detail, I've set my sights on pursuing graduate research in Japan. This, paired with my longstanding interest in the country, has pushed me to embark on this new language learning journey.",[128,136,138],{"id":137},"why-anki","Why Anki?",[85,140,141],{},"I feel like my inclination towards Anki as my preferred SRS platform is not very unique. Especially among Japanese learners (and even more so for those interested in the JATT learning approach), Anki feels more like a rite of passage than anything else. With such a huge mountain of unknown language structures ahead of you, it only feels natural to start with learning the basics through flashcards. Of course, its FOSS nature also attracted me very much. Who knows, if I find any egregious bugs I might end up submitting a PR someday.",[128,143,145],{"id":144},"the-results","The Results",[85,147,148,149,154],{},"And now for the million-dollar question: how much did I learn in my one month of continuous Anki use? Before I answer, I'd like to share some details around my approach. For anyone interested, I picked the Kaishi 1.5k core deck as my starter deck, which contains 1,500 of the most common Japanese words, with each card containing an example sentence using the word. I started off with 10 new cards a day (as per the recommendation of the YouTuber ",[121,150,153],{"href":151,"rel":152},"https:\u002F\u002Fwww.youtube.com\u002F@Livakivi",[125],"Livakivi","). That felt waaay too easy at the time, so I bumped it up to 20 new cards. Shortly after, I understood the concept of review piling, and suddenly I had to lower it again to 10 new cards. Luckily, I could keep up this pace until the end of the month, occasionally oscillating between 9 and 11 daily new cards. Eventually, my reviews got really comfy, so I've been trying to increase my daily new cards to 14, making my daily total reviews about 120 (about 50 minutes a day).",[85,156,157],{},[158,159],"img",{"alt":160,"src":161},"Anki Screenshot of Reviews Stats","content\u002Fanki-screenshot-1.png",[85,163,164],{},"Above you can see a clear picture of how all my totals grew throughout the month. Funnily enough, my daily time spent actually resembles more of a bell curve. As I got more comfortable reviewing old cards, I started taking drastically less time to review the same number of cards every day.",[85,166,167],{},[158,168],{"alt":169,"src":170},"Anki Screenshot of Cards Stats","content\u002Fanki-screenshot-2.png",[85,172,173],{},"In this breakdown, you can see my total cards learned amounts to 335! Not too bad in my opinion. At this point, I still struggle to understand even the most basic audio and text fragments of Japanese. However, I can now noticeably intuit the general structures of sentences, and once in a while (when I'm lucky enough), I hear a sentence containing mostly familiar vocabulary, allowing me to understand it fully. Moreover, I really enjoyed learning straight away with kanji. Having picked up hiragana and katakana shortly before starting Anki, I now pick up kanji radicals here and there, making learning new words slightly easier.",[128,175,177],{"id":176},"future-goals-aspirations","Future Goals & Aspirations",[85,179,180],{},"If my projections are correct and I can keep up this pace, I will finish this core deck in late October. Until then, I'll supplement my grammar study with my copy of the Genki I textbook and immersion whenever possible. I'm very eager to try sentence mining; however, I'll have to be patient until I have a stronger comprehension base before such an approach will be effective. I'll definitely try to drop another article if and when I finish the entire Kaishi 1.5k deck!",{"title":89,"searchDepth":90,"depth":90,"links":182},[183,184,185,186],{"id":130,"depth":90,"text":131},{"id":137,"depth":90,"text":138},{"id":144,"depth":90,"text":145},{"id":176,"depth":90,"text":177},"2026-07-17","After knowing about Anki and SRS for a while I decided to give it a try for my study of Japanese. These are my experience after my first month of daily use.","https:\u002F\u002Fus1.discourse-cdn.com\u002Fflex002\u002Fuploads\u002Fanki2\u002Foriginal\u002F3X\u002Fa\u002F9\u002Fa916db752275f53e75be467f7a972e8c70fec9a2.png",{},4,{"title":10,"description":188},"vdtEsKS1-V-L4ON7sY8j5DcUj9i3-BlUaDP0avub4h0",{"id":195,"title":14,"author":53,"badge":196,"body":197,"date":382,"description":383,"extension":94,"image":384,"meta":385,"minRead":191,"navigation":65,"path":15,"seo":386,"stem":16,"__hash__":387},"blog\u002Fblog\u002Fgsoc-2026-a-hopeful-rejection.md","OpenStreetMap",{"type":77,"value":198,"toc":375},[199,201,207,209,219,223,240,244,258,261,264,268,271,279,287,290,300,304,307,317,320,323,329,332,335,340,349,353,362,365,372],[80,200,83],{"id":82},[85,202,203,204,206],{},"I was rejected for GSoC 2026 after months of preparation. The disappointment was tough, but the entire experience integrated me into open-source culture. My prep work involved building a deep understanding of routing engine mechanics and systems engineering principles, giving me the necessary foundation to start my own FOSS project, ",[121,205,40],{"href":41},".",[80,208,113],{"id":112},[115,210,211],{},[85,212,213,214],{},"\"Google Summer of Code is a global, online program focused on bringing new contributors into open source software development. GSoC Contributors work with an open source organization on a 12+ week programming project under the guidance of mentors.\" — ",[121,215,218],{"href":216,"rel":217},"https:\u002F\u002Fsummerofcode.withgoogle.com\u002F",[125],"summerofcode.withgoogle.com",[128,220,222],{"id":221},"how-it-started","How It Started",[85,224,225,226,231,232,234,235,239],{},"In late 2025, I happened to discover Google Summer of Code while watching a ",[121,227,230],{"href":228,"rel":229},"https:\u002F\u002Fwww.youtube.com\u002F@Green-Code",[125],"Green Code"," video. It caught my eye due to its \"meritocratic\" appeal (very attractive for any self-taught engineer), and let's be honest, the fact that it was backed by Google also had something to do with it :-). Being in the midst of my work on ",[121,233,36],{"href":37}," and just being introduced to the world of GIS technology, I looked through the org list to find a project that could fit me. I ended up landing on ",[121,236,196],{"href":237,"rel":238},"https:\u002F\u002Fwiki.openstreetmap.org\u002F",[125]," thanks to the sheer variety of prominent GIS projects they maintain. I checked the proposed projects page on the official wiki only to find out there was just one project up for grabs at the time. Remembering that \"the early bird gets the worm,\" I scanned the repo and contacted the mentor to share my interest in the project.",[128,241,243],{"id":242},"the-prep-phase","The Prep Phase",[85,245,246,247,252,253,257],{},"My preparations kicked off by exploring target repo (",[121,248,251],{"href":249,"rel":250},"https:\u002F\u002Fgithub.com\u002Fsosm\u002Ftemporary-road-closures",[125],"closure.osm.ch",") more thoroughly. In short, the project consists of a monorepo including a Next.js frontend and a FastAPI backend. As expected, the frontend handles the web UI while the Python backend handles most of the business logic. The main goal of closure.osm.ch is to expose a new routing API that community-sources and aggregates public closure data. At the time, it only featured a landing page plus a \"closure-aware\" routing demo. With the main mentor stated goal being integration with one or more dynamic routing engines, I took a look at what engine was currently being used for the demo. It turned out to be ",[121,254,44],{"href":255,"rel":256},"https:\u002F\u002Fgithub.com\u002Fvalhalla\u002Fvalhalla",[125],", making my very next step exploring and understanding that repository in order to propose an optimal integration.",[85,259,260],{},"Valhalla, being an enormous decade-plus C++ codebase, was especially challenging to wrap my head around. It was designed with strict OOP principles in mind, which on one hand made it easier for me to focus on studying individual aspects of the system, while on the other hand the countless levels of abstraction and polymorphism made my head spin. Luckily for me, the core maintainer team numbers only four people, making discussions with far-reaching system consequences much easier. I have to give a big thanks to this very team for not only hearing out my (at times naive) ideas but also those of all the other participants equally, without a show of favoritism.",[85,262,263],{},"I'd like to mention that open-source software took a bit of adjusting for me. It was quite intimidating, and I was self-conscious about not fitting in and seeming too much like a clueless noob (maybe something worth delving into in another blog post). In fact, this very feeling of fear made me delve into documentation for hours, scour subreddits, and quintuple-check every message and contribution before sending.",[128,265,267],{"id":266},"pre-submission-contributions","Pre-submission Contributions",[85,269,270],{},"As clearly stated by the GSoC and OSM guidelines, contributions to the projects one applies to are obligatory for consideration. Knowing this, I set out to deliver my first-ever PRs to both closures.osm.ch and Valhalla.",[85,272,273,274,278],{},"To summarize, my ",[121,275,277],{"href":276},"\u002Fwork\u002Fvalhalla#docker-version-logging","main merged PR"," for Valhalla added version logging during the startup of the official Docker image. Besides that, I spent some time helping with issue triage and technical discussions surrounding vector tiles and the live traffic subsystem.",[85,280,281,282,286],{},"On the closures.osm.ch side, I fixed Docker compatibility issues, integrated ",[283,284,285],"code",{},"vitest"," together with an initial unit test suite, improved backend build reliability, and reported a handful of architectural issues I encountered while studying the codebase.",[85,288,289],{},"Lastly, I wanted to experience the OpenStreetMap ecosystem from the perspective of a data contributor as well. To that end, I made a handful of map edits around both Barcelona and Nijmegen, getting familiar with the editing workflow and the review process along the way.",[115,291,292],{},[85,293,294,295,299],{},"If you're interested in seeing a compilation of all my contributions, feel free to check out my ",[121,296,298],{"href":297},"#full-propsal-anchor","full GSoC proposal"," below.",[128,301,303],{"id":302},"summary-of-the-proposal","Summary of the Proposal",[85,305,306],{},"I'll try to keep the breakdown of the proposal short, as you can view the entire submitted document below.",[85,308,309,310,313,314,316],{},"In a nutshell, I identified a handful of problems with the current closure-aware routing approach that were keeping it from being production-ready. Supposedly due to its \"demo\" nature, I encountered a few spots with a very unclear separation of concerns. Firstly, the entire routing logic was located in the Next.js frontend, defeating the purpose of the REST API as the project's focal point. It basically just sent a routing request to a remote Valhalla instance with the ",[283,311,312],{},"exclude_locations"," parameter filled with closures fetched from the accompanying PostGIS database. ",[283,315,312],{}," is limited to 50 location points max, which is way too little for any serious trip, and uses closest-point map matching, leading to unreliable routing accuracy. Moreover, the OpenLR module was built from scratch but was not compliant with the actual open standard.",[85,318,319],{},"The main considerations for the solution were to have a system that could provide native-like integration with routing engines while staying engine-agnostic, adding as little runtime overhead as possible, and integrating seamlessly with the existing closures.osm.ch infrastructure.",[85,321,322],{},"The solution I came up with was the addition of a new sidecar service called Closure Sync. Its design is best illustrated by the diagram below:",[85,324,325],{},[158,326],{"alt":327,"src":328},"Closure Sync Architecture Diagram","content\u002Fclosure-sync-diagram.png",[85,330,331],{},"As you can see, Closure Sync represents an independent service that is meant to be placed in the same host environment as the routing engine. It utilizes a Live Traffic overlay provided by many routing engines, which offers an API to change edge costs dynamically at runtime. In essence, the service polls a remote closures.osm.ch instance every couple of minutes to fetch live data and transforms the fetched data into a readable traffic overlay format, where all closed edges are assigned an infinite cost, effectively closing them.",[85,333,334],{},"I planned to initially write Closure Sync in Python to comply with a realistic milestone-based schedule. However, after a successful PoC, the service would ideally be rewritten in a compiled language to increase native performance and ship as a single binary.",[85,336,337],{},[121,338],{"id":339},"full-propsal-anchor",[341,342,346],"card",{"target":343,"title":344,"to":345},"_blank","PDF","\u002Fcontent\u002FGSoC-proposal.pdf",[85,347,348],{},"Click Here To View The Full Proposal",[128,350,352],{"id":351},"moving-on","Moving On",[85,354,355,356,361],{},"As suggested by the title of this post, my proposal was unfortunately rejected. It really wasn't the rejection itself that disappointed me, but rather some of the details surrounding it. Prior to submitting my proposal, I had publicly asked for some feedback in the ",[121,357,360],{"href":358,"rel":359},"https:\u002F\u002Fgithub.com\u002Fvalhalla\u002Fvalhalla\u002Fdiscussions\u002F5944",[125],"Valhalla discussion on GitHub",". I shortly afterward found out that asking for feedback like that is not really in accordance with the \"GSoC mentality,\" but the maintainers were still patient with me and engaged thoroughly. The point is that a couple of days after the conclusion of the discussion (still a few days before the deadline), another GSoC-aspiring commenter joined, name-dropping the mentor and thanking them because some of his doubts had been answered. Thinking little of it, I moved on with my preparations.",[85,363,364],{},"After getting rejected, I naturally wanted to know what the winning picks were. To my surprise, I found the same commenter presenting a rework of closures.osm.ch that included a \"sidecar service called Closure Sync.\" This is not meant as an exposé of this particular participant. I publicly shared my idea, so it's only natural that he used it in his own preparation, and I did not see his project, so it is not my place to judge it. It did, however, make the rejection sting more. On top of that, I was slightly put off by the follow-up contact with the mentor when asking for feedback and clarity surrounding the situation.",[85,366,367,368,371],{},"That being said, I in no way regret the time I put in. I knew from the start that the majority of proposals never see the light of day. In exploring Valhalla, I've developed a deep appreciation for routing technology, and I'm determined to grow my status as a trusted contributor to the project (more contributions in ",[121,369,370],{"href":45},"works","). Moreover, I learned from scratch what it means to create open-source software. It deeply resonated with my principles, and I'm looking forward to finding more cool projects to help out on.",[85,373,374],{},"While working on the GSoC proposal, I also deepened my knowledge of embedded systems, leaving me with an itch to do more impactful work on a real project. In essence, that's where the idea for Pico Router was born. Having gained the fundamental knowledge of how one of the most powerful and impactful routing engines works on the inside, I started looking for MCU-compatible alternatives. Sadly (or luckily), no one has yet tried to take on the challenge in a FOSS format, clearing the way for this new adventure!",{"title":89,"searchDepth":90,"depth":90,"links":376},[377,378,379,380,381],{"id":221,"depth":90,"text":222},{"id":242,"depth":90,"text":243},{"id":266,"depth":90,"text":267},{"id":302,"depth":90,"text":303},{"id":351,"depth":90,"text":352},"2026-05-19","A detailed breakdown of my personal experience applying to 2026 edition of Google Summer of Code.","https:\u002F\u002Fwww.collabora.com\u002Fassets\u002Fimages\u002Fblog\u002FGSoC-2020b.png",{},{"title":14,"description":383},"iuIb9ROvOJpGEGp3KaVicXHp4sK8ZsuqDEReqqyu6Z0",[389,511,537,558,736],{"id":390,"title":28,"body":391,"color":497,"date":498,"description":499,"extension":94,"image":500,"meta":501,"navigation":65,"oneliner":502,"path":29,"seo":503,"stem":30,"tags":504,"url":509,"__hash__":510},"work\u002Fwork\u002Fdmap-cache.md",{"type":77,"value":392,"toc":493},[393,397,400,404,412,417,445,449,483,489],[128,394,396],{"id":395},"whats-the-point-of-adding-such-a-simple-cache","What's the point of adding such a simple cache?",[85,398,399],{},"The cache provides a simplified model of a cached memory hierarchy, allowing experiments with temporal and spatial locality and the performance impact of cache-aware versus cache-unaware memory-access patterns.",[128,401,403],{"id":402},"helpful-links","Helpful Links",[341,405,409],{"icon":406,"target":343,"title":407,"to":408},"i-simple-icons-github","Merged PR","https:\u002F\u002Fgithub.com\u002Fnuxt-ui-templates\u002Fdocs",[85,410,411],{},"Link to final merged Pull Request on GitHub",[413,414,416],"h1",{"id":415},"draw-diagram-of-current-soc-setup","Draw Diagram of Current SoC setup!!",[418,419,424],"pre",{"className":420,"code":421,"filename":422,"language":423,"meta":89,"style":89},"language-plaintext shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","           Tag           Index\n┌───────────┴───────────┐┌─┴┐\n00000000000000000000000000000000 ──── Unused\n","Address Slicing (Maybe Replace With Hand Drawing)","plaintext",[283,425,426,434,439],{"__ignoreMap":89},[427,428,431],"span",{"class":429,"line":430},"line",1,[427,432,433],{},"           Tag           Index\n",[427,435,436],{"class":429,"line":90},[427,437,438],{},"┌───────────┴───────────┐┌─┴┐\n",[427,440,442],{"class":429,"line":441},3,[427,443,444],{},"00000000000000000000000000000000 ──── Unused\n",[413,446,448],{"id":447},"explain-fsm","Explain FSM!!",[450,451,452,465],"table",{},[453,454,455],"thead",{},[456,457,458,462],"tr",{},[459,460,461],"th",{},"Original",[459,463,464],{},"Mine",[466,467,468],"tbody",{},[456,469,470,477],{},[471,472,473],"td",{},[158,474],{"alt":475,"src":476},"reference demo gif","\u002Fcontent\u002Fd-map-cache-gif-2.gif",[471,478,479],{},[158,480],{"alt":481,"src":482},"buggy demo gif","\u002Fcontent\u002Fd-map-cache-gif-1.gif",[85,484,485],{},[158,486],{"alt":487,"src":488},"signal traces","\u002Fcontent\u002Fd-map-cache-screenshot-1.png",[490,491,492],"style",{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":89,"searchDepth":90,"depth":90,"links":494},[495,496],{"id":395,"depth":90,"text":396},{"id":402,"depth":90,"text":403},"#8c1826","2026-08-01","Designed and developed a simple Direct Mapped CPU cache for the CFU Proving Ground RISC-V implementation.","https:\u002F\u002Fp7i.vogel.de\u002Fwcms\u002F0a\u002F43\u002F0a436a696528c4a9675af94c2e79d960\u002F0126825714v1.jpeg",{},"RISC-V Cache",{"title":28,"description":499},[505,506,507,508],"RISC-V","Verilog","FPGA","Architecture","#","vrKUS3VLrFC7YJ3QTEYMLSKtJpAizLBjFcqEaSTJpq0",{"id":512,"title":32,"body":513,"color":525,"date":526,"description":527,"extension":94,"image":528,"meta":529,"navigation":65,"oneliner":530,"path":33,"seo":531,"stem":34,"tags":532,"url":509,"__hash__":536},"work\u002Fwork\u002Fedge-relax.md",{"type":77,"value":514,"toc":523},[515],[418,516,521],{"className":517,"code":519,"language":520},[518],"language-text","Baseline C implementation\n↓\nprofile\n↓\nidentify hotspot\n↓\ndesign custom RISC-V instruction\n↓\nimplement in CFU Playground HDL\n↓\nadd opcode\n↓\ncall through intrinsic\n↓\nbenchmark\n","text",[283,522,519],{"__ignoreMap":89},{"title":89,"searchDepth":90,"depth":90,"links":524},[],"#FF6C3A","2026-07-01","Designed and developed the user interface for an indie music streaming service, focusing on discovery features and creating a unique, engaging listening experience using custom audio visualizations.","https:\u002F\u002Fwww.idtools.com.au\u002Fsite_2021\u002Fwp-content\u002Fuploads\u002F2021\u002F05\u002F3D-network-plots-python-mplot3d-toolkit.png",{},"FPGA Accelerator",{"title":32,"description":527},[533,534,535],"UI Design","Front-End Dev","Animation","x_qI6qwzUBxuSHSFYgxZbsD4h3Lpeu2ca96ZPzXAUng",{"id":538,"title":40,"body":539,"color":546,"date":547,"description":548,"extension":94,"image":549,"meta":550,"navigation":65,"oneliner":551,"path":41,"seo":552,"stem":42,"tags":553,"url":509,"__hash__":557},"work\u002Fwork\u002Fpico-router.md",{"type":77,"value":540,"toc":543},[541],[128,542,83],{"id":82},{"title":89,"searchDepth":90,"depth":90,"links":544},[545],{"id":82,"depth":90,"text":83},"#FF4B84","2026-05-01","Creator and maintainer of an open source navigation firmware project. The main goal being autonomous GPS routing for Raspberry Pi Pico 2 hardware targets using OpenStreetMap data.","https:\u002F\u002Fstatic0.xdaimages.com\u002Fwordpress\u002Fwp-content\u002Fuploads\u002F2024\u002F11\u002Fraspberry-pi-pico-2-w-featured.png?w=1200&h=628&fit=crop",{},"Navigation Firmware",{"title":40,"description":548},[554,555,556,196],"Embedded Systems","C++","Python","gTT3JLfPHUqtAR_JrcHHtzAJDNtPz9my8qAZlesH8f8",{"id":559,"title":44,"body":560,"color":724,"date":725,"description":726,"extension":94,"image":727,"meta":728,"navigation":65,"oneliner":729,"path":45,"seo":730,"stem":46,"tags":731,"url":509,"__hash__":735},"work\u002Fwork\u002Fvalhalla.md",{"type":77,"value":561,"toc":702},[562,567,571,574,577,581,588,592,595,599,602,604,607,633,636,638,642,645,647,650,666,669,671,675,678,680,683,697,700],[563,564],"table-of-contents",{":title":565,":max-depth":566},"Index","2",[128,568,570],{"id":569},"segfault-bug-fix","Segfault Bug Fix",[572,573,83],"h3",{"id":82},[85,575,576],{},"Add TL;DR...",[572,578,580],{"id":579},"relevant-links","Relevant Links",[582,583,584],"ul",{},[585,586,587],"li",{},"Nothing yet...",[572,589,591],{"id":590},"breakdown","Breakdown",[85,593,594],{},"Add outcome...",[128,596,598],{"id":597},"python-openlr-module","Python OpenLR Module",[572,600,83],{"id":601},"tldr-1",[85,603,576],{},[572,605,580],{"id":606},"relevant-links-1",[582,608,609,617,625],{},[585,610,611,612],{},"Discussion: ",[121,613,616],{"href":614,"rel":615},"https:\u002F\u002Fgithub.com\u002Fvalhalla\u002Fvalhalla\u002Fdiscussions\u002F6035",[125],"Python bindings for OpenLR #6035",[585,618,619,620],{},"Issue: ",[121,621,624],{"href":622,"rel":623},"https:\u002F\u002Fgithub.com\u002Fvalhalla\u002Fvalhalla\u002Fissues\u002F6037",[125],"OpenLR Python bindings #6037",[585,626,627,628],{},"Pull Request: ",[121,629,632],{"href":630,"rel":631},"https:\u002F\u002Fgithub.com\u002Fvalhalla\u002Fvalhalla\u002Fpull\u002F6087",[125],"feat: openlr python bindings #6087",[572,634,591],{"id":635},"breakdown-1",[85,637,594],{},[128,639,641],{"id":640},"python-bindging-restructuring","Python Bindging Restructuring",[572,643,83],{"id":644},"tldr-2",[85,646,576],{},[572,648,580],{"id":649},"relevant-links-2",[582,651,652,659],{},[585,653,611,654],{},[121,655,658],{"href":656,"rel":657},"https:\u002F\u002Fgithub.com\u002Fvalhalla\u002Fvalhalla\u002Fdiscussions\u002F6096#discussioncomment-17225673",[125],"re-design python binding structure #6096",[585,660,627,661],{},[121,662,665],{"href":663,"rel":664},"https:\u002F\u002Fgithub.com\u002Fvalhalla\u002Fvalhalla\u002Fpull\u002F6133",[125],"BREAKING(python): change layout of bindings, no signature change #6133",[572,667,591],{"id":668},"breakdown-2",[85,670,594],{},[128,672,674],{"id":673},"docker-version-logging","Docker Version Logging",[572,676,83],{"id":677},"tldr-3",[85,679,576],{},[572,681,580],{"id":682},"relevant-links-3",[582,684,685,690],{},[585,686,611,687],{},[121,688,658],{"href":656,"rel":689},[125],[585,691,627,692],{},[121,693,696],{"href":694,"rel":695},"https:\u002F\u002Fgithub.com\u002Fvalhalla\u002Fvalhalla\u002Fissues\u002F5721",[125],"Include Valhalla version in Docker container startup logs #5721",[572,698,591],{"id":699},"breakdown-3",[85,701,594],{},{"title":89,"searchDepth":90,"depth":90,"links":703},[704,709,714,719],{"id":569,"depth":90,"text":570,"children":705},[706,707,708],{"id":82,"depth":441,"text":83},{"id":579,"depth":441,"text":580},{"id":590,"depth":441,"text":591},{"id":597,"depth":90,"text":598,"children":710},[711,712,713],{"id":601,"depth":441,"text":83},{"id":606,"depth":441,"text":580},{"id":635,"depth":441,"text":591},{"id":640,"depth":90,"text":641,"children":715},[716,717,718],{"id":644,"depth":441,"text":83},{"id":649,"depth":441,"text":580},{"id":668,"depth":441,"text":591},{"id":673,"depth":90,"text":674,"children":720},[721,722,723],{"id":677,"depth":441,"text":83},{"id":682,"depth":441,"text":580},{"id":699,"depth":441,"text":591},"#64748B","2026-02-01","Contributor to Valhalla is a high-performance routing engine built on OpenStreetMap data. My work focused on Python interoperability, OpenLR support, developer experience, and infrastructure improvements.","https:\u002F\u002Fvalhalla.github.io\u002Fvalhalla\u002Fapi\u002Fimages\u002Fisochrone.png",{},"OSM Routing Engine",{"title":44,"description":726},[732,733,734,555,556],"OSM","GIS","Routing Engine","4C6_czmGkrJIxlL9qD_RzEGWTut1uVYwuy9f-MI1G2E",{"id":737,"title":36,"body":738,"color":2519,"date":2520,"description":2521,"extension":94,"image":2522,"meta":2523,"navigation":65,"oneliner":2524,"path":37,"seo":2525,"stem":38,"tags":2526,"url":509,"__hash__":2531},"work\u002Fwork\u002Ffocal-grid.md",{"type":77,"value":739,"toc":2512},[740,742,745,749,758,762,765,769,777,780,783,786,790,794,801,815,818,821,824,828,831,834,1478,1481,1485,1505,1508,1973,1979,2029,2032,2035,2066,2078,2081,2180,2183,2245,2260,2270,2374,2377,2455,2464,2509],[572,741,83],{"id":82},[85,743,744],{},"Built a full-stack GIS platform for visualizing Dutch energy grid data using Nuxt, Mapbox, Supabase, and PostGIS. Implemented a GeoJSON ingestion pipeline, custom map interactions, and a performant vector tile backend that serves geospatial data directly from PostGIS while keeping Mapbox purely as the rendering engine.",[128,746,748],{"id":747},"mini-demo","Mini Demo",[750,751,752],"callout",{},[85,753,754],{},[158,755],{"alt":756,"src":757},"Focal Grid Demo GIF","content\u002Ffocal-grid-demo.gif",[128,759,761],{"id":760},"background","Background",[85,763,764],{},"While working full-time on freelance full-stack projects got in contact with a client looking to build a GIS-based visualisation platform. The pitch was straightfoward, in service of energy grid buildout in the Netherlands many private providers are looking to efficiently aggregate data in order make decisions on planning, contracts, budgets, etc. The goal was to build a minimum viable web-based prototype that demonstrates the basic desired visualisation and interaction funcionallity to show to stakeholders. Taking on the challenge as a solo developer, this was one of my most impactful freelance projects seeing as it steered me to GIS engineering for the very first time.",[128,766,768],{"id":767},"tech-stack","Tech Stack",[750,770,771],{},[85,772,773],{},[158,774],{"alt":775,"src":776},"Focal Grid Architecture Overview","content\u002Ffocal-grid-architecture.jpg",[85,778,779],{},"The application follows a fairly straightforward full-stack architecture. The frontend is built with Nuxt and Vue, which pair nicely with the Mapbox GL JS SDK while also giving me a framework I'm comfortable moving quickly in. Since almost all application data is user-specific and fetched on demand, there was little reason to lean on SSR beyond the initial page shell.",[85,781,782],{},"On the backend I used Supabase as the primary BaaS solution. Besides hosting a PostGIS-enabled PostgreSQL database, it also provided authentication and Edge Functions for serving vector tiles directly from the database. This let Mapbox handle rendering while keeping the actual geospatial data entirely outside of the Mapbox ecosystem.",[85,784,785],{},"The project itself was deployed on Vercel, with TypeScript used throughout both the frontend and server code. Finally, GSAP was sprinkled in for a handful of subtle interface animations, mostly around menus and transitions without becoming a central part of the application.",[128,787,789],{"id":788},"key-features","Key Features",[80,791,793],{"id":792},"custom-ui-map-interactions","Custom UI & Map Interactions",[85,795,796,797,800],{},"Perhaps this might be the easiest part to grasp intuitively seeing the ",[121,798,748],{"href":799},"#mini-demo"," above already reveals most of the built functionality. Still, it might be good to list out the available features after concluding my work. The app largely consists of, number one, a interactive map render component, and two, a header component offering different control options. Controls offered by the popover menu include:",[582,802,803,806,809,812],{},[585,804,805],{},"Toggling different energy grid layers on and off which were seperated by their voltage;",[585,807,808],{},"Toggling energy substations connected to each kind of voltage level;",[585,810,811],{},"On-map polygon drawing and deletion for grouping data points geographically;",[585,813,814],{},"Drag-and-drop icons for projecting energy asset placement;",[85,816,817],{},"Furthermore, the top bar additionally includes simple sattelite view and pitched angle toggle buttons.",[85,819,820],{},"When talking about the map itself, the user is able to zoom and inspect a limited geographical area (in this case Amersfoort in the Netherlands) as you might expect from any other map-based web application. On top of that, user can click on substations to dynamically load and view the stations metadata.",[85,822,823],{},"From a technical standpoint, most of the interaction logic lives in small Vue composables that bridge UI state to Mapbox through some SDK-provided hooks. The trickiest part for me was definetily keeping all of this stable across style reloads. Switching between the custom basemap and satellite view tears down and rebuilds Mapbox layers. This forced me to add special hooks to re-sync restore connection layers and persist drawn points in memory before re-injecting them after the style loads.",[80,825,827],{"id":826},"geojson-to-postgis-pipeline","GeoJSON to PostGIS Pipeline",[85,829,830],{},"The data source for the energy grid related data was open data provided by the municipality of Amersfoort. At the time, only data in GeoJSON format was available. This meant that I needed to find a reliable way in which import this data directly into a Postgres table in Supabase while maintaining the usability of the data.",[85,832,833],{},"I landed on the following helper function. Provided with a local file path, desired table name and Supabase client object it automatically loads the GeoJSON into a PostGIS compatible Supabase table.",[835,836,837],"code-collapse",{},[418,838,843],{"className":839,"code":840,"filename":841,"language":842,"meta":89,"style":89},"language-js shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","async function importGeoJSON(\n  filePath, \n  tableName, \n  supabaseClient\n) {\n  try {\n    const geojson = JSON.parse(fs.readFileSync(filePath, 'utf8'))\n    \n    \u002F\u002F Process in batches to avoid timeouts\n    const batchSize = 1000\n    const features = geojson.features.map(feature => ({\n      geom: feature.geometry,\n      properties: feature.properties || {}\n    }))\n\n    console.log(`Importing ${features.length} features...`)\n    \n    for (let i = 0; i \u003C features.length; i += batchSize) {\n      const batch = features.slice(i, i + batchSize)\n      const { data, error } = await supabaseClient\n        .from(tableName)\n        .insert(batch)\n      \n      if (error) {\n        console.error('Error inserting batch:', error)\n        break\n      }\n      \n      console.log(`Inserted batch ${Math.floor(i\u002FbatchSize) + 1}\u002F${Math.ceil(features.length\u002FbatchSize)}`)\n    }\n    \n    console.log('Import completed!')\n  } catch (error) {\n    console.error('Import failed:', error)\n  }\n}\n","JavaScript","js",[283,844,845,862,875,884,889,898,907,958,964,971,985,1021,1041,1062,1070,1076,1116,1121,1168,1202,1229,1245,1260,1266,1281,1306,1312,1318,1323,1394,1400,1405,1425,1442,1466,1472],{"__ignoreMap":89},[427,846,847,851,854,858],{"class":429,"line":430},[427,848,850],{"class":849},"spNyl","async",[427,852,853],{"class":849}," function",[427,855,857],{"class":856},"s2Zo4"," importGeoJSON",[427,859,861],{"class":860},"sMK4o","(\n",[427,863,864,868,871],{"class":429,"line":90},[427,865,867],{"class":866},"sHdIc","  filePath",[427,869,870],{"class":860},",",[427,872,874],{"class":873},"sTEyZ"," \n",[427,876,877,880,882],{"class":429,"line":441},[427,878,879],{"class":866},"  tableName",[427,881,870],{"class":860},[427,883,874],{"class":873},[427,885,886],{"class":429,"line":191},[427,887,888],{"class":866},"  supabaseClient\n",[427,890,892,895],{"class":429,"line":891},5,[427,893,894],{"class":860},")",[427,896,897],{"class":860}," {\n",[427,899,901,905],{"class":429,"line":900},6,[427,902,904],{"class":903},"s7zQu","  try",[427,906,897],{"class":860},[427,908,909,912,915,918,921,923,926,930,933,935,938,940,943,945,948,952,955],{"class":429,"line":97},[427,910,911],{"class":849},"    const",[427,913,914],{"class":873}," geojson",[427,916,917],{"class":860}," =",[427,919,920],{"class":873}," JSON",[427,922,206],{"class":860},[427,924,925],{"class":856},"parse",[427,927,929],{"class":928},"swJcz","(",[427,931,932],{"class":873},"fs",[427,934,206],{"class":860},[427,936,937],{"class":856},"readFileSync",[427,939,929],{"class":928},[427,941,942],{"class":873},"filePath",[427,944,870],{"class":860},[427,946,947],{"class":860}," '",[427,949,951],{"class":950},"sfazB","utf8",[427,953,954],{"class":860},"'",[427,956,957],{"class":928},"))\n",[427,959,961],{"class":429,"line":960},8,[427,962,963],{"class":928},"    \n",[427,965,967],{"class":429,"line":966},9,[427,968,970],{"class":969},"sHwdD","    \u002F\u002F Process in batches to avoid timeouts\n",[427,972,974,976,979,981],{"class":429,"line":973},10,[427,975,911],{"class":849},[427,977,978],{"class":873}," batchSize",[427,980,917],{"class":860},[427,982,984],{"class":983},"sbssI"," 1000\n",[427,986,988,990,993,995,997,999,1002,1004,1007,1009,1012,1015,1018],{"class":429,"line":987},11,[427,989,911],{"class":849},[427,991,992],{"class":873}," features",[427,994,917],{"class":860},[427,996,914],{"class":873},[427,998,206],{"class":860},[427,1000,1001],{"class":873},"features",[427,1003,206],{"class":860},[427,1005,1006],{"class":856},"map",[427,1008,929],{"class":928},[427,1010,1011],{"class":866},"feature",[427,1013,1014],{"class":849}," =>",[427,1016,1017],{"class":928}," (",[427,1019,1020],{"class":860},"{\n",[427,1022,1024,1027,1030,1033,1035,1038],{"class":429,"line":1023},12,[427,1025,1026],{"class":928},"      geom",[427,1028,1029],{"class":860},":",[427,1031,1032],{"class":873}," feature",[427,1034,206],{"class":860},[427,1036,1037],{"class":873},"geometry",[427,1039,1040],{"class":860},",\n",[427,1042,1044,1047,1049,1051,1053,1056,1059],{"class":429,"line":1043},13,[427,1045,1046],{"class":928},"      properties",[427,1048,1029],{"class":860},[427,1050,1032],{"class":873},[427,1052,206],{"class":860},[427,1054,1055],{"class":873},"properties",[427,1057,1058],{"class":860}," ||",[427,1060,1061],{"class":860}," {}\n",[427,1063,1065,1068],{"class":429,"line":1064},14,[427,1066,1067],{"class":860},"    }",[427,1069,957],{"class":928},[427,1071,1073],{"class":429,"line":1072},15,[427,1074,1075],{"emptyLinePlaceholder":65},"\n",[427,1077,1079,1082,1084,1087,1089,1092,1095,1098,1100,1102,1105,1108,1111,1113],{"class":429,"line":1078},16,[427,1080,1081],{"class":873},"    console",[427,1083,206],{"class":860},[427,1085,1086],{"class":856},"log",[427,1088,929],{"class":928},[427,1090,1091],{"class":860},"`",[427,1093,1094],{"class":950},"Importing ",[427,1096,1097],{"class":860},"${",[427,1099,1001],{"class":873},[427,1101,206],{"class":860},[427,1103,1104],{"class":873},"length",[427,1106,1107],{"class":860},"}",[427,1109,1110],{"class":950}," features...",[427,1112,1091],{"class":860},[427,1114,1115],{"class":928},")\n",[427,1117,1119],{"class":429,"line":1118},17,[427,1120,963],{"class":928},[427,1122,1124,1127,1129,1132,1135,1137,1140,1143,1145,1148,1150,1152,1154,1156,1158,1161,1163,1166],{"class":429,"line":1123},18,[427,1125,1126],{"class":903},"    for",[427,1128,1017],{"class":928},[427,1130,1131],{"class":849},"let",[427,1133,1134],{"class":873}," i",[427,1136,917],{"class":860},[427,1138,1139],{"class":983}," 0",[427,1141,1142],{"class":860},";",[427,1144,1134],{"class":873},[427,1146,1147],{"class":860}," \u003C",[427,1149,992],{"class":873},[427,1151,206],{"class":860},[427,1153,1104],{"class":873},[427,1155,1142],{"class":860},[427,1157,1134],{"class":873},[427,1159,1160],{"class":860}," +=",[427,1162,978],{"class":873},[427,1164,1165],{"class":928},") ",[427,1167,1020],{"class":860},[427,1169,1171,1174,1177,1179,1181,1183,1186,1188,1191,1193,1195,1198,1200],{"class":429,"line":1170},19,[427,1172,1173],{"class":849},"      const",[427,1175,1176],{"class":873}," batch",[427,1178,917],{"class":860},[427,1180,992],{"class":873},[427,1182,206],{"class":860},[427,1184,1185],{"class":856},"slice",[427,1187,929],{"class":928},[427,1189,1190],{"class":873},"i",[427,1192,870],{"class":860},[427,1194,1134],{"class":873},[427,1196,1197],{"class":860}," +",[427,1199,978],{"class":873},[427,1201,1115],{"class":928},[427,1203,1205,1207,1210,1213,1215,1218,1221,1223,1226],{"class":429,"line":1204},20,[427,1206,1173],{"class":849},[427,1208,1209],{"class":860}," {",[427,1211,1212],{"class":873}," data",[427,1214,870],{"class":860},[427,1216,1217],{"class":873}," error",[427,1219,1220],{"class":860}," }",[427,1222,917],{"class":860},[427,1224,1225],{"class":903}," await",[427,1227,1228],{"class":873}," supabaseClient\n",[427,1230,1232,1235,1238,1240,1243],{"class":429,"line":1231},21,[427,1233,1234],{"class":860},"        .",[427,1236,1237],{"class":856},"from",[427,1239,929],{"class":928},[427,1241,1242],{"class":873},"tableName",[427,1244,1115],{"class":928},[427,1246,1248,1250,1253,1255,1258],{"class":429,"line":1247},22,[427,1249,1234],{"class":860},[427,1251,1252],{"class":856},"insert",[427,1254,929],{"class":928},[427,1256,1257],{"class":873},"batch",[427,1259,1115],{"class":928},[427,1261,1263],{"class":429,"line":1262},23,[427,1264,1265],{"class":928},"      \n",[427,1267,1269,1272,1274,1277,1279],{"class":429,"line":1268},24,[427,1270,1271],{"class":903},"      if",[427,1273,1017],{"class":928},[427,1275,1276],{"class":873},"error",[427,1278,1165],{"class":928},[427,1280,1020],{"class":860},[427,1282,1284,1287,1289,1291,1293,1295,1298,1300,1302,1304],{"class":429,"line":1283},25,[427,1285,1286],{"class":873},"        console",[427,1288,206],{"class":860},[427,1290,1276],{"class":856},[427,1292,929],{"class":928},[427,1294,954],{"class":860},[427,1296,1297],{"class":950},"Error inserting batch:",[427,1299,954],{"class":860},[427,1301,870],{"class":860},[427,1303,1217],{"class":873},[427,1305,1115],{"class":928},[427,1307,1309],{"class":429,"line":1308},26,[427,1310,1311],{"class":903},"        break\n",[427,1313,1315],{"class":429,"line":1314},27,[427,1316,1317],{"class":860},"      }\n",[427,1319,1321],{"class":429,"line":1320},28,[427,1322,1265],{"class":928},[427,1324,1326,1329,1331,1333,1335,1337,1340,1342,1345,1347,1350,1353,1355,1358,1361,1364,1366,1368,1370,1372,1374,1377,1380,1382,1384,1386,1389,1392],{"class":429,"line":1325},29,[427,1327,1328],{"class":873},"      console",[427,1330,206],{"class":860},[427,1332,1086],{"class":856},[427,1334,929],{"class":928},[427,1336,1091],{"class":860},[427,1338,1339],{"class":950},"Inserted batch ",[427,1341,1097],{"class":860},[427,1343,1344],{"class":873},"Math",[427,1346,206],{"class":860},[427,1348,1349],{"class":856},"floor",[427,1351,1352],{"class":873},"(i",[427,1354,66],{"class":860},[427,1356,1357],{"class":873},"batchSize) ",[427,1359,1360],{"class":860},"+",[427,1362,1363],{"class":983}," 1",[427,1365,1107],{"class":860},[427,1367,66],{"class":950},[427,1369,1097],{"class":860},[427,1371,1344],{"class":873},[427,1373,206],{"class":860},[427,1375,1376],{"class":856},"ceil",[427,1378,1379],{"class":873},"(features",[427,1381,206],{"class":860},[427,1383,1104],{"class":873},[427,1385,66],{"class":860},[427,1387,1388],{"class":873},"batchSize)",[427,1390,1391],{"class":860},"}`",[427,1393,1115],{"class":928},[427,1395,1397],{"class":429,"line":1396},30,[427,1398,1399],{"class":860},"    }\n",[427,1401,1403],{"class":429,"line":1402},31,[427,1404,963],{"class":928},[427,1406,1408,1410,1412,1414,1416,1418,1421,1423],{"class":429,"line":1407},32,[427,1409,1081],{"class":873},[427,1411,206],{"class":860},[427,1413,1086],{"class":856},[427,1415,929],{"class":928},[427,1417,954],{"class":860},[427,1419,1420],{"class":950},"Import completed!",[427,1422,954],{"class":860},[427,1424,1115],{"class":928},[427,1426,1428,1431,1434,1436,1438,1440],{"class":429,"line":1427},33,[427,1429,1430],{"class":860},"  }",[427,1432,1433],{"class":903}," catch",[427,1435,1017],{"class":928},[427,1437,1276],{"class":873},[427,1439,1165],{"class":928},[427,1441,1020],{"class":860},[427,1443,1445,1447,1449,1451,1453,1455,1458,1460,1462,1464],{"class":429,"line":1444},34,[427,1446,1081],{"class":873},[427,1448,206],{"class":860},[427,1450,1276],{"class":856},[427,1452,929],{"class":928},[427,1454,954],{"class":860},[427,1456,1457],{"class":950},"Import failed:",[427,1459,954],{"class":860},[427,1461,870],{"class":860},[427,1463,1217],{"class":873},[427,1465,1115],{"class":928},[427,1467,1469],{"class":429,"line":1468},35,[427,1470,1471],{"class":860},"  }\n",[427,1473,1475],{"class":429,"line":1474},36,[427,1476,1477],{"class":860},"}\n",[85,1479,1480],{},"This function can work perfectly as an Edge Function. However, for the sake of the \"minimum viable\" aspect of the project I just ran it as a local Node.JS application",[80,1482,1484],{"id":1483},"dynamic-vector-tile-querying","Dynamic Vector Tile Querying",[85,1486,1487,1488,1492,1493,1498,1499,1504],{},"Early on the client mentioned the desire to use Mapbox styling on the frontend while avoiding vendor lock-in risks on the data storage. This posed an interesting challenge to me, namely, ",[1489,1490,1491],"em",{},"'how can I render map layers natively on the frontend while keeping the data completely external to Mapbox?'",". Lucky for me, Mapbox allows for ",[121,1494,1497],{"href":1495,"rel":1496},"https:\u002F\u002Fdocs.mapbox.com\u002Fmapbox-gl-js\u002Fapi\u002Fsources\u002F#vectortilesource",[125],"querying external sources"," by sending simple HTTP requests to a given endpoint using the users current bounding box in the query parameters (example later on). The response has to conform to the Mapbox ",[121,1500,1503],{"href":1501,"rel":1502},"https:\u002F\u002Fmapbox.github.io\u002Fvector-tile-spec\u002F",[125],"Vector Tile specification"," (basically Google's Protocol Buffer format).",[85,1506,1507],{},"Using Supabase made things slighlty easier. Using their Edge Functions I could quickly setup a read-only endpoint to receive the incoming Mapbox requests. This Edge Function internally calls an RPC function which does the heavy lifting in terms of geometry fetching and processing. Below you can see a simplified outline main Edge Function body. Notice how I added a custom query parameter allowing for a dynamic dataset selection.",[835,1509,1510],{},[418,1511,1516],{"className":1512,"code":1513,"filename":1514,"language":1515,"meta":89,"style":89},"language-typescript shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","serve(async (req)=>{\n  \u002F\u002F Parses query parameters from url \n  \u002F\u002F example: \u002Ffunctions\u002Fmvt\u002Fdataset123\u002F{z}\u002F{x}\u002F{y}\n  const url = new URL(req.url);\n  const parts = url.pathname.split('\u002F').filter((p)=>p.length > 0);\n  const mvtIndex = parts.indexOf('mvt');\n  const dataset = parts[mvtIndex + 1];\n  const z = Number(parts[mvtIndex + 2]);\n  const x = Number(parts[mvtIndex + 3]);\n  const y = Number(parts[mvtIndex + 4]);\n\n  \u002F\u002F Calls supabase RPC function to query the actual geo data\n  \u002F\u002F Args are parsed earlier from query parameters\n  const { data: vectorTileBinary, error } = await supabase.rpc('get_dynamic_lines_mvt', {\n    dataset,\n    z,\n    x,\n    y\n  });\n \n  \u002F\u002F Returns mvt data in raw bytes\n  return new Response(vectorTileBinary, {\n    status: 200,\n    headers: {\n      ...corsHeaders,\n      'Content-Type': 'application\u002Fvnd.mapbox-vector-tile'\n    }\n  });\n});\n","Supabase Edge Function","typescript",[283,1517,1518,1539,1544,1549,1579,1640,1669,1695,1726,1754,1782,1786,1791,1796,1840,1847,1854,1861,1866,1874,1878,1883,1902,1914,1923,1933,1953,1957,1965],{"__ignoreMap":89},[427,1519,1520,1523,1525,1527,1529,1532,1534,1537],{"class":429,"line":430},[427,1521,1522],{"class":856},"serve",[427,1524,929],{"class":873},[427,1526,850],{"class":849},[427,1528,1017],{"class":860},[427,1530,1531],{"class":866},"req",[427,1533,894],{"class":860},[427,1535,1536],{"class":849},"=>",[427,1538,1020],{"class":860},[427,1540,1541],{"class":429,"line":90},[427,1542,1543],{"class":969},"  \u002F\u002F Parses query parameters from url \n",[427,1545,1546],{"class":429,"line":441},[427,1547,1548],{"class":969},"  \u002F\u002F example: \u002Ffunctions\u002Fmvt\u002Fdataset123\u002F{z}\u002F{x}\u002F{y}\n",[427,1550,1551,1554,1557,1559,1562,1565,1567,1569,1571,1574,1576],{"class":429,"line":191},[427,1552,1553],{"class":849},"  const",[427,1555,1556],{"class":873}," url",[427,1558,917],{"class":860},[427,1560,1561],{"class":860}," new",[427,1563,1564],{"class":856}," URL",[427,1566,929],{"class":928},[427,1568,1531],{"class":873},[427,1570,206],{"class":860},[427,1572,1573],{"class":873},"url",[427,1575,894],{"class":928},[427,1577,1578],{"class":860},";\n",[427,1580,1581,1583,1586,1588,1590,1592,1595,1597,1600,1602,1604,1606,1608,1610,1612,1615,1617,1619,1621,1623,1625,1627,1629,1631,1634,1636,1638],{"class":429,"line":891},[427,1582,1553],{"class":849},[427,1584,1585],{"class":873}," parts",[427,1587,917],{"class":860},[427,1589,1556],{"class":873},[427,1591,206],{"class":860},[427,1593,1594],{"class":873},"pathname",[427,1596,206],{"class":860},[427,1598,1599],{"class":856},"split",[427,1601,929],{"class":928},[427,1603,954],{"class":860},[427,1605,66],{"class":950},[427,1607,954],{"class":860},[427,1609,894],{"class":928},[427,1611,206],{"class":860},[427,1613,1614],{"class":856},"filter",[427,1616,929],{"class":928},[427,1618,929],{"class":860},[427,1620,85],{"class":866},[427,1622,894],{"class":860},[427,1624,1536],{"class":849},[427,1626,85],{"class":873},[427,1628,206],{"class":860},[427,1630,1104],{"class":873},[427,1632,1633],{"class":860}," >",[427,1635,1139],{"class":983},[427,1637,894],{"class":928},[427,1639,1578],{"class":860},[427,1641,1642,1644,1647,1649,1651,1653,1656,1658,1660,1663,1665,1667],{"class":429,"line":900},[427,1643,1553],{"class":849},[427,1645,1646],{"class":873}," mvtIndex",[427,1648,917],{"class":860},[427,1650,1585],{"class":873},[427,1652,206],{"class":860},[427,1654,1655],{"class":856},"indexOf",[427,1657,929],{"class":928},[427,1659,954],{"class":860},[427,1661,1662],{"class":950},"mvt",[427,1664,954],{"class":860},[427,1666,894],{"class":928},[427,1668,1578],{"class":860},[427,1670,1671,1673,1676,1678,1680,1683,1686,1688,1690,1693],{"class":429,"line":97},[427,1672,1553],{"class":849},[427,1674,1675],{"class":873}," dataset",[427,1677,917],{"class":860},[427,1679,1585],{"class":873},[427,1681,1682],{"class":928},"[",[427,1684,1685],{"class":873},"mvtIndex",[427,1687,1197],{"class":860},[427,1689,1363],{"class":983},[427,1691,1692],{"class":928},"]",[427,1694,1578],{"class":860},[427,1696,1697,1699,1702,1704,1707,1709,1712,1714,1716,1718,1721,1724],{"class":429,"line":960},[427,1698,1553],{"class":849},[427,1700,1701],{"class":873}," z",[427,1703,917],{"class":860},[427,1705,1706],{"class":856}," Number",[427,1708,929],{"class":928},[427,1710,1711],{"class":873},"parts",[427,1713,1682],{"class":928},[427,1715,1685],{"class":873},[427,1717,1197],{"class":860},[427,1719,1720],{"class":983}," 2",[427,1722,1723],{"class":928},"])",[427,1725,1578],{"class":860},[427,1727,1728,1730,1733,1735,1737,1739,1741,1743,1745,1747,1750,1752],{"class":429,"line":966},[427,1729,1553],{"class":849},[427,1731,1732],{"class":873}," x",[427,1734,917],{"class":860},[427,1736,1706],{"class":856},[427,1738,929],{"class":928},[427,1740,1711],{"class":873},[427,1742,1682],{"class":928},[427,1744,1685],{"class":873},[427,1746,1197],{"class":860},[427,1748,1749],{"class":983}," 3",[427,1751,1723],{"class":928},[427,1753,1578],{"class":860},[427,1755,1756,1758,1761,1763,1765,1767,1769,1771,1773,1775,1778,1780],{"class":429,"line":973},[427,1757,1553],{"class":849},[427,1759,1760],{"class":873}," y",[427,1762,917],{"class":860},[427,1764,1706],{"class":856},[427,1766,929],{"class":928},[427,1768,1711],{"class":873},[427,1770,1682],{"class":928},[427,1772,1685],{"class":873},[427,1774,1197],{"class":860},[427,1776,1777],{"class":983}," 4",[427,1779,1723],{"class":928},[427,1781,1578],{"class":860},[427,1783,1784],{"class":429,"line":987},[427,1785,1075],{"emptyLinePlaceholder":65},[427,1787,1788],{"class":429,"line":1023},[427,1789,1790],{"class":969},"  \u002F\u002F Calls supabase RPC function to query the actual geo data\n",[427,1792,1793],{"class":429,"line":1043},[427,1794,1795],{"class":969},"  \u002F\u002F Args are parsed earlier from query parameters\n",[427,1797,1798,1800,1802,1804,1806,1809,1811,1813,1815,1817,1819,1822,1824,1827,1829,1831,1834,1836,1838],{"class":429,"line":1064},[427,1799,1553],{"class":849},[427,1801,1209],{"class":860},[427,1803,1212],{"class":928},[427,1805,1029],{"class":860},[427,1807,1808],{"class":873}," vectorTileBinary",[427,1810,870],{"class":860},[427,1812,1217],{"class":873},[427,1814,1220],{"class":860},[427,1816,917],{"class":860},[427,1818,1225],{"class":903},[427,1820,1821],{"class":873}," supabase",[427,1823,206],{"class":860},[427,1825,1826],{"class":856},"rpc",[427,1828,929],{"class":928},[427,1830,954],{"class":860},[427,1832,1833],{"class":950},"get_dynamic_lines_mvt",[427,1835,954],{"class":860},[427,1837,870],{"class":860},[427,1839,897],{"class":860},[427,1841,1842,1845],{"class":429,"line":1072},[427,1843,1844],{"class":873},"    dataset",[427,1846,1040],{"class":860},[427,1848,1849,1852],{"class":429,"line":1078},[427,1850,1851],{"class":873},"    z",[427,1853,1040],{"class":860},[427,1855,1856,1859],{"class":429,"line":1118},[427,1857,1858],{"class":873},"    x",[427,1860,1040],{"class":860},[427,1862,1863],{"class":429,"line":1123},[427,1864,1865],{"class":873},"    y\n",[427,1867,1868,1870,1872],{"class":429,"line":1170},[427,1869,1430],{"class":860},[427,1871,894],{"class":928},[427,1873,1578],{"class":860},[427,1875,1876],{"class":429,"line":1204},[427,1877,874],{"class":928},[427,1879,1880],{"class":429,"line":1231},[427,1881,1882],{"class":969},"  \u002F\u002F Returns mvt data in raw bytes\n",[427,1884,1885,1888,1890,1893,1895,1898,1900],{"class":429,"line":1247},[427,1886,1887],{"class":903},"  return",[427,1889,1561],{"class":860},[427,1891,1892],{"class":856}," Response",[427,1894,929],{"class":928},[427,1896,1897],{"class":873},"vectorTileBinary",[427,1899,870],{"class":860},[427,1901,897],{"class":860},[427,1903,1904,1907,1909,1912],{"class":429,"line":1262},[427,1905,1906],{"class":928},"    status",[427,1908,1029],{"class":860},[427,1910,1911],{"class":983}," 200",[427,1913,1040],{"class":860},[427,1915,1916,1919,1921],{"class":429,"line":1268},[427,1917,1918],{"class":928},"    headers",[427,1920,1029],{"class":860},[427,1922,897],{"class":860},[427,1924,1925,1928,1931],{"class":429,"line":1283},[427,1926,1927],{"class":860},"      ...",[427,1929,1930],{"class":873},"corsHeaders",[427,1932,1040],{"class":860},[427,1934,1935,1938,1941,1943,1945,1947,1950],{"class":429,"line":1308},[427,1936,1937],{"class":860},"      '",[427,1939,1940],{"class":928},"Content-Type",[427,1942,954],{"class":860},[427,1944,1029],{"class":860},[427,1946,947],{"class":860},[427,1948,1949],{"class":950},"application\u002Fvnd.mapbox-vector-tile",[427,1951,1952],{"class":860},"'\n",[427,1954,1955],{"class":429,"line":1314},[427,1956,1399],{"class":860},[427,1958,1959,1961,1963],{"class":429,"line":1320},[427,1960,1430],{"class":860},[427,1962,894],{"class":928},[427,1964,1578],{"class":860},[427,1966,1967,1969,1971],{"class":429,"line":1325},[427,1968,1107],{"class":860},[427,1970,894],{"class":873},[427,1972,1578],{"class":860},[85,1974,1975,1976,1978],{},"Now for the interesting part. The PostgreSQL ",[283,1977,1833],{}," RPC function was made to have very rudamentary API, as shown below:",[418,1980,1985],{"className":1981,"code":1982,"filename":1983,"language":1984,"meta":89,"style":89},"language-ts shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","CREATE OR REPLACE FUNCTION public.get_dynamic_lines_mvt(\n  dataset text,\n  z integer,\n  x integer,\n  y integer\n) RETURNS bytea\n","PostgreSQL","ts",[283,1986,1987,1998,2005,2012,2019,2024],{"__ignoreMap":89},[427,1988,1989,1992,1994,1996],{"class":429,"line":430},[427,1990,1991],{"class":873},"CREATE OR REPLACE FUNCTION public",[427,1993,206],{"class":860},[427,1995,1833],{"class":856},[427,1997,861],{"class":873},[427,1999,2000,2003],{"class":429,"line":90},[427,2001,2002],{"class":873},"  dataset text",[427,2004,1040],{"class":860},[427,2006,2007,2010],{"class":429,"line":441},[427,2008,2009],{"class":873},"  z integer",[427,2011,1040],{"class":860},[427,2013,2014,2017],{"class":429,"line":191},[427,2015,2016],{"class":873},"  x integer",[427,2018,1040],{"class":860},[427,2020,2021],{"class":429,"line":891},[427,2022,2023],{"class":873},"  y integer\n",[427,2025,2026],{"class":429,"line":900},[427,2027,2028],{"class":873},") RETURNS bytea\n",[85,2030,2031],{},"What actually happens inside of the function body can best be described in a handful of procedural steps.",[85,2033,2034],{},"Firstly, the sent coordinates values (x, y, z) are translated into rectangular polygon coordinates representing the user's bounding box.",[418,2036,2038],{"className":1981,"code":2037,"filename":1983,"language":1984,"meta":89,"style":89},"bounds3857 := ST_TileEnvelope(z, x, y);\n",[283,2039,2040],{"__ignoreMap":89},[427,2041,2042,2046,2049,2052,2055,2057,2059,2061,2064],{"class":429,"line":430},[427,2043,2045],{"class":2044},"sBMFI","bounds3857",[427,2047,2048],{"class":860}," :=",[427,2050,2051],{"class":856}," ST_TileEnvelope",[427,2053,2054],{"class":873},"(z",[427,2056,870],{"class":860},[427,2058,1732],{"class":873},[427,2060,870],{"class":860},[427,2062,2063],{"class":873}," y)",[427,2065,1578],{"class":860},[2067,2068,2069],"note",{},[85,2070,2071,2074,2075],{},[283,2072,2073],{},"bounds385"," is generated and later injected at runtime using alias ",[283,2076,2077],{},"$1",[85,2079,2080],{},"Next, geometries that land outside of the requested tile are filtered out before moving on to expensive intersection computations.",[418,2082,2084],{"className":1981,"code":2083,"filename":1983,"language":1984,"meta":89,"style":89},"WITH filtered AS (\n  SELECT ST_Transform(wkb_geometry, 3857) AS geom3857 FROM %I.%I\n  WHERE ST_Transform(wkb_geometry, 3857) && $1::geometry\n  AND ST_Intersects(ST_Transform(wkb_geometry, 3857), $1::geometry)\n),\n",[283,2085,2086,2097,2128,2149,2174],{"__ignoreMap":89},[427,2087,2088,2091,2094],{"class":429,"line":430},[427,2089,2090],{"class":873},"WITH filtered ",[427,2092,2093],{"class":856},"AS",[427,2095,2096],{"class":873}," (\n",[427,2098,2099,2102,2105,2108,2110,2113,2116,2119,2122,2125],{"class":429,"line":90},[427,2100,2101],{"class":873},"  SELECT ",[427,2103,2104],{"class":856},"ST_Transform",[427,2106,2107],{"class":873},"(wkb_geometry",[427,2109,870],{"class":860},[427,2111,2112],{"class":983}," 3857",[427,2114,2115],{"class":873},") AS geom3857 FROM ",[427,2117,2118],{"class":860},"%",[427,2120,2121],{"class":873},"I",[427,2123,2124],{"class":860},".%",[427,2126,2127],{"class":873},"I\n",[427,2129,2130,2133,2135,2137,2139,2141,2143,2146],{"class":429,"line":441},[427,2131,2132],{"class":873},"  WHERE ",[427,2134,2104],{"class":856},[427,2136,2107],{"class":873},[427,2138,870],{"class":860},[427,2140,2112],{"class":983},[427,2142,1165],{"class":873},[427,2144,2145],{"class":860},"&&",[427,2147,2148],{"class":873}," $1::geometry\n",[427,2150,2151,2154,2157,2159,2161,2163,2165,2167,2169,2171],{"class":429,"line":191},[427,2152,2153],{"class":873},"  AND ",[427,2155,2156],{"class":856},"ST_Intersects",[427,2158,929],{"class":873},[427,2160,2104],{"class":856},[427,2162,2107],{"class":873},[427,2164,870],{"class":860},[427,2166,2112],{"class":983},[427,2168,894],{"class":873},[427,2170,870],{"class":860},[427,2172,2173],{"class":873}," $1::geometry)\n",[427,2175,2176,2178],{"class":429,"line":891},[427,2177,894],{"class":873},[427,2179,1040],{"class":860},[85,2181,2182],{},"To further help performance, before any of the expensive geometry checks are done, the caller's z-value gets evaluated and classified. This is done in oreder to not overfetch very detailed geometry data for a large zoomed-out bounding box when geometry resolution is least important.",[418,2184,2186],{"className":1981,"code":2185,"filename":1983,"language":1984,"meta":89,"style":89},"simplification := CASE\n  WHEN z \u003C= 5  THEN 80\n  WHEN z \u003C= 10 THEN 15\n  ELSE 0\nEND;\n",[283,2187,2188,2198,2215,2230,2238],{"__ignoreMap":89},[427,2189,2190,2193,2195],{"class":429,"line":430},[427,2191,2192],{"class":2044},"simplification",[427,2194,2048],{"class":860},[427,2196,2197],{"class":873}," CASE\n",[427,2199,2200,2203,2206,2209,2212],{"class":429,"line":90},[427,2201,2202],{"class":873},"  WHEN z ",[427,2204,2205],{"class":860},"\u003C=",[427,2207,2208],{"class":983}," 5",[427,2210,2211],{"class":873},"  THEN ",[427,2213,2214],{"class":983},"80\n",[427,2216,2217,2219,2221,2224,2227],{"class":429,"line":441},[427,2218,2202],{"class":873},[427,2220,2205],{"class":860},[427,2222,2223],{"class":983}," 10",[427,2225,2226],{"class":873}," THEN ",[427,2228,2229],{"class":983},"15\n",[427,2231,2232,2235],{"class":429,"line":191},[427,2233,2234],{"class":873},"  ELSE ",[427,2236,2237],{"class":983},"0\n",[427,2239,2240,2243],{"class":429,"line":891},[427,2241,2242],{"class":873},"END",[427,2244,1578],{"class":860},[2067,2246,2247],{},[85,2248,2249,2250,2252,2253,2256,2257],{},"Just as ",[283,2251,2073],{},", ",[283,2254,2255],{},"simplifications"," is also generated and injected at runtime using alias ",[283,2258,2259],{},"$2",[85,2261,2262,2263,2266,2267,206],{},"This value is subsequently used as an argument to clip and simplify the data to varying degrees. At the same time, ",[283,2264,2265],{},"ST_AsMVTGeom"," turns the simplified geometries from \"Well-Known Binary\" representation found in the actual table into the coordinate space of a MVT tile stored in ",[283,2268,2269],{},"mvtgeom",[418,2271,2273],{"className":1981,"code":2272,"filename":1983,"language":1984,"meta":89,"style":89},"mvtgeom AS (\n  SELECT\n    ST_AsMVTGeom(\n      CASE\n        WHEN $2 > 0\n          THEN ST_SimplifyPreserveTopology(geom3857, $2)\n        ELSE geom3857\n      END,\n      $1, 4096, 0, true\n    ) AS geom\n  FROM filtered\n)\n",[283,2274,2275,2284,2289,2296,2301,2312,2328,2333,2340,2360,2365,2370],{"__ignoreMap":89},[427,2276,2277,2280,2282],{"class":429,"line":430},[427,2278,2279],{"class":873},"mvtgeom ",[427,2281,2093],{"class":856},[427,2283,2096],{"class":873},[427,2285,2286],{"class":429,"line":90},[427,2287,2288],{"class":873},"  SELECT\n",[427,2290,2291,2294],{"class":429,"line":441},[427,2292,2293],{"class":856},"    ST_AsMVTGeom",[427,2295,861],{"class":873},[427,2297,2298],{"class":429,"line":191},[427,2299,2300],{"class":873},"      CASE\n",[427,2302,2303,2306,2309],{"class":429,"line":891},[427,2304,2305],{"class":873},"        WHEN $2 ",[427,2307,2308],{"class":860},">",[427,2310,2311],{"class":983}," 0\n",[427,2313,2314,2317,2320,2323,2325],{"class":429,"line":900},[427,2315,2316],{"class":873},"          THEN ",[427,2318,2319],{"class":856},"ST_SimplifyPreserveTopology",[427,2321,2322],{"class":873},"(geom3857",[427,2324,870],{"class":860},[427,2326,2327],{"class":873}," $2)\n",[427,2329,2330],{"class":429,"line":97},[427,2331,2332],{"class":873},"        ELSE geom3857\n",[427,2334,2335,2338],{"class":429,"line":960},[427,2336,2337],{"class":873},"      END",[427,2339,1040],{"class":860},[427,2341,2342,2345,2347,2350,2352,2354,2356],{"class":429,"line":966},[427,2343,2344],{"class":873},"      $1",[427,2346,870],{"class":860},[427,2348,2349],{"class":983}," 4096",[427,2351,870],{"class":860},[427,2353,1139],{"class":983},[427,2355,870],{"class":860},[427,2357,2359],{"class":2358},"sfNiH"," true\n",[427,2361,2362],{"class":429,"line":973},[427,2363,2364],{"class":873},"    ) AS geom\n",[427,2366,2367],{"class":429,"line":987},[427,2368,2369],{"class":873},"  FROM filtered\n",[427,2371,2372],{"class":429,"line":1023},[427,2373,1115],{"class":873},[85,2375,2376],{},"Finally, filtered and simplified data is serialized into raw mvt bytes to be returned to the caller and later the user.",[418,2378,2380],{"className":1981,"code":2379,"filename":1983,"language":1984,"meta":89,"style":89},"SELECT ST_AsMVT(\n  q,\n  'lines',\n  4096,\n  'geom'\n)\nFROM (\n  SELECT geom\n  FROM mvtgeom\n) q;\n",[283,2381,2382,2392,2399,2411,2418,2427,2431,2438,2443,2448],{"__ignoreMap":89},[427,2383,2384,2387,2390],{"class":429,"line":430},[427,2385,2386],{"class":873},"SELECT ",[427,2388,2389],{"class":856},"ST_AsMVT",[427,2391,861],{"class":873},[427,2393,2394,2397],{"class":429,"line":90},[427,2395,2396],{"class":873},"  q",[427,2398,1040],{"class":860},[427,2400,2401,2404,2407,2409],{"class":429,"line":441},[427,2402,2403],{"class":860},"  '",[427,2405,2406],{"class":950},"lines",[427,2408,954],{"class":860},[427,2410,1040],{"class":860},[427,2412,2413,2416],{"class":429,"line":191},[427,2414,2415],{"class":983},"  4096",[427,2417,1040],{"class":860},[427,2419,2420,2422,2425],{"class":429,"line":891},[427,2421,2403],{"class":860},[427,2423,2424],{"class":950},"geom",[427,2426,1952],{"class":860},[427,2428,2429],{"class":429,"line":900},[427,2430,1115],{"class":873},[427,2432,2433,2436],{"class":429,"line":97},[427,2434,2435],{"class":856},"FROM",[427,2437,2096],{"class":873},[427,2439,2440],{"class":429,"line":960},[427,2441,2442],{"class":873},"  SELECT geom\n",[427,2444,2445],{"class":429,"line":966},[427,2446,2447],{"class":873},"  FROM mvtgeom\n",[427,2449,2450,2453],{"class":429,"line":973},[427,2451,2452],{"class":873},") q",[427,2454,1578],{"class":860},[85,2456,2457,2458,2463],{},"As you may already have noticed, in order to achieve much of the geometry translation and serialization functionallity I made use of PostGIS' exhaustive list of Special Functions. Perhaps this post is not the ideal place to go into detail on them so I highly recommend checking out the official ",[121,2459,2462],{"href":2460,"rel":2461},"https:\u002F\u002Fpostgis.net\u002Fdocs\u002FPostGIS_Special_Functions_Index.html",[125],"PostGIS Special Function Index"," in case you'd like to know more. The main Special Functions I ended up using were the following:",[750,2465,2466],{},[418,2467,2469],{"className":1981,"code":2468,"language":1984,"meta":89,"style":89},"ST_TileEnvelope()\nST_Transform()\nST_SimplifyPreserveTopology()\nST_Intersects()\nST_AsMVTGeom()\nST_AsMVT()\n",[283,2470,2471,2479,2485,2491,2497,2503],{"__ignoreMap":89},[427,2472,2473,2476],{"class":429,"line":430},[427,2474,2475],{"class":856},"ST_TileEnvelope",[427,2477,2478],{"class":873},"()\n",[427,2480,2481,2483],{"class":429,"line":90},[427,2482,2104],{"class":856},[427,2484,2478],{"class":873},[427,2486,2487,2489],{"class":429,"line":441},[427,2488,2319],{"class":856},[427,2490,2478],{"class":873},[427,2492,2493,2495],{"class":429,"line":191},[427,2494,2156],{"class":856},[427,2496,2478],{"class":873},[427,2498,2499,2501],{"class":429,"line":891},[427,2500,2265],{"class":856},[427,2502,2478],{"class":873},[427,2504,2505,2507],{"class":429,"line":900},[427,2506,2389],{"class":856},[427,2508,2478],{"class":873},[490,2510,2511],{},"html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"title":89,"searchDepth":90,"depth":90,"links":2513},[2514,2515,2516,2517,2518],{"id":82,"depth":441,"text":83},{"id":747,"depth":90,"text":748},{"id":760,"depth":90,"text":761},{"id":767,"depth":90,"text":768},{"id":788,"depth":90,"text":789},"#137E6E","2025-10-01","Client project where I was tasked with building a full-stack energy grid visualization platform by ingesting open municipality data into a PostGIS databse.","content\u002Ffocal-grid-banner.png",{},"Full-Stack GIS App",{"title":36,"description":2521},[2527,2528,2529,2530],"TypeScript","Nuxt.js","Mapbox SDK","PostGIS","ZKB6mbMU6LBl33krBX0ge03owrD65U-ujLG04jc7zuo",1786205225832]