Map Disaster Relief Shelters

Build a live shelter finder map using Leaflet.js and FEMA's open API.

Introduction

30 Second Summary

When a natural disaster strikes, knowing where the nearest shelter is can make all the difference. Finding that information quickly, on a map you can actually interact with, is harder than it sounds.

In this project, you will build a browser-based disaster relief shelter finder that fetches live shelter data from FEMA. It detects your location and maps everything with color-coded markers so you can instantly see which shelters are closest.

What You'll Build

Open the page and watch it pinpoint your location on a full-screen map powered by Leaflet.js and OpenStreetMap, then click any shelter marker to see its name, address, and distance from you.

By the end of this project, you'll have:

  • A live interactive map where you can pan, zoom, and explore real FEMA disaster relief shelters plotted across the United States.
  • Automatic location detection using the browser's Geolocation API that marks your position with a blue pulsing dot and calculates the exact distance to every shelter.
  • Color-coded shelter markers (green for close, orange for medium, red for far) with rich detail popups showing each shelter's name, address, status, capacity, and distance from you in miles.
  • Secret Mission: Add a sortable sidebar panel that lists the 10 closest shelters with click-to-zoom navigation.

Are there any prerequisites?

You just need VS Code and a modern web browser (Chrome, Firefox, or Edge). Everything else loads from CDN links in your HTML file, so there is nothing extra to install.

Before We Start

Before we dive in, take a moment to think about what you're building and why it matters.

Set Up Your Project Files

Before the map can display anything, you need a project structure with the right files and a mapping library loaded. This step creates that foundation.

The mapping library for this project is Leaflet.js. It loads from a CDN link in your HTML, so there is nothing to install on your computer.

In this step, get ready to:
  • Create a shelter-finder project folder with three files.
  • Load the Leaflet.js mapping library via CDN and build the HTML structure.
  • Initialize a full-screen interactive map with OpenStreetMap tiles.
Create your project folder and files

Every project needs a home. You will create a folder on your Desktop and set up three empty files inside it.

  • In VS Code, open the integrated terminal by pressing Ctrl+` (backtick key, below Escape).
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir shelter-finder
cd shelter-finder
  • Create the three project files by running this command:

macOS

touch index.html style.css app.js

Windows

New-Item index.html -ItemType File
New-Item style.css -ItemType File
New-Item app.js -ItemType File
  • Confirm the files exist by running:
ls

You should see app.js, index.html, and style.css listed in the output.

  • Click File in the VS Code menu bar.
  • Click Open Folder.
  • Navigate to your Desktop and select the shelter-finder folder, then click Open.

Your three files now appear in the VS Code Explorer sidebar on the left.

Don't see your files in the sidebar?

Make sure you selected the shelter-finder folder itself, not one of the files inside it. VS Code needs the folder as the workspace root.

If the Explorer sidebar is hidden, click the top icon in the left sidebar (two overlapping file icons) or press Cmd+Shift+E (macOS) or Ctrl+Shift+E (Windows) to reveal it.

Still stuck? Help me find my project files in VS Code.

Set up the HTML structure

The HTML file is the backbone of your app. It loads the Leaflet library from a CDN and creates a container for the map.

  • Click index.html in the Explorer sidebar to open it.
  • Paste the following HTML into the file:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Disaster Relief Shelter Finder</title>
  <!-- Load Leaflet's CSS for map controls and styling -->
  <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
    integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
    crossorigin="" />
  <!-- Load your custom styles -->
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <!-- This div is where Leaflet renders the map -->
  <div id="map"></div>
  <!-- Load Leaflet's JavaScript library -->
  <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
    integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
    crossorigin=""></script>
  <!-- Load your custom JavaScript -->
  <script src="app.js"></script>
</body>
</html>

What does this HTML do?

  • The two unpkg.com links load Leaflet 1.9.4 directly from a CDN. The integrity hashes verify the files have not been tampered with.
  • The <div id="map"> is an empty container. Leaflet fills it with the interactive map when your JavaScript runs.
  • The app.js script loads last so the <div id="map"> element already exists in the page when your code runs.
  • Save index.html by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Open the file in your browser by right-clicking index.html in Finder (macOS) or File Explorer (Windows) and choosing your browser.

You should see a blank white page. The Leaflet library is loaded, but the map container has no height yet and no JavaScript is telling it what to display.

Style the map and initialize Leaflet

The map div needs CSS to fill the whole screen, and JavaScript to tell Leaflet what to show. These two files work together to bring the map to life.

  • Click style.css in the Explorer sidebar to open it.
  • Make the map fill the entire viewport by pasting this CSS:
/* Reset default browser margins and padding */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

/* Make the page fill the full viewport height */
html, body {
  height: 100%;
  font-family: Arial, sans-serif;
}

/* Make the map container fill the entire page */
#map {
  width: 100%;
  height: 100%;
}

What does this CSS do?

  • The * selector removes all default margins and padding that browsers add to elements.
  • Setting height: 100% on html, body, and #map chains the full browser window height down to the map container. Without this, the map div collapses to zero height and nothing appears.
  • Save style.css.
  • Click app.js in the Explorer sidebar to open it.
  • Initialize the Leaflet map by pasting this JavaScript:
// Initialize the map centered on the US at zoom level 4
var map = L.map('map').setView([39.8283, -98.5795], 4);

// Add OpenStreetMap tiles as the base layer
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
  maxZoom: 19,
  attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);

What does this JavaScript do?

  • L.map('map') tells Leaflet to take over the <div id="map"> element and turn it into an interactive map.
  • .setView([39.8283, -98.5795], 4) centers the map on the geographic middle of the United States. The 4 is the zoom level, which shows the whole country at a glance.
  • L.tileLayer() loads map images from OpenStreetMap. These tiles are the roads, borders, and labels you see when you pan and zoom.
  • Save app.js.

Before you refresh the browser, what do you think you will see? A blank page again, or something new?

  • Switch to your browser and refresh the page.

You should see a full-screen interactive map centered on the United States with zoom controls in the top-left corner. Try clicking and dragging to pan around, and use the +/- buttons to zoom in and out.

Seeing a blank page or a grey box?

  • If the page is completely blank, open your browser's developer console (press F12 and click the Console tab) and look for red error messages. A typo in the Leaflet CDN URL or integrity hash prevents the library from loading.
  • If you see a grey box with no map tiles, check that your internet connection is active. The map tiles load from OpenStreetMap's servers.
  • Make sure style.css has height: 100% on html, body AND #map. If the map container has zero height, Leaflet has nowhere to render.

My map is not showing up in the browser. Help me debug it.

✔️ Awesome, I've got everything!

Great. Double check you have saved all three files.

ⓧ I'd like to double check the full code

Compare your files against the code below. Make sure each file matches exactly.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Disaster Relief Shelter Finder</title>
  <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
    integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
    crossorigin="" />
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <div id="map"></div>
  <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
    integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
    crossorigin=""></script>
  <script src="app.js"></script>
</body>
</html>
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html, body {
  height: 100%;
  font-family: Arial, sans-serif;
}

#map {
  width: 100%;
  height: 100%;
}
var map = L.map('map').setView([39.8283, -98.5795], 4);

L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
  maxZoom: 19,
  attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);

Your map is live. Next up, you will detect the user's real location and mark it right on the map.

Detect Your Location and Mark It on the Map

Your map is live and you can already pan and zoom across the entire world. Now the app needs to answer a personal question: where are you?

Every modern browser has a built-in Geolocation API that can share your latitude and longitude with a web page. In this step, you will use it to detect the user's position and drop a pulsing blue marker on the map.

In this step, get ready to:
  • Request the user's location with the browser's Geolocation API.
  • Create a custom blue pulsing marker at the user's position with a "You are here" popup.
Request the user's location

Before you can calculate how far away each shelter is, you need to know where the user is standing. The browser can provide this through navigator.geolocation.getCurrentPosition(), which prompts the user for permission and then returns their coordinates.

  • In app.js, find the tile layer block that ends with .addTo(map);.
  • Add the following code below it to detect the user's location:
var userLat = null;
var userLon = null;

if ('geolocation' in navigator) {
  navigator.geolocation.getCurrentPosition(
    function(position) {
      userLat = position.coords.latitude;
      userLon = position.coords.longitude;
    },
    function(error) {
      console.warn('Geolocation error: ' + error.message);
      alert('Could not get your location. Shelters will still appear on the map, but distances will not be calculated.');
    },
    {
      enableHighAccuracy: true,
      timeout: 10000,
      maximumAge: 0
    }
  );
} else {
  alert('Geolocation is not supported by your browser. Shelters will still appear on the map.');
}

What does this code do?

  • userLat and userLon start as null and get filled in once the browser shares the user's position.
  • 'geolocation' in navigator checks whether the browser supports the Geolocation API at all before trying to use it.
  • getCurrentPosition() takes three arguments: a success callback that receives the coordinates, an error callback that fires if the user denies permission, and an options object.
  • enableHighAccuracy: true asks for the best possible location fix. timeout: 10000 gives the browser 10 seconds to respond.
  • Save app.js.
  • Refresh your browser. You should see a popup at the top of the page asking for permission to access your location.
  • Click Allow. The map won't visually change yet because you have not added a marker for the location. You will add that next.

Not seeing the location permission popup?

  • If you opened index.html directly (the URL starts with file://), some browsers block geolocation on the file:// protocol. Try using VS Code's Live Server extension to serve the page over localhost instead.
  • Check that you did not accidentally deny location permission previously. In Chrome, click the lock icon in the address bar and set Location to Allow, then refresh.
  • help me debug why the geolocation permission popup is not appearing
Create a custom location marker

The coordinates are flowing in, but the user cannot see where they are on the map. Leaflet's L.divIcon lets you build a marker from pure HTML and CSS instead of an image file. You will use it to create a blue pulsing circle that stands out from the shelter markers you will add later.

  • Switch back to style.css. Add the following styles below the #map rule to define the blue pulsing marker:
.user-marker {
  width: 20px;
  height: 20px;
  background-color: #3498db;
  border: 3px solid #fff;
  border-radius: 50%;
  box-shadow: 0 0 10px rgba(52, 152, 219, 0.5);
  animation: pulse 2s infinite;
}

@keyframes pulse {
  0% {
    box-shadow: 0 0 0 0 rgba(52, 152, 219, 0.6);
  }
  70% {
    box-shadow: 0 0 0 15px rgba(52, 152, 219, 0);
  }
  100% {
    box-shadow: 0 0 0 0 rgba(52, 152, 219, 0);
  }
}

What does this CSS do?

  • .user-marker creates a 20x20 pixel blue circle with a white border. The border-radius: 50% turns the square div into a perfect circle.
  • @keyframes pulse creates a repeating animation that grows and fades a blue glow around the dot. This makes it easy to spot on a busy map.
  • Save style.css.

The CSS is ready. Now you need to wire it up in JavaScript so Leaflet actually places a marker using this style.

  • Switch back to app.js. Inside the success callback, find these two lines:
      userLat = position.coords.latitude;
      userLon = position.coords.longitude;
  • Add the following code directly below userLon = position.coords.longitude; to create the marker and pan the map:
      // Create a blue circle marker using CSS instead of an image
      var userIcon = L.divIcon({
        className: 'user-marker',
        iconSize: [20, 20],
        iconAnchor: [10, 10],
        popupAnchor: [0, -12]
      });

      // Place the marker on the map and open its popup
      L.marker([userLat, userLon], { icon: userIcon })
        .bindPopup('You are here')
        .addTo(map)
        .openPopup();

      // Pan and zoom to the user's location
      map.setView([userLat, userLon], 10);

What does this code do?

  • L.divIcon() creates a marker icon from a plain HTML div instead of a PNG image. The className: 'user-marker' connects it to the CSS you just wrote.
  • iconAnchor: [10, 10] tells Leaflet that the center of the 20x20 icon sits at pixel (10, 10). This makes the dot land exactly on the user's coordinates instead of being offset.
  • .bindPopup('You are here') attaches a popup to the marker. .openPopup() opens it immediately so the user sees the label right away.
  • map.setView([userLat, userLon], 10) smoothly pans and zooms the map to center on the user at zoom level 10, close enough to see the neighbourhood.
  • Save app.js.

Before you refresh, what do you expect to see on the map this time?

  • Refresh your browser. If the browser asks for location permission again, click Allow.

The map pans to your current location and zooms in. You should see a blue pulsing circle with a popup that says "You are here" sitting right on your position.

Not seeing the blue marker?

  • Make sure you saved both style.css and app.js. Unsaved CSS means the marker div has no visible styles.
  • Check that className: 'user-marker' in your JavaScript matches the .user-marker class name in your CSS exactly.
  • Make sure the marker code is inside the success callback (the first function(position) block), not outside it. Code outside the callback runs before the browser has the coordinates.
  • help me figure out why my blue location marker is not appearing on the map

✔️ Awesome, I've got everything!

Great. Make sure both style.css and app.js are saved before moving on.

ⓧ I'd like to double check the full code

Scroll up and confirm your files contain each of the following sections, in order.

style.css should have:

  • The global reset (*, html, body, #map) from Step 1.
  • The .user-marker class with the blue background, white border, and animation: pulse 2s infinite.
  • The @keyframes pulse animation with three stops at 0%, 70%, and 100%.

app.js should have:

  • The L.map() and L.tileLayer() setup from Step 1.
  • var userLat = null and var userLon = null declarations.
  • The if ('geolocation' in navigator) check wrapping getCurrentPosition().
  • The success callback storing coordinates, creating userIcon with L.divIcon(), adding L.marker() with .bindPopup('You are here'), and calling map.setView().
  • The error callback with console.warn() and an alert().
  • The options object with enableHighAccuracy, timeout, and maximumAge.
  • The else branch showing an alert for unsupported browsers.

Your app now knows where you are and shows it with a satisfying pulsing dot. Next up, you will fetch real disaster relief shelter data from FEMA's public API and scatter those shelters across the map.

Fetch and Display FEMA Shelter Data

Your map now knows where you are. It detects your location, drops a blue pulsing marker, and pans right to your neighborhood.

A map of yourself is not very useful on its own. To turn this into a real shelter finder, you need actual shelter data. FEMA's National Shelter System provides a free REST API with live data on disaster relief shelters across the US. In this step, you will fetch that data and plot every shelter on your map.

In this step, get ready to:
  • Write a function that fetches live shelter data from the FEMA OpenShelters API.
  • Loop through the API response and add a map marker for each shelter.
  • Wire up the fetch function so it runs automatically after geolocation resolves.
Write the fetchShelters function

FEMA's OpenShelters endpoint is an ArcGIS REST service. You send it a URL with query parameters, and it returns JSON containing an array of shelter records. No API key or signup is required.

  • In app.js, find the line that says var userLon = null; and add this new function below it:
function fetchShelters() {
  // Build the FEMA API query URL
  var url = 'https://gis.fema.gov/arcgis/rest/services/NSS/OpenShelters/FeatureServer/0/query'
    + '?where=1%3D1'
    + '&outFields=*'
    + '&returnGeometry=true'
    + '&f=json'
    + '&resultRecordCount=200';

  // Send the request and parse the JSON response
  fetch(url)
    .then(function(response) {
      return response.json();
    })
    .then(function(data) {
      // Extract the shelter records array
      var features = data.features;
      console.log('Shelters fetched: ' + features.length);
    })
    .catch(function(error) {
      console.error('Error fetching shelter data:', error);
    });
}

What does this code do?

The URL points to FEMA's OpenShelters FeatureServer. The query parameters tell the API what to return:

  • where=1%3D1 means "give me all records" (it is a SQL-style filter that always evaluates to true).
  • outFields=* requests every available field (name, address, status, capacity, and more).
  • returnGeometry=true includes each shelter's latitude and longitude coordinates.
  • resultRecordCount=200 caps the response at 200 shelters to keep things fast.

The fetch() call sends the request. The first .then() converts the raw response to JSON. The second .then() pulls out the features array and logs how many shelters came back.

The function exists now, but nothing calls it yet. Before wiring it up, you need to add the code that actually creates map markers from the data.

Add shelter markers to the map

Each shelter record in the API response contains a geometry object with x (longitude) and y (latitude) values. You will loop through the records, extract these coordinates, and place a Leaflet marker at each one.

  • In the fetchShelters function you just wrote, find the line:
      console.log('Shelters fetched: ' + features.length);
  • Add this loop directly below that line, inside the same .then() callback:
      // Loop through each shelter record
      for (var i = 0; i < features.length; i++) {
        var shelter = features[i];
        var geom = shelter.geometry;

        // Skip shelters with no valid coordinates
        if (!geom || geom.x === null || geom.y === null) {
          continue;
        }

        // Extract lat/lon from the geometry object
        var shelterLat = geom.y;
        var shelterLon = geom.x;

        // Place a default Leaflet marker at this shelter
        L.marker([shelterLat, shelterLon]).addTo(map);
      }

What does this loop do?

For each shelter in the features array, the code grabs the geometry object. Some records might have missing coordinates, so the if check skips those to avoid errors.

The FEMA API stores longitude in geom.x and latitude in geom.y. Leaflet's L.marker() expects coordinates in [latitude, longitude] order, so shelterLat comes from y and shelterLon comes from x.

Wire up fetchShelters and verify

The function and the marker loop are ready. Now you need to call fetchShelters() at the right moment. Shelters should load whether or not geolocation succeeds.

  • In the geolocation success callback, find this line:
      map.setView([userLat, userLon], 10);
  • Add fetchShelters(); on the next line, so the function now looks like this:
      map.setView([userLat, userLon], 10);

      fetchShelters();
  • In the geolocation error callback, find the alert(...) line. Add fetchShelters(); on the next line:
      alert('Could not get your location. Shelters will still appear on the map, but distances will not be calculated.');
      fetchShelters();
  • In the else block at the bottom (the "geolocation not supported" fallback), add fetchShelters(); after the existing alert(...):
  alert('Geolocation is not supported by your browser. Shelters will still appear on the map.');
  fetchShelters();

Why call fetchShelters in three places?

Shelters should always appear on the map, even if the user denies location access or their browser does not support geolocation. Calling fetchShelters() in all three branches (success, error, and no-support) ensures the shelters load no matter what.

  • Save app.js.

Before you refresh your browser, what do you think you will see? Will the map look any different from before?

  • Refresh your browser. Grant location permission if prompted.
  • Open your browser's developer console by pressing Cmd+Option+J (macOS) or Ctrl+Shift+J (Windows).

You should see a console message like Shelters fetched: 47 (the exact number depends on how many shelters are currently open). On the map, blue marker pins should be scattered across the US, one for each shelter.

Not seeing any shelter markers?

  • Check the console for errors. If you see a network error, the FEMA API might be temporarily unreachable. Wait a moment and refresh.
  • Make sure fetchShelters(); appears inside the geolocation success callback, after map.setView. If it is outside the callback, it may run before the map is ready.
  • Verify that the for loop is inside the second .then() block, after the console.log line. If it is outside, the markers will not be created.

help me debug why my FEMA shelter markers are not appearing on the map

✔️ Awesome, I've got everything!

Great. Double check you have saved app.js and move on to the next step.

ⓧ I'd like to double check the full code

Your app.js file is now too long to show in a single block. Scroll up through your code and confirm these sections appear in order:

  • Map initialization with L.map and L.tileLayer at the top.
  • var userLat = null; and var userLon = null; declarations.
  • The complete fetchShelters() function with the URL, fetch() call, console.log, the for loop that creates L.marker for each shelter, and the .catch() block.
  • The geolocation if/else block at the bottom, with fetchShelters(); called in the success callback (after map.setView), the error callback (after the alert), and the else block (after the alert).

Your index.html and style.css files are unchanged from the previous step.

You have live government shelter data on your map. But every marker looks the same. You cannot tell which shelters are close to you, and clicking a marker does nothing. Next, you will add distance calculations, color-coded markers, and rich popups that turn these dots into useful information.

Add Distance Calculation, Color-Coded Markers, and Detail Popups

In the last step, you pulled live shelter data from FEMA and scattered markers across the map. That is a solid start, but every marker looks identical.

You cannot tell which shelters are close to you or what services they offer. In this step, you will add distance calculation, color-coded markers, and detail popups that turn the raw data into something genuinely useful.

In this step, get ready to:
  • Write helper functions for distance calculation and marker color coding.
  • Style shelter markers and popups with CSS.
  • Refactor the marker loop to show colored markers with detail popups.
Write the distance calculation function

To figure out which shelters are nearest, the app needs to calculate the real-world distance between two latitude/longitude points. The Haversine formula handles this using pure math and the Earth's radius.

  • In app.js, find the line var userLon = null;. Add the following function directly below it:
// Calculate miles between two lat/lon points
function haversineDistance(lat1, lon1, lat2, lon2) {
  var R = 3959; // Earth's radius in miles
  var dLat = (lat2 - lat1) * Math.PI / 180;
  var dLon = (lon2 - lon1) * Math.PI / 180;
  var a =
    Math.sin(dLat / 2) * Math.sin(dLat / 2) +
    Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
    Math.sin(dLon / 2) * Math.sin(dLon / 2);
  var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  return R * c;
}

What does this code do?

The Haversine formula calculates the shortest distance between two points on a sphere. It converts latitude and longitude differences into radians, then uses trigonometry to account for the Earth's curvature.

var R = 3959 sets the Earth's mean radius in miles. The function returns the distance in miles between any two coordinate pairs.

  • Save app.js.
  • Refresh the page in your browser and open the browser console by pressing Cmd+Option+J (macOS) or Ctrl+Shift+J (Windows).
  • Test the function by running this in the console:
haversineDistance(40.7128, -74.0060, 34.0522, -118.2437)

This calculates the distance from New York City to Los Angeles. You should see a number close to 2,451 in the console output.

Seeing NaN or an error?

  • Make sure you saved app.js and refreshed the page before testing in the console.
  • Check that the function is placed between var userLon = null; and function fetchShelters(), not inside another function.
  • Verify that Math.PI is capitalized exactly as shown. JavaScript's Math object is case-sensitive.

Still stuck? Ask for help.

Add color and icon helper functions

With distances calculated, you can color-code each marker so the learner instantly sees which shelters are close and which are far. Green means close, orange means medium distance, and red means far away.

  • In app.js, add these two functions directly below haversineDistance:
// Return a color based on how far the shelter is
function getMarkerColor(distance) {
  if (distance < 100) {
    return '#2ecc71'; // Green for under 100 miles
  } else if (distance < 500) {
    return '#f39c12'; // Orange for 100-500 miles
  } else {
    return '#e74c3c'; // Red for over 500 miles
  }
}

// Create a circular Leaflet icon with the given color
function createShelterIcon(color) {
  return L.divIcon({
    className: '',
    html: '<div class="shelter-marker" style="background-color: ' + color + ';"></div>',
    iconSize: [16, 16],
    iconAnchor: [8, 8],
    popupAnchor: [0, -10]
  });
}

What do these functions do?

  • getMarkerColor(distance) picks a hex color based on distance thresholds: green (#2ecc71) for under 100 miles, orange (#f39c12) for 100 to 500 miles, and red (#e74c3c) for anything farther.
  • createShelterIcon(color) returns a Leaflet L.divIcon that renders as a small CSS-styled circle instead of the default blue pin. The shelter-marker class will be styled in CSS shortly.
  • Save app.js.
  • Refresh the page and test in the browser console by running:
getMarkerColor(50)

You should see #2ecc71 (green) in the console. Try getMarkerColor(200) for orange and getMarkerColor(600) for red.

Function not defined?

  • Make sure you placed both functions after the closing } of haversineDistance and before function fetchShelters().
  • Check for missing curly braces. Each function needs its own opening and closing {}.

Need a hand? Ask for help.

Refactor the shelter markers with colors and popups

The helper functions are ready. Now you need CSS to style the markers and popups, then you need to update the loop inside fetchShelters() to use everything you just built.

  • In style.css, add these styles below the @keyframes pulse block to style the shelter markers and popup content:
/* Style the colored circle markers */
.shelter-marker {
  width: 16px;
  height: 16px;
  border-radius: 50%;
  border: 2px solid #fff;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
}

/* Style the popup heading */
.leaflet-popup-content h3 {
  margin: 0 0 8px 0;
  font-size: 14px;
  color: #2c3e50;
}

/* Style popup body text */
.leaflet-popup-content p {
  margin: 4px 0;
  font-size: 12px;
  color: #555;
}

/* Highlight the distance line in the popup */
.leaflet-popup-content .distance {
  font-weight: bold;
  color: #2980b9;
  font-size: 13px;
  margin-top: 8px;
}

What do these styles do?

  • .shelter-marker turns each shelter icon into a small circle with a white border and subtle drop shadow.
  • The .leaflet-popup-content rules style the popup heading, body text, and a bold blue distance line so shelter details are easy to scan.
  • Save style.css.

✔️ Awesome, I've got everything!

Great. Make sure you saved style.css before moving on.

ⓧ I'd like to double check the full code

Your style.css should contain these sections in order. Scroll up and confirm each one is present:

  • The * reset, html, body, and #map rules from Step 1.
  • The .user-marker and @keyframes pulse rules from Step 2.
  • The .shelter-marker and .leaflet-popup-content rules you just added.

Now switch back to app.js to update the shelter marker loop. You will make two changes inside the for loop of fetchShelters().

  • In app.js, scroll to the for loop inside fetchShelters().
  • Find this line:
        var shelter = features[i];
        var geom = shelter.geometry;
  • Add var attrs = shelter.attributes; on a new line between them, so the three lines now look like this:
        var shelter = features[i];
        var attrs = shelter.attributes;
        var geom = shelter.geometry;
  • Now find the old marker line further down in the same loop:
        L.marker([shelterLat, shelterLon]).addTo(map);
  • Delete that line and replace it with this code, which calculates the distance, picks a color, builds a popup, and creates the marker:
        // Calculate distance and pick a marker color
        var distance = null;
        var color = '#95a5a6';

        if (userLat !== null && userLon !== null) {
          distance = haversineDistance(userLat, userLon, shelterLat, shelterLon);
          color = getMarkerColor(distance);
        }

        var icon = createShelterIcon(color);

        // Build HTML popup with shelter details
        var popupContent = '<h3>' + (attrs.shelter_name || 'Unknown Shelter') + '</h3>'
          + '<p>' + (attrs.address || '') + '</p>'
          + '<p>' + (attrs.city || '') + ', ' + (attrs.state || '') + ' ' + (attrs.zip || '') + '</p>'
          + '<p>Status: <strong>' + (attrs.shelter_status || 'Unknown') + '</strong></p>'
          + '<p>Current Population: ' + (attrs.total_population !== null ? attrs.total_population : 'N/A') + '</p>';

        // Append distance if user location is known
        if (distance !== null) {
          popupContent += '<p class="distance">' + distance.toFixed(1) + ' miles from you</p>';
        }

        // Create the marker with the colored icon and popup
        L.marker([shelterLat, shelterLon], { icon: icon })
          .bindPopup(popupContent)
          .addTo(map);

What does this code do?

  • var color = '#95a5a6' sets a grey default color. If the user's location is available, it calculates the real distance and overrides the color with green, orange, or red.
  • popupContent builds an HTML string with the shelter's name, full address, status, and current population from the FEMA data.
  • distance.toFixed(1) rounds the distance to one decimal place so it reads cleanly in the popup.
  • .bindPopup(popupContent) attaches the HTML popup to each marker so it opens when clicked.

Before you refresh, what do you think the markers will look like now? Will they still be blue pins, or something different?

  • Save app.js and refresh the page in your browser.

The shelter markers now appear as colored circles instead of blue pins. Green markers are within 100 miles of you, orange markers are 100 to 500 miles away, and red markers are over 500 miles.

  • Click any shelter marker to open its popup. You should see the shelter name, full address, status, current population, and distance in miles from your location.

Markers missing or popups not showing?

  • If no markers appear at all, open the browser console and look for errors. A missing comma or unmatched quote in popupContent will break the loop silently.
  • If markers appear but clicking does nothing, check that .bindPopup(popupContent) is chained directly onto the L.marker() call with no semicolon between them.
  • If all markers are grey, the user location may not have loaded. Make sure you allowed location access when your browser prompted you.

Still stuck? Ask for help.

✔️ Awesome, I've got everything!

Great. Make sure both app.js and style.css are saved.

ⓧ I'd like to double check the full code

Your app.js should contain these sections in order. Scroll up and confirm each one is present:

  • Map initialization with L.map('map').setView([39.8283, -98.5795], 4) and the tile layer.
  • var userLat = null; and var userLon = null; declarations.
  • The haversineDistance function (added in this step).
  • The getMarkerColor and createShelterIcon functions (added in this step).
  • The fetchShelters() function with the updated loop that uses attrs, distance calculation, colored icons, and popup content.
  • The geolocation block with success callback, error callback, and the else fallback.

Your shelter finder now shows live distance-aware data with color-coded markers and rich detail popups. Next up, you will take on a Secret Mission to add a sorted sidebar that lists the closest shelters with click-to-zoom navigation.

Secret mission

Build a Closest Shelters Sidebar

Color-coded markers are great for a visual overview, but during an actual emergency, people need a quick sorted list of the closest options. In this mission, you will add a sidebar panel that lists the 10 closest shelters sorted by distance, with click-to-zoom navigation that pans the map and opens the popup.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely on your local machine with no cloud resources or API keys. There are no ongoing costs to worry about.

Resources you used:

  • Local project folder shelter-finder containing index.html, style.css, and app.js.

Keep everything running

No action needed. Choose this if you want to keep building on your shelter finder or revisit the code later.

  • Your shelter-finder folder stays on your Desktop. It takes up almost no disk space.
  • The app fetches live data from FEMA each time you open it, so the shelter information stays up to date automatically.
  • You could deploy this as a free static site on GitHub Pages or Netlify to share it with others.

Pause - I'll come back to this later

There are no running processes or background services to pause. Your files sit on disk until you open them again.

  • If you have Live Server or any local server running in VS Code, stop it by closing the terminal or pressing Ctrl+C.
  • Close the browser tab running your shelter finder.
  • Your shelter-finder folder and all three files will be right where you left them when you come back.

Delete - I don't want to use this again

Remove the entire project folder from your computer. This deletes all three files permanently.

  • Close the shelter-finder folder in VS Code.
  • Close any browser tabs running the app.
  • Delete the shelter-finder folder by running this command on macOS/Linux:
rm -rf ~/Desktop/shelter-finder
  • On Windows, run this instead:
rmdir /s /q %USERPROFILE%\Desktop\shelter-finder
  • Confirm the folder is gone by running:
ls ~/Desktop

You should no longer see shelter-finder in the output.

That's a Wrap!

That's a Wrap!

Nice work! You just built a fully functional disaster relief shelter finder that pulls live government data, pinpoints your location, and maps everything on an interactive Leaflet.js map.

You've learned how to:

  • Built an interactive web map from scratch using Leaflet.js and OpenStreetMap tiles, then consumed live shelter data from FEMA's OpenShelters ArcGIS REST API without needing any API key.
  • Integrated the browser Geolocation API to detect your position and implemented the Haversine formula to calculate real-world distances between coordinates in miles.
  • Created color-coded CSS markers (green, orange, red) based on distance and built rich HTML popups showing each shelter's name, address, status, population, and distance from you.
  • Secret Mission: Built a sorted sidebar listing the 10 closest shelters with click-to-zoom navigation that pans the map and opens each shelter's popup.

Ready to quiz yourself?