|
| 1 | +import os |
| 2 | +from pathlib import Path |
| 3 | + |
| 4 | +from dotenv import load_dotenv |
| 5 | +import geopandas as gpd |
| 6 | +from jinja2 import Environment, FileSystemLoader |
| 7 | +import matplotlib |
| 8 | +import numpy as np |
| 9 | +from shapely.geometry import box |
| 10 | +import typer |
| 11 | + |
| 12 | +try: |
| 13 | + from typing import Annotated |
| 14 | +except ImportError: |
| 15 | + # for Python 3.9 |
| 16 | + from typing_extensions import Annotated |
| 17 | + |
| 18 | + |
| 19 | +app = typer.Typer() |
| 20 | + |
| 21 | + |
| 22 | +def h3_to_webmap(): |
| 23 | + return |
| 24 | + |
| 25 | + |
| 26 | +@app.command() |
| 27 | +def main( |
| 28 | + input_file: Annotated[ |
| 29 | + Path, |
| 30 | + typer.Argument(help="Path to file containing h3 polygons."), |
| 31 | + ], |
| 32 | + filename: Annotated[ |
| 33 | + str, typer.Argument(help="(Optional) Path to file for HTML output.") |
| 34 | + ] = "./data/processed/gvi_webmap.html", |
| 35 | + zoom_level: Annotated[ |
| 36 | + int, |
| 37 | + typer.Argument( |
| 38 | + help="""(Optional) Starting zoom level for webmap. |
| 39 | + Takes integer between 0 (small scale) and 20 (large scale). |
| 40 | + """ |
| 41 | + ), |
| 42 | + ] = 12, |
| 43 | +): |
| 44 | + gdf = gpd.read_file(input_file) |
| 45 | + # if crs is not 4326, convert to 4326 |
| 46 | + if gdf.crs == "EPSG:4326": |
| 47 | + pass |
| 48 | + else: |
| 49 | + gdf = gdf.to_crs("EPSG:4326") |
| 50 | + |
| 51 | + # Round GVI score to make map labels more readable |
| 52 | + gdf["gvi_score"] = round(gdf["gvi_score"], 2) |
| 53 | + |
| 54 | + # get central coordinates of all features |
| 55 | + centre = ( |
| 56 | + str(gdf.dissolve().centroid.x.values[0]) |
| 57 | + + ", " |
| 58 | + + str(gdf.to_crs("4326").dissolve().centroid.y.values[0]) |
| 59 | + ) |
| 60 | + centre_str = "[" + centre + "]" |
| 61 | + |
| 62 | + # Calculate datasdet bounds (with a buffer of 0.5 degrees) to limit webmap bounds |
| 63 | + gdf_bounds = gdf.total_bounds |
| 64 | + bbox = box(*gdf_bounds).buffer(0.25, cap_style="square", join_style="mitre") |
| 65 | + bounds = bbox.bounds |
| 66 | + bounds_str = ( |
| 67 | + "[" |
| 68 | + + str(bounds[0]) |
| 69 | + + "," |
| 70 | + + str(bounds[1]) |
| 71 | + + "], [" |
| 72 | + + str(bounds[2]) |
| 73 | + + "," |
| 74 | + + str(bounds[3]) |
| 75 | + + "]" |
| 76 | + ) |
| 77 | + |
| 78 | + # Load API key for map tiles from environment variable |
| 79 | + load_dotenv() |
| 80 | + |
| 81 | + if "MAPTILER_API_KEY" in os.environ: |
| 82 | + maptiler_api_key = os.getenv("MAPTILER_API_KEY") |
| 83 | + else: |
| 84 | + maptiler_api_key = "None" |
| 85 | + |
| 86 | + if maptiler_api_key in ["MY_MAPTILER_API_KEY", ""]: |
| 87 | + maptiler_api_key = "None" |
| 88 | + |
| 89 | + # Lookup the colourmap values for each GVI score |
| 90 | + cmap = matplotlib.colormaps["Greens"] |
| 91 | + gdf["gvi_norm"] = (gdf.gvi_score - np.min(gdf.gvi_score)) / ( |
| 92 | + np.max(gdf.gvi_score) - np.min(gdf.gvi_score) |
| 93 | + ) |
| 94 | + gdf["html_color"] = gdf["gvi_norm"].apply( |
| 95 | + lambda x: matplotlib.colors.rgb2hex(cmap(x)) |
| 96 | + ) |
| 97 | + |
| 98 | + # Generate divs for legend |
| 99 | + # Pick 4 evenly-spaced values from the gvi scores to use in the legend |
| 100 | + legend_gvi = list( |
| 101 | + np.arange( |
| 102 | + gdf.gvi_score.min(), |
| 103 | + gdf.gvi_score.max(), |
| 104 | + (gdf.gvi_score.max() - gdf.gvi_score.min()) / 4, |
| 105 | + dtype=float, |
| 106 | + ) |
| 107 | + ) |
| 108 | + |
| 109 | + # Generate labels by looking up what the GVI score would be for those values |
| 110 | + legend_label_1 = round( |
| 111 | + np.linspace(gdf.gvi_score.min(), gdf.gvi_score.max(), 100)[0], 1 |
| 112 | + ) |
| 113 | + legend_label_2 = round( |
| 114 | + np.linspace(gdf.gvi_score.min(), gdf.gvi_score.max(), 100)[33], 1 |
| 115 | + ) |
| 116 | + legend_label_3 = round( |
| 117 | + np.linspace(gdf.gvi_score.min(), gdf.gvi_score.max(), 100)[66], 1 |
| 118 | + ) |
| 119 | + legend_label_4 = round( |
| 120 | + np.linspace(gdf.gvi_score.min(), gdf.gvi_score.max(), 100)[99], 1 |
| 121 | + ) |
| 122 | + |
| 123 | + # Normalise the label values to lookup against the colourmap |
| 124 | + legend_gvi_norm = (legend_gvi - np.min(legend_gvi)) / ( |
| 125 | + np.max(legend_gvi) - np.min(legend_gvi) |
| 126 | + ) |
| 127 | + |
| 128 | + # Generate the html colour code from the normalised values |
| 129 | + legend_colours = [] |
| 130 | + for i in legend_gvi_norm: |
| 131 | + legend_colours.append(matplotlib.colors.rgb2hex(cmap(i))) |
| 132 | + # Assign patch colours to use in HTML template |
| 133 | + legend_patch_1, legend_patch_2, legend_patch_3, legend_patch_4 = legend_colours |
| 134 | + |
| 135 | + # Load the MapLibre HMTL template |
| 136 | + environment = Environment(loader=FileSystemLoader("src/templates")) |
| 137 | + template = environment.get_template("maplibre_template.html") |
| 138 | + |
| 139 | + # Generate the HTML file from the template, filling dynamic values |
| 140 | + with open(filename, mode="w", encoding="utf-8") as message: |
| 141 | + message.write( |
| 142 | + template.render( |
| 143 | + title="GVI score hex map", |
| 144 | + geojson=gdf.to_json(), |
| 145 | + centre_coords=centre_str, |
| 146 | + zoom=zoom_level, |
| 147 | + bounds=bounds_str, |
| 148 | + maptiler_api_key=maptiler_api_key, |
| 149 | + legend_label_1=legend_label_1, |
| 150 | + legend_label_2=legend_label_2, |
| 151 | + legend_label_3=legend_label_3, |
| 152 | + legend_label_4=legend_label_4, |
| 153 | + legend_patch_1=legend_patch_1, |
| 154 | + legend_patch_2=legend_patch_2, |
| 155 | + legend_patch_3=legend_patch_3, |
| 156 | + legend_patch_4=legend_patch_4, |
| 157 | + ) |
| 158 | + ) |
| 159 | + |
| 160 | + |
| 161 | +if __name__ == "__main__": |
| 162 | + app() |
0 commit comments