Desenvolvendo um previsor do tempo (web app)

Desenvolvendo um previsor do tempo (web app) logo HTML logo CSS logo JavaScript logo Bootstrap logo jQuery logo GitHub Repository logo Chrome DevTools

Olá, pessoal! Aqui estamos nós, ficando fera em Web Services, afinal já consumimos uma REST API em nosso primeiro post! No entanto, podemos evoluir ainda mais. Para isso consumiremos mais um web service, só que agora, um cujo response é de XML! Neste serão exigidos dois requests. Será que conseguimos?

Bom, desta vez consumiremos dados do CPTEC/INPE, imprimindo na página a previsão do tempo dos próximos 4 dias de qualquer cidade de nosso país.

Antes de iniciarmos, atentemos às seguintes informações:

  • Para obter os dados da previsão do tempo dos próximos 4 dias é nos fornecido o endpoint http://servicos.cptec.inpe.br/XML/cidade/{codigoCidade}/previsao.xml
  • Já para obter o codigoCidade é o endpoint http://servicos.cptec.inpe.br/XML/listaCidades?city={nomeCidade}, e deste, do response resultante, obtêm-se o valor do elemento id.

Tudo isto é exemplificado no código abaixo:

<!DOCTYPE html>
<html>
  <head>
    <title>Previsor do Tempo</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <!-- Incorpora o jQuery ao site -->
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <script>
    
      function getCidade(nomeCidade){
        // Faz um HTTP Request, por meio de uma GET call (assíncrona)
        // para um endpoint definido
        // Aguarda como dados vindos do response um XML object
        $.ajax({
          url: "http://servicos.cptec.inpe.br/XML/listaCidades?city=" + nomeCidade,
          method: "get",
          dataType: "xml"
        })
        // Se o servidor retornar SUCCESS
        .done(function(data){        
          // chama a getPrevisaoTempoProximosQuatroDias
          // passando como argumento o valor do id          
          getPrevisaoTempoProximosQuatroDias($(data).find("id").text());
        })
        // Se o servidor retornar ERRO
        .fail(function(jqXHR){
          // imprime o HTTP error number
          console.log("Erro HTTP: " + jqXHR.status);
        });
      }
      
      function getPrevisaoTempoProximosQuatroDias(codigoCidade){
        // Faz um HTTP Request, por meio de uma GET call (assíncrona)
        // para um endpoint definido
        // Aguarda como dados vindos do response um XML object
        $.ajax({
          url: "http://servicos.cptec.inpe.br/XML/cidade/" + codigoCidade + "/previsao.xml",
          method: "get",
          dataType: "xml"
        })
        // Se o servidor retornar SUCCESS
        .done(function(data){
          // imprime os dados do response no console do browser
          console.log(data);
        })
        // Se o servidor retornar ERRO
        .fail(function(jqXHR){
          // imprime o HTTP error number
          console.log("Erro HTTP: " + jqXHR.status);
        });
      }
      
    </script>
  </head>
  <body>
    <!-- Aciona um evento de clique chamando a getCidade function
    por meio de um nome de cidade -->
    <button type="button" onclick="getCidade('Atibaia')">getCidadeByName</button>
  </body>
</html>

Bom, a partir disto, nossa tarefa é criar uma página web, na qual o usuário preenche o nome da cidade e requisita. Ao fazer isto, devemos imprimir na página o nome da cidade, o UF, o dia, além dos dados da previsão, isto é, tempo, temperatura máxima, temperatura mínima e IUV dos 4 dias conseguintes. O layout é livre. Lembremos sempre de recorrer à documentação quando surgirem dúvidas.

É ideal também, consultarmos os métodos do jQuery utilizados no exemplo para obter uma melhor compreensão. Assim, seguem os links: jQuery.ajax(), jQuery(), .find() e .text().

E que venha o desafio!

A resolução:

Bom, iniciamos obtendo os arquivos do Bootstrap v5.1.3 e do jQuery v3.6.0, juntamente com uma imagem favicon.png de nossa escolha, e então, criamos a seguinte estrutura de arquivos e diretórios:

p2/
  assets/
    css/
      bootstrap.min.css
      bootstrap.min.css.map
      style.css
    image/
      favicon.png
    js/
      bootstrap.min.js
      bootstrap.min.js.map
      jquery-3.6.0.min.js
      script.js
  index.html

No arquivo index.html, adicionamos o código-exemplo fornecido, removendo deste os blocos de script e as tags button. A este mesmo arquivo, linkamos os respectivos assets. Feito isto, nosso arquivo ficou assim:

<!DOCTYPE html>
<html>
  <head>
    <title>Previsor do Tempo</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link href="assets/css/bootstrap.min.css" rel="stylesheet">
    <link href="assets/css/style.css" rel="stylesheet">    
    <link href="assets/image/favicon.png" rel="icon" type="image/png">    
  </head>
  <body>  
    <script src="assets/js/jquery-3.6.0.min.js"></script>
    <script src="assets/js/bootstrap.min.js"></script>
    <script src="assets/js/script.js"></script>  
  </body>
</html>

Agora entre as tags body criamos a estrutura abaixo. Esta usa o Grid System do Bootstrap, além de Forms e Icons.

<!-- cria o CONTAINER -->
<body class="container-fluid">
  <!-- cria a LINHA 1 -->    
  <div class="box row">
    <!-- cria a COLUNA 1 -->      
    <div class="box col-sm-8 col-xs-12">
      <img alt="" src="assets/image/favicon.png">                    
      <h2>Previsão do Tempo</h2>    
    </div>
    <!-- cria a COLUNA 2 -->      
    <div class="box col-sm-4 col-xs-12">    
      <!-- Bootstrap Form Item: Input group > Button addons -->
      <div class="input-group">
        <input class="form-control" placeholder="Cidade" type="text">
        <span class="input-group-btn">
          <button class="btn" type="button">
            <!-- Bootstrap Icon: Search (Copy HTML) -->
            <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-search" viewBox="0 0 16 16">
              <path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z"/>
            </svg>
          </button>
        </span>
      </div>    
    </div>
  </div>
  <!-- cria a LINHA 2 -->
  <div class="box row"></div>
  <script src="assets/js/jquery-3.6.0.min.js"></script>
  <script src="assets/js/bootstrap.min.js"></script>
  <script src="assets/js/script.js"></script>  
</body>

Agora estilizamos nossa página, atualizando o arquivo style.css com as seguintes configurações:

.container-fluid {
    font-family: Arial, Helvetica, sans-serif;    
    padding: 0;
}

.row {
    margin: 0;
}

.box {
    padding: 10px;
}

.container-fluid > .row:first-child,
.form-control,
.form-control:focus,
.btn {
    background-color: #24292E;    
}

.container-fluid > .row:first-child div {
    align-items: center;
    display: flex;
}

.row div img {
    float: left;
}

h2 {
    font-size: 28px;
    margin-bottom: 0;
    margin-left: 10px;
}

h2,
.form-control,
.form-control:focus,
svg {
    color: #FFF;
}

.form-control::placeholder {
    color: #999;
}

.form-control,
.form-control:focus,
.btn {
    border: solid 1px #999;
}

.form-control:focus,
.btn:focus {
    box-shadow: none;
}

.btn {
    border-radius: 0 4px 4px 0;
}

Bom, com isto feito, nossa interface ficou com a seguinte aparência:

Interface simplificada, feita com o framework Bootstrap.

Figura 1 - Interface simplificada, feita com o framework Bootstrap.
Fonte: Autor (CC BY-NC-SA 4.0)

Bom, agora é hora de codificar as funções! Vamos lá? Para isso, primeiro copiamos as duas functions fornecidas no código-exemplo para nosso arquivo script.js, conforme o código abaixo:

function getCidade(nomeCidade){  
  $.ajax({
    url: "http://servicos.cptec.inpe.br/XML/listaCidades?city=" + nomeCidade,
    method: "get",
    dataType: "xml"
  })
  .done(function(data){  
    getPrevisaoTempoProximosQuatroDias($(data).find("id").text());
  })
  .fail(function(jqXHR){
    console.log("Erro HTTP: " + jqXHR.status);
  });
}
      
function getPrevisaoTempoProximosQuatroDias(codigoCidade){
  $.ajax({
    url: "http://servicos.cptec.inpe.br/XML/cidade/" + codigoCidade + "/previsao.xml",
    method: "get",
    dataType: "xml"
  })
  .done(function(data){
    console.log(data);
  })
  .fail(function(jqXHR){
    console.log("Erro HTTP: " + jqXHR.status);
  });
}

No arquivo index.html, na tag input, adicionamos um id com o valor cidade.

<input class="form-control" id="cidade" placeholder="Cidade" type="text">

Já na segunda row adicionamos um id com o valor localContent.

<div class="box row" id="localContent"></div>

Agora criamos no arquivo script.js um atributo constante, de escopo global, que armazene o #localContent. Criamos também uma nova function para impedir requests vazios.

const localContent = $("#localContent");

function isEmpty(){
  // Verifica se o input está vazio
  if($.trim($("#cidade").val()) === ""){
    // imprime mensagem de erro
    localContent.html("<h3>Informe a cidade para que a pesquisa seja realizada!</h3>");
  } else {
    // senão chama a getCidade passando como argumento o valor do input
    getCidade($("#cidade").val());
  }
}

No arquivo style.css, incluímos a tag h3 ao seletor .box:

.box,
h3 {
    padding: 10px !important;
}

Também movemos a propriedade margin-bottom do seletor h2, para o seletor combinado:

h2,
h3 {
    margin-bottom: 0;
}

e ainda adicionamos:

h3 {
    color: #24292E;
    font-size: 25px;
}

Já no index.html chamamos a function no atributo onclick da tag button.

<button class="btn" onclick="isEmpty()" type="button">

E agora abrimos a index.html no Google Chrome e testamos nossa aplicação, clicando no button sem preencher o input de texto. Assim, temos a mensagem abaixo, confirmando que estamos no caminho certo!

Mensagem de erro impressa em caso de request com input vazio

Figura 2 - Mensagem de erro impressa em caso de request com input vazio.
Fonte: Autor (CC BY-NC-SA 4.0)

Agora testaremos a nossa aplicação, informando o nome de uma cidade (atibaia | ATIBAIA | Atibaia, por exemplo) e clicando no botão de pesquisa, atentando-nos, neste momento, em manter o Console aberto. Feito isso, obtemos o seguinte resultado:

Resposta em XML de um request feito ao Web service CPTEC/INPE no Console

Figura 3 - Resposta em XML de um request feito ao Web service CPTEC/INPE no Console.
Fonte: Autor (CC BY-NC-SA 4.0)

Percebemos com este response que esta API não distingue maiúsculas de minúsculas, assim não precisamos nos preocupar nesse sentido. No entanto, outros testes ainda devem ser feitos, tais como a acentuação.

Para tanto, testamos com a cidade Jacareí:

Erro HTTP 404 é reportado: termo acentuado não existe.

Figura 4 - Erro HTTP 404 é reportado: termo acentuado não existe.
Fonte: Autor (CC BY-NC-SA 4.0)

E então percebemos dois problemas. O primeiro referente à acentuação, evidenciado por um HTTP error code, o 404. Já o segundo, é a impressão de uma mensagem de texto no console, ao invés da própria página.

Bom, solucionamos primeiro o segundo problema, substituindo a instrução console.log("Erro HTTP: " + jqXHR.status); presente nas functions getCidade e getPrevisaoTempoProximosQuatroDias por:

localContent.html("<h3>Erro HTTP: " + jqXHR.status + "</h3>");

Já para solucionarmos a questão da acentuação, devemos primeiro normalizar todo o texto digitado pelo usuário. Em seguida sobre este, aplicar uma função nativa JS, o replace() que recebe como primeiro argumento uma Regex que aceite apenas letras e espaços em branco. Desse modo, qualquer coisa diferente disso, deverá ser substituída por "" (vazio), o segundo argumento da função.

Considerando estas informações, realizamos a seguinte alteração em nossa isEmpty function:

getCidade($("#cidade").val().normalize("NFD").replace(/[^a-zA-Z\s]/g, ""));

Com isto feito, testamos Jacareí (acento), Guarar@ema (caractere especial) e rio de janeiro (espaços em branco), tendo êxito em todos estes cenários.

Bom, agora já temos os dados, basta-nos apenas imprimí-los na página! Para imprimir os dados do XML object, usamos a sintaxe $(response-object).find("nomeElemento").text(), que em resumo faz:

  1. Seleciona o objeto response
  2. Deste response localiza o elemento de nome "nomeElemento"
  3. Deste elemento, obtém o seu conteúdo textual

Aplicando a sintaxe apresentada teremos os seguintes resultados:

  • nome da cidade: $(data).find("nome").text()
  • UF: $(data).find("uf").text()
  • dia: $(data).find("dia").text()
  • tempo: $(data).find("tempo").text()
  • temperatura máxima: $(data).find("maxima").text()
  • temperatura mínima: $(data).find("minima").text()
  • IUV: $(data).find("iuv").text()

Bom, para imprimir a previsão do tempo iremos criar a estrutura abaixo. Consideramos que 4 blocos desta estrutura serão necessários para exibir os dados dos 4 dias requeridos. Consideramos também que estes estarão contidos no elemento #localContent.

<div class="box col-md-3 col-sm-4 col-12">
  <div class="row">
    <div class="box col-md-6 col-6">TEMPO</div>
    <div class="box col-md-6 col-6">
      <p>NOME DA CIDADE, UF</p>
      <p>DIA</p>
      <p>TEMPERATURA MÁXIMAº C | TEMPERATURA MÍNIMAº C | IUV</p>
    </div>
  </div>
</div>

Para isto, acessamos o arquivo script.js e editamos o bloco done da getPrevisaoTempoProximosQuatroDias, substituindo a instrução console.log(data) por:

// Cria um atributo
let content = "";
// Faz com que o código interno às chaves ({}) seja repetido 4 vezes
for (let i = 0; i < 4; i++) {
  // Adiciona à content o conteúdo do bloco anterior, totalizando assim os 4 blocos de maneira sequenciada.
  content = content.concat("<div class=\"box col-md-3 col-sm-4 col-12\"><div class=\"row\"><div class=\"box col-md-6 col-6\">" + $(data).find("tempo").text() + "</div><div class=\"box col-md-6 col-6\"><p>" + $(data).find("nome").text() + ", " + $(data).find("uf").text() + "</p><p>" + $(data).find("dia").text() + "</p><p>" + $(data).find("maxima").text() + "º C | " + $(data).find("minima").text() + "º C | " + $(data).find("iuv").text() + "</p></div></div></div>");
}
// Imprime na localContent o content
localContent.html(content);

Editamos também o arquivo style.css, modificando o seletor h2,h3 por:

h2,
h3,
div p:nth-child(3) {
    margin-bottom: 0;
}

Movendo a propriedade color do seletor h3 para o novo seletor:

h3,
p {
    color: #24292E; 
}

E ainda, adicionando ao mesmo arquivo as seguintes novas configurações:

div p:first-child {
    font-size: 18px;
    font-weight: bold;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

div p:first-child,
div p:nth-child(2) {
    margin-bottom: 10px;
}

div p:nth-child(2) {
    font-size: 16px;
}

div p:nth-child(3) {
    font-size: 14px;
}

Com isto feito, testamos:

Impressão dos dados meteorológicos dos próximos 4 dias para a cidade de Bertioga (dados repetidos)

Figura 5 - Impressão dos dados meteorológicos dos próximos 4 dias para a cidade de Bertioga (dados repetidos).
Fonte: Autor (CC BY-NC-SA 4.0)

Hum... não era exatamente esse resultado que esperávamos! Vejam que os elementos de previsão, isto é, tempo, dia, temperatura máxima, temperatura mínima, e IUV, todos foram impressos em cada um dos 4 dias. Mas porque isto aconteceu? Isto ocorreu porque não definimos quais dados especificadamente devem pertencer à cada bloco. Para fazer isso, devemos utilizar a função jQuery .eq(), que nada mais faz, que selecionar um elemento por meio de um índice informado.

Assim sendo, nossas instruções devem ficar:

  • $(data).find("tempo").text() => $(data).find("tempo").eq(i).text()
  • $(data).find("dia").text() => $(data).find("dia").eq(i).text()
  • $(data).find("maxima").text() => $(data).find("maxima").eq(i).text()
  • $(data).find("minima").text() => $(data).find("minima").eq(i).text()
  • $(data).find("iuv").text() => $(data).find("iuv").eq(i).text()

Realizando a modificação em nosso arquivo script.js e depois testando, obtemos:

Impressão dos dados meteorológicos dos próximos 4 dias para a cidade de Bertioga (dados corretos)

Figura 6 - Impressão dos dados meteorológicos dos próximos 4 dias para a cidade de Bertioga (dados corretos).
Fonte: Autor (CC BY-NC-SA 4.0)

Bem melhor, não é mesmo? Mas vamos melhorar ainda mais! Observamos que a exibição da data está no formato AAAA-MM-DD. Vamos no entanto, convertê-la para o formato Dia-da-semana (curto), DD/MM. E como fazê-lo?

Primeiramente lemos sobre a função nativa JS toLocaleDateString() para compreender em detalhes a conversão realizada. Depois criamos em nosso arquivo script.js a seguinte function:

function getDataFormatada(dataPrevisao) {
    const option = { 
        weekday: "short",
        day: "numeric", 
        month: "numeric" 
    };
    return new Date(dataPrevisao).toLocaleDateString("pt-BR", option).replace(".", "");
}

E chamamos-a na function getPrevisaoTempoProximosQuatroDias, substituindo a instrução $(data).find("dia").eq(i).text() por:

getDataFormatada($(data).find("dia").eq(i).text())

Já em nosso style.css adicionamos o seletor abaixo, fazendo com que a primeira letra do elemento selecionado, que neste caso é a data convertida, seja exibida em maiúsculo.

div p:nth-child(2)::first-letter {
    text-transform: capitalize;
}

Com tudo isto feito, testamos e obtemos:

Impressão do nome da cidade com inicial maiúscula e a data formatada em dia da semana, mês e ano

Figura 7 - Impressão do nome da cidade com inicial maiúscula e a data formatada em dia da semana, mês e ano.
Fonte: Autor (CC BY-NC-SA 4.0)

Observando a imagem, nos perguntamos, o que significam os textos "ps" e "c", apresentados no elemento tempo na imagem apresentada? Para descobrir faz-nos necessário recorrer à documentação, na seção Siglas das condições do tempo.

Agora tudo ficou mais claro, no entanto, quem utilizar nosso previsor ainda não saberá o que representam as siglas. Podemos, simplesmente substituir as siglas, pelo nomes completo equivalentes, mas há uma solução que pode ser ainda mais interessante, que é a substituição das siglas por uma imagens representativa. Imagens vetorizadas e animadas, que tal?

Para isso, fizemos o download do repositório Weather Animated SVG Icon Set.

Com o download concluído, descompactamos o arquivo. Deste migramos as imagens SVG para nosso diretório /assets/image/.

Comumente imagens são inseridas utilizando a tag img, mas conforme sugerido pelo autor do repositório, utilizaremos a tag iframe.

Agora, criamos uma function em nosso arquivo script.js que retorna uma imagem, de acordo com a siglaTempo informada. Como há mais siglas do que imagens, haverá múltiplas siglas compartilhando as mesmas imagens. Sem mais delongas, segue:

function getImagem(siglaTempo) {
    let imagem = "";
    switch (siglaTempo) {
        case "c":   // chuva
        case "cm":  // chuva pela manhã
        case "cn":  // chuva à noite
        case "ct":  // chuva à tarde       
            imagem = "c_1_rainy.svg";
            break;
        case "ci":  // chuvas isoladas
        case "cv":  // chuvisco
        case "e":   // encoberto     
        case "ec":  // encoberto com chuvas isoladas
        case "in":  // instável
        case "ncm": // nublado com possibilidade de chuva pela manhã
        case "nct": // nublado com possibilidade de chuva à tarde
        case "npp": // nublado com possibilidade de chuva
        case "pcm": // possibilidade de chuva pela manhã
        case "pct": // possibilidade de chuva à tarde
        case "pp":  // possibilidade de pancadas de chuva
        case "ppm": // possibilidade de pancadas de chuva pela manhã
        case "ppt": // possibilidade de pancadas de chuva à tarde
        case "psc": // possibilidade de chuva
            imagem = "b_3_very_cloudy.svg";
            break;
        case "ch":  // chuvoso
            imagem = "c_2_heavy_rain.svg";
            break;
        case "cl":  // céu claro
            imagem = "a_1_sunny.svg";
            break;
        case "g":   // geada
            imagem = "d_1_snow.svg";
            break;
        case "n":   // nublado
        case "vn":  // variação de nebulosidade
            imagem = "b_2_cloudy.svg";
            break;
        case "ncn": // nublado com possibilidade de chuva à noite
        case "pcn": // possibilidade de chuva à noite
        case "ppn": // possibilidade de pancadas de chuva à noite        
            imagem = "b_4_cloudy_night.svg";
            break;
        case "nd":  // não definido
            imagem = "";
            break;
        case "ne":  // neve
            imagem = "d_2_heavy_snow.svg";
            break;
        case "np":  // nublado e pancadas de chuva
        case "npm": // nublado com pancadas pela manhã
        case "npn": // nublado com pancadas à noite
        case "npt": // nublado com pancadas à tarde
            imagem = "g_1_stormy.svg";
            break;
        case "nv":  // nevoeiro
            imagem = "d_4_fog.svg";
            break;
        case "pc":  // pancadas de chuva
        case "pm":  // pancadas de chuva pela manhã
        case "pnt": // pancadas de chuva à noite
        case "pt":  // pancadas de chuva à tarde
            imagem = "g_2_very_stormy.svg";
            break;
        case "pn":  // parcialmente nublado
            imagem = "b_1_partly_cloudy.svg";
            break;
        case "ps":  // predomínio de sol
            imagem = "a_3_very_sunny.svg";
            break;
        case "t":  // tempestade
            imagem = "c_3_thunderstorm.svg";
            break;
    }
    return imagem;
}

Por fim, em nossa function getPrevisaoTempoProximosQuatroDias substituímos o trecho <div class=\"box col-md-6 col-6\">" + $(data).find("tempo").eq(i).text() + "</div> por:

<div class=\"box col-md-6 col-6\"><iframe src=\"assets/image/" + getImagem($(data).find("tempo").eq(i).text()) + "\"></iframe></div>

E adicionamos ao style.css:

iframe {
    border: none;
    height: 100px;
    width: 100px;
}

E então, testamos, obtendo:

Uso de imagens vetorizadas (e animadas) para representar as Siglas das condições do tempo

Figura 8 - Uso de imagens vetorizadas (e animadas) para representar as Siglas das condições do tempo.
Fonte: Autor (CC BY-NC-SA 4.0)

Com isto feito, finalizamos esta resolução! Se gostaram, por favor, deixem seus COMENTÁRIOS, nos SIGAM e principalmente COMPARTILHEM para que assim, seja possível alcançar mais pessoas, que como vocês, curtem Programação, com conteúdos PRÁTICOS e GRATUITOS!

Para ver e/ou baixar o código-fonte desta resolução, ou mesmo melhorá-lo, visitem o repositório no GitHub.

Previsão do Tempo Demo

Figura 9 - Previsão do Tempo Demo.
Fonte: Autor (CC BY-NC-SA 4.0)

Até a próxima!

Cite este material

FERNANDES, Fábio. Desenvolvendo um previsor do tempo (web app). aprendaCodar, 01 de maio de 2022. Disponível em: <https://aprendacodar.blogspot.com/2022/05/desenvolva-um-web-app-de-previsao-do.html>. Acesso em:

Fábio Fernandes

Graduado em Ciência da Computação e Especialista em Análise de Dados com BI e Big Data. Instrutor, Desenvolvedor e Produtor de Conteúdo. Apaixonado por Tecnologia e pelo compartilhamento de conhecimentos.

Postar um comentário (0)
Postagem Anterior Próxima Postagem