DEV Community

loading...
Cover image for 🖲Create a COVID-Tracker with Reverse Geocoding Feature using JavaScript and Mapbox🗺[FULL CODE]

🖲Create a COVID-Tracker with Reverse Geocoding Feature using JavaScript and Mapbox🗺[FULL CODE]

The Nerdy Dev
Building products is my profession, Creating content is my passion. We believe that Education should be freely accessible to everyone and no one should be deprived of it.
・10 min read

Hey everyone 👋🏻,

In this article, let us see how we can build a COVID-19 Tracker Application with Reverse Geocoding Feature using JavaScript and Mapbox.

✏ Demo for the Project & Initial Setup

Alt Text

Features

  1. Clustering of points based on the countries and each cluster will represent a set of locations within it that are prone to COVID19.

  2. Reverse Geocoding using Latitude and Longitude to get the place name and getting the number of deaths and number of confirmed cases for each location that is present within the country.

Live Link: https://covid2019-tracker.netlify.com/

Basic Requisites

HTML, CSS and basic knowledge of JavaScript is enough to get started.

CODE 👨🏻‍💻

📝 Create an index.html file

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Corona Virus Tracker</title>
  <link rel="stylesheet" href="styles.css">
  <link rel="stylesheet" href="https://api.mapbox.com/mapbox-gl-js/v1.8.1/mapbox-gl.css">
  <link rel="stylesheet"
    href="https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v2.3.0/mapbox-gl-geocoder.css" type="text/css">
</head>

<body>

  <div class="container">
    <h1>Corona Virus Tracker</h1>
    <hr>
    <!-- Corona World Details Render here -->
    <div id="corona-world-details">
      <!-- Render the table here -->
    </div>
    <div id="map"></div>
    <select name="select-country">
    </select>
    <div id="corona-details">
    </div>
  </div>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/nprogress/0.2.0/nprogress.min.js"></script>
  <script src="https://api.mapbox.com/mapbox-gl-js/v1.8.1/mapbox-gl.js"></script>
  <script
    src="https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v2.3.0/mapbox-gl-geocoder.min.js"></script>
  <script src="app.js"></script>
</body>

</html>
Enter fullscreen mode Exit fullscreen mode

🎨 styles.css (Styles for our Tracker Application)

/* normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */button,hr,input{overflow:visible}progress,sub,sup{vertical-align:baseline}[type=checkbox],[type=radio],legend{box-sizing:border-box;padding:0}html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}details,main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:ButtonText dotted 1px}fieldset{padding:.35em .75em .625em}legend{color:inherit;display:table;max-width:100%;white-space:normal}textarea{overflow:auto}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}[hidden],template{display:none}

/* Variables */
html {
  --grey: #e7e7e7;
  --gray: var(--grey);
  --blue: #0072B9;
  --pink: #D60087;
  --yellow: #ffc600;
  --black: #2e2e2e;
  --red: #c73737;
  --green: #61e846;
  --text-shadow: 2px 2px 0 rgba(0,0,0,0.2);
  --box-shadow: 0 0 5px 5px rgba(0,0,0,0.2);
  font-size: 62.5%;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
  box-sizing: border-box;
}

*, *:before, *:after {
  box-sizing: inherit;
}

body {
  font-size: 2rem;
  line-height: 1.5;
  background-color: var(--blue);
  background-image: url("data:image/svg+xml,%3Csvg width='20' height='100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 21.184c.13.357.264.72.402 1.088l.661 1.768C4.653 33.64 6 39.647 6 50c0 10.271-1.222 15.362-4.928 24.629-.383.955-.74 1.869-1.072 2.75v6.225c.73-2.51 1.691-5.139 2.928-8.233C6.722 65.888 8 60.562 8 50c0-10.626-1.397-16.855-5.063-26.66l-.662-1.767C1.352 19.098.601 16.913 0 14.85v6.335zm20 0C17.108 13.258 16 8.077 16 0h2c0 5.744.574 9.951 2 14.85v6.334zm0 56.195c-2.966 7.86-4 13.123-4 22.621h2c0-6.842.542-11.386 2-16.396v-6.225zM6 0c0 8.44 1.21 13.718 4.402 22.272l.661 1.768C14.653 33.64 16 39.647 16 50c0 10.271-1.222 15.362-4.928 24.629C7.278 84.112 6 89.438 6 100h2c0-10.271 1.222-15.362 4.928-24.629C16.722 65.888 18 60.562 18 50c0-10.626-1.397-16.855-5.063-26.66l-.662-1.767C9.16 13.223 8 8.163 8 0H6z' fill='%23fff' fill-rule='nonzero' fill-opacity='.1' opacity='.349'/%3E%3C/svg%3E%0A");
  background-size: 15px;
  margin: 0;
  padding: 0;
}

/* Table Styles */


table {
  border-radius: 5px;
  overflow: hidden;
  margin-bottom: 2rem;
  border-collapse: collapse;
}

td, th {
  border: 1px solid var(--grey);
  padding: 0.5rem;
}


.container {
  max-width: 1000px;
  margin: 4rem auto;
  padding: 2rem;
  background: white;
  box-shadow: 0 0 3px 5px rgba(0,0,0,0.08653);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;

}
.container h1,
.container h2,
.container h3,
.container h4,
.container h5,
.container h6 {
  color: var(--black);
  text-shadow: none;
}
#map {
  /* position: relative; */
  top: 0;
  bottom: 0;
  height: 500px;
  width: 500px;
}


:focus {
  outline-color: var(--pink);
}

h1,
h2,
h3,
h4,
h5,
h6 {
  color: white;
  margin-top: 0;
  line-height: 1;
  text-shadow: var(--text-shadow);
}

 select {
  display: block;
  padding: 1rem;
  margin: 20px;
  border: 1px solid var(--grey);
  outline: none;
}
.recovered {
  color:var(--green);
}
.deaths {
  color: var(--red);
}
.cases {
  color: var(--yellow);
}

/* Make clicks pass-through */
#nprogress {
  pointer-events: none;
}

#nprogress .bar {
  background: red;
  position: fixed;
  z-index: 1031;
  top: 0;
  left: 0;

  width: 100%;
  height: 5px;
}

/* Fancy blur effect */
#nprogress .peg {
  display: block;
  position: absolute;
  right: 0px;
  width: 100px;
  height: 100%;
  box-shadow: 0 0 10px red, 0 0 5px red;
  opacity: 1.0;

  -webkit-transform: rotate(3deg) translate(0px, -4px);
      -ms-transform: rotate(3deg) translate(0px, -4px);
          transform: rotate(3deg) translate(0px, -4px);
}

/* Remove these to get rid of the spinner */
#nprogress .spinner {
  display: block;
  position: fixed;
  z-index: 1031;
  top: 15px;
  right: 15px;
}

#nprogress .spinner-icon {
  width: 18px;
  height: 18px;
  box-sizing: border-box;

  border: solid 2px transparent;
  border-top-color: red;
  border-left-color: red;
  border-radius: 50%;

  -webkit-animation: nprogress-spinner 400ms linear infinite;
          animation: nprogress-spinner 400ms linear infinite;
}

.nprogress-custom-parent {
  overflow: hidden;
  position: relative;
}

.nprogress-custom-parent #nprogress .spinner,
.nprogress-custom-parent #nprogress .bar {
  position: absolute;
}

@-webkit-keyframes nprogress-spinner {
  0%   { -webkit-transform: rotate(0deg); }
  100% { -webkit-transform: rotate(360deg); }
}
@keyframes nprogress-spinner {
  0%   { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}
Enter fullscreen mode Exit fullscreen mode

app.js (JavaScript 🤖)


window.addEventListener('DOMContentLoaded',initializeApp);

const baseEndPoint = 'https://coronavirus-tracker-api.herokuapp.com/v2/locations';

let coronaDetailsContainer;
let countrySelectDropdown;
let coronaWorldDetailsContainer;

const coronaData = {
  latest: {},
  locations: []
}
const countriesWithCountryCodes = {
  "TH": "Thailand",
  "JP": "Japan",
  "SG": "Singapore",
  "NP": "Nepal",
  "MY": "Malaysia",
  "CA": "Canada",
  "AU": "Australia",
  "KH": "Cambodia",
  "LK": "Sri Lanka",
  "DE": "Germany",
  "FI": "Finland",
  "AE": "United Arab Emirates",
  "PH": "Philippines",
  "IN": "India",
  "IT": "Italy",
  "SE": "Sweden",
  "ES": "Spain",
  "BE": "Belgium",
  "EG": "Egypt",
  "LB": "Lebanon",
  "IQ": "Iraq",
  "OM": "Oman",
  "AF": "Afghanistan",
  "BH": "Bahrain",
  "KW": "Kuwait",
  "DZ": "Algeria",
  "HR": "Croatia",
  "CH": "Switzerland",
  "AT": "Austria",
  "IL": "Israel",
  "PK": "Pakistan",
  "BR": "Brazil",
  "GE": "Georgia",
  "GR": "Greece",
  "MK": "North Macedonia",
  "NO": "Norway",
  "RO": "Romania",
  "EE": "Estonia",
  "SM": "San Marino",
  "BY": "Belarus",
  "IS": "Iceland",
  "LT": "Lithuania",
  "MX": "Mexico",
  "NZ": "New Zealand",
  "NG": "Nigeria",
  "IE": "Ireland",
  "LU": "Luxembourg",
  "MC": "Monaco",
  "QA": "Qatar",
  "EC": "Ecuador",
  "AZ": "Azerbaijan",
  "AM": "Armenia",
  "DO": "Dominican Republic",
  "ID": "Indonesia",
  "PT": "Portugal",
  "AD": "Andorra",
  "LV": "Latvia",
  "MA": "Morocco",
  "SA": "Saudi Arabia",
  "SN": "Senegal",
  "AR": "Argentina",
  "CL": "Chile",
  "JO": "Jordan",
  "UA": "Ukraine",
  "HU": "Hungary",
  "LI": "Liechtenstein",
  "PL": "Poland",
  "TN": "Tunisia",
  "BA": "Bosnia and Herzegovina",
  "SI": "Slovenia",
  "ZA": "South Africa",
  "BT": "Bhutan",
  "CM": "Cameroon",
  "CO": "Colombia",
  "CR": "Costa Rica",
  "PE": "Peru",
  "RS": "Serbia",
  "SK": "Slovakia",
  "TG": "Togo",
  "MT": "Malta",
  "MQ": "Martinique",
  "BG": "Bulgaria",
  "MV": "Maldives",
  "BD": "Bangladesh",
  "PY": "Paraguay",
  "AL": "Albania",
  "CY": "Cyprus",
  "BN": "Brunei",
  "US": "US",
  "BF": "Burkina Faso",
  "VA": "Holy See",
  "MN": "Mongolia",
  "PA": "Panama",
  "CN": "China",
  "IR": "Iran",
  "KR": "Korea, South",
  "FR": "France",
  "XX": "Cruise Ship",
  "DK": "Denmark",
  "CZ": "Czechia",
  "TW": "Taiwan*",
  "VN": "Vietnam",
  "RU": "Russia",
  "MD": "Moldova",
  "BO": "Bolivia",
  "HN": "Honduras",
  "GB": "United Kingdom",
  "CD": "Congo (Kinshasa)",
  "CI": "Cote d'Ivoire",
  "JM": "Jamaica",
  "TR": "Turkey",
  "CU": "Cuba",
  "GY": "Guyana",
  "KZ": "Kazakhstan",
  "ET": "Ethiopia",
  "SD": "Sudan",
  "GN": "Guinea",
  "KE": "Kenya",
  "AG": "Antigua and Barbuda",
  "UY": "Uruguay",
  "GH": "Ghana",
  "NA": "Namibia",
  "SC": "Seychelles",
  "TT": "Trinidad and Tobago",
  "VE": "Venezuela",
  "SZ": "Eswatini",
  "GA": "Gabon",
  "GT": "Guatemala",
  "MR": "Mauritania",
  "RW": "Rwanda",
  "LC": "Saint Lucia",
  "VC": "Saint Vincent and the Grenadines",
  "SR": "Suriname",
  "XK": "Kosovo",
  "CF": "Central African Republic",
  "CG": "Congo (Brazzaville)",
  "GQ": "Equatorial Guinea",
  "UZ": "Uzbekistan",
  "NL": "Netherlands",
  "BJ": "Benin",
  "LR": "Liberia",
  "SO": "Somalia",
  "TZ": "Tanzania",
  "BB": "Barbados",
  "ME": "Montenegro",
  "KG": "Kyrgyzstan",
  "MU": "Mauritius",
  "ZM": "Zambia",
  "DJ": "Djibouti",
  "GM": "Gambia, The",
  "BS": "Bahamas, The",
  "TD": "Chad",
  "SV": "El Salvador",
  "FJ": "Fiji",
  "NI": "Nicaragua",
  "MG": "Madagascar",
  "HT": "Haiti",
  "AO": "Angola",
  "CV": "Cape Verde",
  "NE": "Niger",
  "PG": "Papua New Guinea",
  "ZW": "Zimbabwe",
  "TL": "Timor-Leste",
  "ER": "Eritrea",
  "UG": "Uganda",
  "DM": "Dominica",
  "GD": "Grenada",
  "MZ": "Mozambique",
  "SY": "Syria"
}
mapboxgl.accessToken = 'your-access-token'

let geocoder;

async function geocodeReverseFromLatLngToPlaceName(lat, lng) {
  return new Promise((resolve, reject) => {
    geocoder.mapboxClient.geocodeReverse({
      latitude: parseFloat(lat),
      longitude: parseFloat(lng)
    }, function (error, response) {
      if (error) {
        reject(error);
      }
      // console.log(response.features[0]?.place_name);
      resolve(response.features[0] && response.features[0].place_name);
    });
  })
}

function renderMap() {
  const map = new mapboxgl.Map({
    container: 'map',
    style: 'mapbox://styles/mapbox/dark-v10',
    center: [-99.9, 41.5],
    zoom: 1
  });
  // Data is of the form

  // coordinates: { latitude: "15", longitude: "101" }
  // country: "Thailand"
  // country_code: "TH"
  // id: 0
  // last_updated: "2020-03-23T09:32:03.487723Z"
  // latest: { confirmed: 599, deaths: 1, recovered: 44 }


  // Geocoder
  geocoder = new MapboxGeocoder({
    accessToken: mapboxgl.accessToken
  });
  map.addControl(geocoder);
  // Add zoom and rotation controls to the map.
  map.addControl(new mapboxgl.NavigationControl());

  map.on('load', async function () {
    map.addSource('places', {
      "type": "geojson",
      "data": {
        "type": "FeatureCollection",
        "crs": {
          "type": "name",
          "properties": {
            "name": "urn:ogc:def:crs:OGC:1.3:CRS84"
          }
        },
        "features": await Promise.all(coronaData.locations.map(async location => {
          // Do reverse geocoding
          const placeName = await geocodeReverseFromLatLngToPlaceName(location.coordinates.latitude, location.coordinates.longitude);
          // console.log('got the name of the place from reverse geocoding',placeName);
          console.log(placeName);
          return {
            "type": "Feature",
            "properties": {
              "description": `
                <table>
                  <thead>
                    <tr>${placeName}</tr>
                  </thead>
                  <tbody>
                    <tr>
                      <td>Confirmed Cases:</td>
                      <td>${location.latest.confirmed}</td>
                    </tr>
                    <tr>
                      <td>Deaths:</td>
                      <td>${location.latest.deaths}</td>
                    </tr>
                    <tr>
                      <td>Latitude:</td>
                      <td>${location.coordinates.latitude}</td>
                    </tr>
                    <tr>
                      <td>Longitude:</td>
                      <td>${location.coordinates.longitude}</td>
                    </tr>
                  </tbody>
                </table>
                `,
              "icon": "rocket"
            },
            "geometry": {
              "type": "Point",
              "coordinates": [
                `${location.coordinates.longitude}`,
                `${location.coordinates.latitude}`
              ]
            }
          };
        }))
      },
      cluster: true,
      clusterMaxZoom: 14,
      clusterRadius: 50
    });
    map.addLayer({
      id: 'clusters',
      type: 'circle',
      source: 'places',
      filter: ['has', 'point_count'],
      paint: {
        'circle-color': [
          'step',
          ['get', 'point_count'],
          '#51bbd6',
          100,
          '#f1f075',
          750,
          '#f28cb1'
        ],
        'circle-radius': [
          'step',
          ['get', 'point_count'],
          20,
          100,
          30,
          750,
          40
        ]
      }
    });

    map.addLayer({
      id: 'cluster-count',
      type: 'symbol',
      source: 'places',
      filter: ['has', 'point_count'],
      layout: {
        'text-field': '{point_count_abbreviated}',
        'text-font': ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'],
        'text-size': 12
      }
    });
    map.addLayer({
      id: 'unclustered-point',
      type: 'circle',
      source: 'places',
      filter: ['!', ['has', 'point_count']],
      paint: {
        'circle-color': '#11b4da',
        'circle-radius': 4,
        'circle-stroke-width': 1,
        'circle-stroke-color': '#fff'
      }
    });
    // inspect a cluster on the click
    map.on('click', 'clusters', function (event) {
      const features = map.queryRenderedFeatures(event.point, {
        layers: ['clusters']
      });
      const clusterId = features[0].properties.cluster_id;
      map.getSource('places').getClusterExpansionZoom(
        clusterId,
        function (error, zoom) {
          if (error) return;
          map.easeTo({
            center: features[0].geometry.coordinates,
            zoom: zoom
          });
        }
      );
    });
    map.on('click', 'unclustered-point', function (event) {
      const coordinates = event.features[0].geometry.coordinates.slice();
      // console.log(coordinates);
      const { description, icon } = event.features[0].properties;
      // Ensure that if the map is zoomed out such that multiple
      // copies of the feature are visible, the popup appears
      // over the copy being pointed to.
      while (Math.abs(event.lngLat.lng - coordinates[0]) > 180) {
        coordinates[0] += event.lngLat.lng > coordinates[0] ? 360 : -360;
      }

      new mapboxgl.Popup().setLngLat(coordinates)
        .setHTML(description)
        .addTo(map);
    });
    //   // Change the cursor to a pointer when the mouse is over the places layer.
    map.on('mouseenter', 'clusters', function () {
      map.getCanvas().style.cursor = 'pointer';
    });
    // Change it back to a pointer when it leaves.
    map.on('mouseleave', 'clusters', function () {
      map.getCanvas().style.cursor = '';
    });
    })
}

function populateLocation(country, country_code) {
  const countryOption = document.createElement('option');
  countryOption.value = country;
  countryOption.textContent = `${country_code}-${country}`;
  countrySelectDropdown.appendChild(countryOption);
}
function populateLocations() {
  Object.entries(countriesWithCountryCodes).forEach(([country_code, country]) => populateLocation(country, country_code));
}

async function initializeApp() {
  console.log('inside the init method');
  setReferences();
  doEventBindings();
  NProgress.start();
  populateLocations();
  await performAsyncCall();
  renderUI(coronaData.latest,true);
  // console.log(coronaData.latest, coronaData.locations);
  renderMap();
  NProgress.done();
}


async function performAsyncCall() {
    const response = await fetch(`${baseEndPoint}`);
    const data = await response.json();
    const { latest, locations } = data;
    coronaData.latest = { ...coronaData.latest, ...latest };
    coronaData.locations.push(...locations);
}

function setReferences() {
  // Set any references here
  coronaDetailsContainer = document.querySelector('#corona-details');
  countrySelectDropdown = document.querySelector('[name="select-country"]');
  coronaWorldDetailsContainer = document.querySelector('#corona-world-details');
}

function renderUI(details,world=false) {
  let html = '';
  html = `
   <table class="table">
     <thead>
        ${world ? `<h1>World Details</h1>` : (
           `<tr>${details.country}(${details.country_code})</tr>`
         )}
     </thead>
     <tbody>
         <tr>
           <td class="cases">Reported Cases:</td>
           <td>${world ? details.confirmed : details.latest.confirmed}</td>
         </tr>
       <tr>
         <td class="deaths">Deaths:</td>
         <td>${world ? details.deaths : details.latest.deaths}</td>
       </tr>
     </tbody>
   </table>
  `
  if (world) {
    coronaWorldDetailsContainer.innerHTML = html;
  } else {
    coronaDetailsContainer.innerHTML = html;
  }
}



function renderDetailsForSelectedLocation(event) {
  // console.log(event.target.value);
  const countrySelected = event.target.value;
  const locationCoronaDetails = coronaData.locations.reduce((accumulator, currentLocation) => {
    if (currentLocation.country === countrySelected) {
      accumulator['country'] = currentLocation.country;
      accumulator['country_code'] = currentLocation.country_code;
      accumulator.latest.confirmed += currentLocation.latest.confirmed;
      accumulator.latest.deaths += currentLocation.latest.deaths;
    }
    return accumulator;
  }, {
    country: '',
    country_code: '',
    latest: {
      confirmed: 0,
      deaths: 0
    }
  });
  // console.log(locationCoronaDetails);
  renderUI(locationCoronaDetails);
}



function doEventBindings() {
  // Do the event bindings here
  countrySelectDropdown.addEventListener('change', renderDetailsForSelectedLocation);
}


Enter fullscreen mode Exit fullscreen mode

I also have a complete video where we create this entire project from scratch. Check this:

So this is it for this article. Thanks for reading.

PS - Want to learn React ? I do have a FULL course on same on my YouTube Channel.

Check this :

Thanks for reading !

PS - If you are looking to learn Web Development, I have curated a FREE course for you on my YouTube Channel, check the below article :

👉🏻 Follow me on Twitter : https://twitter.com/The_Nerdy_Dev

👉🏻 Check out my YouTube Channel : https://youtube.com/thenerdydev

Discussion (0)