{"id":1231,"date":"2026-06-29T21:33:03","date_gmt":"2026-06-29T21:33:03","guid":{"rendered":"https:\/\/geteach.com\/blog\/?p=1231"},"modified":"2026-06-29T21:33:04","modified_gmt":"2026-06-29T21:33:04","slug":"building-a-3d-world-cup-stadium-map-with-google-maps-street-view","status":"publish","type":"post","link":"https:\/\/geteach.com\/blog\/2026\/06\/29\/building-a-3d-world-cup-stadium-map-with-google-maps-street-view\/","title":{"rendered":"Building a 3D World Cup Stadium Map with Google Maps &#038; Street View"},"content":{"rendered":"<p><h1>Google Maps 3D API &#8211; World Cup 2026: <a href=\"https:\/\/geteach.com\/maps3d\/worldcup2026\/\" target=\"_blank\" rel=\"noopener\">https:\/\/geteach.com\/maps3d\/worldcup2026\/<\/a><\/h1>\n<\/p>\n<div style=\"position:relative; padding-bottom:56.25%; height:0; overflow:hidden;\"><iframe style=\"position:absolute; top:0; left:0; width:100%; height:100%;\" src=\"https:\/\/www.youtube.com\/embed\/_P84VYCAPJQ?si=1llQU2eWk3pwjojt\" title=\"Intro World Cup 2026\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\" referrerpolicy=\"strict-origin-when-cross-origin\" allowfullscreen><\/iframe><\/div>\n<p>&nbsp;<\/p>\n<p>For the 2026 World Cup, I wanted a way to explore all 16 host cities across the US, Canada, and Mexico \u2014 not as a flat map with pins, but as something you could actually <em>fly into<\/em>. The result uses Google&#8217;s new <strong>Maps 3D JavaScript API<\/strong> alongside classic <strong>Street View<\/strong>, stitched together with a custom sidebar I built from scratch.<\/p>\n<p>Here&#8217;s how the pieces fit.<\/p>\n<h2>The 3D layer<\/h2>\n<div style=\"position:relative; padding-bottom:56.25%; height:0; overflow:hidden;\"><iframe style=\"position:absolute; top:0; left:0; width:100%; height:100%;\" src=\"https:\/\/www.youtube.com\/embed\/AvQp1hiC4Ao?si=jkshp5U7c7dBpFF3\" title=\"World Cup 2026 - 3D View\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\" referrerpolicy=\"strict-origin-when-cross-origin\" allowfullscreen><\/iframe><\/div>\n<p>&nbsp;<\/p>\n<p>The map itself is a <code>Map3DElement<\/code> \u2014 part of the <code>maps3d<\/code> library and loaded dynamically via <code>google.maps.importLibrary<\/code>. It opens in satellite mode, tilted flat, looking down at North America:<\/p>\n<pre><code>let map = new Map3DElement({\n  center: { lat: 36.337, lng: -63.598, altitude: 2233 },\n  mode: MapMode.SATELLITE,\n  tilt: 0,\n  range: 63167767\n});<\/code><\/pre>\n<p>Each stadium gets a <code>Marker3DInteractiveElement<\/code> with a custom-colored <code>PinElement<\/code> \u2014 red for Canada, green for Mexico, blue for the US \u2014 clickable to jump straight to that city.<\/p>\n<h2>The fun part: flying the camera<\/h2>\n<div style=\"position:relative; padding-bottom:56.25%; height:0; overflow:hidden;\"><iframe style=\"position:absolute; top:0; left:0; width:100%; height:100%;\" src=\"https:\/\/www.youtube.com\/embed\/Y493NYPDRFk?si=4kXjdPsfdcYMsVb6\" title=\"World Cup 2026 - FlyTo\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\" referrerpolicy=\"strict-origin-when-cross-origin\" allowfullscreen><\/iframe><\/div>\n<p>&nbsp;<\/p>\n<p>The signature feel of the site comes from <code>map.flyCameraTo()<\/code>, which animates the camera between two positions. I wrote a small <code>Flyto()<\/code> wrapper that takes a center point, heading, tilt, range, roll, and duration, and every stadium page in the sidebar triggers its own flight when selected.<\/p>\n<p>The hard part wasn&#8217;t the API \u2014 it was <em>finding<\/em> good camera angles. There&#8217;s no UI for &#8220;drag the camera until it looks right,&#8221; so I bound a keyboard shortcut (Ctrl+Space) that dumps the map&#8217;s current center, heading, tilt, range, and roll to the console. I&#8217;d fly around manually, hit the shortcut when I found an angle I liked, and paste the values straight into the stadium&#8217;s entry. It&#8217;s a crude workflow, but it turned manual camera-hunting into something fast enough to do for all 16 stadiums in one sitting.<\/p>\n<h2>Street View, the old-school way<\/h2>\n<div style=\"position:relative; padding-bottom:56.25%; height:0; overflow:hidden;\"><iframe style=\"position:absolute; top:0; left:0; width:100%; height:100%;\" src=\"https:\/\/www.youtube.com\/embed\/WMJm9XvgrXY?si=cDrQIZtuXTl_Pkzk\" title=\"YouTube video player\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\" referrerpolicy=\"strict-origin-when-cross-origin\" allowfullscreen><\/iframe><\/div>\n<p>&nbsp;<\/p>\n<p>Each stadium page also embeds a classic <code>StreetViewPanorama<\/code> \u2014 no 3D involved, just the regular Street View service. This had its own quirks:<\/p>\n<ul>\n<li><strong>Saved <code>panoId<\/code>s aren&#8217;t permanent.<\/strong> Google occasionally retires or swaps panorama IDs, so I built a fallback: try the saved <code>panoId<\/code> first, and if that fails, fall back to a <code>getPanorama()<\/code> lookup by lat\/lng with a small search radius.<\/li>\n<li><strong>Hidden containers don&#8217;t render.<\/strong> Street View panoramas measure their container&#8217;s dimensions on init \u2014 if you initialize one while its parent is <code>display: none<\/code> (which mine were, since they live in collapsed sidebar pages), you get a blank gray box. I solved this with a visibility poll: each panorama waits, checking every 100ms, until its container actually has rendered dimensions before initializing.<\/li>\n<\/ul>\n<h2>The sidebar, built by hand<\/h2>\n<p>Rather than reach for a UI framework, the entire sidebar \u2014 table of contents, pagination arrows, collapse\/expand, label and pin toggles \u2014 is vanilla JS syncing against a single source of truth: a <code>currentpage<\/code> attribute on a custom <code>&lt;sidebar-pagination&gt;<\/code> element. Every interaction (clicking a TOC item, clicking a map pin, hitting next\/prev) funnels through that one attribute, which then triggers the camera flight and swaps the visible panel. It&#8217;s not React-level elegant, but for 16 fixed pages it kept the logic easy to trace.<\/p>\n<h2>A known rough edge: rapid clicking<\/h2>\n<p>One thing worth flagging if you build something similar: both APIs can get unstable if a user clicks around quickly \u2014 jumping between stadiums, mashing the next\/prev buttons, or rapid-toggling the Street View panel. This doesn&#8217;t seem to be a bug in my code so much as something baked into the APIs themselves right now.<\/p>\n<p>For the 3D side, the Maps 3D API is still in <strong>Preview<\/strong> (not General Availability), and Google&#8217;s own best-practices guide for it recommends sequencing actions off <code>gmp-steadystate<\/code> and <code>gmp-animationend<\/code> events rather than firing camera moves back-to-back \u2014 which suggests overlapping <code>flyCameraTo()<\/code> calls before the previous one resolves is a known stress point, not just something I happened to hit.<\/p>\n<div style=\"position:relative; padding-bottom:56.25%; height:0; overflow:hidden;\"><iframe style=\"position:absolute; top:0; left:0; width:100%; height:100%;\" src=\"https:\/\/www.youtube.com\/embed\/VsxHz3Hv1nM?si=EoLSRxNSbPSgr3kG\" title=\"World Cup 2026 - StreetView Bug\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\" referrerpolicy=\"strict-origin-when-cross-origin\" allowfullscreen><\/iframe><\/div>\n<p>&nbsp;<\/p>\n<p>Street View has its own long-standing quirk: rapidly toggling a panorama&#8217;s visibility can leave it in a broken state where <code>getVisible()<\/code> reports <code>true<\/code> but nothing is actually rendered. This has been reported as far back as 2010, and the old workaround people found was nudging the POV or zoom slightly to force a redraw \u2014 which lines up with the &#8220;needs a mouse shake&#8221; behavior I&#8217;ve seen. I haven&#8217;t found anything suggesting Google has fully fixed this at the API level.<\/p>\n<p>Net takeaway: if you&#8217;re building on either of these, debounce rapid input where you can, and don&#8217;t assume every glitch is your own code.<\/p>\n<h2>What I&#8217;d explore next<\/h2>\n<p>The Maps 3D API is still relatively new, and it shows in small ways. One I noticed: Google Earth Pro has had a declarative KML <a href=\"https:\/\/developers.google.com\/kml\/documentation\/touring\" rel=\"noopener\">touring format<\/a> for years \u2014 you define a playlist of fly-tos, waits, and sound cues as data, and the player runs the whole sequence for you. Maps 3D doesn&#8217;t have anything like that yet; <code>flyCameraTo()<\/code> and <code>flyCameraAround()<\/code> only handle one leg of a flight at a time, so chaining a full multi-stop tour (like flying through all 16 stadiums) means hand-rolling your own sequencing with <code>gmp-animationend<\/code> listeners \u2014 which is basically what my <code>Flyto()<\/code> wrapper does. A KML-style tour playlist for Maps 3D would be a neat addition. It&#8217;s also genuinely demanding on hardware: it&#8217;s rendering photorealistic 3D tiles in real time, and on lower-end graphics cards I&#8217;ve seen it struggle or crash outright, independent of anything related to clicking. For now, manual camera-hunting plus a console-dump shortcut got the job done. While I am at it&#8230; would love a way to programmatically disable the clouds.<\/p>\n<p>If you&#8217;re building something similar, the maps3d + Street View combination works well together \u2014 just budget extra time for the visibility timing issues Street View doesn&#8217;t usually have on a normal page.<\/p>\n<h2>Other Google Maps API 3D Examples<\/h2>\n<ul>\n<li>Planet Money Makes A T-Shirt: <a href=\"https:\/\/geteach.com\/maps3d\/tshirt\/\" target=\"_blank\" rel=\"noopener\">https:\/\/geteach.com\/maps3d\/tshirt\/<\/a>\n<\/li>\n<li>Neighborhoods Worlds Apart: <a href=\"https:\/\/geteach.com\/maps3d\/divide\/\" target=\"_blank\" rel=\"noopener\">https:\/\/geteach.com\/maps3d\/divide\/<\/a>\n<\/li>\n<li>US Geography Quiz: <a href=\"https:\/\/geteach.com\/maps3d\/divide\/\" target=\"_blank\" rel=\"noopener\">https:\/\/geteach.com\/maps3d\/usquiz\/<\/a>\n<\/li>\n<li>I&#8217;m Australian Too: <a href=\"https:\/\/geteach.com\/maps3d\/australiantoo\/\" target=\"_blank\" rel=\"noopener\">https:\/\/geteach.com\/maps3d\/australiantoo\/<\/a>\n<\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Google Maps 3D API &ndash; World Cup 2026: https:\/\/geteach.com\/maps3d\/worldcup2026\/ &nbsp; For the 2026 World Cup, I wanted a way to explore all 16 host cities across the US, Canada, and Mexico &mdash; not as a flat map with pins, but as something you could actually fly into. The result uses Google&rsquo;s new Maps 3D JavaScript [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-1231","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/geteach.com\/blog\/wp-json\/wp\/v2\/posts\/1231","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/geteach.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/geteach.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/geteach.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/geteach.com\/blog\/wp-json\/wp\/v2\/comments?post=1231"}],"version-history":[{"count":12,"href":"https:\/\/geteach.com\/blog\/wp-json\/wp\/v2\/posts\/1231\/revisions"}],"predecessor-version":[{"id":1243,"href":"https:\/\/geteach.com\/blog\/wp-json\/wp\/v2\/posts\/1231\/revisions\/1243"}],"wp:attachment":[{"href":"https:\/\/geteach.com\/blog\/wp-json\/wp\/v2\/media?parent=1231"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/geteach.com\/blog\/wp-json\/wp\/v2\/categories?post=1231"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/geteach.com\/blog\/wp-json\/wp\/v2\/tags?post=1231"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}