diff --git a/docs/source/conf.py b/docs/source/conf.py index 8528f03..8727501 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,3 +1,10 @@ +import os +import sys + +sys.path.insert(0, os.path.abspath("../../tests")) +sys.path.insert(0, os.path.abspath("../../src")) +sys.path.insert(0, os.path.abspath("../../")) + # Configuration file for the Sphinx documentation builder. # # For the full list of built-in configuration values, see the documentation: diff --git a/docs/source/explanations/datasets.rst b/docs/source/explanations/datasets.rst new file mode 100644 index 0000000..3d190ff --- /dev/null +++ b/docs/source/explanations/datasets.rst @@ -0,0 +1,17 @@ +Dataset Choices +=============== + +Since creating a synthesized dataset based on multiple datasets is a critical part of the data processing pipeline, we have to make some choices regarding the datasets that we use. This document serves to explain the significant yet non-obvious logic and choices regarding different types of datasets. + + +Crosswalk Dataset +***************** + +The crosswalk dataset should contain polygons that represent the boundaries of the crosswalks. The dataset ideally should accurately reflect the real world, but there is an implicit understanding that the dataset may not be perfect since most cities do not have a comprehensive dataset of crosswalks. The dataset should be in a format that can be easily read by the software, such as GeoJSON. In the testing module, you'll find the tests for fetching the crosswalk dataset and mapping it for the city of Boston. We've chosen Boston for its relative ease of access to various datasets and physical proximity to the team at Olin College of Engineering. + +UMass Amherst has been developing a dataset of all crosswalks in Massachusetts using computer vision model (YOLOv8) and aerial imagery. The dataset is not perfect, but it is a good starting point for our project, and has the potential to be applicable for states that also do not have a thorough catalog of their crosswalk assets. The dataset can be viewed at the `following link `_. + +Traffic Dataset +*************** + +Since our project is focused on pedestrian safety at nighttime on crosswalks, we need a dataset that contains information about the volume of traffic. MassDOT provides a convenient dataset that includes average annual daily traffic (AADT) counts for most roads in Massachusetts. The counts will be used to inform the risk of a pedestrian being hit by a car at a given crosswalk. The dataset can be viewed at the `following link `_. \ No newline at end of file diff --git a/docs/source/explanations/index.rst b/docs/source/explanations/index.rst index debdc0a..a18e19c 100644 --- a/docs/source/explanations/index.rst +++ b/docs/source/explanations/index.rst @@ -3,5 +3,7 @@ Explanations .. toctree:: - :maxdepth: 2 - :caption: Contents: + :maxdepth: 1 + :caption: Contents: + + datasets \ No newline at end of file diff --git a/docs/source/generate_test_docs.py b/docs/source/generate_test_docs.py new file mode 100644 index 0000000..d1ec599 --- /dev/null +++ b/docs/source/generate_test_docs.py @@ -0,0 +1,22 @@ +import os + +# Paths configuration +test_dir = "../../tests" +output_dir = "./tests" + +if not os.path.exists(output_dir): + os.makedirs(output_dir) + +# Iterate over tests files in the tests directory +for filename in os.listdir(test_dir): + if filename.startswith("test_") and filename.endswith(".py"): + module_name = filename.replace(".py", "") + + # Create an .rst file for each test module + with open(os.path.join(output_dir, f"{module_name}.rst"), "w") as rst_file: + rst_file.write(f"{module_name}\n") + rst_file.write("=" * len(module_name) + "\n\n") + rst_file.write(f".. automodule:: {module_name}\n") + rst_file.write(" :members:\n") + rst_file.write(" :undoc-members:\n") + rst_file.write(" :show-inheritance:\n") diff --git a/docs/source/guides/index.rst b/docs/source/guides/index.rst index 9f0bd62..a2d4997 100644 --- a/docs/source/guides/index.rst +++ b/docs/source/guides/index.rst @@ -3,5 +3,5 @@ How-to Guides .. toctree:: - :maxdepth: 2 - :caption: Contents: + :maxdepth: 2 + :caption: Contents: diff --git a/docs/source/index.rst b/docs/source/index.rst index 228e8a3..f352035 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -3,10 +3,11 @@ Night Light documentation .. toctree:: - :maxdepth: 2 - :caption: Contents: + :maxdepth: 1 + :caption: Contents: - tutorials/index - guides/index - references/index - explanations/index \ No newline at end of file + tutorials/index + guides/index + references/index + explanations/index + tests/index \ No newline at end of file diff --git a/docs/source/references/index.rst b/docs/source/references/index.rst index c7201e9..7c6dce0 100644 --- a/docs/source/references/index.rst +++ b/docs/source/references/index.rst @@ -3,5 +3,7 @@ References .. toctree:: - :maxdepth: 2 - :caption: Contents: \ No newline at end of file + :maxdepth: 2 + :caption: Contents: + + utils/index \ No newline at end of file diff --git a/docs/source/references/utils/functions.rst b/docs/source/references/utils/functions.rst new file mode 100644 index 0000000..a67d80f --- /dev/null +++ b/docs/source/references/utils/functions.rst @@ -0,0 +1,13 @@ +Utility functions +================= + +.. automodule:: night_light.utils.mapping + :members: + :undoc-members: + :show-inheritance: + + +.. automodule:: night_light.utils.query_geojson + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/references/utils/index.rst b/docs/source/references/utils/index.rst new file mode 100644 index 0000000..fd4212c --- /dev/null +++ b/docs/source/references/utils/index.rst @@ -0,0 +1,7 @@ +Utils Module +============ + +.. toctree:: + :maxdepth: 1 + + functions \ No newline at end of file diff --git a/docs/source/tests/index.rst b/docs/source/tests/index.rst new file mode 100644 index 0000000..36e1978 --- /dev/null +++ b/docs/source/tests/index.rst @@ -0,0 +1,14 @@ +Test Modules +============ + +This section contains the tests for the project. + +.. toctree:: + :maxdepth: 1 + + test_adt_query + test_crosswalk_query + test_crosswalk_mapping + test_adt_mapping + test_mapping + diff --git a/docs/source/tests/test_adt_mapping.rst b/docs/source/tests/test_adt_mapping.rst new file mode 100644 index 0000000..851c3be --- /dev/null +++ b/docs/source/tests/test_adt_mapping.rst @@ -0,0 +1,7 @@ +Test Average Annual Daily Traffic Mapping +========================================= + +.. automodule:: test_adt_mapping + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/tests/test_adt_query.rst b/docs/source/tests/test_adt_query.rst new file mode 100644 index 0000000..aaa61f6 --- /dev/null +++ b/docs/source/tests/test_adt_query.rst @@ -0,0 +1,7 @@ +Test Average Annual Daily Traffic Query +======================================= + +.. automodule:: test_adt_query + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/tests/test_crosswalk_mapping.rst b/docs/source/tests/test_crosswalk_mapping.rst new file mode 100644 index 0000000..0a7b1b2 --- /dev/null +++ b/docs/source/tests/test_crosswalk_mapping.rst @@ -0,0 +1,7 @@ +Test Crosswalk Mapping +====================== + +.. automodule:: test_crosswalk_mapping + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/tests/test_crosswalk_query.rst b/docs/source/tests/test_crosswalk_query.rst new file mode 100644 index 0000000..ff87817 --- /dev/null +++ b/docs/source/tests/test_crosswalk_query.rst @@ -0,0 +1,7 @@ +Test Crosswalk Query +==================== + +.. automodule:: test_crosswalk_query + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/tests/test_mapping.rst b/docs/source/tests/test_mapping.rst new file mode 100644 index 0000000..1a70fc1 --- /dev/null +++ b/docs/source/tests/test_mapping.rst @@ -0,0 +1,7 @@ +Test Complete Map Generation +============================ + +.. automodule:: test_mapping + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/tutorials/index.rst b/docs/source/tutorials/index.rst index 495ef72..d9dd119 100644 --- a/docs/source/tutorials/index.rst +++ b/docs/source/tutorials/index.rst @@ -3,5 +3,5 @@ Tutorials .. toctree:: - :maxdepth: 2 - :caption: Contents: + :maxdepth: 2 + :caption: Contents: diff --git a/pyproject.toml b/pyproject.toml index 0eadc82..398b5f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,21 @@ [project] name = "night-light" requires-python = ">=3.12" +version = "0.0.1" +readme = "README.md" +classifiers = [ + "Programming Language :: Python :: 3", + "Operating System :: OS Independent", +] +dynamic = ["dependencies"] + +[tool.setuptools.dynamic] +dependencies = { file = ["requirements.txt"] } [tool.black] -line-length = 88 \ No newline at end of file +line-length = 88 + +[build-system] +requires = ["setuptools>=70.0.0"] +build-backend = "setuptools.build_meta" + diff --git a/requirements.txt b/requirements.txt index 781c2f3..f8b3125 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,4 +12,5 @@ ultralytics~=8.3.9 requests~=2.32.3 geopandas~=1.0.1 folium~=0.18.0 +pytest~=8.3.3 sphinx~=8.1.3 \ No newline at end of file diff --git a/src/night_light/crosswalks/__init__.py b/src/night_light/crosswalks/__init__.py index e69de29..8b13789 100644 --- a/src/night_light/crosswalks/__init__.py +++ b/src/night_light/crosswalks/__init__.py @@ -0,0 +1 @@ + diff --git a/src/night_light/crosswalks/create_map.py b/src/night_light/crosswalks/create_map.py deleted file mode 100644 index 2c9c234..0000000 --- a/src/night_light/crosswalks/create_map.py +++ /dev/null @@ -1,21 +0,0 @@ -import folium -import geopandas as gpd - - -def create_folium_map( - gdf: gpd.GeoDataFrame, center: list, zoom_start: int, map_filename: str -): - """Create a Folium map with the given GeoDataFrame and save it to an HTML file.""" - m = folium.Map(location=center, zoom_start=zoom_start) - folium.GeoJson( - gdf, - name="Crosswalk Polygons", - style_function=lambda x: { - "fillColor": "cyan", - "color": "black", - "weight": 1, - "fillOpacity": 0.5, - }, - ).add_to(m) - folium.LayerControl().add_to(m) - m.save(map_filename) diff --git a/src/night_light/crosswalks/query_geojson.py b/src/night_light/crosswalks/query_geojson.py deleted file mode 100644 index 9628ca8..0000000 --- a/src/night_light/crosswalks/query_geojson.py +++ /dev/null @@ -1,20 +0,0 @@ -import requests -import geopandas as gpd - - -def fetch_geojson_data(url: str, params: dict) -> gpd.GeoDataFrame: - """Fetch GeoJSON data from the given URL with specified parameters.""" - response = requests.get(url, params=params) - response.raise_for_status() - try: - gdf = gpd.GeoDataFrame.from_features( - response.json()["features"], crs="EPSG:4326" - ) - return gdf - except KeyError: - raise ValueError("Invalid GeoJSON data") - - -def save_geojson(gdf: gpd.GeoDataFrame, filename: str): - """Save GeoDataFrame to a GeoJSON file.""" - gdf.to_file(filename, driver="GeoJSON") diff --git a/src/night_light/utils/__init__.py b/src/night_light/utils/__init__.py new file mode 100644 index 0000000..4bb93d6 --- /dev/null +++ b/src/night_light/utils/__init__.py @@ -0,0 +1,18 @@ +from .mapping import ( + create_folium_map, + Tooltip, + LAYER_STYLE_DICT, + LAYER_HIGHLIGHT_STYLE_DICT, + open_html_file, +) +from .query_geojson import fetch_geojson_data, save_geojson + +__all__ = [ + create_folium_map, + Tooltip, + LAYER_STYLE_DICT, + LAYER_HIGHLIGHT_STYLE_DICT, + open_html_file, + fetch_geojson_data, + save_geojson, +] diff --git a/src/night_light/utils/mapping.py b/src/night_light/utils/mapping.py new file mode 100644 index 0000000..23e3828 --- /dev/null +++ b/src/night_light/utils/mapping.py @@ -0,0 +1,77 @@ +import os + +import folium +from functools import partial + +Tooltip = partial( + folium.GeoJsonTooltip, + localize=True, + sticky=True, + labels=True, + style=""" + background-color: #F0EFEF; + border: 2px solid black; + border-radius: 3px; + box-shadow: 3px; + """, +) + +LAYER_STYLE_DICT = { + "fillColor": "cyan", + "color": "black", + "weight": 1, + "fillOpacity": 0.5, +} + +LAYER_HIGHLIGHT_STYLE_DICT = { + "fillColor": "red", + "color": "red", + "weight": 3, + "fillOpacity": 0.5, +} + + +def create_folium_map( + layers: list[folium.GeoJson], + center: list, + zoom_start: int, + map_filename: str, +): + """ + Create a Folium map from a GeoDataFrame and save it to an HTML file. + + This function generates a Folium map centered at a specified location and zoom + level, overlays the geometries from the provided GeoDataFrame, and saves the map as + an HTML file. + + Args: + layers (list[folium.GeoJson]): GeoJson layers containing geometries to be added + to the map. + center (list): A list containing the latitude and longitude for the map center + [latitude, longitude]. + zoom_start (int): The initial zoom level for the map. + map_filename (str): The file path where the HTML map will be saved. + + Returns: + None + """ + m = folium.Map(location=center, zoom_start=zoom_start) + for layer in layers: + layer.add_to(m) + folium.LayerControl().add_to(m) + m.save(map_filename) + + +def open_html_file(file_path: str | os.PathLike[str]): + file_path = os.path.abspath(file_path) + + if os.name == "posix": + os.system( + f"open '{file_path}'" + if "Darwin" in os.uname().sysname + else f"xdg-open '{file_path}'" + ) + elif os.name == "nt": + os.system(f"start {file_path}") + else: + print("Unsupported operating system") diff --git a/src/night_light/utils/query_geojson.py b/src/night_light/utils/query_geojson.py new file mode 100644 index 0000000..ba1948e --- /dev/null +++ b/src/night_light/utils/query_geojson.py @@ -0,0 +1,47 @@ +import requests +import geopandas as gpd + + +def fetch_geojson_data(url: str, params: dict) -> gpd.GeoDataFrame: + """ + Fetch GeoJSON data from a specified URL with given parameters. + + This function sends a GET request to the provided URL with the specified query + parameters, retrieves the GeoJSON data, and converts it into a GeoDataFrame with an + EPSG:4326 CRS. + + Args: + url (str): The URL to request the GeoJSON data from. + params (dict): A dictionary of query parameters to include in the request. + + Returns: + gpd.GeoDataFrame: A GeoDataFrame containing the geometries and properties from + the fetched GeoJSON data. + + Raises: + requests.HTTPError: If the HTTP request fails. + ValueError: If the retrieved data does not contain valid GeoJSON features. + """ + response = requests.get(url, params=params) + response.raise_for_status() + try: + gdf = gpd.GeoDataFrame.from_features( + response.json()["features"], crs="EPSG:4326" + ) + return gdf + except KeyError: + raise ValueError("Invalid GeoJSON data") + + +def save_geojson(gdf: gpd.GeoDataFrame, filename: str): + """ + Save a GeoDataFrame to a GeoJSON file. + + Args: + gdf (gpd.GeoDataFrame): The GeoDataFrame to be saved as GeoJSON. + filename (str): The file path where the GeoJSON will be saved. + + Returns: + None + """ + gdf.to_file(filename, driver="GeoJSON") diff --git a/test/__init__.py b/tests/__init__.py similarity index 100% rename from test/__init__.py rename to tests/__init__.py diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..6edb173 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,51 @@ +import pytest +from night_light.utils import query_geojson + +MA_CROSSWALK_URL = "https://gis.massdot.state.ma.us/arcgis/rest/services/Assets/Crosswalk_Poly/FeatureServer/0/query" +MA_TRAFFIC_URL = "https://gis.massdot.state.ma.us/arcgis/rest/services/Roads/VMT/FeatureServer/10/query" +MA_MUNICIPALITIES_URL = "https://arcgisserver.digital.mass.gov/arcgisserver/rest/services/AGOL/Towns_survey_polym/FeatureServer/0/query" + +MUNICIPALITIES_MA_PARAMS = { + "where": "1=1", + "outFields": "TOWN, TOWN_ID, FIPS_STCO", + "outSR": "4326", + "f": "geojson", +} + +MA_MUNICIPALITIES = query_geojson.fetch_geojson_data( + url=MA_MUNICIPALITIES_URL, params=MUNICIPALITIES_MA_PARAMS +) +BOSTON_TOWN_ID = MA_MUNICIPALITIES[MA_MUNICIPALITIES["TOWN"] == "BOSTON"][ + "TOWN_ID" +].values[0] + +CROSSWALK_BOSTON_PARAMS = { + "where": "TOWN='BOSTON'", + "outFields": "*", + "outSR": "4326", + "f": "geojson", +} + +TRAFFIC_BOSTON_PARAMS = { + "where": f"CITY={BOSTON_TOWN_ID}", + "outFields": "*", + "outSR": "4326", + "f": "geojson", +} + + +BOSTON_CENTER_COORD = [42.3601, -71.0589] + + +@pytest.fixture +def boston_crosswalk(): + return query_geojson.fetch_geojson_data( + url=MA_CROSSWALK_URL, params=CROSSWALK_BOSTON_PARAMS + ) + + +@pytest.fixture +def boston_traffic(): + return query_geojson.fetch_geojson_data( + url=MA_TRAFFIC_URL, params=TRAFFIC_BOSTON_PARAMS + ) diff --git a/tests/test_adt_mapping.py b/tests/test_adt_mapping.py new file mode 100644 index 0000000..4b83b05 --- /dev/null +++ b/tests/test_adt_mapping.py @@ -0,0 +1,42 @@ +import os +import time + +import folium + +from night_light.utils import ( + create_folium_map, + LAYER_STYLE_DICT, + LAYER_HIGHLIGHT_STYLE_DICT, + Tooltip, +) +from night_light.utils.mapping import open_html_file +from tests.conftest import BOSTON_CENTER_COORD + + +def test_boston_adt_map(boston_traffic, boston_crosswalk): + """Test creating a map of Boston's average annual daily traffic""" + map_filename = "test_boston_crosswalk.html" + adt_layer = folium.GeoJson( + boston_traffic, + name=f"Average Annual Daily Traffic", + style_function=lambda x: LAYER_STYLE_DICT, + highlight_function=lambda x: LAYER_HIGHLIGHT_STYLE_DICT, + smooth_factor=2.0, + tooltip=Tooltip( + fields=["Route_ID", "AADT", "Facility"], + aliases=["Route ID", "Average Annual Daily Traffic", "Facility Type"], + max_width=800, + ), + ) + create_folium_map( + layers=[adt_layer], + zoom_start=12, + center=BOSTON_CENTER_COORD, + map_filename=map_filename, + ) + + assert os.path.exists(map_filename) + open_html_file(map_filename) + time.sleep(1) + os.remove(map_filename) + assert not os.path.exists(map_filename) diff --git a/tests/test_adt_query.py b/tests/test_adt_query.py new file mode 100644 index 0000000..3f5bc67 --- /dev/null +++ b/tests/test_adt_query.py @@ -0,0 +1,63 @@ +import os + +from night_light.utils import query_geojson +from tests.conftest import BOSTON_TOWN_ID + + +def test_query_boston_adt(boston_traffic): + """Test querying the Boston average annual daily traffic""" + assert boston_traffic.shape[0] > 0 + assert boston_traffic.crs == "EPSG:4326" + assert boston_traffic.geometry.name == "geometry" + assert set(boston_traffic.geometry.geom_type.unique()) == { + "MultiLineString", + "LineString", + } + assert boston_traffic.columns.to_list() == [ + "geometry", + "OBJECTID", + "Route_ID", + "From_Measure", + "To_Measure", + "Route_System", + "Route_Number", + "Route_Direction", + "Facility", + "Mile_Count", + "F_Class", + "Urban_Area", + "Urban_Type", + "F_F_Class", + "Jurisdictn", + "NHS", + "Fd_Aid_Rd", + "Control", + "City", + "County", + "Hwy_Dist", + "MPO", + "SP_Station", + "AADT", + "VMT", + "Length", + "From_Date", + "To_Date", + "Shape__Length", + ] + assert boston_traffic["City"].unique().size == 1 + assert boston_traffic["City"].unique()[0] == BOSTON_TOWN_ID + + +def test_save_boston_geojson(boston_traffic): + """Test saving the Boston average annual daily traffic to a GeoJSON file""" + query_geojson.save_geojson(boston_traffic, "test_boston_crosswalk.geojson") + geojson_filename = "test_boston_crosswalk.geojson" + saved_gdf = query_geojson.gpd.read_file(geojson_filename) + + assert boston_traffic.crs == saved_gdf.crs + assert set(boston_traffic.columns) == set(saved_gdf.columns) + assert boston_traffic.index.equals(saved_gdf.index) + assert boston_traffic.shape == saved_gdf.shape + + os.remove(geojson_filename) + assert not os.path.exists(geojson_filename) diff --git a/tests/test_crosswalk_mapping.py b/tests/test_crosswalk_mapping.py new file mode 100644 index 0000000..139c18f --- /dev/null +++ b/tests/test_crosswalk_mapping.py @@ -0,0 +1,41 @@ +import os +import time + +import folium + +from night_light.utils import ( + create_folium_map, + LAYER_STYLE_DICT, + LAYER_HIGHLIGHT_STYLE_DICT, + Tooltip, +) +from night_light.utils.mapping import open_html_file +from tests.conftest import BOSTON_CENTER_COORD + + +def test_boston_crosswalk_map(boston_crosswalk): + """Test creating a map of the Boston crosswalk""" + map_filename = "test_boston_crosswalk.html" + crosswalk_layer = folium.GeoJson( + boston_crosswalk, + name="Crosswalk", + style_function=lambda x: LAYER_STYLE_DICT, + highlight_function=lambda x: LAYER_HIGHLIGHT_STYLE_DICT, + smooth_factor=2.0, + tooltip=Tooltip( + fields=["OBJECTID", "CrossType", "Conf_Score"], + aliases=["Crosswalk ID", "Crosswalk Type", "Confidence Score"], + max_width=800, + ), + ) + create_folium_map( + layers=[crosswalk_layer], + zoom_start=12, + center=BOSTON_CENTER_COORD, + map_filename=map_filename, + ) + assert os.path.exists(map_filename) + open_html_file(map_filename) + time.sleep(1) + os.remove(map_filename) + assert not os.path.exists(map_filename) diff --git a/tests/test_crosswalk_query.py b/tests/test_crosswalk_query.py new file mode 100644 index 0000000..fecfe6f --- /dev/null +++ b/tests/test_crosswalk_query.py @@ -0,0 +1,45 @@ +import os +from night_light.utils import query_geojson + + +def test_query_boston_crosswalk(boston_crosswalk): + """Test querying the Boston crosswalk""" + assert boston_crosswalk.shape[0] > 0 + assert boston_crosswalk.crs == "EPSG:4326" + assert boston_crosswalk.geometry.name == "geometry" + assert boston_crosswalk.geometry.geom_type.unique()[0] == "Polygon" + assert boston_crosswalk.columns.to_list() == [ + "geometry", + "OBJECTID", + "Conf_Score", + "tilename", + "layer", + "path", + "TOWN", + "Degradatio", + "MPOName", + "Class_Loc", + "DistrictName", + "DistrictNumber", + "RPA_NAME", + "CrossType", + "Shape__Area", + "Shape__Length", + ] + assert boston_crosswalk["TOWN"].unique().size == 1 + assert boston_crosswalk["TOWN"].unique()[0] == "BOSTON" + + +def test_save_boston_geojson(boston_crosswalk): + """Test saving the Boston crosswalk to a GeoJSON file""" + query_geojson.save_geojson(boston_crosswalk, "test_boston_crosswalk.geojson") + geojson_filename = "test_boston_crosswalk.geojson" + saved_gdf = query_geojson.gpd.read_file(geojson_filename) + + assert boston_crosswalk.crs == saved_gdf.crs + assert set(boston_crosswalk.columns) == set(saved_gdf.columns) + assert boston_crosswalk.index.equals(saved_gdf.index) + assert boston_crosswalk.shape == saved_gdf.shape + + os.remove(geojson_filename) + assert not os.path.exists(geojson_filename) diff --git a/tests/test_mapping.py b/tests/test_mapping.py new file mode 100644 index 0000000..eaa3e32 --- /dev/null +++ b/tests/test_mapping.py @@ -0,0 +1,60 @@ +import os +import time + +import folium + +from night_light.utils import ( + create_folium_map, + LAYER_STYLE_DICT, + LAYER_HIGHLIGHT_STYLE_DICT, + Tooltip, +) +from night_light.utils.mapping import open_html_file +from tests.conftest import BOSTON_CENTER_COORD + + +def test_boston_map(boston_traffic, boston_crosswalk): + """ + Test creating a complete map of the Boston with multiple layers. + + Layers: + - Average Annual Daily Traffic + - Crosswalk + """ + map_filename = "test_boston_crosswalk.html" + adt_layer = folium.GeoJson( + boston_traffic, + name=f"Annual Daily Traffic", + style_function=lambda x: LAYER_STYLE_DICT, + highlight_function=lambda x: LAYER_HIGHLIGHT_STYLE_DICT, + smooth_factor=2.0, + tooltip=Tooltip( + fields=["Route_ID", "AADT", "Facility"], + aliases=["Route ID", "Average Annual Daily Traffic", "Facility Type"], + max_width=800, + ), + ) + crosswalk_layer = folium.GeoJson( + boston_crosswalk, + name="Crosswalk", + style_function=lambda x: LAYER_STYLE_DICT, + highlight_function=lambda x: LAYER_HIGHLIGHT_STYLE_DICT, + smooth_factor=2.0, + tooltip=Tooltip( + fields=["OBJECTID", "CrossType", "Conf_Score"], + aliases=["Crosswalk ID", "Crosswalk Type", "Confidence Score"], + max_width=800, + ), + ) + create_folium_map( + layers=[adt_layer, crosswalk_layer], + zoom_start=12, + center=BOSTON_CENTER_COORD, + map_filename=map_filename, + ) + + assert os.path.exists(map_filename) + open_html_file(map_filename) + time.sleep(1) + os.remove(map_filename) + assert not os.path.exists(map_filename)