Welcome to the Treehouse Community

The Treehouse Community is a meeting place for developers, designers, and programmers of all backgrounds and skill levels to get support. Collaborate here on code errors or bugs that you need feedback on, or asking for an extra set of eyes on your latest project. Join thousands of Treehouse students and alumni in the community today. (Note: Only Treehouse students can comment or ask questions, but non-students are welcome to browse our conversations.)

Looking to learn something new?

Treehouse offers a seven day free trial for new students. Get access to thousands of hours of content and a supportive community. Start your free trial today.

JavaScript Node.js Basics (2014) Building a Command Line Application Perfecting: Getting Multiple Profiles

problem coding node.js extra credit challenge forecast.io command line app

hello,

this is mainly for Andrew Chalkley and Pasan Premaratne, but if anyone else, please, thinks they can be helpful, that'd be great too.

i finished chalkers very good node.js basics course and really enjoyed it. i wanted to try my hand at the extra credit challenge of creating a node.js command line app where you run the node.js app and give a command line argv of a u.s. zip code and it gets the current weather from forecast.io. like thus —

$ node forecast.js 10011

the problem is, forecast.io only takes as parameters longitude/latitude in their api to give back weather data.

so i went to the geonames.org and used their api to have them give longitude/latitude from the general center of a zip code area.

i also wanted to teach myself modules in node.js and javascript better, so i separated the work out into three files —

  • forecast.js — the main app that calls zip-forecast.js
  • zip-forecast.js — uses node.js' 'http' module to get the forecast from the forecast.io api and also calls zip-convert.js to convert the u.s. zip code into longitude and latitude.
  • zip-convert.js — this uses the node.js 'http' module to do the work of converting the zip to longitude and latitude from the geonames.org API.

the problem: (see files below) if you uncomment out lines 35 & 49 in zip-convert.js, and run it in a node server from the command line with $ node zip-convert.js 10011, you get the correct array returned with longitude and latitude. however, if you re-comment lines 35 & 49 in zip-convert.js and run it as a module from zip-forecast.js like so: $ node zip-forecast.js 97210, it returns as undefined, even though zip-convert.js is supposed to return the array of longitude and latitude.

i have the two files below (in the case of zip-forecast.js, i only included the code function with which i am having the errors). i can't proceed until i get this undefined problem sorted, so any help, please, would be appreciated. thanks in advance.

best,

— faddah wolf portland, oregon, u.s.a.

zip-convert.js —

//  Problem: forecast.io only will take arguments in longitude and latitude, and input from users will be zip/postal codes.

//  Solution:  use node going to geonames.org server api to get back JSON with longitude and latitude for given zip code

// send user input u.s. zip code to geonames server via node

var http = require("http");

// Prints out error messages
function printError(error) {
    console.error(error.message);
}

function getLocation(zip) {
    //  Connect to the API URL ("http://api.geonames.org/postalCodeLookupJSON?postalcode=[ZIP]&country=US&username=[USERNAME]")
    var username = "faddah";
    var geonamesAPIURL = "http://api.geonames.org/postalCodeLookupJSON?postalcode=" + zip + "&country=US&username=" + username;
    var request = http.get(geonamesAPIURL, function(response) {
        var finalData = "";
        //  Read the data
        response.on('data', function (dataStream) {
            finalData += dataStream;
        });
        response.on('end', function() {
          // if we have a functional server connection...
            if(response.statusCode === 200) {
                try {
                // console.log(JSON.parse(finalData));
                // Parse the returned finalData object, knowing it's first key is an array
                // (corrected below, per @jerrysv & @justinabrahms of Freenode #pdxnode - thanx guys!)
                    var location = JSON.parse(finalData).postalcodes[0];
                    // Print and return the longitude/latitude data objects
                    var arrLongLat = [location.lng, location.lat];
                    // console.log(zipLongLat);
                    return arrLongLat;
                } catch(error) {
                    // Parse Error
                    printError(error);
                }
            } else {
                // Status Code Error
                printError({message: "There was an error getting the profile for this Forecast.io call for \"" + zip + ",\" this may be a server error or a zip code that may not exist in the U.S.. (Status Code Error: \'" + response.statusCode + " - " + http.STATUS_CODES[response.statusCode] + "\')"});
            }
        });
    });
}

// getLocation(process.argv.slice(2));

// export module for use in zip-forecast.js
module.exports.getLocation = getLocation;

// api url call to geonames for zip code:  http://api.geonames.org/postalCodeLookupJSON?postalcode=97215&country=US&username=faddah

/* * * * * * * * * * * * * * * *

Example returned JSON data from geonames.com —

{ postalcodes:
   [ { adminName2: 'Multnomah',
       adminCode2: '051',
       postalcode: '97215',
       adminCode1: 'OR',
       countryCode: 'US',
       lng: -122.599001,
       placeName: 'Portland',
       lat: 45.514282,
       adminName1: 'Oregon' } ] }



* * * * * * * * * * * * * * * */

zip-forecast.js —

//  Problem:  Need to return weather data from forecast.io, but they only take .

//  Solution:  First, use our own created zip-convert.js to convert zip code to longitude/latitude.

var http = require("http");
var zipConvert = require("./zip-convert.js");
console.log("We are now after the requires.")

// Prints out error messages
function printError(error) {
    console.error(error.message);
}
console.log("We are now after the printError function.");

console.dir("The argv's are: " + process.argv);

var zipLongLat = {};
zipLongLat = zipConvert.getLocation(process.argv.slice[2]);
console.dir(zipLongLat);

6 Answers

Andrew Chalkley
STAFF
Andrew Chalkley
Treehouse Guest Teacher

Actually, on closer reflection that return arraLongLat is nested in a callback which means the return doesn't return from the getLocation.

are you saying i should write this, contrary to how you usually code in node.js, as a blocking, non-callback function?

Nope, you can move the printing function inside the "end" callback like we did in the course. We didn't print from a return value.

So, this is what you could do

1) in your forecast.js include the zip-convert.js 2) call the getLocation maybe you should rename it to getWeatherForZip 3) In the zip-convert.js include the zip-forecast.js 4) Call the getWeatherForGeolocation in the "end" callback. The getWeatherForGeolocation implemented in the zip-forecast.js. 5) In getWeatherForGeolocation in the "end" callback print out the weather.

Hope that helps :)

Regards
Andrew

Andrew Chalkley
STAFF
Andrew Chalkley
Treehouse Guest Teacher

Hi Faddah Wolf

The reason it's undefined is that the asynchronous call to get the Latitude and Longitude doesn't block. This means that the line immediately after it, the console.dir gets executed. The value hasn't been set yet, so therefore it's undefined.

One way you can get around this is instead of returning the array of lat and lng you could the call to the api in the zip-convert.js file.

There are more advanced ways of handling stuff like this that we'll go in to in courses down the road.

Regards,
Andrew

hi Andrew Chalkley , et al.,

i got it running!

this can all be found in my github repository for a node forecast.io serve

below are my code examples. i ended up doing several files, which i did primarily to teach myself modularity and splitting up code into work group chunks and then call it all via module exports from the main forecast.js file. as thus —

  • forecast.js — calls all the other files, including the npm async module, to print out the city location and weather info. uses async.eachSeries() module call to print out all zip code location & weather info in sequence.
  • zip-convert.js — the module that takes the u.s. zip code and gets a location object from geonames.org server, so it can pass longitude and latitude to zip-forecast.js
  • zip-forecast.js — module which, taking longitude and latitude from location object from geonames.org, gets weather for that location.
  • messages.js — module containing messages to print out location, print out weather forecast or print out error messages to console log.

in addition, in messages.js, i have it so if there is a special weather alert for the region in the returned weather object from forecast.io, it prints that out also. i also convert compass degrees to cardinal directions (N, NNW, NW, WNW, W,...etc.). it also converts the humidity number to a percentage, and the Atmospheric Pressure number from hPA to the more common U.S. Barometric Pressure inHG (inches Mercury).

i also, at the advisement of a friend (@s5fs on twitter), used npm and got the node async module and used async.eachSeries(...) instead of forEach(...). forEach(...) is faster, but async.eachSeries(...) prints everything out in the desired order.

here are my files for the solution (and i got lots of help from friends in the @pdxnode community, whom i thank in the README.md on github.

/* forecast.js */
//
// Called like
// $> node forecast.js 97214
//  - or -
// $> node forecast.js 10011 20050 91308 90069 94101 97215

var async = require('async'),
    getLocation = require('./zip-convert').getLocation,
    showLocation = require('./zip-convert').showLocation,
    getForecast = require('./zip-forecast').getForecast,
    showForecast = require('./zip-forecast').showForecast,
    zipcodes = process.argv.slice(2),
    printError = require('./messages').printError;

// for async module call, the middle iterator argument call, calling all other modules to do THE BIG WORK.
var iterator = function (zipcode, next) {
    // Do something w/the zipcode.
  getLocation(zipcode, function (error, location) {
    showLocation(error, location);
    getForecast(location, function (error, forecast) {
      showForecast(error, forecast, location);
      next(null, forecast);
    })
  });
 };

// for async mdule eachSeries() call, last "done" argument call.
var allDone = function (err) {
  printError(err);
  console.log('...and those are your forecasts for today!');
}

 // Loop over each zipcode, one after another.
async.eachSeries(zipcodes, iterator, allDone);


/*  *  *  *  *  *  *  *  *  *  

// optional other way to do it with forEach() — works, faster, but does not work as well because: not all in order
zipcodes.forEach(function (zipcode) {
  getLocation(zipcode, function (error, location) {
    showLocation(error, location);
    getForecast(location, function (error, forecast) {
      showForecast(error, forecast, location);
    });
  });
});

*  *  *  *  *  *  *  *  *  */
// zip-convert.js
//  Problem: forecast.io only will take arguments in longitude and latitude, and input from users will be zip/postal codes.
//
//  Solution: use node going to geonames.org server api to get back JSON with longitude and latitude for given zip code
//  send user input u.s. zip code to geonames server via node

var http = require("http");
var messages = require("./messages.js");
var zip = process.argv.slice(2);

var printError = messages.printError;
var printLocationInfo = messages.printLocationInfo;

function getLocation(zip, ready) {
    //  Connect to the API URL ("http://api.geonames.org/postalCodeLookupJSON?postalcode=[ZIP]&country=US&username=[USERNAME]")
    var username = "faddah";
    var geonamesAPIURL = "http://api.geonames.org/postalCodeLookupJSON?postalcode=" + zip + "&country=US&username=" + username;
    var request = http.get(geonamesAPIURL, function(response) {
        var finalData = "";
        //  Read the data
        response.on('data', function (dataStream) {
            finalData += dataStream;
        });
        response.on('end', function() {
          // if we have a functional server connection...
            if(response.statusCode === 200 && finalData) {
                try {
                // Parse the returned finalData object, knowing it's first key is an array
          // (corrected below, per @jerrysv & @justinabrahms of Freenode #pdxnode - thanx guys!)
                    var location = JSON.parse(finalData).postalcodes[0];
                    // Per @chrisdickinson (thanks, chris!), use 'ready' to return the location data object, including longitude (location.lng) and latitude (location.lat).
          ready(null, location);
                } catch(error) {
                    // Parse Error
          printError(error);
                }
            } else {
                // Status Code Error
        printError({message: "There was an error getting the locaton for this zip code to the geonames.org call for \"" + zip + ",\" this may be a server error or a zip code that may not exist in the U.S.. (Status Code Error: \'" + response.statusCode + " - " + http.STATUS_CODES[response.statusCode] + "\')"});
            }
        });
    });
}

var showLocation = function(error, location) {
  if(location){
    try {
      printLocationInfo(location.postalcode, location.placeName, location.adminName1, location.countryCode, location.lng, location.lat);
      return location;
    } catch(error) {
            // Locaton object null or undefined error
            printError(error);
        }
  } else {
        // Status Code Error
        printError({message: "There was an error getting the weather info from the forecast.io server. (Status Code Error: \'" + response.statusCode + " - " + http.STATUS_CODES[response.statusCode] + "\')"});
  }
};

// export module for use in zip-forecast.js
module.exports.getLocation = getLocation;
module.exports.showLocation = showLocation;

// example api url call to geonames for zip code:  http://api.geonames.org/postalCodeLookupJSON?postalcode=97215&country=US&username=faddah

/* * * * * * * * * * * * * * * *

Example returned JSON data from geonames.com —

{ postalcodes:
   [ { adminName2: 'Multnomah',
       adminCode2: '051',
       postalcode: '97215',
       adminCode1: 'OR',
       countryCode: 'US',
       lng: -122.599001,
       placeName: 'Portland',
       lat: 45.514282,
       adminName1: 'Oregon' } ] }

* * * * * * * * * * * * * * * */
// zip-forecast.js
//  Problem:  Need to return weather data from forecast.io, but they only take longitude & latitude in API.

//  Solution:  First, use our own created zip-convert.js to convert zip code to longitude/latitude.

// my forecast.io API KEY - 6b0701ccfa469e7b92cac363130fa2bb
// example forecast.io API URL Call to return JSON with weather —
// https://api.forecast.io/forecast/6b0701ccfa469e7b92cac363130fa2bb/37.8267,-122.423

var https = require("https")
var async = require("async");
var zipConvert = require("./zip-convert.js");
var messages = require("./messages.js");

var printForecastMessage = messages.printForecastMessage;
var printError = messages.printError;

// gets the forecast after being passed the location object to get longitude and latitude within.
function getForecast(location, ready) {
    //  Connect to the Forecast.io API URL (https://api.forecast.io/forecast/[APIKEY]/[LATITUDE],[LONGITUDE],[TIME])
  var apiKey = '6b0701ccfa469e7b92cac363130fa2bb';
  var latitude = location.lat;
  var longitude = location.lng;
    var request = https.get("https://api.forecast.io/forecast/" + apiKey + "/" + latitude + "," + longitude, function(response) {
        var finalData = "";
        //  Read the data
        response.on('data', function (dataStream) {
            finalData += dataStream;
        });
        response.on('end', function() {
            if(response.statusCode === 200) {
                try {
                    // Parse the data
                    var forecast = JSON.parse(finalData);
                    // Print the data
                    ready(null, forecast);
                } catch(error) {
                    // Parse Error
                    printError(error);
                }
            } else {
                // Status Code Error
                printError({message: "There was an error getting the profile for the Treehouse user \"" + username + ",\" user name may not exist. (Status Code Error: \'" + response.statusCode + " - " + https.STATUS_CODES[response.statusCode] + "\')"});
            }
        });
        //  console.log(response.statusCode);
    });
    // Connection Error
    request.on("error", printError);
}

// calls on messages module to print out the forecast message.
var showForecast = function(error, forecast, location) {
  if(forecast){
    try {
      printForecastMessage(forecast, location);
      return forecast;
    } catch(error) {
            // Locaton object null or undefined error
            printError(error);
        }
  } else {
        // Status Code Error
        printError({message: "There was an error getting the weather info from the forecast.io server. (Status Code Error: \'" + response.statusCode + " - " + https.STATUS_CODES[response.statusCode] + "\')"});
  }
};

// this is if you want it to return the whole forecast.io JSON object (very big, has minute, hourly & daily forecast data as objects in arrays).
var showForecastObject = function(error, forecast) {
  if(forecast){
    try {
      console.dir(forecast);
      return forecast;
    } catch(error) {
            // Locaton object null or undefined error
            printError(error);
        }
  } else {
        // Status Code Error
        printError({message: "There was an error getting the weather info from the forecast.io server. (Status Code Error: \'" + response.statusCode + " - " + https.STATUS_CODES[response.statusCode] + "\')"});
  }
};

module.exports.getForecast = getForecast;
module.exports.showForecast = showForecast;
module.exports.showForecastObject = showForecastObject;
// messages.js
// Prints out the location message
function printLocationInfo(zipCode, city, state, country, long, lat) {
    var zipMessage = "The city for the zip code " + zipCode + " is: " + city + ", " + state +  ", " + country + ", longitude: " + long + ", latitude: " +  lat  + ".";
    console.log(zipMessage);
}

// used in printForecastMessage() to assign compass degree number to general compass heading
var windDirection = function(bearing) {
  if (bearing >= 357.0 && bearing <= 3.0){
    return "N";
  } else if(bearing >= 3.01 && bearing <= 38.50) {
    return "NNW";
  } else if(bearing >= 38.51 && bearing <= 51.50) {
    return "NW"
  } else if(bearing >= 51.51 && bearing <= 87.0) {
    return "WNW";
  } else if(bearing >= 87.01  && bearing <= 93.0) {
    return "W";
  } else if(bearing >= 93.01 && bearing <= 128.50) {
    return "WSW";
  } else if(bearing >= 128.51 && bearing <= 141.50) {
    return "SW";
  } else if(bearing >= 141.51 && bearing <= 177.0) {
    return "SSW";
  } else if(bearing >= 177.01 && bearing <= 183.0) {
    return "S"
  } else if(bearing >= 183.01 && bearing <= 218.50) {
    return "SSE";
  } else if(bearing >= 218.51 && bearing <= 231.50) {
    return "SE";
  } else if(bearing >= 231.51 && bearing <= 267.0) {
    return "ESE";
  } else if(bearing >= 267.0 && bearing <= 273.0) {
    return "E";
  } else if(bearing >= 273.01 && bearing <= 308.50) {
    return "ENE";
  } else if(bearing >= 308.51 && bearing <= 321.50) {
    return "NE";
  } else {
    return "NNE";
  }
}

// used with printForecastMessage to convert hPA Atmospheric Pressure units to more common, U.S. Barometric Pressure inHG (inches Mercury).
var hPAtoinHGConversion = function(hPaUnits){
  return (hPaUnits / 33.8638866667);
}

var changeToPercent = function(float) {
  return float * 100;
}

// Prints out the final weather forecast message
function printForecastMessage(forecast, location) {
  var fCurr = forecast.currently;
  var emDash = '\u2014';
  var degF = '\u2109';
  var percent = '\uFF05';
  var forecastMsg = "The current weather forecast for " + location.placeName + ", " + location.adminName1 + ", " + location.countryCode + ", is: " + fCurr.summary + " " + fCurr.icon + ", temperature: " + fCurr.temperature + degF + ", feels like: " + fCurr.apparentTemperature + degF + ", wind speed: " + fCurr.windSpeed + "MPH out of the " + windDirection(fCurr.windBearing) + ", precipitation of: " + fCurr.precipIntensity + ", humidity of: " + changeToPercent(fCurr.humidity) + percent + ", a barometric pressure of: " + hPAtoinHGConversion(fCurr.pressure).toFixed(2) + " inHG (inches Mercury), and a visibility of: " + fCurr.visibility + ".";
  console.log(forecastMsg);
  if(forecast.alerts) {
    var fAlert = forecast.alerts[0];
    alertMsg = "There is also a Special Weather Alert Message for this region " + emDash + "\n" + fAlert.title + ": \n\n" + fAlert.description + "\nMore information can be found at this web address: " + fAlert.uri + ".\n";
    console.log(alertMsg);
  }
}

// Prints out error messages
function printError(error) {
  if (error) {
      console.error(error.message);
  }
}

module.exports.printLocationInfo = printLocationInfo;
module.exports.printForecastMessage = printForecastMessage;
module.exports.printError = printError;

... so there you go! thank you for all your help with everything. my next project is to create a front-end for it, maybe in angular.js. thanks for the great course in node.js, @chalkers!

best,

— faddah portland, oregon, u.s.a.

thank you, Andrew Chalkley. i will try out your steps later tonight and report back here what i found. much thanks again for the extra help, it is really appreciated.

best,

— faddah wolf portland, oregon, u.s.a.

hi Andrew Chalkley ,

thank you for responding and i appreciate your help and the input. i do state above that this could have gone all in one file, but i did it this way to better teach myself “modules” & the “require” statement in node.js and javascript.

so i understand you correctly, a few questions to clarify please?: are you saying i should write this, contrary to how you usually code in node.js, as a blocking, non-callback function? the console.dir as a temporary coding test statement is just in there to make sure the array does get populated with the correct info. you're saying it is not returning because it executes the console.dir after. however, when i run zip-convert with the argv just by itself, it does show the arrLongLat does show as having the correct longitude and latitude members. i just can't figure out how to get this over, as a module with require, into zip-forecast.js. should i move the return statement just down to the end of the function? is it a matter of the array variable's scope?

i am grateful for any help offered here. thank you again for getting back to me.

best,

— faddah wolf portland, oregon, u.s.a.

Chalkers and Faddah,

Thank you for posting this! The course was great and this is excellent additional information for learning and getting our hands dirty.