Skip to content

Commit eead7f7

Browse files
committed
add transsect tool function
1 parent 7914fd6 commit eead7f7

File tree

11 files changed

+3356
-1
lines changed

11 files changed

+3356
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
data/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
FROM python:3.12
2+
3+
ENV PYTHONUNBUFFERED=1
4+
5+
WORKDIR /app/
6+
7+
# Install uv
8+
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#installing-uv
9+
COPY --from=ghcr.io/astral-sh/uv:0.5.11 /uv /uvx /bin/
10+
11+
# Place executables in the environment at the front of the path
12+
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#using-the-environment
13+
ENV PATH="/app/.venv/bin:$PATH"
14+
15+
# Compile bytecode
16+
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#compiling-bytecode
17+
ENV UV_COMPILE_BYTECODE=1
18+
19+
# uv Cache
20+
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#caching
21+
ENV UV_LINK_MODE=copy
22+
23+
24+
RUN apt-get update && apt-get install -y libgdal-dev libgl1
25+
26+
# Install dependencies
27+
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers
28+
RUN --mount=type=cache,target=/root/.cache/uv \
29+
--mount=type=bind,source=uv.lock,target=uv.lock \
30+
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
31+
uv sync --frozen --no-install-project
32+
33+
ENV PYTHONPATH=/app
34+
35+
COPY ./pyproject.toml ./uv.lock /app/
36+
37+
COPY ./src /app/src
38+
39+
# Sync the project
40+
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers
41+
RUN --mount=type=cache,target=/root/.cache/uv \
42+
uv sync
43+
44+
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Delta DTM Line Plot
2+
3+
A FastAPI-based web service that generates elevation profile plots along transect lines using the Deltares Digital Terrain Model (Delta DTM) dataset.
4+
5+
## Overview
6+
7+
This application provides an API endpoint that accepts a LineString geometry and returns an elevation profile plot along that line. It uses the Delta DTM dataset stored in Azure Blob Storage to extract elevation data.
8+
9+
![Example Elevation Profile](./images/transect_plot.png)
10+
11+
## For Scientists
12+
13+
### How It Works
14+
15+
The core functionality is implemented in `src/transsect_plot.py` and follows these steps:
16+
17+
1. **Data Selection**: When given a LineString geometry, the application identifies which Delta DTM tiles intersect with the line.
18+
2. **Data Retrieval**: The relevant raster tiles are fetched from Azure Blob Storage.
19+
3. **Data Processing**:
20+
- The raster tiles are merged into a single data array
21+
- Elevation values are sampled along the transect line
22+
- Distances along the transect are calculated in meters
23+
4. **Visualization**: A matplotlib figure is generated showing the elevation profile.
24+
25+
### Key Functions
26+
27+
- `get_intersecting_item_ids()`: Identifies which Delta DTM tiles intersect with the input line
28+
- `get_datasets()`: Asynchronously fetches the raster data for the intersecting tiles
29+
- `merge_and_mask_datasets()`: Combines multiple raster datasets into a single dataset
30+
- `transsect()`: Samples elevation values along the transect line
31+
- `get_distances()`: Calculates distances along the transect in meters
32+
- `create_profile_plot()`: Generates the matplotlib figure
33+
- `plot_transsect()`: Main function that orchestrates the entire process
34+
35+
### Modifying the Scientific Logic
36+
37+
If you want to modify how the elevation data is processed or visualized:
38+
39+
1. The sampling density can be adjusted in the `transsect()` function by changing the `n_samples` parameter
40+
2. The plot appearance can be customized in the `create_profile_plot()` function
41+
3. The distance calculation in `get_distances()` uses a simple approximation - you may want to implement a more accurate geodesic calculation
42+
43+
## For Developers
44+
45+
### API Endpoints
46+
47+
- `POST /line-plot`: Accepts a GeoJSON LineString and returns a PNG image of the elevation profile
48+
- `GET /`: Simple welcome message
49+
50+
### Installation & Local Development
51+
52+
#### Prerequisites
53+
54+
- Python 3.11+
55+
- [uv](https://github.com/astral-sh/uv) for dependency management
56+
57+
#### Setup
58+
59+
1. Clone the repository
60+
61+
2. Install dependencies:
62+
```bash
63+
uv sync
64+
```
65+
3. Run the application:
66+
```bash
67+
uv run src/main.py
68+
```
69+
4. Access the API documentation at http://localhost:8000/docs
70+
71+
### Docker Deployment
72+
73+
The application includes a Dockerfile for containerized deployment:
74+
75+
```bash
76+
# Build the Docker image
77+
docker build -t delta-dtm-line-plot .
78+
79+
# Run the container
80+
docker run -p 8000:8000 delta-dtm-line-plot
81+
```
82+
83+
### Environment Variables
84+
85+
- No specific environment variables are required for basic operation
86+
- The application uses hardcoded URLs for Azure Blob Storage access
87+
88+
### Performance Considerations
89+
90+
- The application uses asynchronous I/O for fetching raster data
91+
- For high-traffic deployments, consider implementing caching for frequently requested transects
92+
- Memory usage scales with the number and size of intersecting raster tiles
93+
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
[project]
2+
name = "delta-dtm-line-plot"
3+
version = "0.1.0"
4+
description = "Add your description here"
5+
# readme = "README.md"
6+
requires-python = ">=3.11"
7+
dependencies = [
8+
"aiohttp>=3.11.6",
9+
"azure-storage-blob>=12.24.1",
10+
"duckdb>=1.1.3",
11+
"fastapi>=0.115.8",
12+
"fastparquet>=2024.11.0",
13+
"geopandas>=1.0.1",
14+
"jupyter>=1.1.1",
15+
"matplotlib>=3.10.0",
16+
"pyarrow>=19.0.1",
17+
"pydantic-shapely>=1.0.0a4",
18+
"pystac-client>=0.8.5",
19+
"rioxarray>=0.18.2",
20+
"uvicorn>=0.34.0",
21+
"xarray>=2024.10.0",
22+
]
23+
24+
[build-system]
25+
requires = ["hatchling"]
26+
build-backend = "hatchling.build"
27+
28+
[tool.hatch.build.targets.wheel]
29+
packages = ["src"]
30+
31+
[dependency-groups]
32+
dev = [
33+
"mypy>=1.15.0",
34+
"pytest>=8.3.4",
35+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import io
2+
import traceback
3+
from typing import Annotated
4+
5+
import fastapi
6+
import matplotlib
7+
8+
matplotlib.use("AGG")
9+
10+
import matplotlib.pyplot as plt
11+
import uvicorn
12+
from fastapi import BackgroundTasks, Body
13+
from fastapi.responses import Response
14+
from pydantic_shapely import FeatureBaseModel, GeometryField
15+
from shapely import LineString
16+
from src.transsect_plot import plot_transsect
17+
18+
app = fastapi.FastAPI(
19+
title="Delta DTM Line Plot API",
20+
description="API for generating elevation profile plots along a transect line",
21+
version="1.0.0",
22+
)
23+
24+
25+
class LineStringModel(FeatureBaseModel, geometry_field="line"):
26+
line: Annotated[LineString, GeometryField()]
27+
28+
29+
@app.post(
30+
"/line-plot",
31+
response_class=Response,
32+
summary="Generate an elevation profile plot along a transect line",
33+
description="Takes a LineString geometry and returns a plot of the elevation profile along that line",
34+
responses={
35+
200: {
36+
"content": {"image/png": {}},
37+
"description": "A PNG image of the elevation profile plot",
38+
},
39+
500: {
40+
"description": "Internal server error",
41+
},
42+
},
43+
)
44+
async def line_plot(
45+
line_string: Annotated[
46+
LineStringModel.GeoJsonDataModel, # type: ignore
47+
Body(
48+
example={
49+
"type": "Feature",
50+
"geometry": {
51+
"type": "LineString",
52+
"coordinates": [[4.9, 52.37], [5.12, 52.09]],
53+
},
54+
"properties": {},
55+
}
56+
),
57+
],
58+
background_tasks: BackgroundTasks,
59+
) -> Response:
60+
try:
61+
# Use the existing plot_transsect function to generate the figure
62+
line: LineString = line_string.geometry.to_shapely()
63+
fig = await plot_transsect(line)
64+
65+
# Create an in-memory bytes buffer
66+
buf = io.BytesIO()
67+
68+
# Save the figure to the in-memory buffer
69+
fig.savefig(buf, format="png")
70+
71+
# Close the figure to free up resources
72+
plt.close(fig)
73+
74+
# Seek to the beginning of the buffer
75+
buf.seek(0)
76+
77+
# Return the image as a response with appropriate headers
78+
background_tasks.add_task(buf.close)
79+
background_tasks.add_task(plt.close, fig)
80+
return Response(
81+
content=buf.getvalue(),
82+
media_type="image/png",
83+
headers={"Content-Disposition": 'attachment; filename="transect_plot.png"'},
84+
)
85+
except Exception as e:
86+
# Log the error
87+
print("Error generating plot:")
88+
print(traceback.format_exc())
89+
# Raise an HTTP exception
90+
raise fastapi.HTTPException(
91+
status_code=500,
92+
detail=f"Error generating plot: {str(e)}",
93+
)
94+
95+
96+
@app.get("/", include_in_schema=False)
97+
async def root():
98+
return {
99+
"message": "Welcome to the Delta DTM Line Plot API. Use /line-plot endpoint to generate elevation profile plots."
100+
}
101+
102+
103+
if __name__ == "__main__":
104+
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)

0 commit comments

Comments
 (0)