munch2024 / munch

2 stars 11 forks source link

Task 2.1: Code Refactoring AIS Search Up Assignment #108

Closed razonm closed 8 months ago

razonm commented 8 months ago

Description

This is code from my first assignment from one of the first CS classes I took. The code reads through a large database of US Coast Guard ship position information. When the user inputs a name or phrase, it searches through to find any matching vessels.

Since this was an assignment created for students, there are some inefficiencies designed to provide extra practice for students. These inefficiencies can be reduced to make the code overall easier to use for anyone. For example, there are some functions that are only used in one other place. The search function also burdens users to call additional formatting functions that should be handled inside the search. This should be integrated into the search function to reduce user error.

Goals

Code

// Include libraries
#include <iostream>
#include <fstream>
#include <vector>
#include <sstream>
#include <string>
#include <cmath>

using namespace std;

// Structure to hold item data
struct AISType
{
    string MMSI;                // 0
    string baseDateTime;        // 1
    double  lattitude;          // 2
    double longitude;           // 3
    double sog;                 // 4
    double cog;                 // 5
    double heading;             // 6
    string vesselName;          // 7
    string imo;                 // 8
    string callSign;            // 9
    string vesselType;          // 10
    string status;              // 11
    double length;              // 12
    double width;               // 13
    double draft;               // 14
    string cargo;               // 15
    string transceiverClass;    // 16
};

// Prototypes for functions
void readFile( ifstream & inFile, vector<AISType> &item, int& count);
bool openInputFile( ifstream & inFile );
string makeStringUpper( string s);
int searchForVesselByName( vector<AISType> & dataBase,  string vesselName,
                    vector<string> & s );
void printRecord( AISType & item );
bool getNextField(string &line, int &index, string &subString);
double stringConvert(string);
int findLastOccurrance(string mmsi, vector<AISType> &d);
int findFirstOccurrance(string mmsi, vector<AISType> &d);
void addUniqueString( vector<string> &s, string value);
void saveField( int fieldNumber, string subString,  AISType &tempItem );
double distanceTraveled( vector<AISType> & dataBase, int first, int last );

int main()
{
    // number of records read into the dataBase
    int count=0;

    // the dataBase
    // vector type is used because it's too big for an array.
    // (on my computer anyway)
    vector<AISType> dataBase;
    vector<string> mmsi;

    // input file
    ifstream inFile;

    // temporary strings
    string temp;
    string ansYN;

    int found=0;
    string stars="";
    int first =0, last =0;

    // open the input file
    if (openInputFile( inFile ) )
           cout << "File opened correctly " << endl;
    else{
        cout << "Error opening file"<< endl << "Existing...." << endl;
        return 0;
    }

    // read the entire file into the dataBase
    readFile( inFile, dataBase, count);

    cout << count << " records read "<<endl;

    cin.ignore( 40, '\n');

    // user interaction loop
    do{

        // prompt the user for the input to search for.  q to quit
        temp.clear();
        mmsi.clear();

        cout << "Enter vessel name: ";

        // read the user input.  getline is used so that spaces may be included
        // in the input
        getline(cin, temp, '\n');

        // check to see if the user wants to exit the program. 
        // If not exiting, output the search string.
        if ( temp != "q" or temp == "Q" ){
            cout << endl<< "Searching for records with names containing \"" 
                 << temp << "\"" << endl;
        }else
            return 0;

        // search for the number of items that contain the name/phrase
        // All names in the vessel dataBase are upper case, so make the search
        // string upper.  MMSI is built by the function and contains the vector
        // of unique vessels that contain the name searched for. 
        found = searchForVesselByName( dataBase,  makeStringUpper(temp), mmsi );

        // Let the user know if any ships were found with the name
        if( found <= 0) {
            cout << "Vessel \"" << temp << "\" not found" << endl;
            continue;
        }else{
            // output the results of the search
            cout << stars << endl;
            cout << found << " vessels found with name containing \"" << temp 
                 << "\", ";
            cout <<  "Unique vessels: " <<  mmsi.size()  << endl;
            cout << stars << endl;

            // ships were found, see if the user wants to display them
            cout << mmsi.size() << " vessels found. Would you like to see their"
                << " first records? [y/n] " ;
            cin >> ansYN;

            if (ansYN =="y" or ansYN == "Y"){

                // print all the first records for the ships found
                for (unsigned int i=0; i<mmsi.size(); i++){

                    // find the vessels using MMSI and print the records
                    int index = findFirstOccurrance( mmsi[i], dataBase );

                    // verify that a valid record was found, print the record
                    if ( index != -1)
                     printRecord( dataBase[index]);
                }

                // Ask user if they want to calculate the distance traveled for
                // the vessel.
                cout << "Would you like to find the distance traveled for a vessel?"
                        " [y/n] " ;
                cin >> ansYN;

                if (  ansYN == "y" or ansYN == "Y"){
                    cout <<  "MMSI for vessel: ";
                    cin >> temp;
                    cout << stars << endl;

                    // locate the index value of the first and last record
                    first = findFirstOccurrance( temp, dataBase);
                    last = findLastOccurrance( temp, dataBase);

                    //output the sitances and miles traveled
                    cout << "Vessel: \"" << dataBase[first].vesselName;
                    cout << "\" MMSI: " << dataBase[first].MMSI;
                    cout << " Trip Starting time: " << dataBase[first].baseDateTime;
                    cout << endl;
                    cout << "Distance Traveled from (" << dataBase[first].lattitude;
                    cout << ", " << dataBase[first].longitude << ") to (";
                    cout << dataBase[last].lattitude << ", ";
                    cout << dataBase[last].longitude << ") ";
                    cout << distanceTraveled(dataBase, first, last);
                    cout << " Miles" << endl;
                    cout << endl;
                }    
            }
        }
        cin.ignore( 40, '\n');

    }  while ( true );

}

/*  distanceTraveled - 
        calculates the distance travelled by the vessel based on its first and last 
        recorded longitude and lattitude, using index values to find the values.
    parameters-
        vector<AISType>& dataBase - collection of vessels in time sequential order according to .csv inputted
        int first - index of the first occurrance of the requested vessel's MMSI
        int last - index of the last occurrance of the requested vessel's MMSI
    return value - 
        distance travelled based on haversine equation as a double
    Algorithm -
        Calculate the distance in three steps: Solve for a, then using the value assigned to a, solve for c. 
        Finally, using the value assigned to c, solve for d, and return d.
*/
double distanceTraveled( vector<AISType> & dataBase, int first, int last ){

    if (static_cast<unsigned int>(first) >= dataBase.size() || static_cast<unsigned int>(last) >= dataBase.size() || first < 0 || last < 0)
      return 0.0; //if any of these terms are met, at least one index is bad

    const double R = 3958.8; //mean radius of the Earth in miles
    const double PI = 3.14159; 
    double latStart = dataBase[first].lattitude;
    double latEnd = dataBase[last].lattitude;
    double longStart = dataBase[first].longitude;
    double longEnd = dataBase[last].longitude;
    double a, c, d;

    a = pow( sin( ((latEnd - latStart)*PI/180)/2), 2) + cos(latStart*PI/180)*cos(latEnd*PI/180)*pow( sin( ((longEnd - longStart)*PI/180)/2), 2); //calculation for a
    c = 2 * atan2(sqrt(a), sqrt(1-a)); //calculation for c
    d = R*c; //calculation for d: distance traveled
    return d;
}

/* findLastOccurrance - finds the last occurrance of an entry in the dataBase 
   using the MMSI as the matching criterion. The vector is search from the 
   last entry forward.  The vector  data is in time sequential order.
   string mmsi - the MMSI of the desired vessel.
   vector<AISType & d - the dataBase of vessels.  This is passed by reference
            for efficiency.

    return value - the index of the last record in the dataBase for the vessel
            with the matching MMSI.  If the MMSI is not found, return -1.

    Algorithm
        Use a for loop the to seach from the last element of the vecotor toward
        the first. Since the data is time ordered, oldest first, search the 
        vector from the bottom towards the top. This is a linear search and
        returns as soon as a match is found.  

*/
int findLastOccurrance(string mmsi, vector<AISType> &d)
{
  for(int i = d.size()-1; i > 0; i--){ //linear search backwards through the vector of vessels
        if(mmsi == d[i].MMSI) //if the matching mmsi is found, indicate which vessel by returning its index
          return i;
    }
  return -1; //if the MMSI could not be found
}

/*  findFirstOccurrance - 
        finds the first occurrance of an entry in the dataBase
        using passed MMSI as reference. Linear search is done from the first
        entry to the last entry. The vector data is in time sequential order.
    parameters - 
        string mmsi - the MMSI of the vessel requested
        vector<AISType &d> - data base of the vessels as according to the .csv
        file passed
    return value - 
        the index of the first occurrance of the vessel with the
        passed MMSI. If the MMSI cannot be found, return -1                                             //deleted content: returns 0 //delet
    Algorithm - 
        Use a for-loop to iterate through the vector database. Since the data is
        time ordered, search front to back. If the matching MMSI is found, return
        the current index. If it isn't found, the for-loop will be naturally left
        and return -1                                                                                   //deleted content: return 0 //delet
*/
int findFirstOccurrance(string mmsi, vector<AISType> &d)
{
    for(int i = 0; static_cast<unsigned int>(i) < d.size(); i++){ //linear search forwards through the vector of vessels
        if(mmsi == d[i].MMSI) //if the matching mmsi is found, indicate which vessel by returning its index
          return i;
    }
  return -1; //if the MMSI could not be found
}

/*  searchForVesselByName - 
        searches for vessels containing the user-inputted string in the vector.
    parameters - 
        vector<AISType> &dataBase - database of the vessels as according to the .csv
        file passed
        string vesselName - user-inputted string that will be matched through the function
        vector<string> &s - list of uniquely named ships that contain the user-inputted string
    return -
        total number of vessels containing the string found as an int
    Algorithm - 
        Since the vessel names are fully in upper case, change the passed string to full caps
        using makeStringUpper() function. Use a for-loop to iterate through the database to look
        for vessels that contain the string. If the string is found in the vessel name, increment
        the vessel count and check if the ship was already found by passing in that vessel's 
        MMSI and checking it against vector<string> s into addUniqueString() function.
*/
int searchForVesselByName( vector<AISType> & dataBase, string vesselName, vector<string> &s)
{
    int size = dataBase.size();
    int numberOfVesselsFound = 0;
    string temp = makeStringUpper(vesselName); //makes search term in all caps since the vector of vessel names is in all caps

    for(int i = 0; i < size; i++){
        if(dataBase[i].vesselName.find(temp) != string::npos){ //checks if the search term exists in the vessel's vesselName field
            addUniqueString(s, dataBase[i].MMSI); //checks to see if the vesselName has already been recorded
            numberOfVesselsFound++;
        }
    }
    return numberOfVesselsFound; 

}

/*  addUniqueString - 
        checks if the passed vessel name already exists in the vector of uniquely named
        vessels
    parameters - 
        vector<string> &s - list of uniquely named vessels
        string value - the vessel name being compared against existing names
    return value - 
        void. passes the vector by reference whether something was added or not
    Algorithm -
        Test if the vector is empty. If it is, add the passed name because if there's
        nothing in the vector, the passed name must be unique. Otherwise, use a for-loop
        to iterate through the list of unique names to check if the passed value already
        exists. If found, end function. If value was not found, let for-loop naturally end
        and add the vessel name to the end of the vector.

*/
void addUniqueString( vector<string> &s, string value){
    int size = s.size();

    if (s.empty()){ //if the vector is empty, then the value must be unique, so add it into the vector
        s.push_back(value);
        return;
    }

    for(int i = 0; i < size; i++){ //iterate through the vector of recorded unique vessel names
        if(s[i] == value) //if the matching value is found, do not add it to the vector, then end function
            return;

    }
    s.push_back(value); //if the matching value was not found, it must be unique, so add it into the vector
}

/*  stringConvert - 
        converts a passed string to a double
    parameters -
        string s - string composed solely of numeric characters
    return value - 
        returns the string converted to a double as a double
    Algorithm - 
        Use stringstream to hold the string in the buffer. Use stringstream to convert
        the held string into a double. Return the value
*/
double stringConvert(string s){
    double value;
    stringstream ss; 

    ss << s; //store the string into the buffer
    ss >> value; //convert the stored string into a double
    return value;  // the string passed as a double
}

/*  printRecord -
        prints the 17 member fields of the vessel passed into the function
    paramaters -
        AISType &item - a vessel that is printed if user wants to read the data
        of the unique vessels
    return value - 
        void. The vessel is passed by referencem but is only accessed and not mutated
    Algorithm - 
        Print a line of asterisks to distinguish the data from previous entries. Print the
        17 member fields of the vessel with a label and line breaks. Print two additional
        line breaks to distinguish the data from future entries
*/
void printRecord( AISType &item )
{
    cout << "************************" << '\n' //dividing asterisk line
         << "MMSI:         " << item.MMSI << '\n'
         << "Base Date Time: " << item.baseDateTime << '\n'
         << "Lattitude: " << item.lattitude << " Longitude: " << item.longitude << '\n'
         << "SOG: " << item.sog << '\n'
         << "COG: " << item.cog << '\n'
         << "Heading: " << item.heading << '\n'
         << "Vessel Name: " << item.vesselName << '\n'
         << "imo: " << item.imo << '\n'
         << "Call Sign: " << item.callSign << '\n'
         << "Vessal Type: " << item.vesselType << '\n' //typo known: to match example output
         << "Status: " << item.status << '\n'
         << "Length: " << item.length << '\n'
         << "Width: " << item.width << '\n'
         << "Draft: " << item.draft << '\n'
         << "Cargo: " << item.cargo << '\n'
         << "Transceiver Class: " << item.transceiverClass << "\n\n\n"; //extra linebreaks in accordance to example output

}

/*  openInputfile - 
        Prompts user for an input file name and tries to open it
    parameters - 
        ifstream &inFile - input file stream that will hold the data from an expected .csv file
    return value - 
        opened file stream is passed by reference. If the user inputted file name could not be 
        opened or user inputs "q" or "Q", return false, which will result in program exiting. 
        If file could be opened, return true.
    Algorithm - 
        Prompt user for an input file name. If the user inputs a "q" or "Q" return false. Convert the
        user input into a c-string and attempt to open the file with it. If it could not be opened, return
        false. If it could be opened, return true.
*/
bool openInputFile( ifstream & inFile )
{
    string fileName;
    cout << "Enter input File Name/(q-quit): ";
    cin >> fileName;

    if (fileName == "q" || fileName == "Q")  //if user wants to quit
        return false;

    inFile.open(fileName.c_str()); //open the c-string of the file name
    if (!inFile.is_open()) //if the file could not be opened
         return false;
    return true; //if user's file could be opened
}

/*  readFile - 
        reads from the .csv file and enters the values into an AISType vector
    parameters -
        ifstream &inFile - input .csv file that contains vessel data in time sequential order
        vector<AISType> &item - database of vessels in time sequential order as per .csv file
        int &count - total number of vessels read through by the program
    return value - 
        void. passes count of vessels read by reference
    Algorithm - 
        Since the number of vessel entries is unknown, use a while-loop to iterate through the file. Read
        the file line by line, until the end of file is reached, in which case, break. Reset the index and
        number of fields to zero to prepare for this line. Use a for-loop within the while-loop to parse
        through the line being read. Call getNextField() to modify local subString variable to hold the value
        found between the commas. Call saveFiled() to send the substring into its AISType record object. Increment
        the field number and empty the subString in preparation for the next entry value. Iterate through the
        line until the end of line is reached, and then exit the for-loop. The vessel should be fully read into the
        file, so add it to the end of the vector database. Increment the count. Keeping iterating through until
        end of file is reached.
*/
void readFile( ifstream & inFile, vector<AISType> &item, int& count)
{
    //variables
    AISType tempItem;
    string line, subString;
    int index, fieldNumber; //controls parsing and assigning
    const int fieldTotal = 17; //total number of fields

    cout << "----------------------" << '\n'; //this line is added to match example output

    while(true){ //exit condition is in body: inFile.eof()
        getline(inFile, line); //gets the entry of the vessel
        if(inFile.eof()) //if the end of file is reached, the file is done being read, so break
            break;
        index = 0; 
        fieldNumber = 0; 
        for(int i = 0; i < fieldTotal; i++){ 
            getNextField(line, index, subString); //parse for each value
            saveField(fieldNumber, subString, tempItem); //send parsed value into the vector
            fieldNumber++;
            subString = "";

            // Output the number processed. If the number update stops, then
            // their may be problem with the program.
            char eraseLine[]={'\r',27,'[','1','K'};
            if( (count % 100000 ) == 0){
                cout << eraseLine << count;
                cout.flush();
            }

        }
        item.push_back(tempItem); //send the filled vessel object into the database
        count++;
    }
   cout << "--- End of file reached ---Items read: " << count << '\n';  //this code had been added to match the example output
}

/*  saveField - 
        Enters the passed substring into the vessel record, converting it if necessary.
    parameters - 
        int fieldNumber - the count of field number used to determine which member field will be filled by the 
        substring
        string subString - the value to be entered into the vessel record as a string
        AISType &tempItem - vessel record object that the function will read into
    return - 
        void. returns the modified vessel record by reference
    Algorithm - 
        Use a switch case to determine which field member the passed substring will assign to. Call stringConvert()
        function to cast the string into a double if needed. If an unknown field number has been passed, output an
        error message
*/
void saveField( int fieldNumber, string subString,  AISType &tempItem ){        
    switch (fieldNumber){
        case 0:
            tempItem.MMSI = subString; break;
        case 1:
            tempItem.baseDateTime = subString; break;
        case 2:
            tempItem.lattitude = stringConvert(subString); break;
        case 3:
            tempItem.longitude = stringConvert(subString); break;
        case 4:
            tempItem.sog = stringConvert(subString); break;
        case 5:
            tempItem.cog = stringConvert(subString); break;
        case 6:
            tempItem.heading = stringConvert(subString); break;
        case 7:
            tempItem.vesselName = subString; break;
        case 8:
            tempItem.imo = subString; break;
        case 9:
            tempItem.callSign = subString; break;
        case 10:
            tempItem.vesselType = subString; break;
        case 11:
            tempItem.status = subString; break;
        case 12:
            tempItem.length = stringConvert(subString); break;
        case 13:
            tempItem.width = stringConvert(subString); break;
        case 14:
            tempItem.draft = stringConvert(subString); break;
        case 15:
            tempItem.cargo = subString; break;
        case 16:
            tempItem.transceiverClass = subString; break;
        default :
            cout << "Error unknown field number\n";
    }

}

/*  getNextField -
        Parses through the line of comma separated values for the next value and storing it as a substring
    parameters - 
        string &line - line of comma separated values containing data to be put in field members of vessel
        int &index - tells the function where to look in the line for the value
        string &subString - holds each character read through until a comma or the line length is found
    return values - 
        return the modified index by reference so the next time the function is called, it will know where
        to start looking in the line. return the subString by reference to be saved into the vessel's record.
        return false if a comma is found and the line is not done being parsed. return true if the line is 
        done being parsed.
    Algorithm - 
        Use a while-loop to parse through the line. If the character of the current index is not a comma (the 
        delimiter) and is not the last index of the line, then the character must be part of the current value
        being read. Concatenate the substring with the character. Otherwise, if the current character is a comma,
        incremement the index to prepare for the next function call and return false to indicate the line still
        needs to be parsed. If the end of the line is reached, exit the while-loop and return true to indicate the 
        line is finished being parsed.
*/
bool getNextField(string &line, int &index, string &subString)
{
    char delimiter = ','; //because it is a .csv

    while(static_cast<unsigned int>(index) != line.length()){
        if (line[index] != delimiter){ //if the character isn't a comma, it must be part of the value being parsed
            subString += line[index];
            index++;

        } else {
            index++;
            return false; //comma found and there is still more to parse out of the line
        }
    }
    return true; //end of line reached
}

/*  makeStringUpper - 
        takes the passed string and converts it so that every alphabetical letter is changed to its uppercase version
    parameters - 
        string s - the string to be changed into all capital letters, barring characters that do not have a capital version
    return value - 
        return the string in all capital letters
    Algorithm - 
        Use a for-loop to parse through the string from the start of the string to the end of it. For every character, use 
        library function toupper() to attempt to capitalize it to its capital version. Concatenate the result to the temporary
        string. Once the end of the string has been reached, return the resulting string.
*/
string makeStringUpper(string s)
{
    string temp;

    for(unsigned int i = 0; i < s.length(); i++){ //go through the entire string
        temp += toupper(s[i]); //concatenate attempted capitalized character into the temporary string 
    }

    return temp; //the string with all lowercase letters changed to capital letters 
}