javascript,  weather

Create a JavaScript Weather App with Location Data Part 1

Create a JavaScript Weather App with Location Data Part 1

In the previous post, we discussed the possibility of writing our own location-based weather application. So let’s try it! In part 1 we will build a simple app that gets the users’ location and fetches weather data from an API using HTML and JavaScript. This approach is similar to using various mapping APIs to find the user location and will take us a bit behind the scenes on how some of the pre-built location map widgets really work.

Here are the steps we will work through to build the app:

  1. Get user coordinates using the HTML5 GeoLocation API
  2. Request weather data for the coordinates using an online weather API
  3. (optional) Reverse Geocode the coordinates to get additional location details
* Not required for most weather APIs, but worth touching on

Before we begin: The GeoLocation API

To request a user location (lat/long coordinates) within the web browser we will leverage the HTML5 GeoLocation API. This functionality is available in all major browsers (desktop and mobile) and includes IE - yeah, crazy I know. But just because it’s supported, doesn’t mean our code will always bring back a valid location. There are a few reasons why we might not get what we are looking for:

  1. When using the API in most browsers the GeoLocation results are only available over secure connections (HTTPS). So the final product should be hosted with HTTPS (we can enable this for free these days, so this shouldn’t be a show-stopper).
    • An exception to the HTTPS rule is local file:// testing which still works.
  2. The Geolocation API might be blocked by the user/settings. In most desktop/mobile operating systems users have the ability to block location services from being exposed to the browser. In addition, the user will be prompted to provide permission for an individual website as per W3C specifications.
  3. A request timeout can occur if the data takes too long to return. An optional PositionOptions object has a timeout setting representing the maximum length of time (in milliseconds) the device is allowed to wait for a return a position.
    • The default timeout value is Infinity, which is a long time to wait. It’s a good idea to set a reasonable timeout. Even slower response times should be expected if using this in combination with PositionOptions.enableHighAccuracy.

Note: As of Chrome 50, the Geolocation API will only work on secure contexts such as HTTPS. If your site is hosted on a non-secure origin (such as HTTP) the requests to get the users location will no longer function ~ w3schools.com

Thankfully the Geolocation API has some error checking available to provide some details when troubles occur, including timeouts. It’s then possible to add alternate location approaches such as IP Address lookup, prompt the user for a city, or to change their browser settings.

1. Get user coordinates using the HTML5 GeoLocation API

To start, we should determine if the browser has basic support for the Geolocation API. Again, even IE9+ supports this but still a good idea to test and very simple to implement. If for some reason there is no support, we can send the user a message letting them know their browser sucks.

//Check if the geolocation API exists
if (navigator.geolocation) {
  //true
  alert('Lets get the location (placeholder)');
} else {
  //false
  alert('geolocation not available?! What browser is this?');
  // prompt for city?
}

Assuming everything goes well we can swap our placeholder with some code to request the actual location coordinates. The GeoLocation API has three main methods:

  • getCurrentPosition() - used to return the user’s position. A single request per call. This is the one we will use for the example
  • watchPosition() - Returns the current position of the user and continues to return updated position as the user moves
  • clearWatch() - Stops the watchPosition() method

The getCurrentPosition and watchPosition methods are asynchronous, which means we create a callback to deal with the location data whenever it’s returned (or when an error is returned). A successful return can have up to 8 properties, however, only 3 of these are guaranteed: latitude, longitude, accuracy.

Here is a short sample with no error checks with an inline success callback so we can easily identify the basics:

//Short sample version with inline success callback
if (navigator.geolocation) {
    //Initial a request for the location
    navigator.geolocation.getCurrentPosition(function(pos){
      //'pos' return object has many properties we can grab
      var geoLat = pos.coords.latitude.toFixed(5);
      var geoLng = pos.coords.longitude.toFixed(5);
      var geoAcc = pos.coords.accuracy.toFixed(1);
    });
}

Here is a more complete sample with both the success and error callbacks in place. Error messages are broken out just in case we want to add more options in the future.

//More complete version
if (navigator.geolocation) {
    // Request the current position
    // If successful, call getPosSuccess; On error, call getPosErr
    navigator.geolocation.getCurrentPosition(getPosSuccess, getPosErr);
} else {
    alert('geolocation not available?! What year is this?');
    // IP address or prompt for city?
}

// getCurrentPosition: Successful return
function getPosSuccess(pos) {
  // Get the coordinates and accuracy properties from the returned object
  var geoLat = pos.coords.latitude.toFixed(5);
  var geoLng = pos.coords.longitude.toFixed(5);
  var geoAcc = pos.coords.accuracy.toFixed(1);
}

// getCurrentPosition: Error returned
function getPosErr(err) {
  switch (err.code) {
    case err.PERMISSION_DENIED:
      alert("User denied the request for Geolocation.");
      break;
    case err.POSITION_UNAVAILABLE:
      alert("Location information is unavailable.");
      break;
    case err.TIMEOUT:
      alert("The request to get user location timed out.");
      break;
    default:
      alert("An unknown error occurred.");
  }
}

That’s all it takes. If all goes well we now have the latitude and longitude coordinates. With this information available, we can to pass it to a weather API and/or convert the coordinates into a known location (reverse geocode).

2. Request weather data for the coordinates using an online weather API

There are many weather APIs available, and most of them have a free tier (for low usage and/or testing). Each will have some pros and cons, including various advanced features (15-day forecast, multiple forecast models, etc). One major change in this area is the recent shutdown of the very popular Yahoo! Weather API, probably one of the most used weather APIs over the past decade. As of January 3rd, 2019 the service is offline with a replacement service just starting its “by-request onboarding” phase. Since that’s currently not available, we are going to use DarkSky. This API has an easy to use free tier and accepts Lat/Long inputs. If using for production purposes it’s advised to review all of the APIs to find one that best fits your needs along with appropriate Terms of Use.

Side note on Weather APIs: Looks like this world is collapsing into more of an oligopoly. Weather Underground was purchased by The Weather Channel (2012), which was then acquired by IBM (finalized in 2017). Weather Underground was set to retire their API Dec 31, 2018, but have extended this to February 15, 2019, to allow more transition time.

Side note on Dark Sky: This is not a sponsored post and I have no affiliation/relationship with DarkSky. The use of their API is based on it being easy to use, good documentation, and a free usage tier.

Using DarkSky’s Weather API

The DarkSky developer API will require you to register an account to get access to a key. Once complete, a request is fairly simple:

<!-- API Params -->
https://api.darksky.net/forecast/[key]/[latitude],[longitude]
<!-- Sample Request -->
https://api.darksky.net/forecast/myFakeKey123abc/43.642567,-79.387054

The resulting JSON data returned from a request like this can be pretty substantial. We can reduce the response using query parameters (filters) to focus on what we want, and also set the measurement units (si, ca, us, uk2, auto). Now we can make a more streamlined request:

<!-- API Params for filters and units (auto) -->
https://api.darksky.net/forecast/[key]/[latitude],[longitude]?exclude=minutely,hourly,daily,alerts,flags&units=auto
<!-- Sample Request with filters and units -->
https://api.darksky.net/forecast/myFakeKey123abc/43.642567,-79.387054?exclude=minutely,hourly,daily,alerts,flags&units=auto

Here is the returned JSON object at the CN Tower in Toronto, Ontario, Canada (43.642567, -79.387054) with filters and units set:

{
   "latitude":43.642567,
   "longitude":-79.387054,
   "timezone":"America/Toronto",
   "currently":{
      "time":1546832805,
      "summary":"Partly Cloudy",
      "icon":"partly-cloudy-night",
      "nearestStormDistance":219,
      "nearestStormBearing":75,
      "precipIntensity":0,
      "precipProbability":0,
      "temperature":-5.11,
      "apparentTemperature":-10.56,
      "dewPoint":-13.61,
      "humidity":0.51,
      "pressure":1032.58,
      "windSpeed":14.42,
      "windGust":26.78,
      "windBearing":38,
      "cloudCover":0.59,
      "uvIndex":0,
      "visibility":16.09,
      "ozone":247.23
   },
   "offset":-5
}

Much easier to work with this smaller dataset, but it does remove some advanced attributes we might want to tap into later. For now, we sent a request based on user location and received the weather data. Time to build a basic GUI and grab a beer (who cares if it’s -5 celsius outside). While we let our beer warm up and breathe a little, we will wrap this request into JavaScript.

Building the request in JavaScript

With the DarkSky URL syntax ready to go, we just need to make the call in our code. Well, almost ready. There is one last hurdle: CORS. DarkSky has disabled Cross-Origin Resource Sharing (CORS) on their servers, and for good reason. As recommend by Dark Sky, you should leverage a secure proxy where you can store your Secret API Key (don’t put this in client-side JavaScript or bad people will do bad things with it).

Your API call includes your secret API key as part of the request. If you were to make API calls from client-facing code, anyone could extract and use your API key, which would result in a bill that you’d have to pay. We disable CORS to help keep your API secret key a secret ~ Dark Sky FAQ

Argghhh, the CORS limitation doesn’t help when building a JavaScript example. As a quick workaround, I’m going to leverage the “Heroku method” which will use the Heroku public CORS proxy and send the secret key directly from the client. THIS IS FOR TESTING ONLY so please don’t use this approach in your published code. Only a private proxy or create your own with your secret behind the scenes. It’s called a “secret” for a reason.

CORS limitations behind us, we just need to make our JavaScript request and use the results for our interface. For this, we are using jQuery’s getJSON() method. jQuery isn’t required here, it’s just a shorthand call to the XMLHttpRequest() constructor which you could use instead. I’ll place this in a separate function to keep things easy, and because you could put this entire function in your proxy if desired (just send geoLat and geoLong to the proxy to keep your secret key hidden).

jQuery’s getJSON() will request JSON-encoded data from the server using a GET HTTP request. In general, it takes a URL (like the weather ones we crafted earlier), and returns a Deferred object. In this case, it implements the Promise interface so we can provide multiple callbacks. A successful callback will route to .done(), while an error response will be directed to .fail().

_dsSecret = "yourSecret"; //Again, for testing only, should be hidden in proxy

function fn_getWeatherByLL(geoLat,geoLng){
  //API Variables
  var proxy = 'https://cors-anywhere.herokuapp.com/';
  var dsAPI = "https://api.darksky.net/forecast/";
  var dsKey = _dsSecret + "/";
  var dsParams = "?exclude=minutely,hourly,daily,alerts,flags&units=auto";
  //Concatenate API Variables into a URLRequest
  var URLRequest = proxy + dsAPI + dsKey + String(geoLat) + "," + String(geoLng) + dsParams
  //Make the jQuery.getJSON request
  $.getJSON( URLRequest )
    //Success promise
    .done(function( data ) {
      var wSummary = data.currently.summary;
      var wTemperature = data.currently.temperature;
      // lots of results available on the data object
      // use the results to populate the GUI here
    })
    //Error promise
    .fail(function() {
      alert('Sorry, something bad happened when retrieving the weather');
    }
  );
}

Have Weather > populate GUI

Once we have weather data for the location, we can populate the user interface. In the code above, the data object contains all the information needed to build your weather dashboard. From current temperature, daily max/mins, and short-term forecast - how you present this to the user is up to you.

Weather GUI

The image above is based on an example JSFiddle adding results that I’ve already requested in advance using the methods above. The only thing needed is to update the HTML text of the IDs when the data object is returned in the promise.

Notice we don’t have a city/place name on the page, just latitude and longitude - Turns out this information isn’t available in the return object. If you want to find the place name of the current location, see the Reverse Geocoding section below.

Dark Sky Forecast Embeds

At this point, I should probably confess that Dark Sky also has a number of embeddable weather widgets. Don’t be mad, if I started with this information, we wouldn’t know what it takes to make it all happen - and possibly miss out on some JavaScript goodness. You’re welcome. Dark Sky has many ways to include their widgets if you can agree to their widget terms of service. These can be accessed directly from Dark Sky as scripts, an iframe (via Forecast.io), and from Weatherwidget.io.

If you just want a widget on your page, good news, no secret keys! Just get the location from the Geolocation API and create an iframe on the page dynamically.

function getPosSuccess(pos) {
  // Get the coordinates of the current possition.
  var geoLat = String(pos.coords.latitude.toFixed(5));
  var geoLng = String(pos.coords.longitude.toFixed(5));

  //Create an iframe and use the current location data
  var iSource = "https://forecast.io/embed/#lat=" + geoLat + "&lon=" + geoLng + "&name=Woot&color=#00aaff";
  $('<iframe>') // Creates the element
    .attr('src', iSource) // Sets the attribute spry:region="myDs"
    .attr('height', 245) // Set the height
    .attr('width', "100%") // Set the width
    .appendTo('#id-weather'); // Append to an existing element ID
}

Code Complete

That covers the basics of finding the device location, getting weather data from the internet machine (DarkSky API), and building a basic HTML interface. Now you can play with the weather data and determine how to enhance the interface.

This is really it for part 1. In part 2 we will build a proxy using Node.js (server-side JavaScript) to show one approach to dealing with secret keys.

Oh, I did mention we could reverse geocode the coordinates as well…

3. Reverse Geocode the coordinates to get additional location details (optional)

For weather data, we don’t need amazing accuracy because weather data doesn’t change at the street level. So even if the accuracy sucks, it should have little consequence. But if you want to find and show the current city, address, or include a pin on a map, reverse geocoding to the rescue. Using a Reverse Geocoding API is pretty much identical to using a weather API - Provide coordinates in a URL and wait for results.

Again, we have lots of API sources to reverse geocode a lat/long to an address: Esri search API, Google Maps/places API, Bing Maps API, and OpenStreetMap API to name just a few. A future article will explore these options in more detail. For now, let’s take a quick look at OSM Nominatim.

OSM Nominatim

OpenStreetMap “Nominatim” has a REST endpoint to reverse geocode a pair of coordinates. The wiki help does an excellent job getting into all the options here but it is actually handled the same way as DarkSky. Nominatim requires three parameters: format, latitude, and longitude. Using the format option in the example below, a JSON object is returned with lots of information.

<!-- Sample request -->
https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=43.642567&lon=-79.387054
// return object
{  
   "place_id":"84050944",
   "licence":"Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright",
   "osm_type":"way",
   "osm_id":"32742038",
   "lat":"43.6425637",
   "lon":"-79.3870871832047",
   "place_rank":"30",
   "category":"tourism",
   "type":"attraction",
   "importance":"0.466188092284943",
   "addresstype":"tourism",
   "name":"CN Tower",
   "display_name":"CN Tower, 301, Front Street West, Entertainment District, Old Toronto, Toronto, Ontario, M5V 2X3, Canada",
   "address":{  
      "attraction":"CN Tower",
      "house_number":"301",
      "road":"Front Street West",
      "neighbourhood":"Entertainment District",
      "city_district":"Old Toronto",
      "city":"Toronto",
      "state":"Ontario",
      "postcode":"M5V 2X3",
      "country":"Canada",
      "country_code":"ca"
   },
   "boundingbox":[  
      "43.6423338",
      "43.6427965",
      "-79.3874416",
      "-79.3867985"
   ]
}

This provides lots of location information. Worth checking in various rural/urban areas to see how the results differ, but overall this gives a good idea of what to expect.


If you found my writing entertaining or useful and want to say thanks, you can always buy me a coffee.
ko-fi