From 0db7e3912edfff02ef7543dd61747e560fc6d54b Mon Sep 17 00:00:00 2001 From: IndigoWizard Date: Sun, 17 Sep 2023 23:04:42 +0100 Subject: [PATCH 1/7] Added multipolygons handeling in geojson file processing function - upload_files_proc function now can read all geometries within the same geojson file, thus making it possible to create multiple polygons in the same geojson for a single time upload and clip the image collection to all exising aoi polygons instead of just the first one in the list --- app.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/app.py b/app.py index e45f662..b740d23 100644 --- a/app.py +++ b/app.py @@ -155,20 +155,22 @@ def upload_files_proc(upload_files): bytes_data = upload_file.read() # Parse GeoJSON data geojson_data = json.loads(bytes_data) - # Extract the coordinates from the GeoJSON data - coordinates = geojson_data['features'][0]['geometry']['coordinates'] - # Creating gee geometry object based on coordinates - geometry = ee.Geometry.Polygon(coordinates) - # Adding geometry to the list - geometry_aoi_list.append(geometry) - # Combine multiple geometries from same/different files + # Extract all the features' coordinates from the GeoJSON data + for feature in geojson_data['features']: + coordinates = feature['geometry']['coordinates'] + # Creating a geometry object based on coordinates + geometry = ee.Geometry.Polygon(coordinates) + # Adding geometry to the list + geometry_aoi_list.append(geometry) + # Combine multiple geometries from the same/different files if geometry_aoi_list: geometry_aoi = ee.Geometry.MultiPolygon(geometry_aoi_list) else: - # Set default geometry if no file uploaded + # Set a default geometry if no file is uploaded geometry_aoi = ee.Geometry.Point([27.98, 36.13]) return geometry_aoi + # Time input processing function def date_input_proc(input_date, time_range): end_date = input_date From 3fa27b0c52e9a9222a990f901bddb790f9c9f836 Mon Sep 17 00:00:00 2001 From: IndigoWizard Date: Tue, 19 Sep 2023 03:02:48 +0100 Subject: [PATCH 2/7] Dynamic map centering implemented dynamic map centering based on the centroid of the last uploaded GeoJSON file; - Updated the `upload_files_proc` function to calculate the centroid of each uploaded GeoJSON file and store it in the global variable `last_uploaded_centroid`. - Modified the main map setup to use the centroid coordinates stored in `last_uploaded_centroid` for dynamically centering the map. --- app.py | 38 ++++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/app.py b/app.py index b740d23..279864e 100644 --- a/app.py +++ b/app.py @@ -148,29 +148,38 @@ def clipCollection(image): collection = collection.map(clipCollection) return collection -# Uplaod function +# Upload function +# Define a global variable to store the centroid of the last uploaded geometry +last_uploaded_centroid = None + +# Upload function def upload_files_proc(upload_files): + # A global variable to track the latest geojson uploaded + global last_uploaded_centroid + # Setting up a variable that takes all polygons/geometries within the same/different geojson geometry_aoi_list = [] + for upload_file in upload_files: bytes_data = upload_file.read() - # Parse GeoJSON data geojson_data = json.loads(bytes_data) - # Extract all the features' coordinates from the GeoJSON data + for feature in geojson_data['features']: coordinates = feature['geometry']['coordinates'] - # Creating a geometry object based on coordinates geometry = ee.Geometry.Polygon(coordinates) - # Adding geometry to the list geometry_aoi_list.append(geometry) - # Combine multiple geometries from the same/different files + + # Update the last uploaded centroid + last_uploaded_centroid = geometry.centroid(maxError=1).getInfo()['coordinates'] + if geometry_aoi_list: geometry_aoi = ee.Geometry.MultiPolygon(geometry_aoi_list) else: - # Set a default geometry if no file is uploaded geometry_aoi = ee.Geometry.Point([27.98, 36.13]) + return geometry_aoi + # Time input processing function def date_input_proc(input_date, time_range): end_date = input_date @@ -274,8 +283,17 @@ def main(): #### User input section - END #### Map section - START - # Setting up main map - m = folium.Map(location=[36.45, 2.85], tiles=None, zoom_start=9, control_scale=True) + global last_uploaded_centroid + + # Create the initial map + if last_uploaded_centroid is not None: + latitude = last_uploaded_centroid[1] + longitude = last_uploaded_centroid[0] + m = folium.Map(location=[latitude, longitude], zoom_start=7, control_scale=True) + else: + # Default location if no file is uploaded + m = folium.Map(location=[36.45, 2.85], zoom_start=5, control_scale=True) + ### BASEMAPS - START ## Primary basemaps @@ -349,7 +367,7 @@ def satImageMask(sat_image): updated_ndvi = satImageMask(updated_ndvi) # ##### NDVI classification: 7 classes - def classify_ndvi(masked_image): # better used an masked image to avoid water bodies obstracting the result + def classify_ndvi(masked_image): # better use a masked image to avoid water bodies obstracting the result as possible ndvi_classified = ee.Image(masked_image) \ .where(masked_image.gte(0).And(masked_image.lt(0.15)), 1) \ .where(masked_image.gte(0.15).And(masked_image.lt(0.25)), 2) \ From e88721d857c29e3be8169389f1dc5ebe9b77d809 Mon Sep 17 00:00:00 2001 From: IndigoWizard Date: Sat, 23 Sep 2023 23:23:10 +0100 Subject: [PATCH 3/7] accessibility fix --- README.md | 12 +++++++++++- app.py | 8 ++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c33f98e..f72ca44 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # NDVI-Viewer -View and compare NDVI values of an area of interest between two time periods. +Monitor Vegetation Health by Viewing & Comparing NDVI Values Through Time and Location with Sentinel-2 Satellite Images on The Fly! Streamlit App: https://ndvi-viewer.streamlit.app/ ### User guide @@ -10,6 +10,16 @@ Streamlit App: https://ndvi-viewer.streamlit.app/ - Select Cloud Coverate Rate: Set the cloude coverage value for better quality images or for larger dataset in your image collection. - Additionally, for people with colorblind disability, it is possible to pick a color palette that's colorblind friendly with most common colorblindness types. +### Preview: + +![](https://www.pixenli.com/image/MeljR-zA) + +### Note: + +The app is hosted on Streamlit Cloud for free and has resource limit (1 GB), sometimes the app may crash when exceeding the limit (too many operations). + +If the app crashes, it needs to be rebooted on dev end so DM me or rise an issue, no other options except deploying to a paid platform with padi resource increases. + ### Project structure ``` diff --git a/app.py b/app.py index 279864e..c2aaef0 100644 --- a/app.py +++ b/app.py @@ -240,7 +240,7 @@ def main(): ## Accessibility: Color palette input st.info("Custom Color Palettes") - accessibility = st.selectbox("Accessibility: Colorblind-friendly Palettes", ["Normal", "Deuteranomaly", "Protanomaly", "Tritanomaly", "Achromatopsia"]) + accessibility = st.selectbox("Accessibility: Colorblind-friendly Palettes", ["Normal", "Deuteranopia", "Protanopia", "Tritanopia", "Achromatopsia"]) # Define default color palettes: used in map layers & map legend default_ndvi_palette = ["#ffffe5", "#f7fcb9", "#78c679", "#41ab5d", "#238443", "#005a32"] @@ -250,13 +250,13 @@ def main(): ndvi_palette = default_ndvi_palette.copy() reclassified_ndvi_palette = default_reclassified_ndvi_palette.copy() - if accessibility == "Deuteranomaly": + if accessibility == "Deuteranopia": ndvi_palette = ["#fffaa1","#f4ef8e","#9a5d67","#573f73","#372851","#191135"] reclassified_ndvi_palette = ["#95a600","#92ed3e","#affac5","#78ffb0","#69d6c6","#22459c","#000e69"] - elif accessibility == "Protanomaly": + elif accessibility == "Protanopia": ndvi_palette = ["#a6f697","#7def75","#2dcebb","#1597ab","#0c677e","#002c47"] reclassified_ndvi_palette = ["#95a600","#92ed3e","#affac5","#78ffb0","#69d6c6","#22459c","#000e69"] - elif accessibility == "Tritanomaly": + elif accessibility == "Tritanopia": ndvi_palette = ["#cdffd7","#a1fbb6","#6cb5c6","#3a77a5","#205080","#001752"] reclassified_ndvi_palette = ["#ed4700","#ed8a00","#e1fabe","#99ff94","#87bede","#2e40cf","#0600bc"] elif accessibility == "Achromatopsia": From 44c391938d7a371be44a4430d0acfa91c4f26c54 Mon Sep 17 00:00:00 2001 From: IndigoWizard Date: Tue, 26 Sep 2023 00:28:42 +0100 Subject: [PATCH 4/7] Improved GeoJSON handling for varied file structures - upgrading the upload_files_proc funciton to look for geometry within a geojson file no matter its structure to match with geojson files made from other sources - Fixing the repetitive osm tile --- app.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/app.py b/app.py index c2aaef0..40ca34f 100644 --- a/app.py +++ b/app.py @@ -163,13 +163,24 @@ def upload_files_proc(upload_files): bytes_data = upload_file.read() geojson_data = json.loads(bytes_data) - for feature in geojson_data['features']: - coordinates = feature['geometry']['coordinates'] - geometry = ee.Geometry.Polygon(coordinates) - geometry_aoi_list.append(geometry) - - # Update the last uploaded centroid - last_uploaded_centroid = geometry.centroid(maxError=1).getInfo()['coordinates'] + if 'features' in geojson_data and isinstance(geojson_data['features'], list): + # Handle GeoJSON files with a 'features' list + features = geojson_data['features'] + elif 'geometries' in geojson_data and isinstance(geojson_data['geometries'], list): + # Handle GeoJSON files with a 'geometries' list + features = [{'geometry': geo} for geo in geojson_data['geometries']] + else: + # handling cases of unexpected file format or missing 'features' or 'geometries' + continue + + for feature in features: + if 'geometry' in feature and 'coordinates' in feature['geometry']: + coordinates = feature['geometry']['coordinates'] + geometry = ee.Geometry.Polygon(coordinates) if feature['geometry']['type'] == 'Polygon' else ee.Geometry.MultiPolygon(coordinates) + geometry_aoi_list.append(geometry) + + # Update the last uploaded centroid + last_uploaded_centroid = geometry.centroid(maxError=1).getInfo()['coordinates'] if geometry_aoi_list: geometry_aoi = ee.Geometry.MultiPolygon(geometry_aoi_list) @@ -179,7 +190,6 @@ def upload_files_proc(upload_files): return geometry_aoi - # Time input processing function def date_input_proc(input_date, time_range): end_date = input_date @@ -289,10 +299,10 @@ def main(): if last_uploaded_centroid is not None: latitude = last_uploaded_centroid[1] longitude = last_uploaded_centroid[0] - m = folium.Map(location=[latitude, longitude], zoom_start=7, control_scale=True) + m = folium.Map(location=[latitude, longitude], tiles=None, zoom_start=7, control_scale=True) else: # Default location if no file is uploaded - m = folium.Map(location=[36.45, 2.85], zoom_start=5, control_scale=True) + m = folium.Map(location=[36.45, 2.85], tiles=None, zoom_start=5, control_scale=True) ### BASEMAPS - START From c25465858096a61827f561cbf4f18c50d0b9689d Mon Sep 17 00:00:00 2001 From: IndigoWizard Date: Wed, 25 Oct 2023 03:06:19 +0100 Subject: [PATCH 5/7] UI update to match streamlit 1.27.2 class names - updating the app from streamlit 1.25.0 to 1.27.2 - updating css class names accord to the new streamlit verison - moving map legend css to main css - updating contribution section - rearanging contribution - about sections order - updating map zoom on aoi --- app.py | 74 +++++++++++++++++++++++++++++++++-------------- requirements.txt | Bin 204 -> 204 bytes 2 files changed, 52 insertions(+), 22 deletions(-) diff --git a/app.py b/app.py index 40ca34f..d09edff 100644 --- a/app.py +++ b/app.py @@ -23,7 +23,7 @@ """ """, unsafe_allow_html=True) @@ -151,8 +178,6 @@ def clipCollection(image): # Upload function # Define a global variable to store the centroid of the last uploaded geometry last_uploaded_centroid = None - -# Upload function def upload_files_proc(upload_files): # A global variable to track the latest geojson uploaded global last_uploaded_centroid @@ -217,8 +242,8 @@ def main(): - [Interpreting the Results](#interpreting-the-results) - [Environmental Index](#using-an-environmental-index-ndvi) - [Data](#data-sentinel-2-imagery-and-l2a-product) - - [About](#about) - [Contribution](#contribute-to-the-app) + - [About](#about) - [Credit](#credit) """) @@ -299,10 +324,10 @@ def main(): if last_uploaded_centroid is not None: latitude = last_uploaded_centroid[1] longitude = last_uploaded_centroid[0] - m = folium.Map(location=[latitude, longitude], tiles=None, zoom_start=7, control_scale=True) + m = folium.Map(location=[latitude, longitude], tiles=None, zoom_start=12, control_scale=True) else: # Default location if no file is uploaded - m = folium.Map(location=[36.45, 2.85], tiles=None, zoom_start=5, control_scale=True) + m = folium.Map(location=[36.45, 10.85], tiles=None, zoom_start=4, control_scale=True) ### BASEMAPS - START @@ -444,7 +469,7 @@ def classify_ndvi(masked_image): # better use a masked image to avoid water bodi with col3: # Create an HTML legend for NDVI classes ndvi_legend_html = """ -
+
Raw NDVI
@@ -462,8 +487,8 @@ def classify_ndvi(masked_image): # better use a masked image to avoid water bodi with col4: # Create an HTML legend for NDVI classes reclassified_ndvi_legend_html = """ -
-
Reclassified NDVI
+
+
NDVI Classes
  • Absent Vegetation. (Water/Clouds/Built-up/Rocks/Sand Surfaces..)
  • Bare Soil.
  • @@ -529,17 +554,16 @@ def classify_ndvi(masked_image): # better use a masked image to avoid water bodi #### Miscs Info - END - #### About App - START - st.subheader("About:") - st.markdown("This project was first developed by me ([IndigoWizard](https://github.com/IndigoWizard)) and [Emmarie-Ahtunan](https://github.com/Emmarie-Ahtunan) as a submission to the **Environemental Data Challenge** of [Global Hack Week: Data](https://ghw.mlh.io/) by [Major League Hacking](https://mlh.io/).
    I continued developing the base project to make it a feature-complete app. Check the project's GitHub Repo here: [IndigoWizard/NDVI-Viewer](https://github.com/IndigoWizard/NDVI-Viewer)", unsafe_allow_html=True) - st.image("https://www.pixenli.com/image/Hn1xkB-6") - #### About App - END - #### Contributiuon - START st.header("Contribute to the App") - st.markdown(""" + con1, con2 = st.columns(2) + con1.image("https://www.pixenli.com/image/SoL3iZMG") + con2.markdown(""" Contributions are welcome from the community to help improve this app! Whether you're interested in fixing bugs 🐞, implementing a new feature 🌟, or enhancing the user experience 🪄, your contributions are valuable. - + + The project is listed under **Hacktoberfest** lalbel for those of you [Hacktoberfest](https://hacktoberfest.com/) enthusiasts! Since the reward for contributing 4 PRs is getting a tree planted in your name through [TreeNation](https://tree-nation.com/), I see it fits the theme of this project. + """) + st.markdown(""" #### Ways to Contribute - **Report Issues**: If you come across any bugs, issues, or unexpected behavior, please report them in the [GitHub Issue Tracker](https://github.com/IndigoWizard/NDVI-Viewer/issues). @@ -551,6 +575,12 @@ def classify_ndvi(masked_image): # better use a masked image to avoid water bodi #### Contributiuon - START + #### About App - START + st.subheader("About:") + st.markdown("This project was first developed by me ([IndigoWizard](https://github.com/IndigoWizard)) and [Emmarie-Ahtunan](https://github.com/Emmarie-Ahtunan) as a submission to the **Environemental Data Challenge** of [Global Hack Week: Data](https://ghw.mlh.io/) by [Major League Hacking](https://mlh.io/).
    I continued developing the base project to make it a feature-complete app. Check the project's GitHub Repo here: [IndigoWizard/NDVI-Viewer](https://github.com/IndigoWizard/NDVI-Viewer)", unsafe_allow_html=True) + st.image("https://www.pixenli.com/image/Hn1xkB-6") + #### About App - END + #### Credit - START st.subheader("Credit:") st.markdown("""The app was developped by [IndigoWizard](https://github.com/IndigoWizard) using; [Streamlit](https://streamlit.io/), [Google Earth Engine](https://github.com/google/earthengine-api) Python API, [geemap](https://github.com/gee-community/geemap), [Folium](https://github.com/python-visualization/folium). Agriculture icons created by dreamicons - Flaticon""", unsafe_allow_html=True) diff --git a/requirements.txt b/requirements.txt index 03a127532302dacaeb9b852f5c5420dd80b2fbae..efb2a05529f56850c64898348b6ce422163a50de 100644 GIT binary patch delta 18 ZcmX@Zc!qI85UV+Z9)r Date: Sat, 28 Oct 2023 19:31:35 +0100 Subject: [PATCH 6/7] updating contribution guideline --- .github/CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 71e7777..15ee8a4 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -10,7 +10,7 @@ All contribution is welcome (_adding features✨, bug fix🔧, bug report🐛, d - Open a new issue stating the subject of your upcoming contribution if no already existing open issue is related to the subject. - If it's a **one-time issue**, ask a maintainer to assign you to the issue before starting to work on it. - Always make sure your own fork of the repo is up to date (sync) with the original repo. - +- Updating the `Streamlit` package changes the class names of elements > Requires updating CSS class names. ### Issues - Issues are labbeled to make them easier for contributors/mantainers to identify. @@ -19,7 +19,7 @@ All contribution is welcome (_adding features✨, bug fix🔧, bug report🐛, d ### Branches -I'm using integrated GitFfow for this project, so the setup is: +I'm using integrated GitFlow for this project, so the setup is: **Branches:** - Master = streamlit-app From d6b2c7fe24ca99eb59b30abddd1488e651edd95b Mon Sep 17 00:00:00 2001 From: IndigoWizard Date: Sat, 28 Oct 2023 20:27:23 +0100 Subject: [PATCH 7/7] Updating readme.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f72ca44..2ff70c5 100644 --- a/README.md +++ b/README.md @@ -40,4 +40,6 @@ This project was first developed as a submission to the Environemental Data Chal ### Credit -The app was developped by [IndigoWizard](https://github.com/IndigoWizard) and [Emmarie-Ahtunan](https://github.com/Emmarie-Ahtunan) using; Streamlit, Google Earth Engine Python API, geemap, Folium. Agriculture icons created by dreamicons - Flaticon +The app was developped by [IndigoWizard](https://github.com/IndigoWizard) using; Streamlit, Google Earth Engine Python API, geemap, Folium. Agriculture icons created by dreamicons - Flaticon + +**Contributors**: [Emmarie-Ahtunan](https://github.com/Emmarie-Ahtunan)