Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat : Added new markers types and live alerts features #23

Merged
merged 35 commits into from
Dec 3, 2020
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
b6ffde7
adding pyronear logo for navbar
Akilditu Oct 31, 2020
f5f2a2a
feat: designing a new navbar
Akilditu Oct 31, 2020
a3fc215
feat: created a one page dashboard on homepage
Akilditu Oct 31, 2020
16fb89e
fix: fixing a miss-operating callback
Akilditu Oct 31, 2020
d0b8714
fix: corrected some typo issues in comments
Akilditu Nov 2, 2020
4bb3088
feat: switching pyro_logo call from the one in repo to the hosted one…
Akilditu Nov 3, 2020
1ed7ff7
feat: deleted repo stored pyro_logo png
Akilditu Nov 3, 2020
b46ad7d
fix: deleted unused Path import
Akilditu Nov 3, 2020
9754c40
Merge remote-tracking branch 'upstream/master'
Akilditu Nov 17, 2020
1d593b1
blank line at the beginning of graphs.py
nrslt Oct 14, 2020
f45f07e
styling errors
nrslt Oct 14, 2020
2dd12df
Marker popup associated with click feature and video display function
VincGargasson Nov 9, 2020
3d53902
updating typo error and codacy docstring checks
VincGargasson Nov 17, 2020
3213a60
Just minor typos corrections :)
nrslt Nov 11, 2020
c50eff2
just some minor typos corrections !
nrslt Nov 11, 2020
f7d81ee
correction of flake8 errors
VincGargasson Nov 17, 2020
d63151f
2nd correction of flake8 errors
VincGargasson Nov 17, 2020
39c95c3
dl.markers objects for alerts and sites
nrslt Nov 24, 2020
4d4b94a
changed the site marker to default
nrslt Nov 25, 2020
abc8e4a
feat: adding new live alert button and markers
Akilditu Nov 29, 2020
f7feada
feat: creating alert metadata to simulate API responses and provide i…
Akilditu Nov 29, 2020
9d0fb67
feat: adding new live alert button and markers
Akilditu Nov 29, 2020
11a710d
feat: adding new callbacks to handle live alert components interactions
Akilditu Nov 29, 2020
2a06a3c
refactor: light instantiation changes
Akilditu Nov 30, 2020
9dc1705
typo : deleting unused imports
Akilditu Nov 30, 2020
e650cce
typo: fixing missspelling
Akilditu Nov 30, 2020
a73ad41
Update utils.py
nrslt Nov 30, 2020
fbd6308
Update homepage.py
nrslt Nov 30, 2020
57293d5
Update homepage.py
nrslt Nov 30, 2020
cf39f68
refactor: merging 2 identical conditions
Akilditu Nov 30, 2020
e957a68
typo: modifying var types and wrong spellings
Akilditu Nov 30, 2020
2e826e1
Merge branch 'nicommit' into new-markers-live-alerts-features
Akilditu Nov 30, 2020
ae53eee
feat: adding ux corrections and deleting some duplicate contents
Akilditu Dec 3, 2020
d814ba3
refactor: adding alert_metada arg in every function that need its inputs
Akilditu Dec 3, 2020
64e46dc
typo : flake8 typo correction
Akilditu Dec 3, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 96 additions & 34 deletions app/alerts.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,28 @@
# Various modules provided by Dash to build the page layout
import dash_core_components as dcc
import dash_html_components as html
import dash_bootstrap_components as dbc
import dash_leaflet as dl
import dash_leaflet.express as dlx

# From navbar.py to add the navigation bar at the top of the page
from navbar import Navbar

# Various imports from utils.py, useful for both Alerts and Risks dashboards
from utils import map_style, build_info_object
from utils import map_style, build_info_object, build_live_alerts_metadata


# ------------------------------------------------------------------------------

# Map layer
# The following block is used to determine what layer we use for the map and enable the user to change it

# This function creates the button that allows users to change the map layer (satellite or topographic)
def build_layer_style_button():

button = html.Button(children='Activer la vue satellite',
id='layer_style_button')
id='layer_style_button',
className="btn btn-warning")
nrslt marked this conversation as resolved.
Show resolved Hide resolved

return html.Center(button)

Expand Down Expand Up @@ -68,15 +71,15 @@ def choose_layer_style(n_clicks):
# The following block is used to display the borders of the departments on the map

# Fetching the departments GeoJSON and building the related map attribute
def build_alerts_geojson():
def build_departments_geojson():

# We fetch the json file online and store it in the departments variable
with open(Path(__file__).parent.joinpath('data', 'departements.geojson'), 'rb') as response:
departments = json.load(response)

# We plug departments in a Dash Leaflet GeoJSON object that will be added to the map
geojson = dl.GeoJSON(data=departments,
id='geojson_alerts',
id='geojson_departments',
zoomToBoundsOnClick=True,
hoverStyle=dict(weight=3,
color='#666',
Expand All @@ -88,50 +91,108 @@ def build_alerts_geojson():


# ------------------------------------------------------------------------------
# Cameras
# The following block is dedicated to fetching information about cameras and displaying them on the map

# Sites markers
# Fetching the positions of detection units in a given department
def get_camera_positions(dpt_code=None):
def build_sites_markers(dpt_code=None):

# As long as the user does not click on a department, dpt_code is None and we return no device
if not dpt_code:
return None
# if not dpt_code:
# return None

# We read the csv file that locates the cameras and filter for the department of interest
camera_positions = pd.read_csv(Path(__file__).parent.joinpath('data', 'cameras.csv'), ';')
camera_positions = camera_positions[camera_positions['Département'] == int(dpt_code)].copy()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for the renaming from build_alerts_geojson to build_departments_geojson or from get_camera_positions to build_sites_markers, it makes the code much clearer! In the same vein, shouldn't we change camera_positions into site_positions?

# camera_positions = camera_positions[camera_positions['Département'] == int(dpt_code)].copy()

# We build a list of dictionaries containing the info of each camera
# We build a list of markers containing the info of each site/camera
markers = []
for _, row in camera_positions.iterrows():
for i, row in camera_positions.iterrows():
lat = row['Latitude']
lon = row['Longitude']
area = row['Tours']
alert_codis = row['Connexion Alerte CODIS']
site_name = row['Tours']
nb_device = row['Nombres Devices']
popup = ["Ville: {} <br>\
Connexion Alerte CODIS: {} <br>\
Nombres Devices: {}".format(area, alert_codis, nb_device)]
markers.append(dict(lat=lat,
lon=lon,
area=area,
alert_codis=alert_codis,
nb_device=nb_device,
popup=popup))
markers.append(dl.Marker(id=f'site_{i}', # Necessary to set an id for each marker to reteive callbacks
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
markers.append(dl.Marker(id=f'site_{i}', # Necessary to set an id for each marker to reteive callbacks
markers.append(dl.Marker(id=f'site_{i}', # Necessary to set an id for each marker to receive callbacks

Small typo in the comment.

Just to confirm and if I am not mistaken, the site table that we will call via the API contains a site_id which we will be able to use instead of the index of the for loop here, right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes I think you're right @pechouc, the f-string id was the previous way of getting a marker, from now on we should have a real site_id

position=(lat, lon),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably not a priority for the moment but I wonder whether it is a good practice to define an extensive set of variables which we use only once, a few lines below (like lat, lon, site_name...). But clearly, we can move forward with this until demo day, as variable names are explicit.

children=[dl.Tooltip(site_name),
dl.Popup([html.H2(f'Site {site_name}'),
html.P(f'Coordonnées : ({lat}, {lon})'),
html.P(f'Nombre de caméras : {nb_device}')])]))

# We group all dl.Marker objects in a dl.LayerGroup object
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# We group all dl.Marker objects in a dl.LayerGroup object
# We group all dl.Marker objects in a dl.MarkerClusterGroup object

I guess this has changed since the comment was written down!

markers_layer = dl.MarkerClusterGroup(children=markers, id='sites_markers')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can call it markers_cluster instead of markers_layer as the type of object seems to have changed?


# We convert it into geojson format (not a dl.GeoJSON object yet) and return it
markers = dlx.dicts_to_geojson(markers)
return markers_layer

return markers

# ------------------------------------------------------------------------------
# Fire alerts
# The following block is dedicated to fetching information about fire alerts and displaying them on the map

# This function creates alerts-related elements such as alert_button, alert_markers
def build_alerts_elements(value):

# Fetching alert status and reusable metadata
alert_metadata = build_live_alerts_metadata()
alert_lat = alert_metadata["lat"]
alert_lon = alert_metadata["lon"]
alert_id = str(alert_metadata["id"])

# Building the button that allows users to zoom towards the alert marker
if value == 0:
alert_button = dbc.Button(
children="Départ de feu, cliquez-ici !",
color="danger",
block=True,
id='alert_button'
)
else:
alert_button = ""

# Building alerts_markers objects and wraps them in a dl.LayerGroup object
if value == 0:
nrslt marked this conversation as resolved.
Show resolved Hide resolved
icon = {
"iconUrl": 'https://marsfireengineers.com/assets/images/resources/firedetection.png',
"iconSize": [40, 40], # Size of the icon
"iconAnchor": [20, 20] # Point of the icon which will correspond to marker's location
# "popupAnchor": [-3, -76] # Point from which the popup should open relative to the iconAnchor
}
alerts_markers = [dl.Marker(
id="marker_{}".format(alert_id), # Setting a unique id for each alerts_markers
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
id="marker_{}".format(alert_id), # Setting a unique id for each alerts_markers
id="alert_marker_{}".format(alert_id), # Setting a unique id for each alerts_markers

To make it more explicit in case we want to call that id in a call-back at some point?

position=(alert_lat, alert_lon),
icon=icon,
children=[dl.Popup(
[
html.H2("Alerte détectée"),
html.P("Coordonées : {}, {} ".format(alert_lat, alert_lon)),
html.Button("Afficher les données de détection",
id=("display_alert_frame_btn{}".format(alert_id)), # Setting a unique btn id
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
id=("display_alert_frame_btn{}".format(alert_id)), # Setting a unique btn id
id=("display_alert_frame_btn_{}".format(alert_id)), # Setting a unique btn id

Amazingly low value-added comment 😅

But so far, for sites and alert markers, we always add an underscore before the id.

n_clicks=0,
className="btn btn-danger")
])])]
alerts_markers_layer = dl.LayerGroup(children=alerts_markers, id='alerts_markers')
else:
alerts_markers_layer = ""

# Once we have the positions of cameras, we output another GeoJSON object gathering these locations
def build_alerts_markers():
markers = dl.GeoJSON(data=get_camera_positions(),
id='markers')
return alert_button, alerts_markers_layer


# This function either triggers a zoom towards the alert point each time the alert button is clicked
# and sets the default zoom and center params for map_object
def define_map_zoom_center(n_clicks):
nrslt marked this conversation as resolved.
Show resolved Hide resolved

# Fetching alert status and reusable metadata
alert_metadata = build_live_alerts_metadata()
alert_lat = alert_metadata["lat"]
alert_lon = alert_metadata["lon"]

# Defining center and zoom parameters for map_object
if n_clicks > 0:
center = [alert_lat, alert_lon]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like the fact that everything is already variabilized outside of the build_live_alerts_metadata function 😄

zoom = 9
else:
center = [46.5, 2]
zoom = 6

return markers
return center, zoom


# ------------------------------------------------------------------------------
Expand All @@ -145,9 +206,10 @@ def build_alerts_map():
zoom=6, # Determines the initial level of zoom around the center point
children=[
dl.TileLayer(id='tile_layer'),
build_alerts_geojson(),
build_departments_geojson(),
build_info_object(app_page='alerts'),
build_alerts_markers()],
build_sites_markers(),
html.Div(id="live_alerts_marker")],
style=map_style, # Reminder: map_style is imported from utils.py
id='map')

Expand Down
152 changes: 120 additions & 32 deletions app/homepage.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,42 @@
# Importing plotly fig objects from graphs.py
from graphs import generate_meteo_fig

# Importing utils fetched API data
from utils import build_live_alerts_metadata


# ------------------------------------------------------------------------------
# Before moving to the app layout

#Fetching reusable alert metadata
alert_metadata = build_live_alerts_metadata()
if alert_metadata:
frame_url = alert_metadata["media_url"]
nrslt marked this conversation as resolved.
Show resolved Hide resolved


# This function creates a radio button in order to simulate an alert event that will be later catched through the API
nrslt marked this conversation as resolved.
Show resolved Hide resolved
def build_alert_radio_button():

alert_radio_button = dcc.RadioItems(
options=[
{'label': 'no alert', 'value': 1},
{'label': 'alert', 'value': 0},
],
value=1,
labelStyle={'display': 'inline-block'},
id='alert_radio_button'
)

return alert_radio_button


# The following block is used to determine what map styles (risks or alerts) we use for and enable the user to change it
# This function creates the button that allows users to change the map style
def build_map_style_button():

button = html.Button(children='Afficher les niveaux de risques',
id='map_style_button')
id='map_style_button',
className='btn btn-warning')

return html.Center(button)

Expand All @@ -57,6 +83,63 @@ def choose_map_style(n_clicks):
return button_content_map, map_object, slider


#This function returns the user selection area in the left side bar
def user_selection_area():

return [build_alert_radio_button(),
dcc.Markdown('---'),
html.H5(("Filtres Carte"), style={'text-align': 'center'}), # Map filters added here
html.P(build_layer_style_button()), # Changes layer view style btn
dcc.Markdown('---'),
html.P(build_map_style_button()), # Changes map style btn
html.P(id="hp_slider"), # Opacity sliders for risks map
html.P(id="hp_alert_frame_metadata") # Displays alert_frame and alert_metadata
]


# Displays alert_frame and metadata related to a specific alert_markers after a click on display_alert_frame_btn
def display_alerts_frames(feature=None):

# Fetching alert status and reusable metadata
alert_metadata = build_live_alerts_metadata()
alert_lat = str(alert_metadata["lat"])
nrslt marked this conversation as resolved.
Show resolved Hide resolved
alert_lon = str(alert_metadata["lon"])
alert_frame = alert_metadata["media_url"]
alert_device = str(alert_metadata["device_id"])
alert_site = alert_metadata["site_name"]
alert_azimuth = alert_metadata["azimuth"]

frame_style = {'width': '50vh',
'height': '35vh',
'margin': 'auto',
'display': 'block',
'text-align': 'center',
}

if feature is not None:
separator1 = dcc.Markdown('---')
frame_title = html.H5("Image de détéction", style={'text-align': 'center'})
alert_frame = html.Img(
id='alert_frame',
src=frame_url,
style=frame_style,)
separator2 = dcc.Markdown('---')
alert_metadata_title = html.H5("Données de détéction", style={'text-align': 'center'})
alert_metadata = html.Div(
id="alert_metadata_user_selection",
children=[
"Tour: {}".format(alert_site), html.Br(),
"Coordonnées de la tour: {}, {}".format(alert_lat, alert_lon), html.Br(),
"Id de caméra: {}".format(alert_device), html.Br(),
"Azimuth: {}".format(alert_azimuth)])

alert_frame_metadata = html.Div(
children=[separator1, frame_title, alert_frame, separator2, alert_metadata_title, alert_metadata])

return html.Div(alert_frame_metadata)
return ""


# This function either displays or hides meteo graphs from graphs.py
def meteo_graphs(display=False):
if display is True:
Expand All @@ -80,52 +163,57 @@ def meteo_graphs(display=False):
# Content and App layout
# The following function is used in the main.py file to instantiate the layout of the homepage

# Body container
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this part a duplicate of what is inside the Homepage function? Sorry if I am missing something obvious, but is there any reason we should keep it?

body = dbc.Container([
dbc.Row(
[dbc.Col(html.H1('Plateforme de Monitoring', style={'text-align': 'center'}), className="pt-4"),
]),
dbc.Row(
[dbc.Col(
#side bar for the user to apply filter
user_selection_area(),
id='user_selection_column',
md=3),
dbc.Col(
# map object added here
html.Div(build_alerts_map(), id='hp_map'),
md=9)]
),
# Instantiating meteo graphs, set to True to display them under the map, False to hide them
meteo_graphs(display=False)
],
fluid=True,
)


# Gathering all these elements in a HTML Div and having it returned by the Homepage function
def Homepage():

# Body container
body = dbc.Container([
dbc.Row(
[dbc.Col(html.H1('Plateforme de Monitoring', style={'text-align': 'center'}), className="pt-4"),
]),
dbc.Row(
[dbc.Col(id='live_alert_header_btn'),
]),
dbc.Row(
[
dbc.Col(
dcc.Dropdown(
id='user_department_input',
options=[
{'label': 'Ardèche', 'value': 'Ardèche'},
{'label': 'Gard', 'value': 'Gard'},
{'label': 'Landes', 'value': 'Landes'}],
placeholder="Départements"),
user_selection_area(),
id='user_selection_column',
md=3),
dbc.Col(
# map object added here
html.Div(build_alerts_map(), id='hp_map'),
md=9)
]
),
dbc.Row(
[dbc.Col(
[
dcc.Markdown('---'),
# Map filters added here
html.H5(("Filtres Carte"), style={'text-align': 'center'}),
# Instantiating map layers button object from alerts.py
html.P(build_layer_style_button()),
dcc.Markdown('---'),
# Instantiating map style button object
html.P(build_map_style_button()),
html.P(id="hp_slider"),
],
md=3),
dbc.Col(
# Instantiating the alerts map object from alerts.py and setting it as the default map object
html.Div(build_alerts_map(), id='hp_map'),
md=9)]
),
# Instantiating meteo graphs, set to True to display them under the map, False to hide them
# meteo graphs added here
meteo_graphs(display=False)
],
fluid=True,
)

layout = html.Div([Navbar(), # Instantiating navbar object from navbar.py
body])

# Instantiating navbar object from navbar.py
layout = html.Div([Navbar(), body])
return layout
Loading