From 35fba6bcb7a3b45a1e02323ae5af0e4666cdc500 Mon Sep 17 00:00:00 2001 From: Michael Mann Date: Tue, 28 May 2024 22:49:18 -0400 Subject: [PATCH 1/5] interval working --- osm_leaderboard/dash_app.py | 183 ++++++++++++++++-------------------- 1 file changed, 83 insertions(+), 100 deletions(-) diff --git a/osm_leaderboard/dash_app.py b/osm_leaderboard/dash_app.py index 4c12515..094eb14 100644 --- a/osm_leaderboard/dash_app.py +++ b/osm_leaderboard/dash_app.py @@ -6,9 +6,9 @@ from dash.dependencies import Input, Output, State import yaml import requests -from multiprocessing import Pool, freeze_support import pandas as pd import geopandas as gpd +from multiprocessing import Pool, freeze_support import webbrowser from threading import Timer import os @@ -41,7 +41,7 @@ dcc.Upload( id="upload-data", children=html.Div( - ["Drag and Drop or ", html.A("Select \n config.yaml file")] + ["Drag and Drop or ", html.A("Select a config.yaml file")] ), style={ "width": "100%", @@ -64,6 +64,15 @@ dash_table.DataTable(id="table"), ], ), + dcc.Interval( + id="interval-component", + interval=0.1 * 60 * 1000, # in milliseconds + n_intervals=0, + max_intervals=12, # Since 5min * 12 = 60min + ), + dcc.Store( + id="stored-data" + ), # Hidden storage for persisting data across callbacks ] ) @@ -75,10 +84,6 @@ def convert_to_iso8601(date_str): return None -# Logging application start -logging.info("Starting application setup.") - - # Function to fetch node count for a username def fetch_node_count(username, newer_date, bbox): logging.debug( @@ -115,110 +120,87 @@ def fetch_node_count(username, newer_date, bbox): return username, "N/A" -# Define a callback to handle the uploaded file +# Callback to handle the file upload and initialize data @app.callback( - [ - Output("output-data-upload", "children"), - Output("map", "srcDoc"), - Output("table", "columns"), - Output("table", "data"), - ], + Output("stored-data", "data"), Input("upload-data", "contents"), State("upload-data", "filename"), ) -def update_output(content, name): - logging.info("Received file upload.") - - if content is not None: - logging.info(f"File name: {name}") - content_type, content_string = content.split(",") +def handle_upload(contents, filename): + if contents: + content_type, content_string = contents.split(",") decoded = base64.b64decode(content_string) try: - if "yaml" in name: - # Assume that the user uploaded a yaml file + if "yaml" in filename: config = yaml.safe_load(io.StringIO(decoded.decode("utf-8"))) - # Use the config data to update your app - bbox = config["bbox"] - usernames = config["usernames"] - newer_date = config.get("newer_than_date") - newer_date = convert_to_iso8601(newer_date) - - with Pool(processes=len(usernames)) as pool: - results = pool.starmap( - fetch_node_count, - [(username, newer_date, bbox) for username in usernames], - ) - - # Initialize a dictionary to store the count of nodes added by each user - user_node_counts = dict(results) - - # convert Node_count to integers using dictionary comprehension - user_node_counts = {k: int(v) for k, v in user_node_counts.items()} - - out = pd.DataFrame( - user_node_counts.items(), columns=["Username", "Node_Count"] - ) - # sort in descending order - df = out.sort_values(by="Node_Count", ascending=False) - - # Create a simple GeoDataFrame with a bounding box - min_lat, min_lon, max_lat, max_lon = map(float, bbox.split(",")) - bbox_gpd = gpd.GeoDataFrame.from_features( - [ - { - "type": "Feature", - "properties": {}, - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [min_lon, min_lat], - [min_lon, max_lat], - [max_lon, max_lat], - [max_lon, min_lat], - [min_lon, min_lat], - ] - ], - }, - } - ], - crs="EPSG:4326", - ) - - map_obj = bbox_gpd.explore( - style_kwds={"fillColor": "blue", "color": "black"} - ) - - map_src = map_obj.get_root().render() - table_columns = [{"name": i, "id": i} for i in df.columns] - table_data = df.to_dict("records") - - return ( - # 'File "{}" successfully uploaded.'.format(name), - "", - map_src, - table_columns, - table_data, - ) + return config # Store the entire configuration except Exception as e: - print(e) - return ( - "There was an error processing this file.", - dash.no_update, - dash.no_update, - dash.no_update, + logging.error("Failed to process uploaded file:", exc_info=True) + return None + return None + + +# Callback to update data based on the interval +@app.callback( + [ + Output("output-data-upload", "children"), + Output("map", "srcDoc"), + Output("table", "columns"), + Output("table", "data"), + ], + Input("interval-component", "n_intervals"), + State("stored-data", "data"), +) +def update_data(n_intervals, stored_data): + if stored_data: + bbox = stored_data["bbox"] + usernames = stored_data["usernames"] + newer_date = convert_to_iso8601(stored_data.get("newer_than_date")) + + with Pool(processes=len(usernames)) as pool: + results = pool.starmap( + fetch_node_count, + [(username, newer_date, bbox) for username in usernames], ) - else: - logging.error("No content uploaded.") - return None, dash.no_update, dash.no_update, dash.no_update + user_node_counts = {username: count for username, count in results} + df = pd.DataFrame(user_node_counts.items(), columns=["Username", "Node_Count"]) + df.sort_values(by="Node_Count", ascending=False, inplace=True) + + # Generate a simple GeoDataFrame with a bounding box + min_lat, min_lon, max_lat, max_lon = map(float, bbox.split(",")) + bbox_gpd = gpd.GeoDataFrame.from_features( + [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [min_lon, min_lat], + [min_lon, max_lat], + [max_lon, max_lat], + [max_lon, min_lat], + [min_lon, min_lat], + ] + ], + }, + } + ], + crs="EPSG:4326", + ) -# Define a function to stop the server -def stop_server(): - if platform.system() == "Windows": - os.kill(os.getpid(), signal.SIGTERM) - else: - os.kill(os.getpid(), signal.SIGINT) + map_obj = bbox_gpd.explore(style_kwds={"fillColor": "blue", "color": "black"}) + map_src = map_obj.get_root().render() + + return ( + f"Data updated at interval {n_intervals}", + map_src, + [{"name": i, "id": i} for i in df.columns], + df.to_dict("records"), + ) + return dash.no_update, dash.no_update, dash.no_update, dash.no_update def open_browser(port): @@ -226,7 +208,7 @@ def open_browser(port): def main(): - print('This app brought to you by https://pygis.io') + print("This app brought to you by https://pygis.io") logging.info("Executing main block.") # Ensure compatibility with Windows exe for threading if platform.system() == "Windows": @@ -253,6 +235,7 @@ def main(): if __name__ == "__main__": main() + # %% # build the app with pyinstaller # pyinstaller --onefile --name leaderboard_linux dash_app.py From 383b9aa504c1fa4d61c59438f2af345249840a9e Mon Sep 17 00:00:00 2001 From: Michael Mann Date: Wed, 29 May 2024 22:04:13 -0400 Subject: [PATCH 2/5] initialize then update working --- osm_leaderboard/dash_app.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/osm_leaderboard/dash_app.py b/osm_leaderboard/dash_app.py index 094eb14..5993787 100644 --- a/osm_leaderboard/dash_app.py +++ b/osm_leaderboard/dash_app.py @@ -1,4 +1,5 @@ # %% + import base64 import io import dash @@ -11,8 +12,6 @@ from multiprocessing import Pool, freeze_support import webbrowser from threading import Timer -import os -import signal from flask import Flask import platform import socket @@ -66,9 +65,8 @@ ), dcc.Interval( id="interval-component", - interval=0.1 * 60 * 1000, # in milliseconds + interval=30 * 1000, # 30 seconds n_intervals=0, - max_intervals=12, # Since 5min * 12 = 60min ), dcc.Store( id="stored-data" @@ -148,8 +146,7 @@ def handle_upload(contents, filename): Output("table", "columns"), Output("table", "data"), ], - Input("interval-component", "n_intervals"), - State("stored-data", "data"), + [Input("interval-component", "n_intervals"), Input("stored-data", "data")], ) def update_data(n_intervals, stored_data): if stored_data: @@ -234,8 +231,6 @@ def main(): if __name__ == "__main__": main() - - # %% # build the app with pyinstaller # pyinstaller --onefile --name leaderboard_linux dash_app.py From 3534601a17f79fa2e1c68735930f076d4ee11643 Mon Sep 17 00:00:00 2001 From: Michael Mann Date: Wed, 29 May 2024 22:13:55 -0400 Subject: [PATCH 3/5] add max intervals --- osm_leaderboard/dash_app.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osm_leaderboard/dash_app.py b/osm_leaderboard/dash_app.py index 5993787..e8821a4 100644 --- a/osm_leaderboard/dash_app.py +++ b/osm_leaderboard/dash_app.py @@ -65,8 +65,9 @@ ), dcc.Interval( id="interval-component", - interval=30 * 1000, # 30 seconds + interval=60 * 1000, # 30 seconds n_intervals=0, + max_intervals=60, ), dcc.Store( id="stored-data" @@ -192,7 +193,7 @@ def update_data(n_intervals, stored_data): map_src = map_obj.get_root().render() return ( - f"Data updated at interval {n_intervals}", + f"Remaining updates: {60-n_intervals}", map_src, [{"name": i, "id": i} for i in df.columns], df.to_dict("records"), @@ -205,7 +206,6 @@ def open_browser(port): def main(): - print("This app brought to you by https://pygis.io") logging.info("Executing main block.") # Ensure compatibility with Windows exe for threading if platform.system() == "Windows": From 8ae9b14861e1c926224e8541ba86cd867c40b61d Mon Sep 17 00:00:00 2001 From: Michael Mann Date: Wed, 29 May 2024 22:18:34 -0400 Subject: [PATCH 4/5] readme merge main --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index c49b606..9d2bc46 100644 --- a/README.md +++ b/README.md @@ -71,3 +71,9 @@ In the browser window select or drag your `config.yaml` file to the upload box. # Demo ![OSM Leaderboard](https://github.com/mmann1123/OSM_LeaderBoard/blob/main/video/leaderboard3.gif?raw=true) + +# Credits +[![GWU Geography](https://github.com/mmann1123/OSM_LeaderBoard/blob/main/video/gw.png?raw=true)](https://geography.columbian.gwu.edu/) pygis.io YouthMappers +


+[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.11387666.svg)](https://doi.org/10.5281/zenodo.11387666) + From a314d551e79647d851a19b63af3e873e35d43b71 Mon Sep 17 00:00:00 2001 From: Michael Mann Date: Wed, 29 May 2024 22:22:58 -0400 Subject: [PATCH 5/5] add footer --- osm_leaderboard/dash_app.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/osm_leaderboard/dash_app.py b/osm_leaderboard/dash_app.py index e8821a4..e455ec3 100644 --- a/osm_leaderboard/dash_app.py +++ b/osm_leaderboard/dash_app.py @@ -72,6 +72,41 @@ dcc.Store( id="stored-data" ), # Hidden storage for persisting data across callbacks + html.Div( + [ + html.A( + html.Img( + src="https://github.com/mmann1123/OSM_LeaderBoard/blob/main/video/gw.png?raw=true", + style={"height": "70px", "margin-right": "10px"}, + ), + href="https://geography.columbian.gwu.edu/", + ), + html.A( + html.Img( + src="https://github.com/mmann1123/OSM_LeaderBoard/blob/main/video/pygis.png?raw=true", + style={"height": "70px", "margin-right": "10px"}, + ), + href="https://pygis.io", + ), + html.A( + html.Img( + src="https://github.com/mmann1123/OSM_LeaderBoard/blob/main/video/youthmappers.webp?raw=true", + style={"height": "70px"}, + ), + href="https://www.youthmappers.org/", + ), + html.Br(), + html.Br(), + html.Br(), + html.A( + html.Img( + src="https://zenodo.org/badge/DOI/10.5281/zenodo.11387666.svg" + ), + href="https://doi.org/10.5281/zenodo.11387666", + ), + ], + style={"textAlign": "center", "padding": "20px"}, + ), ] )