Pintar una ciudad con R

Pintar una ciudad con R

Como empezó todo

Llevo tiempo pensando cómo enseñar a programar a un niño. Mis hijos va a clases de robótica en la que usan Lego para crear objetos a los que programan movimientos. Son unas clases divertidas, se lo pasan muy bien al tiempo que aprenden.

Quiero hacer algo parecido con R, talleres sencillos, y que llamen la atención de un niño, vamos, que aguante un rato atento y sin distraerse.

Con este simple objetivo voy a desarrollar una serie de ejemplos de programación y modelización de la realidad. Este primero es una copia de un ejemplo que vi en Processing, un lenguaje gráfico fantástico, y que me encantó. Processing es un lenguaje de programación gráfico orientado más hacia el mundo de la creación artística. Está genial, pero aquí somos de R.

Objetivo

Vamos a programar un gráfico de R, que nos pinte una ciudad de rascacielos altos al estilo de un skyline de Nueva York. Para ello partiremos de cero, y usaremos funciones muy simples.

El resultado final será algo como la imagen inicial.

Primer paso. Pintar rectángulo y línea

Empezaremos creando un simple rectángulo en una gráfica de R. En R ya existe una función que pinta un rectángulo en una gráfica se llama rect(). Para que lo pinte hay que pasar las coordenadas x e y de los vértices de esta forma:

  • rect(xleft, ybottom, xright, ytop)

También podemos pasarle otros parámetros gráficos en la misma llamada a la función rect(), como por ejemplo el color de fondo añadiendo tras una coma: col="red" La función rect() necesita que haya un gráfico sobre el que pintar, por eso antes de llamarla debemos crear un gráfico en R con la función más simple plot() al que le pasamos una serie de datos, por ejemplo la serie del 0 al 10 y le decimos que no la pinte con type = "n".

  plot(0:10,type = "n", main="Pintar un rectángulo")
  rect(1,1,10,8, col="grey")

  # Pintar linea
  x0<-c(2,8)
  y0<-c(4,4)
  lines(x0,y0,col="blue", lwd=3)

Para pintar una linea en la gráfica está la función lines() a la que le pasamos como argumentos las coordenadas de los puntos inicio y final de la linea.

También existen otras funciones para pintar formas como polygon() que pinta un polígono a partir de las coordenadas.

Paso 2. Crear función edificio

Ahora que conocemos las funciones de pintar vamos a crear una función personalizada que nos pinte un edificio que en el fondo es un simple rectángulo sobre el suelo, en el que pintaremos los pisos y las ventanas.

Para definir un edificio necesitaremos antes aportar el número de pisos que va a tener y el ancho del edificio la definiremos por el número de ventanas, ya que las pondremos equidistantes.

Como somos buenos arquitectos hay cosas que damos por sentadas, por ejemplo que la altura de un piso es de 3,5 m. o que las ventanas van a ser todas iguales de 1x1m. También damos por supuesto que cada ventana es de una habitación que tiene 4 metros de ancho de fachada.

Con esta información ya podemos crear un modelo que genere y pinte un edificio al pasar los argumentos de número de pisos (npisos) y número de ventanas en fachada (nvent). Vamos a suponer que el vértice inicial del edificio está en las coordenadas (0,0).

# funcion edificio
edificio<-function (npisos,nvent){
          h<-3.5 # altura de un piso
          a<- 4 #ancho de la habitación de fachada
          altura<-h*npisos # altura total del edificio
          ancho<-a*nvent # ancho total del edificio 
          
          #pintamos una grafica vacía que contenga la escala de maximos
          plot(1:max(altura,ancho),type="n", xlim = c(0,max(altura,ancho)))
          # pintamos el edificio de color gris
          rect(0, 0, ancho,altura, col = "grey")
}
# llamamos a la función y creamos el primer edificio
edificio(34,5)

Vamos a añadir a esta función unas líneas que marquen cada piso y las ventanas.

edificio1<-function (npisos,nvent){
          h<-3.5 # altura de un piso
          a<- 4 #ancho de la habitación de fachada
          ancho_ventana<-1
          altura<-h*npisos # altura total del edificio
          ancho<-a*nvent # ancho total del edificio 
          
          #pintamos una grafica vacía que contenga la escala de maximos
          plot(1:max(altura,ancho),type="n", xlim = c(0,max(altura,ancho)))
          # pintamos el edificio de color gris
          rect(0, 0, ancho,altura, col = "grey")
          # bucle para los pisos
          x0=0
          y0=0
          # pintamos lineas horizontales
          for (i in 2:npisos-1){# no incluye el piso bajo
            x1<-c(x0,x0+ancho)
            y_piso<-i*h+y0 # altura del piso
            y1<-c(y_piso,y_piso)
            lines(x1,y1)
            # por cada piso hacemos las ventanas
            altura_ventana_suelo<-1
            for (k in 0:(nvent-1)){
              # vertice x bajo de la ventana
              xv<-a/2-ancho_ventana/2+a*k
              # vertice y bajo de la ventana
              yv<-y_piso+altura_ventana_suelo
              # pintamos ventana
              rect(xv,yv,xv+ancho_ventana,yv+ancho_ventana, col = "black")
              #rect(xleft, ybottom, xright, ytop) 
            }
          }          
}

edificio1(14,3)

Paso 3. Añadir desplazamiento

La función está muy avanzada. Podríamos añadir como argumentos los parámetros que se introducen por defecto y generar una función más compleja de dibujo, pero vamos a continuar añadiendo la posibilidad de crear el edificio en otro punto de coordenadas diferente, para así poder generar muchos edificios y crear una ciudad.

Hasta ahora el edificio se genera en el vértice (0,0) de coordenadas. Vamos a añadir la opción de que se genere en otro punto del eje x cualquiera y también la opción de que no se borre en cada repintado permitiendo añadir más edificios a la gráfica.

edificio2<-function(npisos=10,nvent=3,x0=0,borrar=T){
          y0 <- 0
          h <-3.5 # altura de un piso
          a <- 4 #ancho de la habitación de fachada
          ancho_ventana <-1.5
          altura <-h*npisos # altura total del edificio
          ancho <-a*nvent # ancho total del edificio 
          # pintamos una grafica vacía que contenga la escala de maximos
          # si borrar=T vacía el lienzo
          if(borrar==T){
                plot(1:max(altura,ancho),type="n",
                     xlim = c(0,max(altura,ancho)),
                     xlab = " ",ylab=" ")
                lines(c(0,max(altura,ancho)),c(0,0))
                }
          # pintamos el edificio de color gris
          rect(x0, y0, ancho+x0,altura+y0, col = "grey")
          # bucle para los pisos
          # pintamos lineas horizontales
          for (i in 2:npisos-1){# no incluye el piso bajo
            x1<-c(x0,x0+ancho)
            y_piso<-i*h+y0 # altura del piso
            y1<-c(y_piso,y_piso)
            lines(x1,y1,lty=2)
            # por cada piso hacemos las ventanas
            altura_ventana_suelo<-1
              for (k in 0:(nvent-1)){
                # vertice x bajo de la ventana
                xv<-a/2-ancho_ventana/2+a*k
                # vertice y bajo de la ventana
                yv<-y_piso+altura_ventana_suelo
                # pintamos ventana
                rect(x0+xv,yv,x0+xv+ancho_ventana,yv+ancho_ventana, col = "black")
                }
          }          
    }

edificio2(26,3,15)
edificio2(12,2,5,borrar=F)
edificio2(22,7,45,borrar=F)

Paso 4. Generar un skyline

Una vez tenemos la función generadora de edificios vamos a crear otra función que la llame varias veces hasta crear una ciudad.

Los argumentos de entrada serán el ancho de la ciudad en m (M), la altura máxima del mayor rascacielos (H).

ciudad<-function(M=200,H=140){
  # Pinta el lienzo de la ciudad
    plot(1:H,type="n",
            xlim = c(0,M),
            xlab = " ",ylab=" ")
            lines(c(0,M),c(0,0))
            
    nedif<-as.integer(M/12)
    # de 2 a 8 ventanas
    v_en_fachadas<-2:7
    alturas<-5:as.integer(H/3.5)
    vector_v<- sample(v_en_fachadas,nedif,replace = TRUE)
    print("ventanas:")
    print(vector_v)
    vector_y<- sample(alturas,nedif)
    print("pisos:")
    print( vector_y)
    x0<-0
    for(i in seq_along(vector_v)){
      edificio2(vector_y[i],vector_v[i],x0,borrar=F)  
      x0<-vector_v[i]*4+x0
      #edificio2(npisos,nvent=3,x0=0,borrar=T){
    }
    
}

ciudad()

## [1] "ventanas:"
##  [1] 3 6 6 5 7 2 5 2 6 7 7 3 4 7 4 3
## [1] "pisos:"
##  [1] 34 22 25  7 14 37 27 18 38  8  6 19 11 28 12 39

Paso 5. Mejoras

Una vez que tenemos la función generadora de edificios y la de ciudades, vamos a personalizar las funciones para mejorar la apariencia, por ejemplo, cambiando el color de los edificios y de las ventanas para que parezcan encendidas o apagadas.

edificio3<-function(npisos=10,nvent=3,x0=0,y0=0,borrar=T,alfa=1,col="grey"){
          #y0 <- 0
          h <-3.5 # altura de un piso
          a <- 4 #ancho de la habitación de fachada
          ancho_ventana <-1.5
          altura <-h*npisos # altura total del edificio
          ancho <-a*nvent # ancho total del edificio 
          # pintamos una grafica vacía que contenga la escala de maximos
          # si borrar=T vacía el lienzo
          if(borrar==T){
                plot(1:max(altura,ancho),type="n",
                     xlim = c(0,max(altura,ancho)),
                     xlab = " ",ylab=" ")
                lines(c(0,max(altura,ancho)),c(0,0))
                }
          # pintamos el edificio de color gris
          #alfa<-runif(1,0.4,1)
          color_edificio<-adjustcolor(col, alpha.f = alfa)
          col_border<-adjustcolor(par("fg"), alpha.f = alfa)
          
          rect(x0, y0, ancho+x0,altura+y0, col = color_edificio, border = col_border)
          # bucle para los pisos
          # pintamos lineas horizontales
          for (i in 2:npisos-1){# no incluye el piso bajo
            x1<-c(x0,x0+ancho)
            y_piso<-i*h+y0 # altura del piso
            y1<-c(y_piso,y_piso)
            lines(x1,y1,lty=2,col = col_border)
            # por cada piso hacemos las ventanas
            altura_ventana_suelo<-1
              for (k in 0:(nvent-1)){
                # vertice x bajo de la ventana
                xv<-a/2-ancho_ventana/2+a*k
                # vertice y bajo de la ventana
                yv<-y_piso+altura_ventana_suelo
                # pintamos ventana
                #color encendido apagado de luz
                color_ventana<-ifelse(runif(1)>0.7,"yellow","black")
                color_ventana<-adjustcolor(color_ventana, alpha.f = alfa) 
                rect(x0+xv,yv,x0+xv+ancho_ventana,yv+ancho_ventana,
                     col = color_ventana,border = col_border)
                }
          }          
    }

edificio3(26,3,15,alfa=0.3,col="wheat")
edificio3(12,2,5,10,borrar=F)
edificio3(22,7,45,23,borrar=F)
edificio3(10,3,50,10,borrar=F,alfa=0.1)

Resultados

Con estos cambios podemos hacer ya muchas cosas chulas, por ejemplo vamos a personalizar la función de ciudad, añadiendo 3 argumentos:

  • y0= para subir la linea de suelo
  • alfa= para controlar la transparencia del color
  • col= para definir el color general de los edificios

La idea es llamar varias veces a la funcion ciudad() empezando por el fondo claro y tramsparente y termnar en el primer plano con alfa=1 y color vivo.

Veamos el resultado:

# Version personalizada de la funcion ciudad
ciudad2<-function(M=200,H=140, y0=0, alfa=1,col="grey",borrar=T){
  # Pinta el lienzo de la ciudad o borra lo que haya
  if(borrar==T){
    plot(1:H,type="n",
            xlim = c(0,M),
            xlab = " ",ylab=" ",axes=FALSE)
            lines(c(0,M),c(0,0))
    }        
    nedif<-as.integer(M/12)# un calculo rápido del nummero de edificios
    # ventanas en cada fachada de 2 a 7 ventanas
    v_en_fachadas<-2:7
    alturas<-5:as.integer(H/3.5) # alturas de los rascacielos posibles
    # toma una muestra aleatoria de ventanas y alturas
    vector_v<- sample(v_en_fachadas,nedif,replace = TRUE)
    vector_y<- sample(alturas,nedif)
    x0<-0
    for(i in seq_along(vector_v)){
      # por orden, npisos, nventanas, x0,y0...
      edificio3(vector_y[i],vector_v[i],x0,y0,borrar=F,alfa=alfa,col=col)  
      x0<-vector_v[i]*4+x0 # posicion x del siguiente edificio
    }
}

# Ahora Creamos una ciudad
  ciudad2(alfa=0.2,col="blue", y0=20)
  ciudad2(alfa=0.5,col="red", y0=0,borrar=FALSE)
  ciudad2(H=100,alfa=1,col="grey", y0=0,borrar=FALSE)

Conclusión

Ha sido muy divertido, en breve continuaremos la saga educativa haciendo más modelos por ejemplo:

  • generador de bosques
  • creador de mapas del tesoro