diff --git a/README.md b/README.md index 546bbd2..aa9589b 100644 --- a/README.md +++ b/README.md @@ -174,7 +174,7 @@ centerline_width.riverCenterline(csv_data=None, **Solutions for sparse data:** -`interpolate_data` is an option that can be used to find a centerline when the existing data generates a Voronoi graph that is jagged or contains gaps due to the combination of sparse data and a narrow river (See: Debugging, Error Handling, and Edge Cases - Fix Gaps and Jagged Centerlines). By default, `interpolate_data=True` will add 5 additional points between each existing point but can be changed with the `interpolate_n` option +`interpolate_data` is an option that can be used to find a centerline when the existing data generates a Voronoi graph that is jagged or contains gaps due to the combination of sparse data and a narrow river (See: Debugging, Error Handling, and Edge Cases - Fix Gaps and Jagged Centerlines). By default, `interpolate_data=True` will add 5 additional points between each existing point but can be increased or decreased by modifying the `interpolate_n` option `interpolate_n_centerpoints` is an option that can be used to increase the resolution (number of points) of the centerline found by the Voronoi vertices. By default, will evenly space out to the size of the dataframe. Can artificially increase the amount of width lines generated by increasing the number of center points. When `interpolate_n_centerpoints` increases, the number of width lines generated will increase (and visa versa) @@ -226,7 +226,7 @@ There are four types of centerline coordinates formed from the riverbank data - **Voronoi centerline**: centerline generated from where Voronoi vertices intersect within the river ![example+png](https://raw.githubusercontent.com/cyschneck/centerline-width/main/data/doc_examples/voronoi_centerline.png) -- **Equal Distance Centerline**: centerline based on Voronoi centerline but each point is equally spaced out from the previous (in meters) +- **Equal Distance Centerline**: centerline based on Voronoi centerline but each point is equally spaced out from the previous (in meters) and takes into account the radius of the Earth to convert degrees to meters ![example+png](https://raw.githubusercontent.com/cyschneck/centerline-width/main/data/doc_examples/equal_distance_centerline.png) - **Evenly Spaced Centerline**: centerline based on Voronoi centerline but evenly spaced with a fixed number of points ![example+png](https://raw.githubusercontent.com/cyschneck/centerline-width/main/data/doc_examples/evenly_spaced_centerline.png) diff --git a/centerline_width/__init__.py b/centerline_width/__init__.py index 60c5db5..24b972d 100644 --- a/centerline_width/__init__.py +++ b/centerline_width/__init__.py @@ -34,5 +34,6 @@ from .error_handling import errorHandlingPlotCenterlineWidth from .error_handling import errorHandlingRiverWidthFromCenterline from .error_handling import errorHandlingSaveCenterlineCSV +from .error_handling import errorHandlingSaveCenterlineMAT from .error_handling import errorHandlingExtractPointsToTextFile from .error_handling import errorHandlingRiverCenterlineClass diff --git a/centerline_width/centerline.py b/centerline_width/centerline.py index b20aa2b..ee12907 100644 --- a/centerline_width/centerline.py +++ b/centerline_width/centerline.py @@ -425,7 +425,11 @@ def centerlineLength(centerline_coordinates=None): def saveCenterlineCSV(river_object=None, save_to_csv=None, latitude_header=None, longitude_header=None, centerline_type="Voronoi"): # Save Centerline Coordinates generated by Voronoi Diagram to .CSV - centerline_width.errorHandlingSaveCenterlineCSV(river_object=river_object, save_to_csv=save_to_csv, centerline_type=centerline_type) + centerline_width.errorHandlingSaveCenterlineCSV(river_object=river_object, + save_to_csv=save_to_csv, + latitude_header=latitude_header, + longitude_header=longitude_header, + centerline_type=centerline_type) centerline_type = centerline_type.title() if centerline_type == "Voronoi": centerline_coordinates_by_type = river_object.centerlineVoronoi @@ -449,6 +453,11 @@ def saveCenterlineCSV(river_object=None, save_to_csv=None, latitude_header=None, def saveCenterlineMAT(river_object=None, save_to_mat=None, latitude_header=None, longitude_header=None, centerline_type="Voronoi"): # Save Centerline Coordinates generated by Voronoi Diagram to .MAT + centerline_width.errorHandlingSaveCenterlineMAT(river_object=river_object, + save_to_mat=save_to_mat, + latitude_header=latitude_header, + longitude_header=longitude_header, + centerline_type=centerline_type) centerline_type = centerline_type.title() if centerline_type == "Voronoi": centerline_coordinates_by_type = river_object.centerlineVoronoi diff --git a/centerline_width/error_handling.py b/centerline_width/error_handling.py index 1b64db2..1f40bdf 100644 --- a/centerline_width/error_handling.py +++ b/centerline_width/error_handling.py @@ -166,7 +166,7 @@ def errorHandlingRiverWidthFromCenterline(river_object=None, logger.critical("\nCRITICAL ERROR, [save_to_csv]: Extension must be a .csv file, current extension = '{0}'".format(save_to_csv.split(".")[1])) exit() -def errorHandlingSaveCenterlineCSV(river_object=None, save_to_csv=None, centerline_type=None): +def errorHandlingSaveCenterlineCSV(river_object=None, latitude_header=None, longitude_header=None, save_to_csv=None, centerline_type=None): # Error Handling for saveCenterlineCSV() if river_object is None: logger.critical("\nCRITICAL ERROR, [river_object]: Requires a river object (see: centerline_width.riverCenterline)") @@ -176,6 +176,14 @@ def errorHandlingSaveCenterlineCSV(river_object=None, save_to_csv=None, centerli logger.critical("\nCRITICAL ERROR, [river_object]: Must be a river object (see: centerline_width.riverCenterline), current type = '{0}'".format(type(river_object))) exit() + if latitude_header is not None and type(latitude_header) != str: + logger.critical("\nCRITICAL ERROR, [latitude_header]: Must be a str, current type = '{0}'".format(type(latitude_header))) + exit() + + if longitude_header is not None and type(longitude_header) != str: + logger.critical("\nCRITICAL ERROR, [longitude_header]: Must be a str, current type = '{0}'".format(type(longitude_header))) + exit() + if save_to_csv is None: logger.critical("\nCRITICAL ERROR, [save_to_csv]: Requires csv filename") exit() @@ -197,6 +205,53 @@ def errorHandlingSaveCenterlineCSV(river_object=None, save_to_csv=None, centerli logger.critical("\nCRITICAL ERROR, [centerline_type]: Must be an available option in {0}, current option = '{1}'".format(centerline_type_options, centerline_type)) exit() +def errorHandlingSaveCenterlineMAT(river_object=None, latitude_header=None, longitude_header=None, save_to_mat=None, centerline_type=None): + # Error Handling for saveCenterlineMAT() + if river_object is None: + logger.critical("\nCRITICAL ERROR, [river_object]: Requires a river object (see: centerline_width.riverCenterline)") + exit() + else: + if not isinstance(river_object, centerline_width.riverCenterline): + logger.critical("\nCRITICAL ERROR, [river_object]: Must be a river object (see: centerline_width.riverCenterline), current type = '{0}'".format(type(river_object))) + exit() + + if latitude_header is not None: + if type(latitude_header) != str: + logger.critical("\nCRITICAL ERROR, [latitude_header]: Must be a str, current type = '{0}'".format(type(latitude_header))) + exit() + if any(not character.isalnum() for character in latitude_header): + logger.critical("\nCRITICAL ERROR, [latitude_header]: Column names cannot contain any whitespace or non-alphanumeric characters, currently = '{0}'".format(latitude_header)) + exit() + + if longitude_header is not None: + if type(longitude_header) != str: + logger.critical("\nCRITICAL ERROR, [longitude_header]: Must be a str, current type = '{0}'".format(type(longitude_header))) + exit() + if any(not character.isalnum() for character in longitude_header): + logger.critical("\nCRITICAL ERROR, [longitude_header]: Column names cannot contain any whitespace or non-alphanumeric characters, currently = '{0}'".format(longitude_header)) + exit() + + if save_to_mat is None: + logger.critical("\nCRITICAL ERROR, [save_to_mat]: Requires mat filename") + exit() + else: + if type(save_to_mat) != str: + logger.critical("\nCRITICAL ERROR, [save_to_mat]: Must be a str, current type = '{0}'".format(type(save_to_mat))) + exit() + else: + if not save_to_mat.lower().endswith(".mat"): + logger.critical("\nCRITICAL ERROR, [save_to_mat]: Extension must be a .mat file, current extension = '{0}'".format(save_to_mat.split(".")[1])) + exit() + + centerline_type_options = ["Voronoi", "Evenly Spaced", "Smoothed", "Equal Distance"] + if type(centerline_type) != str: + logger.critical("\nCRITICAL ERROR, [centerline_type]: Must be a str, current type = '{0}'".format(type(centerline_type))) + exit() + else: + if centerline_type.title() not in centerline_type_options: + logger.critical("\nCRITICAL ERROR, [centerline_type]: Must be an available option in {0}, current option = '{1}'".format(centerline_type_options, centerline_type)) + exit() + # Error Handling: getCoordinatesKML.py def errorHandlingExtractPointsToTextFile(left_kml=None, right_kml=None, text_output_name=None): # Error Handling for extractPointsToTextFile() @@ -239,7 +294,8 @@ def errorHandlingRiverCenterlineClass(csv_data=None, optional_cutoff=None, interpolate_data=None, interpolate_n=None, - interpolate_n_centerpoints=None): + interpolate_n_centerpoints=None, + equal_distance=None): # Error Handling for riverCenterlineClass() if csv_data is None: logger.critical("\nCRITICAL ERROR, [csv_data]: Requires csv_data location") @@ -273,3 +329,10 @@ def errorHandlingRiverCenterlineClass(csv_data=None, if interpolate_n_centerpoints < 2: logger.critical("\nCRITICAL ERROR, [interpolate_n_centerpoints]: Must be a greater than 1, currently = '{0}'".format(interpolate_n_centerpoints)) exit() + + if type(equal_distance) != int and type(equal_distance) != float: + logger.critical("\nCRITICAL ERROR, [equal_distance]: Must be a int or float, current type = '{0}'".format(type(equal_distance))) + exit() + if equal_distance <= 0: + logger.critical("WARNING, [equal_distance]: Must be a postive value, greater than 0, currently = '{0}'".format(equal_distance)) + exit() diff --git a/centerline_width/pytests/test_centerline.py b/centerline_width/pytests/test_centerline.py index b7d6b9c..6ef8eb9 100644 --- a/centerline_width/pytests/test_centerline.py +++ b/centerline_width/pytests/test_centerline.py @@ -140,6 +140,26 @@ def test_saveCenterlineCSV_csvRequired(caplog): assert log_record.levelno == logging.CRITICAL assert log_record.message == "\nCRITICAL ERROR, [save_to_csv]: Extension must be a .csv file, current extension = 'txt'" +@pytest.mark.parametrize("latitude_header_invalid, latitude_header_error_output", invalid_non_str_options) +def test_saveCenterlineCSV_latitudeHeaderTypeInvalidTypes(caplog, latitude_header_invalid, latitude_header_error_output): + with pytest.raises(SystemExit): + centerline_width.saveCenterlineCSV(river_object=river_class_example, + save_to_csv="testing.csv", + latitude_header=latitude_header_invalid) + log_record = caplog.records[0] + assert log_record.levelno == logging.CRITICAL + assert log_record.message == "\nCRITICAL ERROR, [latitude_header]: Must be a str, current type = '{0}'".format(latitude_header_error_output) + +@pytest.mark.parametrize("longitude_header_invalid, longitude_header_error_output", invalid_non_str_options) +def test_saveCenterlineCSV_longitudeHeaderTypeInvalidTypes(caplog, longitude_header_invalid, longitude_header_error_output): + with pytest.raises(SystemExit): + centerline_width.saveCenterlineCSV(river_object=river_class_example, + save_to_csv="testing.csv", + longitude_header=longitude_header_invalid) + log_record = caplog.records[0] + assert log_record.levelno == logging.CRITICAL + assert log_record.message == "\nCRITICAL ERROR, [longitude_header]: Must be a str, current type = '{0}'".format(longitude_header_error_output) + @pytest.mark.parametrize("centerline_type_invalid, centerline_type_error_output", invalid_non_str_options) def test_saveCenterlineCSV_centerlineTypeInvalidTypes(caplog, centerline_type_invalid, centerline_type_error_output): with pytest.raises(SystemExit): @@ -157,4 +177,83 @@ def test_saveCenterlineCSV_centerlineTypeInvalidOptions(caplog): centerline_type="not valid") log_record = caplog.records[0] assert log_record.levelno == logging.CRITICAL - assert log_record.message == "\nCRITICAL ERROR, [centerline_type]: Must be an available option in ['Voronoi', 'Evenly Spaced', 'Smoothed'], current option = 'not valid'" + assert log_record.message == "\nCRITICAL ERROR, [centerline_type]: Must be an available option in ['Voronoi', 'Evenly Spaced', 'Smoothed', 'Equal Distance'], current option = 'not valid'" + +## saveCenterlineMAT() ##################################################### +def test_saveCenterlineMAT_riverObjectRequired(caplog): + with pytest.raises(SystemExit): + centerline_width.saveCenterlineMAT(river_object=None) + log_record = caplog.records[0] + assert log_record.levelno == logging.CRITICAL + assert log_record.message == "\nCRITICAL ERROR, [river_object]: Requires a river object (see: centerline_width.riverCenterline)" + +def test_saveCenterlineMAT_matInvalidExtension(caplog): + with pytest.raises(SystemExit): + centerline_width.saveCenterlineMAT(river_object=river_class_example, save_to_mat=None) + log_record = caplog.records[0] + assert log_record.levelno == logging.CRITICAL + assert log_record.message == "\nCRITICAL ERROR, [save_to_mat]: Requires mat filename" + +def test_saveCenterlineMAT_matRequired(caplog): + with pytest.raises(SystemExit): + centerline_width.saveCenterlineMAT(river_object=river_class_example, save_to_mat="filename.txt") + log_record = caplog.records[0] + assert log_record.levelno == logging.CRITICAL + assert log_record.message == "\nCRITICAL ERROR, [save_to_mat]: Extension must be a .mat file, current extension = 'txt'" + +@pytest.mark.parametrize("latitude_header_invalid, latitude_header_error_output", invalid_non_str_options) +def test_saveCenterlineMAT_latitudeHeaderTypeInvalidTypes(caplog, latitude_header_invalid, latitude_header_error_output): + with pytest.raises(SystemExit): + centerline_width.saveCenterlineMAT(river_object=river_class_example, + save_to_mat="testing.mat", + latitude_header=latitude_header_invalid) + log_record = caplog.records[0] + assert log_record.levelno == logging.CRITICAL + assert log_record.message == "\nCRITICAL ERROR, [latitude_header]: Must be a str, current type = '{0}'".format(latitude_header_error_output) + +def test_saveCenterlineMAT_latitudeHeaderTypeInvalidAlphanumeric(caplog): + with pytest.raises(SystemExit): + centerline_width.saveCenterlineMAT(river_object=river_class_example, + save_to_mat="testing.mat", + latitude_header="invalid whitespace") + log_record = caplog.records[0] + assert log_record.levelno == logging.CRITICAL + assert log_record.message == "\nCRITICAL ERROR, [latitude_header]: Column names cannot contain any whitespace or non-alphanumeric characters, currently = 'invalid whitespace'" + +@pytest.mark.parametrize("longitude_header_invalid, longitude_header_error_output", invalid_non_str_options) +def test_saveCenterlineMAT_longitudeHeaderTypeInvalidTypes(caplog, longitude_header_invalid, longitude_header_error_output): + with pytest.raises(SystemExit): + centerline_width.saveCenterlineMAT(river_object=river_class_example, + save_to_mat="testing.mat", + longitude_header=longitude_header_invalid) + log_record = caplog.records[0] + assert log_record.levelno == logging.CRITICAL + assert log_record.message == "\nCRITICAL ERROR, [longitude_header]: Must be a str, current type = '{0}'".format(longitude_header_error_output) + +def test_saveCenterlineMAT_longitudeHeaderTypeInvalidAlphanumeric(caplog): + with pytest.raises(SystemExit): + centerline_width.saveCenterlineMAT(river_object=river_class_example, + save_to_mat="testing.mat", + longitude_header="invalid whitespace") + log_record = caplog.records[0] + assert log_record.levelno == logging.CRITICAL + assert log_record.message == "\nCRITICAL ERROR, [longitude_header]: Column names cannot contain any whitespace or non-alphanumeric characters, currently = 'invalid whitespace'" + +@pytest.mark.parametrize("centerline_type_invalid, centerline_type_error_output", invalid_non_str_options) +def test_saveCenterlineMAT_centerlineTypeInvalidTypes(caplog, centerline_type_invalid, centerline_type_error_output): + with pytest.raises(SystemExit): + centerline_width.saveCenterlineMAT(river_object=river_class_example, + save_to_mat="testing.mat", + centerline_type=centerline_type_invalid) + log_record = caplog.records[0] + assert log_record.levelno == logging.CRITICAL + assert log_record.message == "\nCRITICAL ERROR, [centerline_type]: Must be a str, current type = '{0}'".format(centerline_type_error_output) + +def test_saveCenterlineMAT_centerlineTypeInvalidOptions(caplog): + with pytest.raises(SystemExit): + centerline_width.saveCenterlineMAT(river_object=river_class_example, + save_to_mat="testing.mat", + centerline_type="not valid") + log_record = caplog.records[0] + assert log_record.levelno == logging.CRITICAL + assert log_record.message == "\nCRITICAL ERROR, [centerline_type]: Must be an available option in ['Voronoi', 'Evenly Spaced', 'Smoothed', 'Equal Distance'], current option = 'not valid'" diff --git a/centerline_width/pytests/test_riverCenterlineClass.py b/centerline_width/pytests/test_riverCenterlineClass.py index 6786138..a377a1e 100644 --- a/centerline_width/pytests/test_riverCenterlineClass.py +++ b/centerline_width/pytests/test_riverCenterlineClass.py @@ -19,6 +19,10 @@ ([], ""), (False, "")] +invalid_non_num_options = [("testing_string", ""), + ([], ""), + (False, "")] + invalid_non_str_options = [(1961, ""), (3.1415, ""), ([], ""), @@ -63,3 +67,11 @@ def test_riverCenterline_interpolateNInvalidTypes(caplog, interpolate_n_invalid, log_record = caplog.records[0] assert log_record.levelno == logging.CRITICAL assert log_record.message == "\nCRITICAL ERROR, [interpolate_n]: Must be a int, current type = '{0}'".format(interpolate_n_error_output) + +@pytest.mark.parametrize("equal_distance_invalid, equal_distance_error_output", invalid_non_num_options) +def test_riverCenterline_equalDistanceInvalidTypes(caplog, equal_distance_invalid, equal_distance_error_output): + with pytest.raises(SystemExit): + centerline_width.riverCenterline(csv_data="csv_example.csv", equal_distance=equal_distance_invalid) + log_record = caplog.records[0] + assert log_record.levelno == logging.CRITICAL + assert log_record.message == "\nCRITICAL ERROR, [equal_distance]: Must be a int or float, current type = '{0}'".format(equal_distance_error_output) diff --git a/centerline_width/riverCenterlineClass.py b/centerline_width/riverCenterlineClass.py index f64974b..6449772 100644 --- a/centerline_width/riverCenterlineClass.py +++ b/centerline_width/riverCenterlineClass.py @@ -18,7 +18,8 @@ def __init__(self, optional_cutoff=optional_cutoff, interpolate_data=interpolate_data, interpolate_n=interpolate_n, - interpolate_n_centerpoints=interpolate_n_centerpoints) + interpolate_n_centerpoints=interpolate_n_centerpoints, + equal_distance=equal_distance) # Description and dataframe self.river_name = csv_data