Mimeteo: el tutorial

26 de Octubre de 2012

[EDIT Enero 2018] La API de World Weather Online ya no es gratis asi que la aplicación no funciona mas. He suprimido la página pero dejo este post como referencia.

Como previsto, pero con mucho retraso (mucha chamba y bueno sabes ...), tome el tiempo de redactar el tutorial sobre la implementación de mi aplicación Mimeteo.

Como funciona?

Mimeteo permite obtener el último boletín meteorológico y los pronósticos en un cierto punto que el usuario escoge al hacer clic en un mapa. Los resultados aparecen luego debajo del mapa. El usuario puede elegir ver los pronósticos hasta 4 días, seleccionando en el menu deslizante arriba del mapa.

Para desarrollar Mimeteo, además de la API de World Weather Online, usé Django 1.3, OpenLayers 2.12 y JQuery 1.7.2.

La primera cosa que hay que hacer es inscribirse (es gratis) en la API de World Weather Online para obtener una clave. Yo usé la API Local Weather. Esta API permite recuperar los pronósticos hasta 4 días. Estos datos pueden ser en XML, JSON or CSV.

El código

En el código a continuación, mi app Django se llama 'labs'. Empezamos con los más simple, el archivo urls.py:

from django.conf.urls.defaults import patterns, include, url

from django.views.generic import TemplateView

 

urlpatterns = patterns('',

  # Creamos una primera 'view'

  (r'^labs/mimeteo$', TemplateView.as_view(template_name="labs/mimeteo.html")),

)

Ahora tenemos que crear nuestro template (mimeteo.html). Para que sea mas facil de entender, empiezo con una versión que solamente contiene el mapa con un fondo Google Maps y otros elementos HTML. Agregaremos el resto (el javascript) después. Como usamos un fondo Google Maps, tenemos que agregar el enlace de la API (linea 5) y indicar la proyección de Google Maps (EPSG: 900913) (notan el fun fact: 900913 ≈ GOOGLE).

<html>

 <head>

 <title>Mimeteo</title>

 <script type="text/javascript" src="/site_media/js/OpenLayers-2.12/OpenLayers.js"></script>

 <script src='http://maps.google.com/maps/api/js?sensor=false&v=3.2'></script>

 <script type='text/javascript'>

  var map;

  function init() {

  map = new OpenLayers.Map('map_element', {

    projection: 'EPSG: 900913',

    units: 'm',

    // Como los datos de World Weather Online no son muy precisos, no quiero que el usuario pueda zoomar al máximo. Entonces pongo un límite

    numZoomLevels: 9

  });

   // Escojo la capa Google Terrain
  var google_map_layer = new OpenLayers.Layer.Google( "Google Physical", {type: google.maps.MapTypeId.TERRAIN, 'sphericalMercator': true } );

  map.addLayer(google_map_layer);

 

  // Centro mi mapa en el Perú y no escojo un zoom muy fuerte para ver la mayoría del pais.

  map.setCenter(new OpenLayers.LonLat(-8350592.4649365,-1145046.9871383),5);

 </script>

  </head>

  <body onload='init();'>

   <!-- Un pequeño texto explicativo -->

   <p id="ayuda">Para usar Mimeteo, escoge cuantos días de pronósticos quieres.

    Luego haz clic en el mapa en el lugar donde quieres conocer

    el clima. Los resultados aparecerán debajo del mapa.</p>

    <br />

    <div id="choix">Quiero conocer el último boletín meteorológico y los pronósticos hasta:

    <!-- El menú deslizante con el cual el usuario escoge hasta cuando quiere los pronósticos -->

    <SELECT name="menu">

      <OPTION VALUE="2">mañana</OPTION>

      <OPTION VALUE="3">2 días</OPTION>

      <OPTION VALUE="4">3 días</OPTION>

      <OPTION VALUE="5">4 días</OPTION>

   </SELECT>

   </div>

   <br/>

   <div id='map_element'></div>

   <br/>

   <h4>Último boletín:</h3>

   <!-- La tabla que contendra el último boletín. Esta invisible al inicio -->

   <table style="display:none" id ="result">

    <tr>

    <th>Hora de Perú</th>

    <th></th>

    <th>Temperatura (C°)</th>

    <th>Humedad (%)</th>

    <th>Precipitaciones (Mm)</th>

    <th>Cobertura de nubes (%)</th>

    <th>Velocidad del viento (Km/h)</th>

    </tr>

    <tr>

    <td id="hr"></td>

    <td id="i"></td>

    <td id="t"></td> 

    <td id="h"></td> 

    <td id="p"></td>

    <td id="n"></td>

    <td id="v"></td>

    </tr>

   </table> 

   <br/>

   <h4>Pronósticos:</h3>

   <!-- En esta div pondre una tabla con los pronósticos -->

   <div id='prediction'>

  </div>

  </body>

</html>

A este nivel tenemos una página con un mapa muy simple.

Ahora vemos la "view" Django (es decir una función Python) que llamaremos al hacer clic en el mapa (con AJAX) y que va a recuperar los datos de la API. Esa función se llama 'meteo_out'.

EDIT del 08/08/2013 : la API de WorldWeather Online ha cambiado asi que he tenido que cambiar de clave y de URL. El URL anterior era :http://free.worldweatheronline.com/feed/weather.ashx. El nuevo es http://api.worldweatheronline.com/free/v1/weather.ashx.

# -*- coding: ISO-8859-1 -*- 

# Importo los modulos necesarios

from django.http import HttpResponse

from urllib2 import Request, urlopen

import simplejson, datetime

 

def meteo_out(request):

  ''' Estas 3 variables son 3 paremetros que el usuario escoge. latitude y longitude corresponden a las coordenadas del punto donde el usuario hace clic en el mapa. dias es  el número de días escogidos en el menu deslizante '''

  latitude = request.GET['latitude']

  longitude = request.GET['longitude']

  dias = request.GET['dias']


  # El URL de la API

  url = 'http://api.worldweatheronline.com/free/v1/weather.ashx'

  # Yo quiero recuperar los datos en formato JSON 

  formato = 'json'

  # La clave de la API:

  clave = 'e7ae7d907fXXXXXXXXXXXX'

  # El URL completo es una concatenación del URL de base con los diferentes parametros.

  url_full = url + '?q=' + latitude + ',' + longitude + '&format=' + formato + '&num_of_days=' + dias + '&key=' + clave

 

   # Mando mi consulta a la API
  req = Request(url_full)

  handle = urlopen(req)


  # La respuesta que obtengo es un objeto JSON (como lo habia pedido).
  # Lo pongo en una variable llamada 'respuesta'

  respuesta = handle.read()


  # Convierto mi objeto JSON en un diccionario python

  resp_dict = simplejson.loads(respuesta)

 

  # En el JSON tengo la hora del último boletín pero es en hora UTC. Yo quiero tener la hora en la hora de Perú.
  # Por eso extraigo la hora UTC

  hora = resp_dict['data']['current_condition'][0]['observation_time']


   # Calculo la hora actual

  ahora = datetime.datetime.now()


  # Quito 5 horas para obtener la hora de Perú (UTC - 5)

  hora_c = datetime.datetime.strptime(str(ahora.day)+' '+str(ahora.month)+' '+str(ahora.year)+' '+

  hora , "%d %m %Y %I:%M %p") - datetime.timedelta(hours=5)

 

   # Algunas manipulaciones no muy importantes para tener la hora
  # en el formato que me gusta

  if len(str(hora_c.minute)) == 1:

    minutos = '0'+ str(hora_c.minute)

  else:

    minutos = str(hora_c.minute)

  if len(str(hora_c.hour)) == 1:

    horas = '0'+ str(hora_c.hour)

  else:

    horas = str(hora_c.hour)


  # Ahora puedo modificar la hora en mi diccionario

  resp_dict['data']['current_condition'][0]['observation_time'] = horas +':'+ minutos #str(hora_c.minute)


  # También quiero tener los días de la semana en español.
  # Entonces modifico mi diccionario

  dico_dias = {0:'Lunes', 1: 'Martes', 2: 'Miércoles', 3: 'Jueves', 4: 'Viernes', 5 : 'Sábado', 6: 'Domingo'}

  for i in range(len(resp_dict['data']['weather'])):

    fecha = resp_dict['data']['weather'][i]['date']

    fecha_c = datetime.datetime.strptime(fecha , "%Y-%m-%d")

    resp_dict['data']['weather'][i]['date'] = dico_dias[fecha_c.weekday()]


  # Ya que hice todas las modificaciones que quería,
  # puedo hacer la conversión en el otro sentido.
  # Es decir convertir mi diccionario python en un objeto JSON

  respuesta2 = simplejson.dumps(resp_dict)


  # Y retorno el objeto JSON modificado

  return HttpResponse(respuesta2, mimetype="application/json")


Ahora el archivo HTML completo con la agregación de los diferentes códigos Javascript (Openlayers para el mapa y JQuery para la presentación de los resultados de la API):

<html>

 <head>

 <title>Mimeteo</title>

 <link rel="stylesheet" type="text/css" media="all" href="/site_media/css/base.css" />

 <link rel="stylesheet" type="text/css" media="all" href="/site_media/css/labs/mimeteo.css" />

 <script type="text/javascript" src="/site_media/js/OpenLayers-2.12/OpenLayers.js"></script>

 <script src='http://maps.google.com/maps/api/js?sensor=false&v=3.2'></script>

 <script type="text/javascript" src="/site_media/js/jquery-1.7.2.min.js"></script>

 <script type='text/javascript'>

  var map;

  OpenLayers.Control.Click = OpenLayers.Class(

  OpenLayers.Control, {

    // Los parametros de mi control Click

    defaultHandlerOptions: {

    'single' : true,

    'double' : false,

    'pixelTolerance' : 0,

    'stopSingle' : false,

    'stopDouble' : false

  },

  initialize: function(options) {

    this.handlerOptions = OpenLayers.Util.extend(

    {}, this.defaultHandlerOptions);

    OpenLayers.Control.prototype.initialize.apply(this, arguments);

    this.handler = new OpenLayers.Handler.Click(

    this, {'click' : this.onClick}, this.handlerOptions);

  },


  onClick: function(e) {

  // Al hacer clic, desactivo el control Click para empedir que el usuario haga varios clicks antes de recibir la respuesta de su primer click.

  //Lo reactivaré una vez recibida toda la información.

  $('#map_element').bind('click', map.controls[4].deactivate());


  // Quiero representar el punto donde el usuario hizo clic con un ícono. Por eso tengo que agregar una capa (ver más abajo).

  // Pero si el usurio hace clic varias veces, quiero solamente mostrar el último lugar seleccionado. Por eso, al hacer clic,
  // si ya hay una capa con un ícino, la suprimo.

  if (map.layers.length > 2) {

    map.layers[2].destroy();

  };

  // Creo la capa que contiene el ícono

  var marker_icono = new OpenLayers.Layer.Markers( "Marker icono" );


  // Agrego una tabla que solamente contienen los 'headers' y que esta invisible por el momento.

  // Esta tabla va contenir los pronósticos

  $('#prediction').html('<table style="display:none" id ="predic"><tr><th>Día</th><th>

    </th><th>Temperatura<br/>(C°)</th><th>Precipitaciones<br/>(Mm)</th>

    <th>Velocidad del viento<br/>(Km/h)</th></tr><tbody>   </tbody></table>')

  

  // Mi capa Google Maps esta proyectada en el SRS (Sistema de Referencia Espacial) EPSG: 900913. Los datos de la API estan en EPSG: 4326 (latitud, longitud).

  // Entonces necesitamos realizar una transformación de proyección

  var proj_4326 = new OpenLayers.Projection('EPSG:4326');

  var proj_900913 = new OpenLayers.Projection('EPSG:900913');

 

  // La variable 'coord' contiene las coordenadas del punto donde el ususario hizo clic. Este punto esta en el SRS EPSG: 900913

  var coord = map.getLonLatFromViewPortPx(e.xy);


  // Aca transformo mi punto en EPSG: 900913 en EPSG: 4326:

  var pt_trans = new OpenLayers.LonLat(coord.lon, coord.lat).transform(proj_900913, proj_4326);


  // A continuación son las variables que van a definir mi ícono (tamaño, ubicación y imagen)

  var size = new OpenLayers.Size(20,29);

  var offset = new OpenLayers.Pixel(-(size.w/2), -size.h);

  var icono = new OpenLayers.Icon('/site_media/img/labs/mimeteo/icone.png',size,offset);

  marker_icono.addMarker(new OpenLayers.Marker(new OpenLayers.LonLat(coord.lon, coord.lat),icono));


   // Agrego la capa que contiene el icono en el mapa
  map.addLayer(marker_icono);

 

  // Aca utilizamos la función 'ajax' de JQuery para ejecutar la view Django meteo_out y recuperar los datos de la API

  $.ajax({

    type: "GET",

    // El URL de mi view Django

    url: 'meteo_out',

    // Paso a mi 'view' Django meteo_out los parametros que necesita

    // El valor del parametro 'dias' corresponde al valor que ha escogido el usuario en el menú deslizante

    data : {longitude:pt_trans.lon, latitude:pt_trans.lat, dias: $('select').val()},

    dataType: "json",

    success: function(data) {

    // Recupero los datos en una variable Javascript que llamo 'datos_meteo'. Corresponde a la variable respuesta2 de mi 'view' meteo_out.

    var datos_meteo = data;


      // Ahora tengo los datos (que incluyen también las imagenes) de la API y puedo mostrarlos en mi página HTML
    $('h4').show()

    $('td#hr').html(datos_meteo.data.current_condition[0].observation_time)

    $('td#i').html('<img src='+ datos_meteo.data.current_condition[0].weatherIconUrl[0].value +'>')

    $('td#t').html(datos_meteo.data.current_condition[0].temp_C)

    $('td#h').html(datos_meteo.data.current_condition[0].humidity)

    $('td#n').html(datos_meteo.data.current_condition[0].cloudcover)

    $('td#p').html(datos_meteo.data.current_condition[0].precipMM)

    $('td#v').html(datos_meteo.data.current_condition[0].windspeedKmph)


    $('table#result').show()

    // Lleno mi tabla de pronósticos 

    for (var i = 1; i < datos_meteo.data.weather.length; i++) {

      $('table#predic').append($('<tr><td>'+ datos_meteo.data.weather[i].date + '</td><td><img src=' + datos_meteo.data.weather[i].weatherIconUrl[0].value +'></td><td><b>'+ datos_meteo.data.weather[i].tempMaxC + '</b><hr />'+ datos_meteo.data.weather[i].tempMinC + '</td><td>'+ datos_meteo.data.weather[i].precipMM + '</td><td>'  +  datos_meteo.data.weather[i].windspeedKmph + '</td></tr>'))

  };

      $('table#predic').show()

      // Ahora puedo reactivar mi control Click

      map.controls[4].activate();

  },

    crossDomain: false

  });

  }

  }

  );

  function init() {

  map = new OpenLayers.Map('map_element', {

    projection: 'EPSG: 900913',

    units: 'm',

    numZoomLevels: 9

  });


  var google_map_layer = new OpenLayers.Layer.Google( "Google Physical", {type: google.maps.MapTypeId.TERRAIN, 'sphericalMercator': true } );

  var vector_layer = new OpenLayers.Layer.Vector('capa vector', {});

  map.addLayers([google_map_layer, vector_layer]);

  map.setCenter(new OpenLayers.LonLat(-8350592.4649365,-1145046.9871383),5);

 

  var click = new OpenLayers.Control.Click();

  map.addControl(click);

  click.activate();

  }

  </script>

  </head>

  <body onload='init();'><br />

    <p id="ayuda">Para usar Mimeteo, escoge cuantos días de pronósticos quieres.

    Luego haz clic en el mapa en el lugar donde quieres conocer

    el clima. Los resultados aparecerán debajo del mapa.</p>

    <br />

    <div id="choix">Quiero conocer el último boletín meteorológico y los pronósticos hasta:

    <SELECT name="menu">

      <OPTION VALUE="2">mañana</OPTION>

      <OPTION VALUE="3">2 días</OPTION>

      <OPTION VALUE="4">3 días</OPTION>

     <OPTION VALUE="5">4 días</OPTION>

   </SELECT>

   </div>

  <br/>

  <div id='map_element'></div>

  <br/>

  <h4>Último boletín:</h3>

  <table style="display:none" id ="result">

  <tr>

  <th>Hora de Perú</th>

  <th></th>

  <th>Temperatura (C°)</th>

  <th>Humedad (%)</th>

  <th>Precipitaciones (Mm)</th>

  <th>Cobertura de nubes (%)</th>

  <th>Velocidad del viento (Km/h)</th>

  </tr>

  <tr>

  <td id="hr"></td>

  <td id="i"></td>

  <td id="t"></td>

  <td id="h"></td>

  <td id="p"></td>

  <td id="n"></td>

  <td id="v"></td>

  </tr>

  </table>

  <br/>

  <h4>Pronósticos:</h3>

  <div id='prediction'>

  </div>

  </body>

</html>

Hecho! Ya vimos todos los pasos para desarollar una aplicación con Django y OpenLayers y como acceder a una API en Python, modificar los datos y enseñarlos en el navegador.

Twittear