Grayscale

❮ View all projects

Context

I wanted to build a React application to grow my developer skills. A friend at work encouraged me to push myself to make a weather app that answered a simple question... "Will it rain today?". I was planning on making something simpler like a nice ToDo list - but with my friend's encouragement I dove in and worked on figuring out a weather app. The app needed to do a few simple things:

  1. Get the location of the device in use.
  2. Get the forecast for that location.
  3. Determine if rain is in the forecast.
  4. Show the City you are in, so users would be confident that the forecast was accurate.

Getting Device Location

My first step was finding a weather API that could give me forecast data, specifically precipitation. I did some Googling and came across the National Weather Service API which had some endpoints for general forecasting, as well as rainfall. You can find documentation about this resource here.

From there I did some reading about the browser geolocation API and how to get a devices lat / lon. It was a relatively simple inclusion, but I hit a couple blockers with the device timing out. There's an options object you can include to allow more time for the device to respond.

//getGeoLocation.js
const GetGeoLocation = (callback) => {
  let location;
  if(navigator.geolocation) {
    const gpsSuccess = (position) => {
      location = {lat: position.coords.latitude, lon: position.coords.longitude}
      return callback(location);
    }
    const gpsError = (err) => {
      console.warn(`ERROR(${err.code}): ${err.message}`);
      return callback(null);
    }
    const gpsOptions = {
      maximumAge: 1000,
      timeout: 25000,
      enableHighAccuracy: false,
    }
    navigator.geolocation.getCurrentPosition(gpsSuccess, gpsError, gpsOptions);
  } else {
    console.warn('ERROR: location disabled');
  }
};

export default GetGeoLocation;

Getting The Forecast

Once I got the lat and lon for the device I stored it in state and passed it off to the fetch API to hit the weather endpoint. They are added to the URL for the fetch, and the response contains a sub URL string that needs to be fetched for precipitation data. For this I needed to build an array of objects with the time and value (chance of rain).

The timestamps returned from the API were all wacky because I didn't get a series of 24 values for a day. I would get a handful of values, or sometimes only 1.

example timestamp: 2019-10-22T11:00:00+00:00/P1DT13H

I learned that P1DT13H means this forecasted value is valid for the next 1 day and 13 hours. This causes some issues when trying to show an hourly forecast. So my next challenge involved parsing that timecode into days and hours, then converting to a total number of hours, then building an array of objects to cover that span of time.

...
//getForecast.js

let cleanData = [];

for (let i = 0; i < array.length; i++) {
  // get the timestamp separate from duration
  const timestamp = array[i]['validTime'].split('/');

  // split the chance for generating more timestamps
  const value = array[i]['value'];

  // split the duration into days / hours
  const duration = timestamp[1].split('T');

  // format time for manipulation
  let time = moment.utc(timestamp[0]).format();

  // the days in the timestamp
  let days = duration[0];

  // the hours in the timestamp
  let hours = duration[1];

  // validate hours is present
  if(!hours) {
    hours = 0;
  } else {
    hours = parseFloat(hours.match(/(\d+)/g));
  }
  if(days.length > 1) {
    days = parseFloat(days.match(/(\d+)/g)) * 24;
    hours += days;
  }
  if(hours > 1) {
    // push the initial forecast object before looping
    cleanData.push({
      validTime: time,
      value: value
    });

    for(let p = 1; p < hours; p++) {
      time = moment.utc(timestamp[0]).add(p, 'hours').format();
      cleanData.push({
        validTime: time,
        value: value
      });
    }

  } else {
    cleanData.push({
      validTime: time,
      value: value
    });
  }
}
return cleanData;
...

WHEW!

In hindsight, I could probably rewrite that to a Do... While loop so it gets processed at least once, and I can remove that initial push. Always ways to get better! This was one of my bigger headaches and I was pretty jazzed when I finally had a nice clean set of data to work with.

Getting City and State

The last piece of my MVP puzzle involved converting the device's lat / lon to a real location, something human readable. Originally I planned to use another API but all the geo-encoding APIs cost money, and I wasn't about to make an investment in a personal project. Again, my work buddy encouraged me to get outside my comfort zone and try to make my own set of data. After some research I found a massive dataset that included every US Zip Code, it's corresponding city, and the "center" lat / lon coordinates.

I downloaded it as a JSON file, wrote a program (thoughtfully called program.js) that parses all the data and only returns an array of objects with relevant data for my app. This trimmed my data from 17.5mb down to 2.9mb, and made me feel like I had super powers. I learned that sometimes to write a program, you need to write smaller programs.

The geolib library was able to take the provided lat / lon from the device and compare it to all the lat / lons in my data set, returning the nearest match. I then returned the associated City and State to the UI.

//getCityState.js
import { findNearest } from 'geolib';
import myData from '../locations.json';

const GetCityState = (location) => {

  let nearest = findNearest(location, myData);
  return `${nearest.c}, ${nearest.s}`
}

export default GetCityState;

Outcome

I was able to publish my React app to Github pages here. The complete source code can be seen on Github.

The app is still a work in progress, but if you load it on mobile you can install it to your device. I've since learned that the geolocation API for web on Android is out of date, and very very slow. It also does not fail gracefully, so if any part fails the user won't know... not ideal.

I have plans to do even more such as manual entry of a zip-code or city / state to get the weather somewhere you ARE NOT. I would love to polish the experience more, too, to have more robust animations and slicker visuals. With my recent work on React Native I've actually given thought to rebuilding it as a hybrid app and polishing it up for app store consumption. I'll update this post when I get something new to share.