Repository: TUW-GEO/dask-flood-mapper
Pull request #57: Userinterface created
Author: login: thaisbeham
Created: 2025-04-03T12:58:37Z | Merged: 2025-04-09T11:35:16Z

how to access it:

on the terminal 

```
cd backend
python app.py
```

then clink on the link that appears:
` Running on http://127.0.0.1:5000`

it will open the UI in the web browser

Now the image can be displayed dynamically like in the notebooks:
![Screenshot from 2025-04-03 13-56-05](https://github.com/user-attachments/assets/6e03d31d-8188-4987-9069-905a7c8e6603)

resolves #50 

Linked issue #50: GUI is missing
Create a GUI with for example: https://github.com/CS-SI/eodag-labextension
Linked issue comments: user: login: thaisbeham
created_at: 2025-03-26T12:40:35Z
body: https://github.com/CS-SI/eodag-labextension
author_association: COLLABORATOR
user: login: thaisbeham
created_at: 2025-03-27T13:31:36Z
body: First idea:

![Image](https://github.com/user-attachments/assets/344b5052-4c45-40ae-94a1-420d6d99d57b)
author_association: COLLABORATOR
user: login: MartinSchobben
created_at: 2025-03-27T14:06:08Z
body: Looks great
author_association: COLLABORATOR
user: login: thaisbeham
created_at: 2025-04-02T08:50:38Z
body: I am able to display the visualization but not dynamically 

![Image](https://github.com/user-attachments/assets/d070bb38-a5ca-4666-9371-e0ed76e01f53)
author_association: COLLABORATOR

Diff:
diff --git a/.gitignore b/.gitignore
index 31860a5..6dd1e33 100644
--- a/.gitignore
+++ b/.gitignore
@@ -55,9 +55,10 @@ venv
 # make file markers
 .install*
 /.quarto/
+
+src/dask_flood_mapper/static/*.html
 notebooks/01_local_dask_files/
 site_libs/
-**/*.html
 _book/
 **/*.zarr.zip
-**/*.zarr
\ No newline at end of file
+**/*.zarr
diff --git a/README.md b/README.md
index 524a686..6ae1a32 100644
--- a/README.md
+++ b/README.md
@@ -14,6 +14,7 @@ To install the package, do the following:
 pip install git+https://github.com/TUW-GEO/dask-flood-mapper
 ```
 
+
 ## Usage
 
 Storm Babet hit the Denmark and Northern coast of Germany at the 20th of October 2023 [Wikipedia](https://en.wikipedia.org/wiki/Storm_Babet). Here an area around Zingst at the Baltic coast of Northern Germany is selected as the study area.
@@ -68,6 +69,24 @@ Map the flood the same way as we have done when processing locally.
 flood.decision(bbox=bbox, datetime=time_range).compute()
 ```
 
+### User Interface
+
+
+It is also possible to run the workflow in an user-friendly interface instead of the Jupyter notebooks, as shown below:
+
+
+![alt text](<docs/images/Screenshot from 2025-04-03 13-56-05.png>)
+
+
+To access it, simyply run the in terminal the command:
+
+```bash
+floodmap
+```
+
+It will open the GUI in the web browser.
+
+
 ## Contributing Guidelines
 
 Please find the contributing guielines in the specific file [here](CONTRIBUTING.md).
diff --git a/docs/images/Screenshot from 2025-04-03 13-56-05.png b/docs/images/Screenshot from 2025-04-03 13-56-05.png
new file mode 100644
index 0000000..63c7bb7
Binary files /dev/null and b/docs/images/Screenshot from 2025-04-03 13-56-05.png differ
diff --git a/setup.cfg b/setup.cfg
index ddf0fe6..9780d3d 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -27,7 +27,7 @@ include_package_data = True
 install_requires =
     pystac_client
     odc-stac
-    dask[distributed]
+    dask[distributed]==2025.2.0
     rioxarray
 
 [options.packages.find]
@@ -55,3 +55,12 @@ dev =
     datashader
     pre-commit
     rich
+app =
+    flask
+    flask_cors
+    panel
+    hvplot
+
+[options.entry_points]
+console_scripts =
+    floodmap = dask_flood_mapper.cli:main
\ No newline at end of file
diff --git a/src/dask_flood_mapper/app.py b/src/dask_flood_mapper/app.py
new file mode 100644
index 0000000..c7e6676
--- /dev/null
+++ b/src/dask_flood_mapper/app.py
@@ -0,0 +1,70 @@
+from flask import Flask, request, jsonify, send_from_directory, render_template
+from flask_cors import CORS
+from dask_flood_mapper import flood
+import hvplot.xarray  # noqa
+import os
+import panel as pn
+
+template_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "templates"))
+static_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "static"))
+app = Flask(__name__, template_folder=template_dir, static_folder=static_dir)
+CORS(app)  # Allow frontend requests
+
+
+@app.route("/")
+def index():
+    return render_template("index.html")
+
+
+@app.route("/check_flood", methods=["POST"])
+def check_flood_status():
+    data = request.json
+    bbox = data.get("bbox")
+    time_range = data.get("time_range")
+
+    if not bbox or len(bbox) != 4:
+        return jsonify({"error": "Invalid bounding box"}), 400
+    if not time_range:
+        return jsonify({"error": "Invalid time range"}), 400
+    print("####### time range: ", time_range)
+    try:
+        # Call flood detection function
+        fd = flood.decision(bbox=bbox, datetime=time_range).compute()
+        print("################### calculation done")
+
+        fd_plot = fd.hvplot.image(
+            x="x",
+            y="y",
+            rasterize=True,
+            geo=True,
+            tiles=True,
+            project=True,
+            cmap=["rgba(0, 0, 1, 0.1)", "darkred"],
+            cticks=[(0, "non-flood"), (1, "flood")],
+            frame_width=600,
+            frame_height=400,
+        )
+        print("############### plot done")
+        img_path = img_path = os.path.join(app.static_folder, "flood_map.html")
+        pn.panel(fd_plot).save(img_path, embed=True)
+        if os.path.exists(img_path):
+            print("############## Image saved successfully.")
+        else:
+            print("################ Failed to save the image.")
+
+        return jsonify({"image_url": "static/flood_map.html"}), 200
+
+    except Exception as e:
+        print(f"############## Error: {e}")
+        return jsonify({"error": str(e)}), 500
+
+
+@app.route("/static/<path:filename>")
+def static_file(filename):
+    return send_from_directory(
+        os.path.join(os.path.dirname(__file__), "static"), filename
+    )
+
+
+if __name__ == "__main__":
+    app.run(debug=True, port=5000)
diff --git a/src/dask_flood_mapper/cli.py b/src/dask_flood_mapper/cli.py
new file mode 100644
index 0000000..db362a7
--- /dev/null
+++ b/src/dask_flood_mapper/cli.py
@@ -0,0 +1,20 @@
+import webbrowser
+import threading
+from dask_flood_mapper.app import app
+
+
+def open_browser():
+    webbrowser.open_new("http://127.0.0.1:5000")
+
+
+def main():
+    threading.Timer(1.5, open_browser).start()
+    app.run()
+
+
+print("🧭 Flask template folder:", app.template_folder)
+print("📁 Static folder:", app.static_folder)
+
+
+# if __name__ == "__main__":
+#   main()
diff --git a/src/dask_flood_mapper/static/.gitkeep b/src/dask_flood_mapper/static/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/dask_flood_mapper/templates/index.html b/src/dask_flood_mapper/templates/index.html
new file mode 100644
index 0000000..40c779b
--- /dev/null
+++ b/src/dask_flood_mapper/templates/index.html
@@ -0,0 +1,105 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Flood Detection</title>
+    <link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" />
+    <script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
+    <style>
+        #map { height: 500px; width: 100%; }
+    </style>
+</head>
+<body>
+
+    <h1>Flood Detection Map</h1>
+    <p>Click once to select the first point, then again to complete the box.</p>
+    
+    <div id="map"></div>
+    
+    <label>Start Date: <input type="date" id="startDate"></label>
+    <label>End Date: <input type="date" id="endDate"></label>
+    
+    <button onclick="checkFlood()" style="margin-top: 10px;">Check Flood</button>
+
+    <h3>Selected Parameters:</h3>
+    <pre id="debugInfo"></pre>  <!-- 🔹 Debugging section -->
+    
+    <h3>Flood Map Result:</h3>
+    <pre id="result"></pre>
+
+    
+    <iframe id="floodIframe" style="display:none; width: 100%; height: 600px; border: none;"></iframe>
+
+    <script>
+        let map = L.map('map').setView([0, 0], 2);
+        L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
+
+        let firstClick = null;
+        let rectangle = null;
+
+        map.on('click', function (e) {
+            if (!firstClick) {
+                firstClick = e.latlng;
+            } else {
+                let secondClick = e.latlng;
+                let bounds = [[firstClick.lat, firstClick.lng], [secondClick.lat, secondClick.lng]];
+
+                if (rectangle) map.removeLayer(rectangle);
+                rectangle = L.rectangle(bounds, { color: 'red' }).addTo(map);
+
+                firstClick = null; // Reset for next selection
+            }
+        });
+
+        async function checkFlood() {
+            if (!rectangle) {
+                alert("Please select a bounding box first.");
+                return;
+            }
+
+            let startDate = document.getElementById("startDate").value;
+            let endDate = document.getElementById("endDate").value;
+            if (!startDate || !endDate) {
+                alert("Please select a valid time range.");
+                return;
+            }
+
+            let timeRange = `${startDate}/${endDate}`;
+            let bounds = rectangle.getBounds();
+            let bbox = [bounds.getWest(), bounds.getSouth(), bounds.getEast(), bounds.getNorth()];
+
+            // 🔹 Display debug info before sending the request
+            document.getElementById("debugInfo").textContent = `Bounding Box: ${JSON.stringify(bbox)}\nTime Range: ${timeRange}`;
+
+            let response = await fetch("http://127.0.0.1:5000/check_flood", {
+                method: "POST",
+                headers: { "Content-Type": "application/json" },
+                body: JSON.stringify({ bbox, time_range: timeRange })
+            });
+
+            const data = await response.json();
+            console.log("Response received:", data);
+
+            document.addEventListener("DOMContentLoaded", () => {
+                const iframe = document.getElementById('floodIframe');
+                if (!iframe) {
+                    console.error("Iframe not found. Make sure the HTML has rendered fully before accessing it.");
+                }
+            });
+
+
+            if (data.image_url) {
+                console.log("Image URL:", data.image_url);  // Debug log
+                const iframe = document.getElementById('floodIframe');
+                iframe.src = data.image_url + '?' + new Date().getTime(); // Prevent caching
+                iframe.style.display = 'block';
+            } else {
+                console.error("Failed to load image URL.");
+            }
+            
+        }
+    </script>
+
+</body>
+</html>


Reviews:
user: login: MartinSchobben
body: This looks already great! Is this work in progress?
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-03T13:05:53Z
user: login: thaisbeham
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-03T13:12:15Z
user: login: MartinSchobben
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-03T13:14:54Z
user: login: thaisbeham
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-03T13:20:25Z
user: login: MartinSchobben
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-03T13:28:40Z
user: login: thaisbeham
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-04T09:22:34Z
user: login: MartinSchobben
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-04T09:38:05Z
user: login: MartinSchobben
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-04T10:42:45Z
user: login: thaisbeham
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-04T12:09:50Z
user: login: thaisbeham
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-07T14:06:05Z
user: login: MartinSchobben
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-08T12:22:17Z
user: login: MartinSchobben
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-08T13:01:40Z
user: login: MartinSchobben
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-09T10:45:12Z
user: login: MartinSchobben
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T10:54:26Z

Review comments:
path: backend/app.py
user: login: MartinSchobben
body: Should the dependencies go to the package requirements? and are these then for documentation only or the standard installation?
created_at: 2025-04-03T13:03:47Z
author_association: COLLABORATOR
path: notebooks/01_local_dask.ipynb
user: login: MartinSchobben
body: I guess this can be removed from the PR
created_at: 2025-04-03T13:05:25Z
author_association: COLLABORATOR
path: notebooks/01_local_dask.ipynb
user: login: thaisbeham
body: I tried but I couldn't manage to do it, do you know how to do it? since the only change is the environment and I cant stage the file if only it is changed, I also tried to reset the file and didnt work as well.
created_at: 2025-04-03T13:12:15Z
author_association: COLLABORATOR
path: notebooks/01_local_dask.ipynb
user: login: MartinSchobben
body: You can `git rebase -i` and change the commit where you added the file 
created_at: 2025-04-03T13:14:54Z
author_association: COLLABORATOR
path: backend/app.py
user: login: thaisbeham
body: not sure, what do you think is better?
created_at: 2025-04-03T13:20:25Z
author_association: COLLABORATOR
path: backend/app.py
user: login: MartinSchobben
body: We could also make a new section in the setup.cfg under extra.requires. For example `app` then one needs to install like `pip install dask_flood_mapper[app]`.

created_at: 2025-04-03T13:28:40Z
author_association: COLLABORATOR
path: notebooks/01_local_dask.ipynb
user: login: thaisbeham
body: fixed
created_at: 2025-04-04T09:22:34Z
author_association: COLLABORATOR
diff_hunk: @@ -68,6 +68,28 @@ Map the flood the same way as we have done when processing locally.
 flood.decision(bbox=bbox, datetime=time_range).compute()
 ```
 
+### User Interface
+
+It is also possible to run the workflow in user-friendly interface instead of the notebooks, as shown below:
+
+
+![alt text](<Screenshot from 2025-04-03 13-56-05.png>)
+
+To access it, being in the root directory, run the in terminal:
+
+```bash
path: README.md
user: login: MartinSchobben
body: Is there no way to expose this as a python function or through the CLI. Now you pip install the package and you have to find the installation site, move in the directory, and launch the app. 
created_at: 2025-04-04T09:37:57Z
author_association: COLLABORATOR
path: Screenshot from 2025-04-03 13-56-05.png
user: login: MartinSchobben
body: Can you also put this in the docs/images
created_at: 2025-04-04T10:42:41Z
author_association: COLLABORATOR
path: Screenshot from 2025-04-03 13-56-05.png
user: login: thaisbeham
body: yes, I simply forgot this folder exists
created_at: 2025-04-04T12:09:50Z
author_association: COLLABORATOR
diff_hunk: @@ -68,6 +68,28 @@ Map the flood the same way as we have done when processing locally.
 flood.decision(bbox=bbox, datetime=time_range).compute()
 ```
 
+### User Interface
+
+It is also possible to run the workflow in user-friendly interface instead of the notebooks, as shown below:
+
+
+![alt text](<Screenshot from 2025-04-03 13-56-05.png>)
+
+To access it, being in the root directory, run the in terminal:
+
+```bash
path: README.md
user: login: thaisbeham
body: I improved it a bit by opening the browser automatically without the need to click on a link but the user still needs to run a command in the terminal. I am currently working on skipping the terminal step and being able to open the UI by only clicking on a file. If it doesn't work I will expose it as a CLI command as you mentioned.
created_at: 2025-04-07T14:06:05Z
author_association: COLLABORATOR
diff_hunk: @@ -68,6 +68,28 @@ Map the flood the same way as we have done when processing locally.
 flood.decision(bbox=bbox, datetime=time_range).compute()
 ```
 
+### User Interface
+
+It is also possible to run the workflow in user-friendly interface instead of the notebooks, as shown below:
+
+
+![alt text](<Screenshot from 2025-04-03 13-56-05.png>)
+
+To access it, being in the root directory, run the in terminal:
+
+```bash
path: README.md
user: login: MartinSchobben
body: The problem seems that you have not included the app in the python module. So, this way it will not be installed with pip.
created_at: 2025-04-08T12:21:31Z
author_association: COLLABORATOR
diff_hunk: @@ -68,6 +68,28 @@ Map the flood the same way as we have done when processing locally.
 flood.decision(bbox=bbox, datetime=time_range).compute()
 ```
 
+### User Interface
+
+It is also possible to run the workflow in user-friendly interface instead of the notebooks, as shown below:
+
+
+![alt text](<Screenshot from 2025-04-03 13-56-05.png>)
+
+To access it, being in the root directory, run the in terminal:
+
+```bash
path: README.md
user: login: MartinSchobben
body: I created a PR on your personal branch, so you can see how it should be
created_at: 2025-04-08T13:01:39Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,74 @@
+from flask import Flask, request, jsonify, send_from_directory, render_template
+from flask_cors import CORS
+from dask_flood_mapper import flood
+from dask.distributed import Client
+import hvplot.xarray  # noqa
+import os
+import panel as pn
+
+template_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "templates"))
+static_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "static"))
+app = Flask(__name__, template_folder=template_dir, static_folder=static_dir)
+CORS(app)  # Allow frontend requests
+
+
+@app.route("/")
+def index():
+    return render_template("index.html")
+
+
+@app.route("/check_flood", methods=["POST"])
+def check_flood_status():
+    data = request.json
+    bbox = data.get("bbox")
+    time_range = data.get("time_range")
+
+    if not bbox or len(bbox) != 4:
+        return jsonify({"error": "Invalid bounding box"}), 400
+    if not time_range:
+        return jsonify({"error": "Invalid time range"}), 400
+    print("####### time range: ", time_range)
+    try:
+        # Call flood detection function
+        client = Client(  # noqa
path: src/dask_flood_mapper/app.py
user: login: MartinSchobben
body: We should not be setting the client, but use dask defaults, as we don't know the users machine
created_at: 2025-04-09T10:52:48Z
author_association: COLLABORATOR

PR comments:
user: login: thaisbeham
created_at: 2025-04-03T13:16:02Z
body: > This looks already great! Is this work in progress?

I can still do some improvements like centralized the output image, or display a error message if the end date is lower than the start date, or just make it look better in general but I would do it later after I finalize the the validation notebook, 
author_association: COLLABORATOR
user: login: MartinSchobben
created_at: 2025-04-03T13:20:42Z
body: That sounds about right. What about documentation? Is it clear for the user how to fire up the app?
author_association: COLLABORATOR
user: login: MartinSchobben
created_at: 2025-04-09T10:52:05Z
body: Would it be possible to add a progress bar?
author_association: COLLABORATOR
user: login: thaisbeham
created_at: 2025-04-09T11:25:14Z
body: > Would it be possible to add a progress bar?

In the terminal or in the UI?
author_association: COLLABORATOR