Skip to content

Commit

Permalink
feat: gitignore update and mailing system
Browse files Browse the repository at this point in the history
  • Loading branch information
Lorenzo Furlan committed Feb 4, 2025
1 parent 72702c5 commit 9bb293f
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 25 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ __pycache__/
*.py[cod]
*$py.class

predictions/
# C extensions
*.so

Expand Down
86 changes: 68 additions & 18 deletions instageo/apps/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,30 @@
interactive map.
"""

import xarray as xr

import glob
import json
import os
from pathlib import Path

from instageo.apps.reporting import (
generate_high_density_report,
format_report,
send_email,
)
import streamlit as st

from instageo import INSTAGEO_APPS_PATH
from instageo.apps.viz import create_map_with_geotiff_tiles


# @st.cache_data
def generate_map(
directory: str,
year: int,
month: int,
country_tiles: list[str],
) -> None:
) -> list[xr.Dataset]:
"""Generate the plotly map.
Arguments:
Expand All @@ -59,54 +66,97 @@ def generate_map(
tiles_to_consider = [
tile
for tile in prediction_tiles
if os.path.basename(tile).split("_")[1].strip("T") in country_tiles
if os.path.basename(tile).split("_")[1][1:] in country_tiles
]

if not tiles_to_consider:
raise FileNotFoundError(
"No GeoTIFF files found for the given year, month, and country."
)

fig = create_map_with_geotiff_tiles(tiles_to_consider)
fig, rasters = create_map_with_geotiff_tiles(tiles_to_consider)
st.session_state.fig = fig
st.plotly_chart(fig, use_container_width=True)
return rasters
except (ValueError, FileNotFoundError, Exception) as e:
st.error(f"An error occurred: {str(e)}")


def main() -> None:
"""Instageo Serve Main Entry Point."""
st.set_page_config(layout="wide")
st.title("InstaGeo Serve")
predictions_path = str(Path(__file__).parent / "predictions")
st.set_page_config(
page_title="Locust busters", page_icon=":cricket:", layout="wide"
)
st.title("Locust busters :cricket:")

st.sidebar.subheader(
"This application enables the visualisation of GeoTIFF files on an interactive map.",
"This application enables the visualisation of GeoTIFF files on an interactive map. You can also receive an alert report",
divider="rainbow",
)
user_email = st.sidebar.text_input("Enter your email for the report (optional):")
send_report = st.sidebar.checkbox("Send me a high-density report")
st.sidebar.header("Settings")
with open(
INSTAGEO_APPS_PATH / "utils/country_code_to_mgrs_tiles.json"
) as json_file:
countries_to_tiles_map = json.load(json_file)

with st.sidebar.container():
directory = st.sidebar.text_input(
"GeoTiff Directory:",
help="Write the path to the directory containing your GeoTIFF files",
)
country_code = st.sidebar.selectbox(
"ISO 3166-1 Alpha-2 Country Code:",
country_codes = st.sidebar.multiselect(
"ISO 3166-1 Alpha-2 Country Codes:",
options=list(countries_to_tiles_map.keys()),
default=["CD", "TZ", "UG", "RW"],
)
year = st.sidebar.number_input("Select Year", 2023, 2024)
month = st.sidebar.number_input("Select Month", 1, 12)

st.markdown(
"""
<style>
.plotly-graph-div { /* This targets the Plotly chart container */
height: 90vh; /* Use 90% of the viewport height */
margin-top: -60px; /* optional: tweak top margin if needed after stretching */
margin-bottom: 10px; /* optional: tweak bottom margin if needed */
}
</style>
""",
unsafe_allow_html=True,
)
if st.sidebar.button("Generate Map"):
country_tiles = countries_to_tiles_map[country_code]
generate_map(str(Path(__file__).parent / directory), year, month, country_tiles)
country_tiles = [
tile
for country_code in country_codes
for tile in countries_to_tiles_map.get(country_code, [])
]
rasters = generate_map(predictions_path, year, month, country_tiles)
if send_report and user_email:
with st.spinner("Generating and sending report..."):
report, map_image_path = generate_high_density_report(
# folium map
# save folium map
fig=st.session_state.fig,
rasters=rasters,
threshold=0.8,
)
formatted_report = format_report(report, map_image_path)
if send_email(
user_email,
"High-Density Report",
formatted_report,
img_path=map_image_path,
):
st.success("Report sent successfully!")
try: # clean up map
os.remove(map_image_path)
except Exception as e:
print(e)
else:
st.error("Error sending report.")
else: # this is to init an empty map
st.plotly_chart(
create_map_with_geotiff_tiles(tiles_to_overlay=[]), use_container_width=True
)
fig, _ = create_map_with_geotiff_tiles(tiles_to_overlay=[])
st.session_state.fig = fig
st.plotly_chart(fig, use_container_width=True)


if __name__ == "__main__":
Expand Down
75 changes: 75 additions & 0 deletions instageo/apps/reporting.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import os
import numpy as np
import xarray as xr
import plotly.io as pio # For saving Plotly figures as images
import plotly.graph_objects as go
from email.mime.image import MIMEImage # For attaching images to emails


def generate_high_density_report(
fig: go.Figure, rasters: dict[str, xr.Dataset], threshold=0.8
):
"""Generates a report of locations with density above the given threshold."""
report = []
for tile_filename, raster in rasters.items():
try:
data_array = raster["band_data"]

if np.any(
data_array.to_numpy() > threshold
): # this maybe will be gone with folium fig
country_code = os.path.basename(tile_filename).split("_")[1][1:]
report.append({"country_code": country_code, "filepath": tile_filename})

except Exception as e:
print(f"Error processing {tile_filename}: {e}") # Or use a proper logger

map_image_path = "temp_map.png"
pio.write_image(fig, map_image_path)
return report, map_image_path


def format_report(report, map_image_path):
if not report:
return "No high-density areas detected."

formatted_report = "High-density areas detected:\n"
for entry in report:
formatted_report += (
f"- Country: {entry['country_code']}, File: {entry['filepath']}\n"
)

formatted_report += (
f"<img src='cid:{map_image_path}' width='500'>\n" # width reduced
)

return formatted_report


def send_email(to_email, subject, body, img_path):
"""Sends an email. Replace with your preferred email sending method."""
try:
import smtplib
from email.mime.text import MIMEText

from_mail = "locustbusters@gmail.com"
msg = MIMEText(body)
msg["Subject"] = subject
msg["From"] = from_mail
msg["To"] = to_email
with open(img_path, "rb") as img_file: # Open in binary read mode
img = MIMEImage(img_file.read())
img.add_header(
"Content-ID", f"<{img_path}>"
) # Important for referencing in HTML
msg.attach(img)

with smtplib.SMTP("smtp.gmail.com", 587) as server:
server.starttls()
server.login(from_mail, "rvabulmpjdtuwvza")
server.send_message(msg)

return True
except Exception as e:
print(f"Error sending email: {e}")
return False
20 changes: 13 additions & 7 deletions instageo/apps/viz.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import xarray as xr
from pyproj import CRS, Transformer
import math
import numpy as np

epsg3857_to_epsg4326 = Transformer.from_crs(3857, 4326, always_xy=True)
MIN_ZOOM = 5
Expand Down Expand Up @@ -60,8 +61,8 @@ def calculate_zoom(lats: list[float], lons: list[float]) -> float:
# This calculation is based on approximate formulas for Mercator projection
# and world dimensions in pixels at different zoom levels. You can fine-tune
# the constant factors (15 and 22) if needed for your specific map display.
zoom_lat = 5 - math.log2(lat_diff)
zoom_lon = 8 - math.log2(lon_diff)
zoom_lat = 8 - math.log2(lat_diff)
zoom_lon = 10 - math.log2(lon_diff)

# Choose the more restrictive zoom level (the smaller one) to fit the entire area
return max(MIN_ZOOM, min(zoom_lat, zoom_lon))
Expand Down Expand Up @@ -157,6 +158,7 @@ def add_raster_to_plotly_figure(
return img, coordinates


# @lru_cache(maxsize=32)
def read_geotiff_to_xarray(filepath: str) -> tuple[xr.Dataset, CRS]:
"""Read a GeoTIFF file into an xarray Dataset.
Expand All @@ -169,23 +171,27 @@ def read_geotiff_to_xarray(filepath: str) -> tuple[xr.Dataset, CRS]:
return xr.open_dataset(filepath).sel(band=1), get_crs(filepath)


def create_map_with_geotiff_tiles(tiles_to_overlay: list[str]) -> go.Figure:
def create_map_with_geotiff_tiles(
tiles_to_overlay: list[str],
) -> tuple[go.Figure, dict[str, xr.Dataset]]:
"""Create a map with multiple GeoTIFF tiles overlaid, centered on the tiles' extent."""

fig = go.Figure(go.Scattermapbox())
mapbox_layers = []
all_lats = []
all_lons = []
all_rasters = {}

for tile in tiles_to_overlay:
if tile.endswith(".tif") or tile.endswith(".tiff"):
xarr_dataset, crs = read_geotiff_to_xarray(tile)
img, coordinates = add_raster_to_plotly_figure(xarr_dataset, crs)
coordinates_np = np.array(coordinates)
all_rasters[tile] = xarr_dataset

# Extract lat/lon from coordinates
for lon, lat in coordinates:
all_lons.append(lon)
all_lats.append(lat)
all_lons.extend(coordinates_np[:, 0])
all_lats.extend(coordinates_np[:, 1])

mapbox_layers.append(
{"sourcetype": "image", "source": img, "coordinates": coordinates}
Expand Down Expand Up @@ -214,4 +220,4 @@ def create_map_with_geotiff_tiles(tiles_to_overlay: list[str]) -> go.Figure:
)

fig.update_layout(mapbox_layers=mapbox_layers)
return fig
return fig, all_rasters

0 comments on commit 9bb293f

Please sign in to comment.