import numpy as np
import pandas as pd
import geopandas as gpd
import folium
import branca.colormap as bcm
import warnings
warnings.filterwarnings("ignore")
from pathlib import Path
from shapely.geometry import box
from shapely import contains_xy
from folium.raster_layers import ImageOverlay
from matplotlib import cm
# Inputs
cover_csv = "field_data/rugu_arbeyal_cover.csv"
poly_path = Path("results/rugu_arbeyal_polygons.geojson")
pics_dir = Path("field_data/pictures")
gps_csv = pics_dir / "photos_gps.csv"
# Load cover points (Description column = percent cover)
df_cover = pd.read_csv(cover_csv)
df_cover["cover_pct"] = pd.to_numeric(df_cover["Description"], errors="coerce")
df_cover = df_cover.dropna(subset=["cover_pct", "Longitude", "Latitude"])
df_cover = df_cover[df_cover["cover_pct"].between(0, 100, inclusive="both")].copy()
gdf_cover = gpd.GeoDataFrame(
df_cover.copy(),
geometry=gpd.points_from_xy(df_cover["Longitude"], df_cover["Latitude"]),
crs="EPSG:4326",
)
pics_data = pd.read_csv(gps_csv).to_dict(orient="records")
def pic_html(filepath, filename):
img_path = str(filepath / filename)
return (
f'<div style="text-align:center">'
f'<b>{filename}</b><br>'
f'<img src="{img_path}" width="300" style="border-radius:4px; margin-top:4px;">'
f'</div>'
)
# Load the two polygons created previously
gdf_poly = gpd.read_file(poly_path)
poly_union = gdf_poly.union_all()
# Build bbox rectangle from polygon corners
minx, miny, maxx, maxy = gdf_poly.total_bounds
bbox_geom = box(minx, miny, maxx, maxy)
gdf_bbox = gpd.GeoDataFrame({"name": ["bbox"]}, geometry=[bbox_geom], crs="EPSG:4326")
# Interpolation grid over bbox
nx, ny = 300, 300
xv = np.linspace(minx, maxx, nx)
yv = np.linspace(miny, maxy, ny)
grid_x, grid_y = np.meshgrid(xv, yv)
px = gdf_cover.geometry.x.to_numpy()
py = gdf_cover.geometry.y.to_numpy()
pz = gdf_cover["cover_pct"].to_numpy()
# Natural neighbour interpolation (MetPy)
try:
from metpy.interpolate import natural_neighbor_to_grid
grid_z = natural_neighbor_to_grid(px, py, pz, grid_x, grid_y)
except Exception:
from scipy.interpolate import griddata
grid_z = griddata((px, py), pz, (grid_x, grid_y), method="linear")
nearest = griddata((px, py), pz, (grid_x, grid_y), method="nearest")
grid_z = np.where(np.isnan(grid_z), nearest, grid_z)
# Clip interpolated raster to the polygons
mask_inside = contains_xy(poly_union, grid_x, grid_y)
grid_z_clip = np.where(mask_inside, grid_z, np.nan)
# Covered area: total polygon area × mean cover fraction inside polygons
total_poly_area_m2 = gdf_poly.to_crs(epsg=25830).area.sum()
mean_cover_fraction = float(np.nanmean(grid_z_clip[mask_inside]) / 100)
covered_area_m2 = total_poly_area_m2 * mean_cover_fraction
# Prepare RGBA image for folium ImageOverlay
vmin = float(np.nanmin(grid_z_clip))
vmax = float(np.nanmax(grid_z_clip))
norm = (grid_z_clip - vmin) / (vmax - vmin + 1e-12)
rgba = cm.get_cmap("YlGn")(np.clip(norm, 0, 1))
rgba[..., 3] = np.where(np.isnan(grid_z_clip), 0.0, 0.75) # transparent outside polygon
rgba_uint8 = (rgba * 255).astype(np.uint8)
# Folium/ImageOverlay expects first row at the north side; flip Y to avoid vertical inversion
rgba_uint8 = np.flipud(rgba_uint8)
# Folium map
center = [(miny + maxy) / 2.0, (minx + maxx) / 2.0]
m_cover = folium.Map(location=center, zoom_start=18, tiles=None)
folium.TileLayer(
tiles="CartoDB Positron",
name="CartoDB Positron",
overlay=False,
control=True,
).add_to(m_cover)
# Interpolated raster layer (already clipped)
ImageOverlay(
image=rgba_uint8,
bounds=[[miny, minx], [maxy, maxx]],
name="Cobertura interpolada",
opacity=1,
interactive=False,
).add_to(m_cover)
# Polygon boundaries and bbox outline
folium.GeoJson(
gdf_poly.__geo_interface__,
name="Poligonos",
style_function=lambda _f: {"color": "red", "weight": 2, "fillOpacity": 0},
).add_to(m_cover)
# Photo markers
for pic in pics_data:
popup_html = pic_html(pics_dir, pic["file"])
folium.Marker(
location=[pic["lat"], pic["lon"]],
popup=folium.Popup(popup_html, max_width=320),
tooltip=pic["file"],
icon=folium.Icon(color="blue", icon="camera", prefix="fa"),
).add_to(m_cover)
# Color legend for interpolated cover
colormap = bcm.LinearColormap(
colors=["#ffffcc", "#78c679", "#006837"],
vmin=vmin,
vmax=vmax,
caption="Cobertura interpolada (%)",
)
colormap.add_to(m_cover)
# HTML legend matching first map style
legend_cover_html = f"""
<div style="
position: fixed;
bottom: 20px;
left: 20px;
z-index: 1000;
background-color: white;
border: 1px solid #666;
border-radius: 6px;
padding: 10px 12px;
font-size: 14px;
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.25);
">
<div style="display: flex; align-items: center; gap: 8px;">
<span style="display: inline-block; width: 26px; border-top: 3px solid red;"></span>
<span><b>Área cubierta <br>{covered_area_m2:,.2f} m2</b></span>
</div>
<div style="display: flex; align-items: center; gap: 8px; margin-top: 8px;">
<span style="display: inline-block; width: 14px; height: 14px;
background: #4286f4; border-radius: 50%; border: 2px solid white;
box-shadow: 0 0 0 1px #4286f4;"></span>
<span><b>Fotografías (clic para ver)</b></span>
</div>
</div>
"""
m_cover.get_root().html.add_child(folium.Element(legend_cover_html))
folium.LayerControl().add_to(m_cover)
m_cover.fit_bounds([[miny, minx], [maxy, maxx]])
# Save interpolated raster as GeoTIFF in results/
import rasterio
from rasterio.transform import from_bounds
from rasterio.crs import CRS
results_dir = Path("results")
results_dir.mkdir(parents=True, exist_ok=True)
raster_out = results_dir / "rugu_arbeyal_cover_raster.tif"
ny_r, nx_r = grid_z_clip.shape
transform = from_bounds(minx, miny, maxx, maxy, nx_r, ny_r)
grid_save = np.where(np.isnan(grid_z_clip), -9999.0, grid_z_clip).astype("float32")
with rasterio.open(
raster_out, "w",
driver="GTiff", height=ny_r, width=nx_r,
count=1, dtype="float32",
crs=CRS.from_epsg(4326),
transform=transform,
nodata=-9999.0,
) as dst:
dst.write(grid_save, 1)
m_cover