Arreglos - Mapa de Puerto Rico
Los arreglos de datos (arrays) nos facilitan guardar y trabajar con grupos de datos del mismo tipo. Los datos se guardan en espacios de memoria consecutivos a los que se puede acceder utilizando el nombre del arreglo con índices o suscritos que indican la posición en que se encuentra el dato. Las estructuras de repetición nos proveen una manera simple de acceder a los datos de un arreglo.
Un objeto es una entidad que se utiliza en muchos lenguajes de programación para integrar los datos y el código que opera en ellos, haciendo más fácil el modificar programas grandes. Una tarea bien común en programación usando C++ es el trabajar con arreglos de objetos. En la experiencia de laboratorio de hoy estarás trabajando con datos georeferenciados de pueblos en Puerto Rico, en donde tendrás atributos, como el nombre del pueblo, la latitud y longitud de su localización, que utilizarás para ilustrar propiedades en un mapa.
Objetivos:
Crear dinámicamente y manipular un arreglo de objetos.
Codificar funciones para procesar arreglos de objetos.
Practicar el pasar arreglos de objetos como parámetros de una función.
Practicar la lectura secuencial de datos en un archivo.
Usar programación modular.
Usar estructuras de repetición y control.
Pre-Lab:
Antes de llegar al laboratorio debes haber:
Repasado los conceptos relacionados a arreglos de objetos.
Repasado los conceptos relacionados con funciones que utilizan arreglos de objetos.
Repasado cómo leer datos de un archivo.
Estudiado los conceptos e instrucciones para la sesión de laboratorio.
Tomado el quiz Pre-Lab, disponible en Moodle.
Datos georeferenciados
Trabajar con arreglos de objetos es una tarea bien común en la programación usando C++. Una vez has leído la información de los objetos de un archivo o provenientes de un usuario, debes depender de tus destrezas algorítmicas y conocimiento sobre C++ para invocar los métodos y funciones adecuadas para procesar los datos correctamente.
En esta experiencia de laboratorio estarás trabajando con datos georeferenciados sobre las ciudades en Puerto Rico. El que un dato sea georeferenciado quiere decir que el dato tiene una localización física asociada. Típicamente, esta localización son coordenadas de longitud y latitud. Por ejemplo, lo que sigue es parte de un archivo que contiene datos georeferenciados de pueblos en Puerto Rico:
Arroyo 17.9658 -66.0614
Bayamon 18.3833 -66.15
Caguas 18.2342 -66.0486
Dorado 18.4589 -66.2678
Fajardo 18.3258 -65.6525
Figura 1. Parte del contenido de una archivo de datos georeferenciados de pueblos en Puerto Rico; contiene nombre del pueblo, coordenada de latitud y coordenada de longitud.
Distancia ortodrómica
Para calcular la distancia entre dos puntos en el plano Euclidiano, trazas el segmento de línea recta que une a los puntos y calculas su largo utilizando la fórmula de distancia que estudiaste en tu curso de Pre-cálculo. Para calcular la distancia entre dos puntos en la superficie de una esfera no utilizas el segmento de línea que los une, utilizas la distancia más corta entre esos puntos medidos sobre la esfera. Esta distancia se llama distancia ortodrómica. Para calcular la distancia ortodrómica entre dos puntos en el globo terráqueo se usan las coordenadas de latitud y longitud.
La clase GISPOI
La manera más común para los programadores en C++ encapsular datos asociados a un ente es utilizando clases. Por ejemplo, en el caso de datos georeferenciados, una manera práctica de encapsular la información para cada pueblo sería implementando una clase GeoreferencedPointOfInterest
que contenga al menos datos (o atributos) para: el nombre del pueblo, su latitud y su longitud. La clase GPOI
también necesitará implementar métodos para acceder, modificar y hacer operaciones en sus atributos.
En esta experiencia de laboratorio te proveemos una clase GPOI
con los siguientes métodos de interfaz:
GISPOI()
: constructor por defectoGISPOI(QString s, double latitude, double longitude)
: constructor que recibe nombre, longitud y latituddouble getLat()
,double getLon()
: "getters" para la latitud y longitudQString getName()
: "getter" para el nombrevoid setAll(string s, double a, double b)
: "setter" para todas las propiedades (a la vez)double odDistance(const GISPOI &B) const
: dado otro objetoB
de la claseGPOI
, devuelve la distancia ortodrómica (orthodromic) (la distancia más corta) entre elGPOI
que la invoca yB
Leyendo datos de un archivo de texto en C++
En este laboratorio crearás un programa que lee datos de un archivo de texto. Puedes saltar esta sección si te sientes cómodo con tus destrezas de manejo de archivos de texto en C++. De lo contrario, sigue leyendo...
C++ provee funciones para leer y escribir datos en archivos. En este laboratorio usarás uno de los métodos más básicos de lectura: lectura secuencial de archivos de texto. Los archivos de texto consisten de caracteres ASCII que representan datos en alguno de los tipos primitivos de C++. Por ejemplo, supón que el archivo nameAge.txt
contiene algunos datos sobre nombres y edades.
Tomas 34
Marta 55
Remigio 88
Andrea 43
Para leer un archivo de texto como parte de un programa en C++, debemos conocer cómo están organizados los datos en el archivo y qué tipo de datos deseamos leer. El archivo ejemplo nameAge.txt
contiene cuatro líneas y cada línea contiene un string y un entero. A continuación un programa para leer el archivo de principio a fin mientras se imprimen los datos que se van leyendo en cada línea. Lee los comentarios del programa para que entiendas sus partes:
#include <iostream>
// fstream es el header file que contiene clases, funciones y objetos
// para trabjar con lectura y escritura de archivos.
#include <fstream>
using namespace std;
int main(){
// Usaremos las siguientes dos variables para asignarle los
// valores que leemos en cada linea del archivo.
string name;
int age;
// Definimos el objeto que representará al archivo
ifstream inFile;
// Invocamos a la funcion open para que abra el file `nameAge.txt`
inFile.open("nameAge.txt");
// Verificamos que el archivo ha sido debidamente abierto
if (!inFile.is_open()) {
cout << "Error openning file nameAge.txt\n";
exit(1);
}
// Mientras haya datos por leer en el archivo, leer un string
// y un entero. Observa como usamos simbolo `>>`, parecido
// a como lo usamos con cin.
while (inFile >> name >> age) {
cout << name << " : " << age << endl;
}
// Cerrar el archivo.
inFile.close();
return 0;
}
El objeto ifstream
permite que leamos el archivo de forma secuencial. Lleva cuenta de la próxima posición a leer dentro del archivo. Cada vez que leemos uno o más datos (usando inFile >> ____
) el objeto adelanta su posición para que el próximo inFile >> ___
pueda leer los datos siguientes.
Observa la linea inFile >> name >> age
. Esa instrucción realiza varias tareas:
- Lee un
string
y unint
del archivo (si queda algo por leer) y se los asigna a las variablesname
yage
. - Si pudo leer ambos datos, la expresión
inFile >> name >> age
evalúatrue
. - De lo contrario, la expresión evalúa
false
, saliendo del bloque while.
A continuación algunos pedazos de código C++ para tareas comunes de lectura de archivos. Observa que en todas ellas:
- Creamos un objecto de clase
ifstream
, llamamos a su funciónopen
y verificamos que el archivo abrió correctamente. - Creamos una o más variables para asignarles los valores que leeremos del archivo.
- Implementamos un ciclo que se repite mientras hayan datos que leer del archivo.
- Cerramos (función
close
) el archivo.
Ejemplo 1: Leer un archivo que solo contiene datos enteros y acumular sus valores.
ifstream inFile;
int n;
int accum = 0;
inFile.open("nums.txt");
if (!inFile.is_open()) {
cout << "Error openning file nums.txt\n";
exit(1);
}
while (inFile >> n) {
accum = accum + n;
}
cout << "Total: " << accum << endl;
inFile.close();
Ejemplo 2: Contar el número de líneas en un archivo que contiene un nombre por línea. Luego imprimir el contenido de la línea del medio.
ifstream inFile;
string name;
int ctr = 0;
inFile.open("names.txt");
if (!inFile.is_open()) {
cout << "Error openning file names.txt\n";
exit(1);
}
while (inFile >> name) {
ctr++;
}
cout << "Total number of lines: " << ctr << endl;
// Estos dos comandos retroceden el file al principio.
inFile.clear();
inFile.seekg(0);
for (int i = 0; i <= ctr / 2; i++) {
inFile >> name;
}
cout << "The name at the position " << ctr / 2 << ": " << name << endl;
inFile.close();
string S[5]
y le asignamos el string "perro"
a cada uno de los objetos de S
. ¿Cuál de las siguientes cambiaría el string del elemento 3 de S
a "perra"
?
S[4] = "a";
S[3][4] = 'a';
S.at(3) = "a";
S.replace(3,4, 'a');
S[3][4] = 'a'
. Este comando realiza la siguiente tarea: cambia el elemento número 4 del objeto de índice 3 de S..
La opción S[4] = "a"
tiene el efecto de cambiar el objeto de índice 4 a "a"
.
La opción S.at(3) = "a"
intenta cambiar el contenido del objeto con índice 3 a "a"
. Sin embargo, la instrucción no es válida pues el resultado del método at()
es una constante que no acepta cambio de valor.
La opción S.replace(3,4, 'a');
es inválida pues el método replace()
puede ser invocado a los objetos de clase string
, no a los arreglos.
string S[10]
y le asignamos un string distinto a cada uno de los objetos de S
. ¿Cuáles de las siguientes imprime el largo del elemento 7 de S?
cout << S[7].length();
cout << S.length()[7];
cout << S.length(7);
cout << S.at(7).length();
cout << S[7].length();
, cout << S.at(7).length();
. Ambas preceden la invocación del método length()
con una expresión que se refiere al elemento 7 del arreglo S. Tanto S[7]
como S.at(7)
son formas de acceder al elemento 7.
La opción cout << S.length()[7];
es inválida pues está tratando de invocar el método length()
al arreglo de strings. El método length()
se le puede invocar a los strings
, no a los arreglos.
La opción cout << S.length()[7];
es inválida pues está tratando de invocar el método length()
al arreglo de strings. El método length()
se le puede invocar a los strings
, no a los arreglos.
La opción cout << S.length(7)
es inválida pues está tratando de invocar el método length()
al arreglo de strings. Además el método length
no requiere argumentos y la invocación está incluyendo uno.
QPoint P[7]
y le hemos asignado valores x
y y
a los objetos del arreglo. ¿Cual(es) de los siguientes se puede utilizar para computar la suma de las coordenadas y de todos los puntos?
La opción alpha contiene P.y()
en la expresión dentro del ciclo. P.y()
no es válida pues P
es un arreglo, no un QPoint, por lo tanto no se le puede invocar el getter y()
a P
.
La opción gamma contiene a[i]
en la expresión dentro del ciclo. a[i]
no es válida pues a[i]
no es un arreglo, es un int
.
Sesión de laboratorio:
Ejercicio 1 - Bajar y entender el código
Instrucciones
Carga a
QtCreator
el proyectoprMap
. Hay dos maneras de hacer esto:- Utilizando la máquina virtual: Haz doble “click” en el archivo
prMap.pro
que se encuentra en el directorio/home/eip/labs/arrays-prmap
de la máquina virtual. - Descargando la carpeta del proyecto de
Bitbucket
: Utiliza un terminal y escribe el commandogit clone http:/bitbucket.org/eip-uprrp/arrays-prmap
para descargar la carpetaarrays-prmap
deBitbucket
. En esa carpeta, haz doble “click” en el archivoprMap.pro
.
- Utilizando la máquina virtual: Haz doble “click” en el archivo
Compila y corre el programa. En su estado actual, el programa solo despliega un mapa de Puerto Rico. En este mapa podrás ver los resultados de tu programa. Quizás ves algunas advertencias ("warnings") debido a que hay algunas funciones que están incompletas. Estarás completando estas funciones durante la experiencia de laboratorio.
Abre el archivo
main.cpp
. En este archivo es que estarás escribiendo tu código. El archivo contiene las siguientes funciones:void printArrayOfCities(GISPOI A[], int size)
: Dado un arregloA
de objetos de la claseGISPOI
y su tamaño, imprime todos los pueblos en el arreglo. Puedes usar esta función como parte de tu proceso de depuración ("debugging").int countLinesInFile(ifstream &file)
: Dada una referencia a un objeto que representa un archivo, esta función cuenta y devuelve el número de filas del archivo.void readFileToArray(ifstream &file, GISPOI A[], int numOfCities)
: Dado el objetoifstream
de un archivo, un arreglo de pueblos, y el número de registros para leer del archivo, esta función lee los valores del archivo y llena el arreglo con objetos. Esta es una función que tú implementarás.void maxDistances(GISPOI A[], int size, int &idxCityA, int &idxCityB)
: Dado un arregloA
de pueblos, determina los dos pueblos que quedan más lejos. Recuerda que la distancia que calcularás será la distancia ortodrómica. La función devuelve (por referencia) los índices de estas ciudades en el arreglo. Esta es una función que tú implementarás.void minDistances(GISPOI A[], int size, int &idxCityA, int &idxCityB)
: Dado un arregloA
de pueblos, determina los dos pueblos que quedan más cerca. Recuerda que la distancia que calcularás será la distancia ortodrómica. La función devuelve (por referencia) los índices de estas ciudades en el arreglo. Esta es una función que tú implementarás.double cycleDistance(GISPOI A[], int size, int P[])
: Dado un arregloA
de pueblos, el tamaño de este arreglo, y un arregloP
con una permutación de los enteros en[0,size-1]
, computa y devuelve la distancia de viajar el ciclo de pueblosA[P[0]]
A[P[1]]
A[P[size-1]]
. Recuerda que la distancia que calcularás será la distancia ortodrómica.Por ejemplo, si los pueblos que se leen del archivo fueran Mayagüez, Ponce, Yauco y San Juan (en ese orden) y la permutación
P
es , la función debe computar la distancia del ciclo San Juan Ponce Mayagüez Yauco San Juan.Esta es una función que tú implementarás.
Hay otras dos funciones que debes conocer:
void MainWindow::drawLine(const GISPOI &city01, const GISPOI &city02)
: Dada una referencia a dos objetosGISPOI
, la función pinta una línea entre ellos.void drawPoints(GISPOI* gisLocations, unsigned int size);
: Dado un arreglo de objetosGISPOI
y su tamaño, despliega sus localizaciones como puntos en el mapa.
Ejercicio 2 - Leer los puntos georeferenciados a un arreglo
Recuerda que solo estarás cambiando código en el archivo main.cpp
. Tu primera tarea será añadir código para leer todo el contenido de un archivo a un arreglo de objetos GISPOI
.
En la función
main()
, añade las instrucciones necesarias para abrir el archivo que contiene la información de los pueblos georeferenciados. El archivo que usarás primero está en el directoriodata
y se llamapr10.txt
. Necesitas dar elpath
completo del archivo como parámetro del métodoopen()
de tu objetoifstream
. Como siempre, cuando uses archivos debes verificar si el nombre del archivo que pusiste se puede abrir para leer exitosamente.Invoca la función
int countLinesInFile(ifstream &inFile)
para obtener el número de líneas en el archivo. Puedes imprimir el número de líneas obtenido de modo que verifiques que tu programa está funcionando correctamente.Crea un arreglo dinámicamente tan grande como el número de líneas en tu programa.
Modifica la función
void readFileToArray(ifstream &file, GISPOI A[], int numOfCities)
de modo que lea todas las líneas en el archivo a los objetos en el arreglo.En la función
main()
, invoca la funciónreadFileToArray
, pasando la referencia al archivo, el arreglo que creaste en el paso 3, y su tamaño.Luego de invocar la función
readFileToArray
puedes invocar la funciónvoid printArrayOfCities(GISPOI A[], int size)
para imprimir los nombres y georeferencias de los puntos leídos del archivo.Invoca el método
drawPoints(GISPOI* gisLocations, unsigned int size)
en el objetow
de modo que se muestre un punto en el mapa para cada pueblo. La invocación debe hacerse de esta manera:w.drawPoints(A, size)
(asumiendo queA
es el nombre de tu arreglo). Debes obtener algo parecido a la siguiente figura:
Ejercicio 3 - Funciones max y min
Una vez que tengas la información de los pueblos georeferenciados en el arreglo de objetos, puedes comenzar a procesarlos de muchas formas interesantes. Comenzaremos con algunas operaciones básicas.
Lee la documentación e implementa la función
void maxDistances(GISPOI A[], int size, int &idxCityA, int &idxCityB)
. Invoca la función desdemain()
.Usa el método
void drawLine(const GISPOI &city01, const GISPOI &city02)
del objetow
para dibujar una línea que conecte los pueblos más lejanos. Nota que el segundo y tercer parámetro de este método son referencias a los objetos que representan los pueblos (no sus índices en el arreglo).Lee la documentación e implementa la función
void minDistances(GISPOI A[], int size, int &idxCityA, int &idxCityB)
. Invoca la función desdemain()
.Usa el método
void drawLine(const GISPOI &city01, const GISPOI &city02)
del objetow
para dibujar una línea que conecte los pueblos más cercanos.
Ejercicio 4 - Computa la distancia del ciclo
Lee la documentación e implementa la función
double cycleDistance(GISPOI A[], int size, int P[])
. Invoca la función desdemain()
como se indica en los comentarios dentro de la funciónmain()
:- Primero, con ,
- Luego, con = .
Ejercicio 5 - ¡Más diversión!
Cambia tu código de modo que ahora abra el archivo pr.txt
. Valida tus resultados y ¡maravíllate de tu gran logro!
Entrega
Utiliza "Entrega" en Moodle para entregar el archivo main.cpp
. Recuerda utilizar buenas prácticas de programación, incluye el nombre de los programadores y documenta tu programa.
Referencias
[1] https://en.wikipedia.org/wiki/Great-circle_distance