CM521 Lecture 2

使用 Javascript 開發跨平台手機 APP

🙋🏻‍♂️ If you need to contact me

今堂學什麼?

今日課題: 模組化 JavaScript 邏輯代碼

JS Funciton 不就是普通 function,有什麼特別?

JS Function 其中最實用的,就是 variable 的範圍定義。

function hello() {
  var x = 123;
  console.log(x); // 123
}
console.log(x); // undefined.

那有什麼用處?

可以做 private variable:

var counter = (function() {
  var count=0;
  return {
    increase: function(){
      count += 1;
    },
    value: function() {
      return count;
    }
  }
})();

counter.value(); // 0
counter.increase();
counter.increase();
counter.value(); // 2

在以上代碼中, count 是不能被直接存取的。所以只能夠通過 Counter 所定義的兩個 API: increase 及 value 來存取。

ES6 的 counter

普通的 ES6 Class

class Counter {
  constructor() {
    this.count = 0;
  }
  value() {
    return this.count;
  }
  increase() {
    this.count += 1;
    return this.count;
  }
}

var c = new Counter();
c.increase(); // 1
c.value(); // 1
c.count; // 1

count 是可以被存取的。

真 private variable

class Counter {
  constructor() {
    var count = 0;
    this.value = () => {
      return count;
    };
    this.increase = () => {
      count += 1;
      return count;
    };
  }
}

var c = new Counter();
c.increase(); // 1
c.value(); // 1
c.count; // undefined

扮 private variable

class Counter {
  constructor() {
    this._count = 0;
  }
  value() {
    return this._count;
  }
  increase() {
    this._count += 1;
    return this._count;
  }
}

var c = new Counter();
c.increase(); // 1
c.value(); // 1
c.count; // undefined
c._count; // 1

Weather app

Geolocation

navigator.geolocation.getCurrentPosition(success, error);

Example from MDN

https://developer.mozilla.org/en-US/docs/Web/API/Geolocation/Using_geolocation

HTML

<p><button onclick="geoFindMe()">Show my location</button></p>
<div id="out"></div>

JS

function geoFindMe() {
  var output = document.getElementById("out");

  if (!navigator.geolocation){
    output.innerHTML = "<p>Geolocation is not supported by your browser</p>";
    return;
  }

  function success(position) {
    var latitude  = position.coords.latitude;
    var longitude = position.coords.longitude;

    output.innerHTML = '<p>Latitude is ' + latitude + '° <br>Longitude is ' + longitude + '°</p>';

    var img = new Image();
    img.src = "https://maps.googleapis.com/maps/api/staticmap?center=" + latitude + "," + longitude + "&zoom=13&size=300x300&sensor=false";

    output.appendChild(img);
  }

  function error() {
    output.innerHTML = "Unable to retrieve your location";
  }

  output.innerHTML = "<p>Locating…</p>";

  navigator.geolocation.getCurrentPosition(success, error);
}



Example—Rain or not?

Example weather app for Macao.

We are going to create an app to let users in Macao knows if they need to bring umbrella or not.

Please take a look at the demo to get a glimpse on what we are going to archive before digging deeper.

I have created an account at OpenWeatherMap for the course teaching purpose.

The API key we’ll use is f88d2c3698ff22d9ed0af88d954ae5f2.

The OpenWeatherMap

For weather information, we will use the API from OpenWeatherMap.

Commercial use is permitted and encouraged

Please mention OpenWeatherMap as a weather data source in your application

Pricing? Here it is. In our example, free plan is more than enough.

Alternatively, you may use Dark Sky API for weather data.

$.getJSON

The API:

http://api.openweathermap.org/data/2.5/weather?q=Macau,MO&APPID=

Viewing JSON output directly in web browser often comes without any formatting and make the data inspection difficult. We can make use of JSON formatter tools. Here just to name few. There are tons of them.

  1. rested
  2. visual JSON
  3. browser extension

In jQuery, we use $.getJSON for this purpose.

;(function($){
  var apiKey = 'f88d2c3698ff22d9ed0af88d954ae5f2';
  $.getJSON('http://api.openweathermap.org/data/2.5/weather?q=Macao,MO&APPID=' + apiKey, function(data){
    console.log(data);
  });
}).call(this, jQuery);

Parse JSON

And here is the result of that API.

{
  "coord": {
      "lon": 113.546112,
      "lat": 22.20056
  },
  "sys": {
      "country": "MO",
      "sunrise": 1383690767,
      "sunset": 1383731168
  },
  "weather": [
      {
          "id": 800,
          "main": "Clear",
          "description": "Sky is Clear",
          "icon": "01n"
      }
  ],
  "base": "cmc stations",
  "main": {
      "temp": 296.215,
      "temp_min": 296.215,
      "temp_max": 296.215,
      "pressure": 1027.78,
      "sea_level": 1031.1,
      "grnd_level": 1027.78,
      "humidity": 95
  },
  "wind": {
      "speed": 4.43,
      "deg": 49.5008
  },
  "clouds": {
      "all": 0
  },
  "dt": 1383755905,
  "id": 1821274,
  "name": "Macau",
  "cod": 200
}

Weather Condition Code

According to the API doc

We check the "weather" code. When the code starts at 5, it rains.

JSON-P

JSON-P means JSON with padding.

For security reason, browser rejects JavaScript loading resource from the other sites.

The following version works without security errors of cross-domain accessing resources.

;(function($){
  var apiKey = 'f88d2c3698ff22d9ed0af88d954ae5f2';
  $.getJSON('http://api.openweathermap.org/data/2.5/weather?q=Macao,MO&APPID=' + apiKey + '&callback=?', function(data){
    console.log(data);
  });
}).call(this, jQuery);

The callback=? is to tell jQuery getJSON method to use the JSON-P approach instead of cross-domain JSON loading. For security reason, browser rejects JavaScript loading resource from the other sites (domains other than where the JavaScript is executed). Either enabling cross-domain resourcing sharing in server or allowing the JSON-P in server can by pass this security restriction.

Preparing graphics

We prepare three graphics for the app.

You can download the 3 images in the following URL:

http://mztests.herokuapp.com/rain303/images/loading.png
http://mztests.herokuapp.com/rain303/images/sunny.png
http://mztests.herokuapp.com/rain303/images/rain.png

Next, we will start crafting our app in JavaScript.

Crafting our Rain-or-Not app

This is our root page.

<div id='rain-content' class='loading'>

The very basic yet working logic.

;(function($){
  $.getJSON('http://api.openweathermap.org/data/2.5/weather?q=Macao,MO&APPID=' + apiKey + '&callback=?', function(data){
    console.log(data);
    var code = data.weather[0].id + ""; // force to string
    if (code[0] === '5') { // rainy code all start at 5
      $('#rain-content').removeClass().addClass('rain');
    } else {
      $('#rain-content').removeClass().addClass('sunny');
    }
  });
}).call(this, jQuery);

Note: the rainy code information is from the API documentation.

And the style:

.sunny {
  background: #ade1f9url(../images/sunny.png) no-repeat center center;
}
.rain {
  background: #ade1f9url(../images/rain.png) no-repeat center center;
}
.loading {
  background: #ade1f9url(../images/loading.png) no-repeat center center;
}

#root {
  background: #ade1f9;
}

#rain-content {
  height: 400px;
}

You can download the 3 images in the following URL:

http://mztests.herokuapp.com/rain303/images/loading.png
http://mztests.herokuapp.com/rain303/images/sunny.png
http://mztests.herokuapp.com/rain303/images/rain.png

Refactoring

This refactoring put DOM manipulation in view. And leave the app controller away from any DOM element.

The model.js file.

;(function($){
  var app = this.app = this.app || {};

  app.model = {
    fetch: function(query, callback) {
      $.getJSON('http://api.openweathermap.org/data/2.5/weather?q=' + query + '&callback=?', function(data){
        callback(data);
      });
    }
  }

}).call(this, jQuery);

Refactoring

The view.js file.

;(function($){
  var app = this.app = this.app || {};

  app.view = {
    update: function(weather) {
      if (weather === 'rain') {
        $('#rain-content').removeClass().addClass('rain');
      } else {
        $('#rain-content').removeClass().addClass('sunny');
      }
    }
  }

}).call(this, jQuery);

Refactoring

The app.js file.

;(function(){
  var app = this.app = this.app || {};

  app.model.fetch('Macao,MO', function(data){
    console.log(data);
    var code = data.weather[0].id + ""; // force to string
    if (code[0] === '5') { // rainy code all start at 5
      app.view.update('rain');
    } else {
      app.view.update('sunny');
    }
  });

}).call(this);

Refactoring again — Thin Controller

This refactoring moves the data checking back to the model module. What app controller needs should be just the essential data, Rain or Sunny.

Thin controller approach:

;(function(){
  var app = this.app = this.app || {};

  app.model.fetch('Macao,MO', function(weather){
    app.view.update(weather);
  });

}).call(this);

Refactoring again — Model

And the model now handles the data.

;(function($){
  var app = this.app = this.app || {};

  app.model = {
    fetch: function(query, callback) {
      $.getJSON('http://api.openweathermap.org/data/2.5/weather?q=' + query + '&callback=?', function(data){
        var code = data.weather[0].id + ""; // force to string
        if (code[0] === '5') { // rainy code all start at 5
          callback('rain');
        } else {
          callback('sunny');
        }
      });
    }
  }

}).call(this, jQuery);

Getting weather from location

We are going to get the weather status by geolocation – latitude and longtitude.

http://api.openweathermap.org/data/2.5/weather?lat=22.19567&lon=113.549194

How to get location on a map?

  1. We will use Google map.
  2. Be sure to use the classic version. The new Google map removed the feature.
  3. Move the map to you destination.
  4. right click on the map where you want the geolocation.
  5. Choose “What’s here” and you will see a lat/lon on the search bar.
  6. Showing the process, in GIF animation:

With the ability of getting weather from geolacation, we can make the app further by grabbing user’s geolocation and use it to fetch the weather.

Getting user location

Getting user’s geolocation

https://developer.mozilla.org/en-US/docs/WebAPI/Using_geolocation

navigator.geolocation.getCurrentPosition(function(location) {
  console.log(location.coords.latitude, location.coords.longitude);
});

Browser will ask for user’s permission before fetching the location. The following is the desktop Safari prompting for the geolocation feature permission.

Please note that getting geolocation requires an HTTPS protocol to work.

Fetch weather from location

navigator.geolocation.getCurrentPosition(function(location) {
  console.log(location.coords.latitude, location.coords.longitude);
  var url = 'http://api.openweathermap.org/data/2.5/weather?lat=' + location.coords.latitude + '&lon=' + location.coords.longitude + '&callback=?';
  $.getJSON(url, function(data){
    console.log(data);
  });
});

The final result in console.

And we can add an error handling callback as the second parameter for the getCurrentPosition method call.

navigator.geolocation.getCurrentPosition(function(location) {
  console.log(location.coords.latitude, location.coords.longitude);
  var url = 'http://api.openweathermap.org/data/2.5/weather?lat=' + location.coords.latitude + '&lon=' + location.coords.longitude + '&callback=?';
  $.getJSON(url, function(data){
    console.log(data);
  });
}, function(error){console.log("ERROR", error)});

Note: Not working anymore due to the lack of free SSL API in OpenWeather. you may check out the Dark Sky API for weather data.

Integrating location into our app

The app module now takes the place parameter to display the location name.

;(function(){
  var app = this.app = this.app || {};

  app.model.fetch('Macao,MO', function(weather, place){
    app.view.update(weather, place);
  });

}).call(this);

We display the name in HTML.

<div data-role='content' id='rain-content' class='loading'>
  <div id="place">Loading</div>
</div>

And the view to reflect the place.

;(function($){
  var app = this.app = this.app || {};

  app.view = {
    update: function(weather, place) {
      $('#place').html(place);
      if (weather == 'rain') {
        $('#rain-content').removeClass().addClass('rain');
      } else {
        $('#rain-content').removeClass().addClass('sunny');
      }
    }
  }

}).call(this, jQuery);

And finally the model.

;(function($){
  var app = this.app = this.app || {};

  app.model = {
    fetch: function(query, callback) {
      navigator.geolocation.getCurrentPosition(function(location) {
        var url = 'http://api.openweathermap.org/data/2.5/weather?lat=' + location.coords.latitude + '&lon=' + location.coords.longitude + '&callback=?';
        $.getJSON(url, function(data){
          var code = data.weather[0].id + ""; // force to string
          if (code[0] == 5) { // rainy code all start at 5
            callback('rain', data.name);
          } else {
            callback('sunny', data.name);
          }
        });
      });
    }
  }

}).call(this, jQuery);

End of Lecture 2

0/0