Creating Cities With Worldview

Worldview is a project I worked on with the inimitable Amber Tunnell, Koren Leslie Cohen and Edward Warren. The idea is to give you an instant snapshot of what it’s like in nearly any city in the world, pulling major news, pictures, weather and tweets related to the location.

The coolest part (in my opinion) is that you can add almost any city you want! Just type it into the input field and the site generates a clock depicting the city’s current local time. When you click the clock, the entire site updates with the city’s info.

The process of adding cities was one of my favorite parts of making this thing, and here’s how we did it.

Frontend Validation

When you first type in a new city, light validation on the JavaScript end checks to make sure what you typed in meets the bare minimum criteria to initiate the API call to Weather Underground – namely that the field isn’t empty and that it doesn’t contain a number.

1
2
3
4
5
6
7
8
9
10
11
12
function submit_new_city(passedInput) {
  if (passedInput !== undefined) {
      var user_input = passedInput;
  } else if ($("#new-city").val() == "") {
      $("#invalid_city").text("Input must not be blank");
      return false;
  } else if (parseInt($("#new-city").val()) * 0 === 0) {
      $("#invalid_city").text("Letters only, please");
      return false;
  } else {
      var user_input = $("#new-city").val();
  }

The optional passedInput argument that the submit_new_city function takes is used to generate the first five cities when you initially load the page, so it can bypass this validation.

API Results Validation

Once the request is accepted past the initial validation, it’s sent via Ajax call to Weather Underground’s awesome autocomplete API. You don’t even have to send the complete name of the city – the API will return the best guess it comes up with! This is undoubtedly useful, but there are still a few wrinkles.

First, we make sure that there’s something in the results array. For example, if you search for New Cheeseburger, you’ll get an empty results array.

1
2
3
4
5
6
7
var i = 0;

function findCityArray(i) {
  if (response.RESULTS.length === 0){
    $("#invalid_city").text("Please enter a valid city");
    return;
  }

Next, the API includes countries as well as cities, so we have to screen for that.

1
2
3
4
5
// continued
else if (response.RESULTS[i].type === "country" || response.RESULTS[i].lon === "-9999.000000") {
  $("#invalid_city").text("Please enter a city and country");
  return;
}

Finally, there are a bunch of edge cases of non-city locations where weather readings are taken, like hospitals and heliports. There are also some alternate names for regions, like Hong Kong S.A.R. (Special Administrative Region), which is a different result than just Hong Kong, China.

To handle a few of these edge cases, we use a basic regex that we update as needed:

1
2
3
4
// continued
else if (response.RESULTS[i].name.match(/(international|hospital|helistop|S\.A\.R|de Olivenca|do Potengi)/i)) {
  return findCityArray(i++);
}

If the initial result isn’t blank, but it doesn’t meet the criteria here, we send another recursive findCityArray call to return the next element in the results API.

Backend Validation

Once we have valid city information, it’s time to hit the server. The aim here is to send the city info to the server via Ajax and create or find a city object that we’ll use to create a new clock that the user can click on.

To do this, we send a POST request to the corresponding controller in Rails, which does exactly this – finding the user (if logged in) and making or finding the city.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@user = User.find(session[:user_id]) if session[:user_id]
@city = City.find_or_create_by(lat: city_params[:lat], lon: city_params[:lon]) do |city|
  city.name = city_params[:name]
  city.bigger_thing = city_params[:bigger_thing]
  city.country = city_params[:country]
end

if !@user #create a temp user tied to session ID
  @user = User.create(name: 'Guest',
      provider: 'anon',
      uid: session[:session_id],
      image: 'https://origin.ih.constantcontact.com/fs197/1110193228531/img/301.jpg?a=1115291249439')
  @user.cities << [City.find(1),
                   City.find(2),
                   City.find(3),
                   City.find(4),
                   City.find(5)]
  session[:user_id] = @user.id
end

Another cool thing here – if the user is not logged in but adds a city, we then create a temporary user and track the different cities they’re using the session cookie. If this anonymous user decides to log in with their Twitter account, we update this anonymous user’s info in the database with their real Twitter info and also import his cities from the session cookie into the new user’s account. Edward took the lead on this feature, and you can read more about it on his blog.

In the controller, we also return the ID of the clock to be removed (del_city_id) from the page to make room for the new one.

Finally, there’s one last bit of validation: If the clock is already being displayed on the page, we show an error message to this effect and don’t update the page.

1
2
3
4
5
6
7
8
unless @user.cities.find_by(lat: city_params[:lat], lon: city_params[:lon])
  @user.cities << @city if @user
  del_city_id = city_params[:lastClock].to_i
  CityUser.find_by(user_id: @user.id, city_id: del_city_id).destroy if @user
  render json: @city
else
  render json: "this city already exists".to_json
end

Otherwise, we return the city object back to JavaScript, call the makeClock function to create the city’s clock, and we’re done!

The four of us presenting the site at QueensJS!

Comments