Por indicação do Djonatas Tenfen achei esse link que descreve como exibir a previsão do tempo na Europa usando o Bing Maps.
Com esse artigo pretendo mostrar como fazer para exibir o clima no Brasil, obviamente usando o Google Maps!
A primeira dificuldade é encontrar um serviço com o clima em todo o Brasil e que seja relativamente fácil de ser utilizado (sem necessidade de fazer engenharia reversa de páginas web). Por sorte acabei encontrando o serviço do INPE (instituto Nacional de Pesquisas Espaciais) que traz a previsão em formato RSS.
1. Criando a Aplicação
Vou criar um projeto ASP.NET MVC utilizando o Visual Studio 2008. Utilizarei um template para substituir o layout default e não perder muito tempo com layout.
O foco desse artigo é o Google Maps e não o C#/.Net então não entrarei em muitos detalhes em relação à parte server do projeto (a não ser o tratamento do RSS)
2. Inserindo o Google Maps
Para cada projeto Google Maps você precisa solicitar uma chave para utilizar a API do Google Maps (a partir da versão 3 da API isso não será mais necessário). Essa chave é relativa à URL do servidor, o que significa que você precisa de uma chave para o desenvolvimento local e outra para a publicação.
Para facilitar a utilização da chave correta criei uma classe helper que analisando a URL do servidor pega a chave correta do web.config:
using System.Configuration;
using System.Web;
namespace Weather.Helpers
{
public class GMapsApi
{
public static string GetMapsAPI()
{
if (HttpContext.Current.Request.Url.Host.ToLower().Contains("localhost"))
{
return ConfigurationManager.AppSettings["GMapsLocalKey"];
}
else
{
return ConfigurationManager.AppSettings["GMapsPublicKey"];
}
}
}
}
Obviamente no web.config preciso configurar essas duas chaves:
<appSettings>
<add key="GMapsLocalKey" value="sua chave para localhost"/>
<add key="GMapsPublicKey" value="sua chave para o servidor onde irá publicar"/>
</appSettings>
O primeiro passo para inserir um mapa do Google Maps é chamar o javascript com a API do Google Maps. Isso deve ser feito colocando a seguinte chamada na tag <head> da página:
<script src="http://maps.google.com/maps?file=api&v=2&
key=<%=Weather.Helpers.GMapsApi.GetMapsAPI() %>&sensor=true"
type="text/javascript"></script>
Os parâmetros são:
v=2: indica que estamos usando a versão 2 da API do Google Maps;
key: sua chave da API. Perceba que estou chamando aqui a minha função que determina qual a chave a ser utilizada (na versão 3 do Google Maps ela não é mais necessária)
sensor=true: indica que o Google Maps deve tentar determinar a posição do usuário (indicado para aplicações mobile)
Procure chamar a API apenas onde ela será utilizada. Por isso não indico colocar no site.master de suas aplicações ASP.NET MVC. O mais indicado é usar o placeholder HeadContent.
Irei criar uma função javascript de nome initialize() que deve instanciar o objeto mapa e definir qual elemento DOM que conterá o mapa. Normalmente o elemento DOM é uma DIV.
Dentro do place holder MainContent irei adicionar um elemento div:
<div id="map" style="width: 100%; height: 750px;"></div>
Colocar uma div ocupando 100% da altura merece um post a parte, então irei fixar aqui em 750 pixels de altura!
Agora já podemos definir a função initialize():
<script type="text/javascript" >
var mapa;
function initialize() {
var map = new GMap2(document.getElementById("map"));
mapa.setCenter(new GLatLng(-16.684185, -50.28125), 5);
mapa.addControl(new GLargeMapControl());
mapa.enableScrollWheelZoom();
mapa.setMapType(G_DEFAULT_MAP_TYPES[2]);
}
</script>
Agora a brincadeira começou a ficar interessante!
A 2a. linha cria uma variável de nome “mapa”. Criamos ela fora de initialize para que tenhamos escopo na página
A 4a. linha inicializa um objeto mapa dentro da div “map”
A 5a. define a posição central inicial (calculei as coordenadas para que sejam exibidas a maioria das capitais brasileiras em uma resolução de 1024x768). O valor “5” define o zoom inicial.
A 6a. linha insere no mapa o controle que permite alterar o zoom e os botões que fazem o “pan” para os lados e para cima e para baixo.
A 7a. linha informa que deve ser habilitado o zoom in/ou com a rodinha do mouse.
A 8a. linha define que o tipo inicial do mapa deve ser o híbrido.
Quando iremos chamar a função initialize? Usaremos o jQuery para chamá-la apenas quando toda a página estiver montada.
No final, a página ficará com esse código:
<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
Inherits="System.Web.Mvc.ViewPage" %>
<asp:Content ID="indexTitle" ContentPlaceHolderID="TitleContent" runat="server">
Tempo no Brasil
</asp:Content>
<asp:Content ID="head" ContentPlaceHolderID="HeadContent" runat="server">
<script src="../../Scripts/jquery-1.3.2.js" type="text/javascript"></script>
<script src="http://maps.google.com/maps?file=api&v=2&
key=<%=Weather.Helpers.GMapsApi.GetMapsAPI() %>&sensor=true"
type="text/javascript"></script>
<script type="text/javascript">
var mapa;
function initialize() {
mapa = new GMap2(document.getElementById("map"));
mapa.setCenter(new GLatLng(-16.684185, -50.28125), 5);
mapa.addControl(new GLargeMapControl());
mapa.enableScrollWheelZoom();
mapa.setMapType(G_DEFAULT_MAP_TYPES[1]);
}
</script>
</asp:Content>
<asp:Content ID="body" ContentPlaceHolderID="BodyPlaceHolder" runat="server">
onUnload="GUnload()"
</asp:Content>
<asp:Content ID="indexContent" ContentPlaceHolderID="MainContent" runat="server">
<div id="map" style="width: 100%; height: 780px;">
</div>
<script type="text/javascript">
$(document).ready(function() {
initialize();
});
</script>
</asp:Content>
Perceba que estou chamando a função GUnload() da própria API do Google Maps que tem como objetivo limpar da memória todo o mapa assim que o usuário sair da página, diminuindo dessa forma a chance de eventuais problemas com gasto excessivo de memória.
O resultado será esse:
3. Construindo o “Banco de Dados” de cidades
Como o objetivo desse post é mostrar a construção do mapa, vou usar um modelo de dados bem simples, colocando em uma lista de memória as capitais dos estados do Brasil.
Obviamente você pode estender esse modelo colocando as maiores cidades, gravando os dados em um banco de dados, etc.
Para o exemplo irei usar a classe abaixo, contendo o ID da cidade no INPE, o nome da capital, a sigla do estado e as coordenadas geográficas da cidade.
using System.Collections.Generic;
namespace Weather.Models
{
public class City
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Latitude { get; set; }
public decimal Longitude { get; set; }
public string StateAcronym { get; set; }
public static List<City> GetCities()
{
List<City> cities = new List<City>();
cities.Add(new City() { Id = 238, Name = "Porto Velho", StateAcronym = "RO",
Latitude = -8.7624674766M, Longitude = -63.9044876278M });
cities.Add(new City() { Id = 240, Name = "Rio Branco", StateAcronym = "AC",
Latitude = -9.9754648208M, Longitude = -67.8105087280M });
cities.Add(new City() { Id = 234, Name = "Manaus", StateAcronym = "AM",
Latitude = -3.1024484634M, Longitude = -60.0254669189M });
cities.Add(new City() { Id = 223, Name = "Boa Vista", StateAcronym = "RR",
Latitude = 2.8195800781M, Longitude = -60.6734657287M });
cities.Add(new City() { Id = 221, Name = "Belém", StateAcronym = "PA",
Latitude = -1.4564432713M, Longitude = -48.5043983304M });
cities.Add(new City() { Id = 232, Name = "Macapá", StateAcronym = "AP",
Latitude = 0.0385661366M, Longitude = -51.0664177261M });
cities.Add(new City() { Id = 236, Name = "Palmas", StateAcronym = "TO",
Latitude = -10.1674923853M, Longitude = -48.3334037018M });
cities.Add(new City() { Id = 243, Name = "São Luís", StateAcronym = "MA",
Latitude = -2.5304510629M, Longitude = -44.3033714168M });
cities.Add(new City() { Id = 245, Name = "Teresina", StateAcronym = "PI"
, Latitude = -5.0894684783M, Longitude = -42.8023605331M });
cities.Add(new City() { Id = 229, Name = "Fortaleza", StateAcronym = "CE",
Latitude = -3.7174611763M, Longitude = -38.5433273026M });
cities.Add(new City() { Id = 235, Name = "Natal", StateAcronym = "RN",
Latitude = -5.7954773774M, Longitude = -35.2093048098M });
cities.Add(new City() { Id = 231, Name = "João Pessoa", StateAcronym = "PB",
Latitude = -7.1154866001M, Longitude = -34.8633003854M });
cities.Add(new City() { Id = 239, Name = "Recife", StateAcronym = "PE",
Latitude = -8.0544929496M, Longitude = -34.8813018808M });
cities.Add(new City() { Id = 233, Name = "Maceió", StateAcronym = "AL",
Latitude = -9.6665029515M, Longitude = -35.7353096042M });
cities.Add(new City() { Id = 220, Name = "Aracaju", StateAcronym = "SE",
Latitude = -10.9115095226M, Longitude = -37.0723190324M });
cities.Add(new City() { Id = 242, Name = "Salvador", StateAcronym = "BA",
Latitude = -12.9715194702M, Longitude = -38.5113372802M });
cities.Add(new City() { Id = 222, Name = "Belo Horizonte", StateAcronym = "MG",
Latitude = -19.8175430307M, Longitude = -43.9563903769M });
cities.Add(new City() { Id = 246, Name = "Vitória", StateAcronym = "ES",
Latitude = -20.3195533734M, Longitude = -40.3383636479M });
cities.Add(new City() { Id = 241, Name = "Rio de Janeiro", StateAcronym = "RJ",
Latitude = -22.9035564806M, Longitude = -43.2083928627M });
cities.Add(new City() { Id = 244, Name = "São Paulo", StateAcronym = "SP",
Latitude = -23.5485496519M, Longitude = -46.6364212029M });
cities.Add(new City() { Id = 227, Name = "Curitiba", StateAcronym = "PR",
Latitude = -25.4285487993M, Longitude = -49.2734451079M });
cities.Add(new City() { Id = 228, Name = "Florianópolis", StateAcronym = "SC",
Latitude = -27.5975551455M, Longitude = -48.5494461504M });
cities.Add(new City() { Id = 237, Name = "Porto Alegre", StateAcronym = "RS",
Latitude = -30.0335502624M, Longitude = -51.2304801940M });
cities.Add(new City() { Id = 225, Name = "Campo Grande", StateAcronym = "MS",
Latitude = -20.4435195922M, Longitude = -54.6464691162M });
cities.Add(new City() { Id = 226, Name = "Cuiabá", StateAcronym = "MT",
Latitude = -15.5965022580M, Longitude = -56.0974616760M });
cities.Add(new City() { Id = 230, Name = "Goiânia", StateAcronym = "GO",
Latitude = -16.6795196708M, Longitude = -49.2544251117M });
cities.Add(new City() { Id = 224, Name = "Brasília", StateAcronym = "DF",
Latitude = -15.7805194860M, Longitude = -47.9304084804M });
return cities;
}
}
}
4. Buscando as informações do INPE
A partir daqui entramos no território sem lei dos XMLs.
Temos que buscar o RSS das cidades desejadas e salvar localmente. Depois temos que ler os XMLs e extrair as informações desejadas.
Criei um helper para efetuar esse download (\Helpers\INPEHelper.cs). A parte principal do helper é esta:
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Web;
using Weather.Models;
using System.Text;
namespace Weather.Helpers
{
public class INPEHelper
{
private string URL = "http://servicos.cptec.inpe.br/RSS/cidade/{0}/previsao.xml";
public INPEHelper()
{
}
public List<Forecast> GetForecasts(List<City> cities)
{
List<Forecast> forecs = new List<Forecast>();
foreach (City city in cities)
{
GetRSS(city.Id);
RSS rs = RSS.Load((HttpContext.Current.Server.MapPath("~/app_data/" +
string.Format("{0}.xml", city.Id))));
var dataXml = DateTime.Parse(rs.channel.item[0].pubDate[0]);
var previsao = rs.channel.item[0].description[0];
var imagem = previsao.Substring(previsao.IndexOf("<img"),
previsao.IndexOf("/>") - 1);
Forecast fore = new Forecast();
fore.City = city;
fore.ForecastImageUrl = imagem;
fore.ForecastTemperature = previsao.Substring(previsao.IndexOf("</b>") + 8);
fore.ForecastTemperature = fore.ForecastTemperature.
Remove(fore.ForecastTemperature.IndexOf("<br>"));
string data = previsao.Substring(previsao.IndexOf("<br>") + 4);
data = data.Remove(data.IndexOf("</b>"));
fore.ForecastDate = GetForeDate(dataXml, data);
File.Delete(HttpContext.Current.Server.MapPath("~/app_data/" +
string.Format("{0}.xml", city.Id)));
}
return forecs;
}
private void GetRSS(int cityCode)
{
DownloadFile(string.Format(URL, cityCode.ToString()),
HttpContext.Current.Server.MapPath("~/app_data/" + cityCode.ToString() + ".xml"));
}
O método DownloadFile está nesse mesmo fonte.
Estou salvando os xml no app_data pois é uma pasta que normalmente tem permissão de gravação.
Um detalhe importante: para pesquisar no XML do RSS eu estou usando o Linq to XML. Já fiz um post tempos atrás sobre como utilizar esse framework. De qualquer forma já estará tudo pronto para você utilizar no código que irei disponibilizar para download. Você pode tratar o XML direto também, existem várias alternativas.
O RSS do INPE retorna a previsão do dia de amanhã (ou de hoje) e mais alguns dias para frente. A previsão de amanhã (ou de hoje) é dada através de uma imagem. As demais através de uma string. Por hora, irei pegar apenas a previsão de amanhã (ou de hoje) e exibir direto a imagem.
5. Exibindo as informações no Mapa
Agora que passamos a parte mais chatinha, falta apenas exibir os dados no mapa, o que será muito fácil de fazer em se tratando de Google Maps!
Utilizaremos a classe GIcon da API do Google Maps para criar um marcador personalizado com a imagem da previsão e a classe GMarker para colocar a previsão no mapa.
Poderíamos fazer o controller Index retornar uma lista de objetos Forecast e iterarmos sobre essa lista. Mas aqui teríamos um problema: o objeto mapa pode não estar criado ainda e você irá tentar adicionar marcadores em um objeto nulo. Então para garantir que não teremos problemas iremos invocar os dados apenas após a chamada do método initialize:
1: <script type="text/javascript">
2: $(document).ready(function() {
3: initialize();
4:
5:
6: var url = "/Home/GetForecasts";
7: $.getJSON(url, null, function(data) {
8: $.each(data, function(index, forec) {
9: var forecIcon = new GIcon(G_DEFAULT_ICON);
10: forecIcon.image = forec.ForecastImageUrl;
11: forecIcon.iconSize = new GSize(73, 59);
12: forecIcon.shadow = "http://maps.google.com/mapfiles/water.gif";
13: forecIcon.shadowSize = new GSize(83, 59);
14: markerOptions = { icon: forecIcon, title: forec.City.Name + '/' +
forec.City.StateAcronym + ' Temperatura: ' + forec.ForecastTemperature };
15: var point = new GLatLng(forec.City.Latitude, forec.City.Longitude)
16: mapa.addOverlay(new GMarker(point, markerOptions));
17:
18: });
19: });
20:
21: });
22:
23: </script>
A linha 7 utiliza ajax para chamar um método no servidor que retornará um json com os dados. O método está no controller Home (mais abaixo colo ele)
A partir da linha 8 eu itero sobre cada elemento json que retornou.
Nas linhas 9,10,11,12,13 eu crio um icone personalizado contendo a imagem do INPE. Como a imagem vai ficar meio ilegível, apliquei um fundo (o resultado final não é dos mais belos, mas serve para nossos fins didáticos).
Na linha 14 crio uma array contendo o ícone personalizado e um título contendo o nome da cidade e a temperatura.
Por último, nas linhas 15 e 16 eu crio um marcador utilizando a latitude e longitude da cidade e o ícone personalizado.
O resultado final é esse:

No link você pode baixar todo o código fonte da solução. Apenas não esqueça de gerar sua chave de API no Google Maps!
Qualquer dúvida, deixe um comentário!