quarta-feira, 19 de janeiro de 2022

Preenchendo retângulos em um plot

Imagine que você tenha uma gride de células em um plot, como mostrado na figura abaixo à esquerda. Agora suponha que você queira selecionar alguns desses retângulos, podendo preenchê-los de outra cor. Um maneira de fazer seria escrever na mão pontos que estão dentro dos retângulos de sua escolha, por exemplo, os pontos (1,5; 2,5) e (8,5;8,5) correspondem aos retângulos preenchidos na figura à direita.





Se você quer selecionar vários retângulos, escrever os pontos à mão é ineficiente (e extremamente cansativo). Por isso, hoje vamos escrever uma função para preencher o retângulo que queremos quando clicamos dentro dele.

Faremos isso utilizando a função locator(). Essa função pega as coordenadas de um clique no gráfico, ou seja, ao clicar em uma área do gráfico, a função retornará as coordenadas do ponto em que houve o clique. Por exemplo:
> locator(1)
$x
[1] 0.3500569

$y
[1] 5.517714

Com as coordenadas em mãos, sabemos que o retângulo escolhido foi o que está entre 0 e 1 no eixo x e entre 5 e 6 no eixo y. Desse modo, sabemos qual retângulo preencher. Como faremos isso? Da seguinte maneira: desenhamos um retângulo (com cor de preenchimento) que tem ponto central igual a 0,5 e 5,5. Obviamente, esse ponto central é particular da gride que eu criei, caso sua gride tenha intervalos diferentes (por exemplo, retângulos com comprimento igual a 2), ajuste esse ponto.

Para computarmos a coordenada do ponto central do retângulo escolhido, faremos o seguinte: 

  • arredondamos a coordenada x obtida para cima e subtraímos metade do comprimento;
  • arredondamos a coordenada y obtida para cima e subtraímos metade da altura.

No caso, o retângulo tem comprimento e altura iguais a 1, ou seja, subtraímos 0,5 de ambas coordenadas arredondadas:

xr = ceiling(p$x) - 0.5;
yr = ceiling(p$y) - 0.5;

Dado esse centro e as dimensões dos retângulos, estamos prontos para desenhar o retângulo preenchido:

rect(xr-0.5,yr-0.5,xr+0.5,yr+0.5,col=1)

Como o código está solto, vamos colocá-lo dentro de uma função. Além de boa prática, nos ajuda a verificar se a função está funcionando corretamente.

desenha = function(){
  p = locator(1)
  xr = ceiling(p$x) - 0.5;
  yr = ceiling(p$y) - 0.5;
  rect(xr-0.5,yr-0.5,xr+0.5,yr+0.5,col=1)
}

Se executarmos a função acima e clicarmos dentro de um retângulo, ele será preenchido com a cor preta (col = 1). Note que, como parâmetro da função locator(), passamos o valor 1. Isso significa que queremos apenas um ponto. Poderíamos passar n como argumento para podermos colorir n pontos. Vamos testar? A única alteração é passar um valor n para a função desenha() que será utilizado na função locator().

desenha = function(n = 1){
  p = locator(n)
  ...
}

No caso, colocamos n = 1 no argumento da função desenha() para atribuirmos um valor padrão, quando o usuário não passa esse argumento. Temos dois "problemas" na função acima:

  1. o preenchimento ocorre apenas depois de selecionarmos todos os n retângulos,
  2. devemos saber exatamente quantos retângulos queremos colorir.

Não seria mais fácil colorir os retângulos, um por um, até ficarmos satisfeitos e depois parar a função? Muito mais prático, não? Vamos implementar! Para isso, voltamos a passar apenas um ponto na função locator().

Vamos pensar... queremos executar a função desenha() completa uma vez, outra vez, e outra vez até termos um sinal de parada. O que isso nos lembra? A estrutura de repetição, é claro. Então, vamos executar a função desenha() até que um critério de parada seja satisfeito. Vamos considerar que a parada ocorre quando a função locator() não retorna coordenadas, ou seja, não há clique de pontos. Isso ocorre quando o usuário aperta ESC enquanto a função locator() está executando.

Agora, se a função desenha() está executando infinitamente, como saber se o usuário terminou sua ação? Simples, quando não há coordenadas para retornar, a função locator() retorna NULL. Dessa forma, verificamos se a variável que armazena o ponto obtido está nula ou não. Vejamos:

desenha = function(){
  p = locator(1);
  if(is.null(p)){
    cat("Seleção de pontos encerrada.\n");
    return(T);
  }else{
    xr = ceiling(p$x) - 0.5;
    yr = ceiling(p$y) - 0.5;
    rect(xr-0.5,yr-0.5,xr+0.5,yr+0.5,col=1)
    return(F);
  }
}

Na função acima, verificamos se p é nulo. Caso positivo, printamos a mensagem "Seleção de pontos encerrada." e a função retorna TRUE. Caso contrário, preenchemos o retângulo escolhido e a função retorna FALSE. Feito isso, basta escrever a estrutura de repetição:

parada_flag = F;
while(parada_flag==F){
  parada_flag = desenha();
}

Utilizamos a variável parada_flag para identificar quando parar a repetição da função desenha(). Quando o usuário pressiona ESC, a função desenha() retorna o valor TRUE e, então, o while() é terminado. Se você testar o código acima, verá que, a cada clique, o retângulo escolhido é preenchido, exatamente do jeito que queríamos. Assim que terminamos, apertamos ESC e a execução é finalizada.

A função está completa, se consideramos o problema proposto inicialmente. Mas e se quisermos guardar a informação de quais retângulos foram preenchidos? Precisamos, então, retornar as coordenadas dos pontos clicados. 

Aqui vai uma observação: se você quer exatamente as coordenadas dos pontos clicados, deve retornar o valor que a variável p armazena. No meu caso, o interesse é apenas identificar os retângulos e, dessa forma, posso armazenar apenas o ponto central deles. É isso o que vamos fazer:

desenha = function(){
  p = locator(1);
  if(is.null(p)){
    cat("Seleção de pontos encerrada.\n");
    p = NULL;
    parada = T;
  }else{
    xr = ceiling(p$x) - 0.5;
    yr = ceiling(p$y) - 0.5;
    rect(xr-0.5,yr-0.5,xr+0.5,yr+0.5,col=1)
    p = c(xr,yr);
    parada = F;
  }
  return(list(p=p,parada=parada));
}

Perceba que agora, além de retornar T ou F, precisamos retornar o ponto clicado. Dessa forma, retornamos uma lista, em que p identifica o ponto central do retângulo ou o valor NULL (caso não haja ponto clicado) e parada retorna a identificação da condição de parada. Como estamos retornando uma lista, devemos modificar nossa estrutura de repetição. Além disso, precisamos armazenar os pontos clicado em algum lugar. Fazemos isso da seguinte forma:

pontos = matrix(ncol=2);
pontos = pontos[-1,]

Ou seja, pontos é uma matriz de duas colunas. Sempre é inicializada com uma linha, caso não haja especificação. Então a segunda linha do código acima exclui a linha da matriz, deixando-a vazia. Assim, ela está pronta para receber as coordenadas dos pontos (primeira coluna para eixo x e segunda coluna para eixo y).

A estrutura de repetição fica:

parada_flag = F;
while(parada_flag==F){
  aux = desenha(1);
  parada_flag = aux$parada;
  pontos = rbind(pontos,aux$p);
}

Perceba que usei uma variável auxiliar aux para receber o que a função desenha() retorna e, a partir dela, atualizei a variável parada_flag e incrementei o ponto central na matriz pontos. Dessa forma, armazenamos as coordenadas dos pontos clicados (no meu caso, do ponto central do retângulo) cada vez que escolhemos um retângulo para preencher.

Vamos testar nosso código? Meu exemplo ficou da seguinte maneira:

> pontos = matrix(ncol=2);
> pontos = pontos[-1,]
> parada_flag = F;
> while(parada_flag==F){
+   aux = desenha();
+   parada_flag = aux$parada;
+   pontos = rbind(pontos,aux$p);
+ }
Seleção de pontos encerrada.
> head(pontos)
     [,1] [,2]
[1,]  3.5  8.5
[2,]  4.5  8.5
[3,]  5.5  8.5
[4,]  2.5  7.5
[5,]  6.5  7.5
[6,]  1.5  6.5


Perceba que a lógica utilizada aqui serve para colorir qualquer figura geométrica, como triângulos e pentágonos, apenas ajustando o código. Claro que, dependendo da complexidade da figura, o ajuste será mais ou menos difícil.

Você deve estar se perguntando: para que serve o código que fizemos? Tenho duas respostas diretas para isso:
  1. aprender a programar. Não é tão simples implementar essas funções como parece e ajuda na prática de programação.
  2. selecionar o estado inicial do Jogo da vida (Conway's Game of Life). Veremos esse algoritmo no próximo post, mas você pode dar uma olhada nele aqui.

Esperam que tenham gostado da aula.
Até a próxima aula!

Nenhum comentário:

Postar um comentário