-
Notifications
You must be signed in to change notification settings - Fork 0
/
mapbox-single-tile.html
140 lines (124 loc) · 10.6 KB
/
mapbox-single-tile.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
<!DOCTYPE html>
<html lang="en" prefix="og: http://ogp.me/ns#">
<head>
<link href="http://gmpg.org/xfn/11" rel="profile">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<!-- Metadata -->
<meta name="description" content="">
<meta property="og:description" content="">
<meta property="og:title" content="Mapbox Single Tile" />
<meta property="og:type" content="article" />
<meta property="og:url" content="https://darrenwiens.github.io/mapbox-single-tile.html" />
<meta property="og:image" content="https://darrenwiens.github.io/images/face.png" />
<!-- Enable responsiveness on mobile devices-->
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1">
<title>Darren Wiens</title>
<!-- CSS -->
<link href="//fonts.googleapis.com/" rel="dns-prefetch">
<link href="//fonts.googleapis.com/css?family=Droid+Serif:400,700,400italic|Abril+Fatface|PT+Sans:400,400italic,700&subset=latin,latin-ext" rel="stylesheet">
<link rel="stylesheet" href="https://darrenwiens.github.io/theme/css/poole.css" />
<link rel="stylesheet" href="https://darrenwiens.github.io/theme/css/hyde.css" />
<link rel="stylesheet" href="https://darrenwiens.github.io/theme/css/syntax.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/css/fork-awesome.min.css" crossorigin="anonymous">
<!-- Feeds -->
<!-- Analytics -->
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-9664J8VT36"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-9664J8VT36');
</script>
<!-- Ads -->
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-2681534008399266"
crossorigin="anonymous"></script>
</head>
<body class="theme-base-08">
<div class="sidebar">
<div class="container sidebar-sticky">
<div class="sidebar-about">
<h1>
<a href="/">
<img class="profile-picture" src="https://darrenwiens.github.io/images/face.png">
Darren Wiens
</a>
</h1>
<p class="lead"></p>
<p class="lead"> </p>
<p></p>
</div>
<ul class="sidebar-nav">
</ul>
<nav class="sidebar-social">
<a class="sidebar-social-item" href="http://twitter.com/dkwiens" target="_blank">
<i class="fa fa-twitter"></i>
</a>
<a class="sidebar-social-item" href="https://fosstodon.org/@dkwiens" target="_blank">
<i class="fa fa-mastodon"></i>
</a>
<a class="sidebar-social-item" href="http://github.com/darrenwiens" target="_blank">
<i class="fa fa-github"></i>
</a>
<a style="display:none" class="sidebar-social-item" href="https://darrenwiens.github.io/None">
<i class="fa fa-rss"></i>
</a>
<a rel="me" style="display:none" href="https://fosstodon.org/@dkwiens">Mastodon</a>
</nav>
</div>
</div> <div class="content container">
<div class="post">
<h1 class="post-title">Mapbox Single Tile</h1>
<span class="post-date">Tue 29 November 2022</span>
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">TIL it's more difficult to show a single tile than all the tiles in <a href="https://twitter.com/Mapbox?ref_src=twsrc%5Etfw">@Mapbox</a> 😅 <a href="https://t.co/wFWQBu5X4g">pic.twitter.com/wFWQBu5X4g</a></p>— Darren Wiens (@dkwiens) <a href="https://twitter.com/dkwiens/status/1597429720823783424?ref_src=twsrc%5Etfw">November 29, 2022</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
<p>I've been poking around in <a href="https://docs.mapbox.com/mapbox-gl-js/guides/">Mapbox GL JS</a> lately (e.g. <a href="https://darrenwiens.github.io/3dep-elevation-source-for-mapbox.html">3DEP Elevation Source for Mapbox</a>), and am having fun trying to do weird things by inserting a <a href="https://fastapi.tiangolo.com/">FastAPI</a> call where normal tile loads would go.</p>
<p>For this post, the goal is to show only a single imagery and terrain map tile at a given pair of coordinates and zoom level. In normal use, Mapbox shows all necessary imagery and terrain tiles (<a href="https://docs.mapbox.com/mapbox-gl-js/example/add-terrain/">example</a>). Too easy! It's much harder to show only a single tile, but as they say, less is more (more effort, I assume).</p>
<p>The first step is to understand how the map decides which tiles to load when the time is right. If you open the network tab in your browser dev tools, you'll see a stream of calls back to Mapbox HQ for tiles. Each url contains the name of the requested data source plus a few numbers (e.g. <code>.../mapbox.satellite/15/5982/13231...</code>). The numbers follow the typical <a href="https://wiki.openstreetmap.org/wiki/Slippy_map">Slippy Map</a> convention of <code>{z}/{x}/{y}</code>, where <code>z</code>, <code>x</code>, and <code>y</code> are <a href="https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames">Slippy Map Tile Names</a> (go ahead and bookmark that site). Just by knowing the request, the tile elves working at Mapbox are able to assemble the necessary tile and send it back to us, where our client is smart enough to display where they should.</p>
<p>Okay, but how do we bust into this process to do <em>something</em> around the time a tile would normally load? Sadly, there's no such event as <code>tile_is_about_to_be_requested</code>! But do not despair, there is a super-handy <a href="https://docs.mapbox.com/mapbox-gl-js/api/properties/#requestparameters">transformRequest</a> option on the Map object, which allows us to do <em>things</em> when requests happen. Want to add some non-default headers to the requests? Sure! Want to print the requests to console for some reason? Okay!</p>
<p>In our case, we're going to listen for a couple things:</p>
<ul>
<li>is the request for the imagery tile we want to show? If so, show it. If not, reroute the request to a null url, resulting in a 404 error (for the sake of your blood pressure, do not open the dev tools console to see all the errors)</li>
<li>is the request for a terrain tile? If so, reroute it to a custom API to do <em>other things</em>.</li>
</ul>
<p>The pseudocode looks like this:</p>
<div class="highlight"><pre><span></span><code><span class="nv">map</span> <span class="o">=</span> <span class="nv">new</span> <span class="nv">mapboxgl</span>.<span class="nv">Map</span><span class="ss">(</span>{
... <span class="nv">map</span> <span class="nv">options</span>
<span class="nv">transformRequest</span>: <span class="ss">(</span><span class="nv">url</span>, <span class="nv">resourceType</span><span class="ss">)</span> <span class="o">=></span> {
<span class="k">if</span> <span class="nv">this</span> <span class="nv">is</span> <span class="nv">a</span> <span class="nv">request</span> <span class="k">for</span> <span class="nv">an</span> <span class="nv">imagery</span> <span class="nv">tile</span> {
<span class="k">if</span> <span class="nv">the</span> <span class="nv">tile</span> <span class="nv">coordinates</span> <span class="k">do</span> <span class="nv">not</span> <span class="nv">match</span> <span class="nv">our</span> <span class="nv">desired</span> <span class="nv">tile</span> <span class="nv">coordinates</span> {
<span class="k">return</span> <span class="nv">a</span> <span class="nv">null</span> <span class="nv">URL</span>
} <span class="k">else</span> {
<span class="k">return</span> <span class="nv">the</span> <span class="nv">original</span> <span class="nv">url</span>
}
}
<span class="k">if</span> <span class="nv">this</span> <span class="nv">is</span> <span class="nv">a</span> <span class="nv">request</span> <span class="k">for</span> <span class="nv">a</span> <span class="nv">terrain</span> <span class="nv">tile</span> {
<span class="k">return</span> <span class="nv">a</span> <span class="nv">URL</span> <span class="nv">to</span> <span class="nv">a</span> <span class="nv">custom</span> <span class="nv">API</span> <span class="nv">that</span> <span class="nv">will</span> <span class="nv">process</span> <span class="nv">the</span> <span class="nv">terrain</span> <span class="nv">further</span>
}
}
}<span class="ss">)</span>
</code></pre></div>
<p>The real code looks like <a href="https://gist.github.com/darrenwiens/222c0d0404540afeeea922a786e66420#file-index-html">this</a>.</p>
<p>Okay, so that more or less takes care of the imagery requests, but the terrain is a little trickier. Behind the scenes, Mapbox GL JS makes a lot of choices for which terrain tiles to show depending on the view angle of the map (e.g. far away tiles are requested at lower resolution and zoom level), so it's hard to know which tiles are going to be requested up front. My solution is to stand up a custom API that accomodates all these requests, uses <a href="https://rasterio.readthedocs.io/en/latest/#">rasterio</a> to fetch the necessary tiles from <a href="https://registry.opendata.aws/terrain-tiles/">AWS Terrain Tiles</a>, clips out only the necessary pixels that overlap the imagery, encodes to Terrarium encoding, and returns the processed tile to be displayed by the map client. It's a little slow and brute-force-y, but it seems to do the job, so just set your expectations accordingly.</p>
<p>The API code is <a href="https://gist.github.com/darrenwiens/222c0d0404540afeeea922a786e66420#file-main-py">here</a>. Deploy it<sup><a href="#deploy">1</a></sup>, point the terrain transformRequest url to it, and if all goes well, the whole thing should look like this:</p>
<p><img alt="Mapbox single tile" src="https://darrenwiens.github.io/images/mapbox_single_tile_ui.gif"></p>
<p>The end! Have a look at the code and see if you can make sense of it. Let me know on <a href="https://twitter.com/dkwiens">Twitter</a> or <a href="https://fosstodon.org/@dkwiens">Mastodon</a> how it goes!</p>
<p><sup><a name="deploy">1</a></sup>You can start the API by:</p>
<ol>
<li><code>pip install</code> the various dependencies (e.g. <code>pip install "fastapi[all]" rasterio shapely pillow</code> etc.)</li>
<li>Start the API with <code>uvicorn main:app --reload</code></li>
</ol>
<span class="post-tags">
Tags:
<ul>
<li><a href="https://darrenwiens.github.io/tag/javascript.html">javascript</a></li>
<li><a href="https://darrenwiens.github.io/tag/web-app.html">web app</a></li>
<li><a href="https://darrenwiens.github.io/tag/mapbox.html">mapbox</a></li>
<li><a href="https://darrenwiens.github.io/tag/fastapi.html">fastapi</a></li>
</ul>
</span>
</div>
</div>
</body>
</html>