abstreet/dev/index.html
2021-01-22 03:42:11 +00:00

424 lines
28 KiB
HTML

<!DOCTYPE HTML>
<html lang="en" class="sidebar-visible no-js light">
<head>
<!-- Book generated using mdBook -->
<meta charset="UTF-8">
<title>Developer guide - A/B Street</title>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff" />
<link rel="shortcut icon" href="../favicon.png">
<link rel="stylesheet" href="../css/variables.css">
<link rel="stylesheet" href="../css/general.css">
<link rel="stylesheet" href="../css/chrome.css">
<link rel="stylesheet" href="../css/print.css" media="print">
<!-- Fonts -->
<link rel="stylesheet" href="../FontAwesome/css/font-awesome.css">
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800" rel="stylesheet" type="text/css">
<link href="https://fonts.googleapis.com/css?family=Source+Code+Pro:500" rel="stylesheet" type="text/css">
<!-- Highlight.js Stylesheets -->
<link rel="stylesheet" href="../highlight.css">
<link rel="stylesheet" href="../tomorrow-night.css">
<link rel="stylesheet" href="../ayu-highlight.css">
<!-- Custom theme stylesheets -->
</head>
<body>
<!-- Provide site root to javascript -->
<script type="text/javascript">
var path_to_root = "../";
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "light" : "light";
</script>
<!-- Work around some values being stored in localStorage wrapped in quotes -->
<script type="text/javascript">
try {
var theme = localStorage.getItem('mdbook-theme');
var sidebar = localStorage.getItem('mdbook-sidebar');
if (theme.startsWith('"') && theme.endsWith('"')) {
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
}
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
}
} catch (e) { }
</script>
<!-- Set the theme before any content is loaded, prevents flash -->
<script type="text/javascript">
var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = default_theme; }
var html = document.querySelector('html');
html.classList.remove('no-js')
html.classList.remove('light')
html.classList.add(theme);
html.classList.add('js');
</script>
<!-- Hide / unhide sidebar before it is displayed -->
<script type="text/javascript">
var html = document.querySelector('html');
var sidebar = 'hidden';
if (document.body.clientWidth >= 1080) {
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
sidebar = sidebar || 'visible';
}
html.classList.remove('sidebar-visible');
html.classList.add("sidebar-" + sidebar);
</script>
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
<div class="sidebar-scrollbox">
<ol class="chapter"><li class="chapter-item expanded "><a href="../index.html"><strong aria-hidden="true">1.</strong> Overview</a></li><li class="chapter-item expanded "><a href="../howto/index.html"><strong aria-hidden="true">2.</strong> Instructions</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../howto/new_city.html"><strong aria-hidden="true">2.1.</strong> Importing a new city</a></li></ol></li><li class="chapter-item expanded "><a href="../how_it_works.html"><strong aria-hidden="true">3.</strong> How it works</a></li><li class="chapter-item expanded "><a href="../case_studies/index.html"><strong aria-hidden="true">4.</strong> Case studies</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../case_studies/lake_wash.html"><strong aria-hidden="true">4.1.</strong> Lake Washington Blvd Stay Healthy Street</a></li><li class="chapter-item expanded "><a href="../case_studies/west_seattle.html"><strong aria-hidden="true">4.2.</strong> West Seattle mitigations</a></li><li class="spacer"></li></ol></li><li class="chapter-item expanded "><a href="../dev/index.html" class="active"><strong aria-hidden="true">5.</strong> Developer guide</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../dev/misc_tricks.html"><strong aria-hidden="true">5.1.</strong> Misc developer tricks</a></li><li class="chapter-item expanded "><a href="../dev/api.html"><strong aria-hidden="true">5.2.</strong> API</a></li><li class="chapter-item expanded "><a href="../dev/testing.html"><strong aria-hidden="true">5.3.</strong> Testing</a></li><li class="chapter-item expanded "><a href="../dev/mass_import.html"><strong aria-hidden="true">5.4.</strong> Importing many maps</a></li><li class="chapter-item expanded "><a href="../dev/data.html"><strong aria-hidden="true">5.5.</strong> Data organization</a></li></ol></li><li class="chapter-item expanded "><a href="../map/index.html"><strong aria-hidden="true">6.</strong> Map model</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../map/details.html"><strong aria-hidden="true">6.1.</strong> Details</a></li><li class="chapter-item expanded "><a href="../map/importing/index.html"><strong aria-hidden="true">6.2.</strong> Importing</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../map/importing/convert_osm.html"><strong aria-hidden="true">6.2.1.</strong> convert_osm</a></li><li class="chapter-item expanded "><a href="../map/importing/geometry.html"><strong aria-hidden="true">6.2.2.</strong> Road/intersection geometry</a></li><li class="chapter-item expanded "><a href="../map/importing/rest.html"><strong aria-hidden="true">6.2.3.</strong> The rest</a></li><li class="chapter-item expanded "><a href="../map/importing/misc.html"><strong aria-hidden="true">6.2.4.</strong> Misc</a></li></ol></li><li class="chapter-item expanded "><a href="../map/edits.html"><strong aria-hidden="true">6.3.</strong> Live edits</a></li><li class="chapter-item expanded "><a href="../map/platform.html"><strong aria-hidden="true">6.4.</strong> Exporting</a></li></ol></li><li class="chapter-item expanded "><a href="../trafficsim/index.html"><strong aria-hidden="true">7.</strong> Traffic simulation</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../trafficsim/discrete_event.html"><strong aria-hidden="true">7.1.</strong> Discrete event simulation</a></li><li class="chapter-item expanded "><a href="../trafficsim/travel_demand.html"><strong aria-hidden="true">7.2.</strong> Travel demand</a></li><li class="chapter-item expanded "><a href="../trafficsim/gridlock.html"><strong aria-hidden="true">7.3.</strong> Gridlock</a></li><li class="chapter-item expanded "><a href="../trafficsim/trips.html"><strong aria-hidden="true">7.4.</strong> Multi-modal trips</a></li><li class="chapter-item expanded "><a href="../trafficsim/live_edits.html"><strong aria-hidden="true">7.5.</strong> Live edits</a></li><li class="chapter-item expanded "><a href="../trafficsim/parking.html"><strong aria-hidden="true">7.6.</strong> Parking</a></li><li class="spacer"></li></ol></li><li class="chapter-item expanded "><a href="../project/index.html"><strong aria-hidden="true">8.</strong> Project</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../project/roadmap.html"><strong aria-hidden="true">8.1.</strong> Roadmap</a></li><li class="chapter-item expanded "><a href="../project/motivations.html"><strong aria-hidden="true">8.2.</strong> Motivations</a></li><li class="chapter-item expanded "><a href="../project/history/index.html"><strong aria-hidden="true">8.3.</strong> History</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../project/history/backstory.html"><strong aria-hidden="true">8.3.1.</strong> Backstory</a></li><li class="chapter-item expanded "><a href="../project/history/year1.html"><strong aria-hidden="true">8.3.2.</strong> Year 1</a></li><li class="chapter-item expanded "><a href="../project/history/year2.html"><strong aria-hidden="true">8.3.3.</strong> Year 2</a></li><li class="chapter-item expanded "><a href="../project/history/year3.html"><strong aria-hidden="true">8.3.4.</strong> Year 3</a></li></ol></li><li class="chapter-item expanded "><a href="../project/CHANGELOG.html"><strong aria-hidden="true">8.4.</strong> Full CHANGELOG</a></li><li class="chapter-item expanded "><a href="../project/references.html"><strong aria-hidden="true">8.5.</strong> References</a></li><li class="chapter-item expanded "><a href="../project/collaborations.html"><strong aria-hidden="true">8.6.</strong> Collaborations</a></li></ol></li><li class="chapter-item expanded "><a href="../side_projects/index.html"><strong aria-hidden="true">9.</strong> Side projects</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../side_projects/santa.html"><strong aria-hidden="true">9.1.</strong> 15-minute Santa</a></li><li class="chapter-item expanded "><a href="../side_projects/parking_mapper.html"><strong aria-hidden="true">9.2.</strong> Mapping on-street parking</a></li><li class="chapter-item expanded "><a href="../side_projects/osm_viewer.html"><strong aria-hidden="true">9.3.</strong> OpenStreetMap viewer</a></li><li class="chapter-item expanded "><a href="../side_projects/fifteen_min.html"><strong aria-hidden="true">9.4.</strong> 15-minute neighborhoods explorer</a></li></ol></li></ol>
</div>
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
</nav>
<div id="page-wrapper" class="page-wrapper">
<div class="page">
<div id="menu-bar-hover-placeholder"></div>
<div id="menu-bar" class="menu-bar sticky bordered">
<div class="left-buttons">
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
<i class="fa fa-bars"></i>
</button>
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
<i class="fa fa-paint-brush"></i>
</button>
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
<li role="none"><button role="menuitem" class="theme" id="light">Light (default)</button></li>
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
</ul>
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
<i class="fa fa-search"></i>
</button>
</div>
<h1 class="menu-title">A/B Street</h1>
<div class="right-buttons">
<a href="../print.html" title="Print this book" aria-label="Print this book">
<i id="print-button" class="fa fa-print"></i>
</a>
</div>
</div>
<div id="search-wrapper" class="hidden">
<form id="searchbar-outer" class="searchbar-outer">
<input type="search" name="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
</form>
<div id="searchresults-outer" class="searchresults-outer hidden">
<div id="searchresults-header" class="searchresults-header"></div>
<ul id="searchresults">
</ul>
</div>
</div>
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
<script type="text/javascript">
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
});
</script>
<div id="content" class="content">
<main>
<h1><a class="header" href="#developer-guide" id="developer-guide">Developer guide</a></h1>
<h2><a class="header" href="#getting-started" id="getting-started">Getting started</a></h2>
<p>You will first need:</p>
<ul>
<li>Stable Rust, at least 1.47. <a href="https://www.rust-lang.org/tools/install">https://www.rust-lang.org/tools/install</a>.
<ul>
<li>On Windows, you may need
<a href="https://visualstudio.microsoft.com/en/downloads/">Visual Studio 2019</a>.</li>
</ul>
</li>
<li>On Linux, <code>sudo apt-get install xorg-dev libxcb-shape0-dev libxcb-xfixes0-dev</code>
or the equivalent for your distro</li>
</ul>
<p>One-time setup:</p>
<ol>
<li>
<p>Download the repository:
<code>git clone https://github.com/dabreegster/abstreet.git</code></p>
</li>
<li>
<p>Grab the minimal amount of data to get started: <code>cargo run --bin updater</code></p>
</li>
<li>
<p>Run the game: <code>RUST_BACKTRACE=1 cargo run --bin game --release</code>. On Windows,
set environment variables like this:
<code>set RUST_BACKTRACE=1 &amp;&amp; cargo run --bin game --release</code></p>
</li>
</ol>
<h2><a class="header" href="#development-tips" id="development-tips">Development tips</a></h2>
<ul>
<li><a href="https://dabreegster.github.io/abstreet/rustdoc/map_model/index.html">Generated API documentation</a></li>
<li>Compile faster by just doing <code>cargo run</code>. The executable will have debug stack
traces and run more slowly. You can do <code>cargo run --release</code> to build in
optimized release mode; compilation will be slower, but the executable much
faster.</li>
<li>Some in-game features are turned off by default or don't have a normal menu to
access them. The list:
<ul>
<li>To toggle developer mode: press <strong>Control+S</strong> in game, or
<code>cargo run -- --dev</code></li>
<li>To warp to an object by numeric ID: press <strong>Control+j</strong></li>
<li>To enter debug mode with all sorts of goodies: press <strong>Control+D</strong></li>
</ul>
</li>
<li>You can start the game in different modes using flags:
<ul>
<li><code>cargo run --bin game -- --dev data/system/seattle/maps/downtown.bin</code> starts
on a particular map</li>
<li><code>cargo run --bin game -- data/system/seattle/scenarios/downtown/weekday.bin</code>
starts with a scenario (which is tied to a certain map)</li>
<li><code>cargo run --bin game -- --challenge=trafficsig/tut2</code> starts on a particular
challenge. See the list of aliases by passing in a bad value here.</li>
<li><code>cargo run --bin game -- data/player/saves/montlake/no_edits_unnamed/00h00m20.3s.bin</code>
restores an exact simulation state. Savestates are found in debug mode
(<strong>Control+D</strong>) -- they're probably confusing for the normal player
experience, so they're hidden for now.</li>
<li><code>cargo run --bin game -- --tutorial=12</code> starts somewhere in the tutorial</li>
<li>Adding <code>--edits='name of edits'</code> starts with edits applied to the map.</li>
</ul>
</li>
</ul>
<h2><a class="header" href="#downloading-more-cities" id="downloading-more-cities">Downloading more cities</a></h2>
<p>As data formats change over time, things in the <code>data/</code> directory not under
version control will get out of date. At any time, you can run
<code>cargo run --bin updater</code> from the main repository directory to update only the
files that have changed.</p>
<p>You can also opt into downloading updates for more cities by editing
<code>data/player/data.json</code>. In the main UI, there's a button to download more
cities that will help you manage this config file.</p>
<h2><a class="header" href="#building-map-data" id="building-map-data">Building map data</a></h2>
<p>You can skip this section if you're just touching code in <code>game</code>, <code>widgetry</code>,
and <code>sim</code>.</p>
<p>To run all pieces of the importer, you'll need some extra dependencies:</p>
<ul>
<li><code>osmconvert</code>: See <a href="https://wiki.openstreetmap.org/wiki/Osmconvert#Download">https://wiki.openstreetmap.org/wiki/Osmconvert#Download</a> or
<a href="https://github.com/interline-io/homebrew-planetutils#installation">https://github.com/interline-io/homebrew-planetutils#installation</a> for Mac</li>
<li><code>libgdal-dev</code>: See <a href="https://gdal.org">https://gdal.org</a> if your OS package manager doesn't have
this. If you keep hitting linking errors, then just remove
<code>--features scenarios</code> from <code>import.sh</code>. You won't be able to build the
Seattle scenarios.</li>
<li>Standard Unix utilities: <code>curl</code>, <code>unzip</code>, <code>gunzip</code></li>
</ul>
<p>The first stage of the importer, <code>--raw</code>, will download input files from OSM,
King County GIS, and so on. If the mirrors are slow or the files vanish, you
could fill out <code>data/config</code> and use the <code>updater</code> described above to grab the
latest input.</p>
<p>Building contraction hierarchies for pathfinding occurs in the --map stage. It
can take a few minutes for larger maps. To view occasional progress updates, you
can run the importer with</p>
<pre><code>RUST_LOG=&quot;fast_paths=debug/contracted node [0-9]+0000 &quot;
</code></pre>
<p>You can rerun specific stages of the importer:</p>
<ul>
<li>If you're modifying the initial OSM data -&gt; RawMap conversion in
<code>convert_osm</code>, you need <code>./import.sh --raw --map</code>.</li>
<li>If you're modifying <code>map_model</code> but not the OSM -&gt; RawMap conversion, then you
just need <code>./import.sh --map</code>.</li>
<li>If you're modifying the demand model for Seattle, you can add <code>--scenario</code> to
regenerate.</li>
<li>By default, all maps are regenerated. You can also specify a single map:
<code>./import.sh --map downtown</code>.</li>
<li>By default, Seattle is assumed as the city. You have to specify otherwise:
<code>./import.sh --city=los_angeles --map downtown_la</code>.</li>
</ul>
<p>You can also make the importer <a href="../howto/new_city.html">import a new city</a>.</p>
<h2><a class="header" href="#understanding-stuff" id="understanding-stuff">Understanding stuff</a></h2>
<p>The docs listed at <a href="https://github.com/dabreegster/abstreet#documentation">https://github.com/dabreegster/abstreet#documentation</a>
explain things like map importing and how the traffic simulation works.</p>
<h3><a class="header" href="#code-organization" id="code-organization">Code organization</a></h3>
<p>If you're going to dig into the code, it helps to know what all the crates are.
The most interesting crates are <code>map_model</code>, <code>sim</code>, and <code>game</code>.</p>
<p>Constructing the map:</p>
<ul>
<li><code>convert_osm</code>: extract useful data from OpenStreetMap and other data sources,
emit intermediate map format</li>
<li><code>kml</code>: extract shapes from KML and CSV shapefiles</li>
<li><code>map_model</code>: the final representation of the map, also conversion from the
intermediate map format into the final format</li>
<li><code>map_editor</code>: GUI for modifying geometry of maps and creating maps from
scratch. pretty abandoned as of June 2020</li>
<li><code>importer</code>: tool to run the entire import pipeline</li>
<li><code>updater</code>: tool to download/upload large files used in the import pipeline</li>
</ul>
<p>Traffic simulation:</p>
<ul>
<li><code>sim</code>: all of the agent-based simulation logic</li>
<li><code>headless</code>: tool to run a simulation without any visualization</li>
</ul>
<p>Graphics:</p>
<ul>
<li><code>game</code>: the GUI and main gameplay</li>
<li><code>map_gui</code>: common code to interact with <code>map_model</code> maps</li>
<li><code>widgetry</code>: a GUI and 2D OpenGL rendering library, using glium + winit +
glutin</li>
</ul>
<p>Common utilities:</p>
<ul>
<li><code>abstutil</code>: a grab-bag timing and logging utilities</li>
<li><code>abstio</code>: Reading/writing files on native/web</li>
<li><code>geom</code>: types for GPS and map-space points, lines, angles, polylines,
polygons, circles, durations, speeds</li>
</ul>
<p>Other:</p>
<ul>
<li><code>collisions</code>: an experimental data format for real-world collision data</li>
<li><code>traffic_seitan</code>: a bug-finding tool that randomly generates live map edits</li>
<li><code>tests</code>: integration tests</li>
<li><code>santa</code>: 15-minute Santa, an arcade game about delivering and zoning</li>
<li><code>parking_mapper</code>: a standalone tool to help map street parking in OSM</li>
<li><code>osm_viewer</code>: a standalone tool to render OSM in detail</li>
<li><code>fifteen_min</code>: a standalone tool to explore 15-minute neighborhoods</li>
<li><code>popdat</code>: use census data to produce traffic simulation input</li>
<li><code>traffic_signal_data</code>: manual timing overrides for some traffic signals</li>
<li><code>sumo</code>: interoperability with <a href="https://www.eclipse.org/sumo">SUMO</a></li>
</ul>
<h2><a class="header" href="#code-conventions" id="code-conventions">Code conventions</a></h2>
<p>All code is automatically formatted using
<a href="https://github.com/rust-lang/rustfmt">https://github.com/rust-lang/rustfmt</a>; please run <code>cargo +nightly fmt</code> before
sending a PR. (You have to install the nightly toolchain just for fmt)</p>
<p>cargo fmt can't yet organize imports, but we follow a convention to minimize
conflict with what some IDEs do. Follow existing code to group imports: std,
external crates, other crates in the project, the current crate, then finally
any module declarations.</p>
<p>See the <a href="testing.html">testing strategy</a> page.</p>
<h2><a class="header" href="#error-handling" id="error-handling">Error handling</a></h2>
<p>The error handling is unfortunately inconsistent. The goal is to gracefully
degrade instead of crashing the game. If a crash does happen, make sure the logs
will have enough context to reproduce and debug. For example, giving up when
some geometry problem happens isn't ideal, but at least make sure to print the
road / agent IDs or whatever will help find the problem. It's fine to crash
during map importing, since the player won't deal with this, and loudly stopping
problems is useful. It's also fine to crash when initially constructing all of
the renderable map objects, because this crash will consistently happen at
startup-time and be noticed by somebody developing before a player gets to it.</p>
<p>Since almost none of the code ever needs to distinguish error cases, use
<a href="https://crates.io/crates/anyhow">anyhow</a>. Most of the errors generated within
A/B Street are just strings anyway; the <code>bail!</code> macro is a convenient way to
return them.</p>
<h2><a class="header" href="#logging" id="logging">Logging</a></h2>
<p>Prefer using <code>info!</code>, <code>warn!</code>, <code>error!</code>, etc from the <code>log</code> crate rather than
<code>println</code>.</p>
<p>Adjust the log level without recompiling via the <code>RUST_LOG</code> env variable.</p>
<pre><code>RUST_LOG=debug cargo run --bin game
</code></pre>
<p>This can be done on a per lib basis:</p>
<pre><code>RUST_LOG=my_lib=debug cargo run --bin game
</code></pre>
<p>Or a module-by-module basis:</p>
<pre><code>RUST_LOG=my_lib::module=debug cargo run --bin game
</code></pre>
<p>You can mix and match:</p>
<pre><code># error logging by default, except the foo:bar module at debug level
# and the entire baz crate at info level
RUST_LOG=error,foo::bar=debug,baz=info cargo run --bin game
</code></pre>
<p>For some special cases, you might want to use regex matching by specifying a
pattern with the &quot;/&quot;:</p>
<pre><code># only log once every 10k
RUST_LOG=&quot;fast_paths=debug/contracted node [0-9]+0000 &quot; mike import_la
</code></pre>
<p>See the <a href="https://docs.rs/env_logger/0.8.2/env_logger/">env_logger documentation</a>
for more usage examples.</p>
<h2><a class="header" href="#profiling" id="profiling">Profiling</a></h2>
<p>Use <a href="https://github.com/flamegraph-rs/flamegraph">https://github.com/flamegraph-rs/flamegraph</a>, just running it on the
binaries you build normally.</p>
</main>
<nav class="nav-wrapper" aria-label="Page navigation">
<!-- Mobile navigation buttons -->
<a rel="prev" href="../case_studies/west_seattle.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
<a rel="next" href="../dev/misc_tricks.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
<div style="clear: both"></div>
</nav>
</div>
</div>
<nav class="nav-wide-wrapper" aria-label="Page navigation">
<a rel="prev" href="../case_studies/west_seattle.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
<a rel="next" href="../dev/misc_tricks.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
</nav>
</div>
<script type="text/javascript">
window.playpen_copyable = true;
</script>
<script src="../elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
<script src="../mark.min.js" type="text/javascript" charset="utf-8"></script>
<script src="../searcher.js" type="text/javascript" charset="utf-8"></script>
<script src="../clipboard.min.js" type="text/javascript" charset="utf-8"></script>
<script src="../highlight.js" type="text/javascript" charset="utf-8"></script>
<script src="../book.js" type="text/javascript" charset="utf-8"></script>
<!-- Custom JS scripts -->
</body>
</html>