Skip to content

Commit 538ca61

Browse files
Change display order (#252)
* add detection selection * add style * drop debug print * fix update * fix input * fix marsk
1 parent 7b9b197 commit 538ca61

File tree

6 files changed

+168
-57
lines changed

6 files changed

+168
-57
lines changed

app/callbacks/data_callbacks.py

Lines changed: 74 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,20 @@ def login_callback(n_clicks, username, password, user_token, lang):
134134
raise PreventUpdate
135135

136136

137+
from dash import Input, Output, State, callback
138+
139+
140+
@callback(
141+
Output("detection_fetch_limit", "data"),
142+
Input("detection_fetch_limit_input", "value"),
143+
prevent_initial_call=True, # optional, remove if you want it to run on first load
144+
)
145+
def update_fetch_limit(value):
146+
if value is None:
147+
return 10 # fallback default
148+
return value
149+
150+
137151
@app.callback(
138152
Output("available-stream-sites", "data"),
139153
Input("user_name", "data"),
@@ -207,19 +221,19 @@ def api_cameras_watcher(n_intervals, api_cameras, user_token):
207221
[
208222
Input("main_api_fetch_interval", "n_intervals"),
209223
Input("api_cameras", "data"),
210-
Input("my-date-picker-single", "date"),
211224
Input("to_acknowledge", "data"),
212225
Input("unmatched_event_id_table", "data"),
226+
Input("my-date-picker-single", "date"),
213227
],
214228
[State("api_sequences", "data"), State("user_token", "data"), State("event_id_table", "data")],
215229
prevent_initial_call=True,
216230
)
217231
def api_watcher(
218232
n_intervals,
219233
api_cameras,
220-
selected_date,
221234
to_acknowledge,
222235
unmatched_event_id_table,
236+
selected_date,
223237
local_sequences,
224238
user_token,
225239
local_event_id_table,
@@ -285,19 +299,30 @@ def api_watcher(
285299
api_sequences, event_id_table = compute_overlap(
286300
api_sequences, unmatched_event_table=unmatched_event_id_table
287301
)
302+
288303
local_event_id_table = pd.read_json(StringIO(local_event_id_table), orient="split")
289304

290-
# Load local sequences safely
291-
if local_sequences:
292-
local_sequences_df = pd.read_json(StringIO(local_sequences), orient="split")
293-
else:
294-
local_sequences_df = pd.DataFrame()
305+
# Load local sequences safely
306+
if local_sequences:
307+
local_sequences_df = pd.read_json(StringIO(local_sequences), orient="split")
308+
else:
309+
local_sequences_df = pd.DataFrame()
310+
311+
# Ensure valid DataFrames
312+
if not isinstance(local_event_id_table, pd.DataFrame):
313+
local_event_id_table = pd.DataFrame()
314+
if not isinstance(event_id_table, pd.DataFrame):
315+
event_id_table = pd.DataFrame()
316+
317+
# Check event condition: either empty or sequences match
318+
event_condition = event_id_table.empty or (
319+
"sequences" in local_event_id_table.columns
320+
and "sequences" in event_id_table.columns
321+
and np.array_equal(local_event_id_table["sequences"].values, event_id_table["sequences"].values)
322+
)
295323

296-
if len(local_event_id_table) == len(event_id_table):
297-
if len(event_id_table) == 0 or (
298-
np.array_equal(local_event_id_table["sequences"].values, event_id_table["sequences"].values)
299-
):
300-
# Skip update if nothing changed
324+
# Now apply sequence comparison only if event condition is true
325+
if event_condition:
301326
if not local_sequences_df.empty and not api_sequences.empty:
302327
if not sequences_have_changed(api_sequences, local_sequences_df):
303328
logger.info("Skipping update: no significant change detected")
@@ -350,7 +375,11 @@ def update_sub_api_sequences(api_sequences, local_sub_sequences):
350375

351376
@app.callback(
352377
[Output("are_detections_loaded", "data"), Output("sequence_on_display", "data"), Output("api_detections", "data")],
353-
[Input("sequence_id_on_display", "data")],
378+
[
379+
Input("sequence_id_on_display", "data"),
380+
Input("detection_fetch_limit", "data"),
381+
Input("detection_fetch_desc", "value"),
382+
],
354383
[
355384
State("api_sequences", "data"),
356385
State("api_detections", "data"),
@@ -359,10 +388,20 @@ def update_sub_api_sequences(api_sequences, local_sub_sequences):
359388
],
360389
prevent_initial_call=True,
361390
)
362-
def load_detections(sequence_id_on_display, api_sequences, api_detections, are_detections_loaded, user_token):
391+
def load_detections(
392+
sequence_id_on_display,
393+
detection_fetch_limit,
394+
detection_fetch_desc,
395+
api_sequences,
396+
api_detections,
397+
are_detections_loaded,
398+
user_token,
399+
):
363400
if user_token is None or sequence_id_on_display is None:
364401
raise PreventUpdate
365402

403+
detection_fetch_desc = detection_fetch_desc if detection_fetch_desc else False
404+
366405
try:
367406
api_sequences = pd.read_json(StringIO(api_sequences), orient="split")
368407
api_detections = dict(json.loads(api_detections))
@@ -378,26 +417,37 @@ def load_detections(sequence_id_on_display, api_sequences, api_detections, are_d
378417
sequence_on_display = pd.DataFrame().to_json(orient="split")
379418
client = get_client(user_token)
380419

381-
if sequence_id_on_display not in api_detections:
420+
detection_key = f"{sequence_id_on_display}_{detection_fetch_limit}_{detection_fetch_desc}"
421+
422+
if detection_key not in api_detections.keys():
382423
try:
383-
response = client.fetch_sequences_detections(sequence_id_on_display)
424+
response = client.fetch_sequences_detections(
425+
sequence_id=sequence_id_on_display, limit=detection_fetch_limit, desc=detection_fetch_desc
426+
)
384427
data = response.json()
385428
detections = pd.DataFrame(data) if isinstance(data, list) else pd.DataFrame()
429+
386430
if not detections.empty and "bboxes" in detections.columns:
387431
detections = detections.iloc[::-1].reset_index(drop=True)
388432
detections["processed_bboxes"] = detections["bboxes"].apply(process_bbox)
389-
api_detections[sequence_id_on_display] = detections.to_json(orient="split")
433+
434+
sequence_meta = api_sequences.loc[api_sequences["id"].astype(str) == sequence_id_on_display]
435+
if not sequence_meta.empty:
436+
lat = sequence_meta.iloc[0].get("lat")
437+
lon = sequence_meta.iloc[0].get("lon")
438+
439+
if "created_at" in detections.columns and pd.notnull(lat) and pd.notnull(lon):
440+
detections["created_at_local"] = detections["created_at"].apply(
441+
lambda dt: convert_dt_to_local_tz(lat, lon, dt) if pd.notnull(dt) else None
442+
)
443+
444+
api_detections[detection_key] = detections.to_json(orient="split")
445+
390446
except Exception as e:
391447
logger.error(f"Error fetching detections for {sequence_id_on_display}: {e}")
392448
return dash.no_update, dash.no_update, dash.no_update
393449

394-
sequence_on_display = api_detections[sequence_id_on_display]
395-
filtered = api_sequences.loc[api_sequences["id"].astype(str) == sequence_id_on_display, "last_seen_at"]
396-
397-
if filtered.empty:
398-
return dash.no_update, dash.no_update, dash.no_update
399-
400-
are_detections_loaded[sequence_id_on_display] = str(filtered.iloc[0])
450+
sequence_on_display = api_detections[detection_key]
401451

402452
return json.dumps(are_detections_loaded), sequence_on_display, json.dumps(api_detections)
403453

app/callbacks/display_callbacks.py

Lines changed: 50 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import boto3 # type: ignore
1313
import dash
1414
import logging_config
15+
import numpy as np
1516
import pandas as pd
1617
from botocore.exceptions import ClientError # type: ignore
1718
from dash import Input, Output, State, ctx
@@ -198,15 +199,22 @@ def update_sequence_on_dropdown_change(selected_sequence_id):
198199
Output("image-slider", "min"),
199200
Output("slider-container", "style"),
200201
],
201-
[Input("image-slider", "value"), Input("sequence_on_display", "data")],
202+
[
203+
Input("image-slider", "value"),
204+
Input("sequence_on_display", "data"),
205+
Input("detection_fetch_desc", "value"),
206+
],
202207
[
203208
State("sequence-list-container", "children"),
204209
State("language", "data"),
205-
State("alert-end-date-value", "children"),
206210
],
207211
prevent_initial_call=True,
208212
)
209-
def update_image_and_bbox(slider_value, sequence_on_display, sequence_list, lang, alert_end_value):
213+
def update_image_and_bbox(slider_value, sequence_on_display, detection_fetch_desc, sequence_list, lang):
214+
from io import StringIO
215+
216+
import pandas as pd
217+
210218
no_alert_image_src = "./assets/images/no-alert-default.png"
211219
if lang == "es":
212220
no_alert_image_src = "./assets/images/no-alert-default-es.png"
@@ -216,8 +224,15 @@ def update_image_and_bbox(slider_value, sequence_on_display, sequence_list, lang
216224
if sequence_on_display.empty or not len(sequence_list):
217225
return no_alert_image_src, *[{"display": "none"}] * 3, 0, {}, 0, {"display": "none"}
218226

219-
images, boxes = zip(
220-
*((alert["url"], alert["processed_bboxes"]) for _, alert in sequence_on_display.iterrows() if alert["url"]),
227+
if not detection_fetch_desc:
228+
sequence_on_display = sequence_on_display[::-1].reset_index(drop=True)
229+
230+
images, boxes, created_at_local_list = zip(
231+
*(
232+
(alert["url"], alert["processed_bboxes"], alert.get("created_at_local"))
233+
for _, alert in sequence_on_display.iterrows()
234+
if alert["url"]
235+
),
221236
strict=False,
222237
)
223238

@@ -244,14 +259,15 @@ def update_image_and_bbox(slider_value, sequence_on_display, sequence_list, lang
244259
}
245260

246261
try:
247-
if isinstance(alert_end_value, str) and alert_end_value.strip():
248-
last_time = datetime.strptime(alert_end_value.strip(), "%H:%M")
249-
else:
250-
raise ValueError("Empty or invalid date string")
262+
latest_time = pd.to_datetime(sequence_on_display["created_at_local"].dropna().max())
251263
except Exception:
252-
last_time = datetime.now()
264+
latest_time = datetime.now()
265+
266+
# Compute 5 evenly spaced tick positions
267+
num_marks = 5
268+
tick_indices = sorted(set(int(round(i)) for i in np.linspace(0, n_images - 1, num=num_marks)))
253269

254-
marks = {i: (last_time - timedelta(seconds=30 * (n_images - 1 - i))).strftime("%H:%M:%S") for i in range(n_images)}
270+
marks = {i: (latest_time - timedelta(seconds=30 * (n_images - 1 - i))).strftime("%H:%M:%S") for i in tick_indices}
255271

256272
return [img_src, *bbox_styles, n_images - 1, marks, 0, {"display": "block"}]
257273

@@ -541,25 +557,25 @@ def update_fire_markers(smoke_location_str, api_sequences):
541557
Input("confirm-non-wildfire", "n_clicks"),
542558
Input("cancel-confirmation", "n_clicks"),
543559
],
544-
[State("sequence_id_on_display", "data"), State("user_token", "data")],
560+
[
561+
State("sequence_id_on_display", "data"),
562+
State("user_token", "data"),
563+
],
545564
prevent_initial_call=True,
546565
)
547566
def acknowledge_event(
548567
acknowledge_clicks, confirm_wildfire, confirm_non_wildfire, cancel, sequence_id_on_display, user_token
549568
):
550569
ctx = dash.callback_context
551570

552-
logger.info("acknowledge_event")
553-
554571
if not ctx.triggered:
555-
raise dash.exceptions.PreventUpdate
572+
raise PreventUpdate
556573

557574
if user_token is None:
558-
return dash.no_update
575+
raise PreventUpdate
559576

560577
triggered_id = ctx.triggered[0]["prop_id"].split(".")[0]
561578

562-
# Modal styles
563579
modal_visible_style = {
564580
"position": "fixed",
565581
"top": "50%",
@@ -571,29 +587,31 @@ def acknowledge_event(
571587
modal_hidden_style = {"display": "none"}
572588

573589
client = get_client(user_token)
590+
client.token = user_token
574591

575592
if triggered_id == "acknowledge-button":
576-
# Show the modal
577-
if acknowledge_clicks > 0:
578-
return modal_visible_style, dash.no_update
593+
if acknowledge_clicks is None or acknowledge_clicks == 0:
594+
raise PreventUpdate
595+
return modal_visible_style, dash.no_update
579596

580597
elif triggered_id == "confirm-wildfire":
581-
# Send wildfire confirmation to the API
582-
client.token = user_token
598+
if confirm_wildfire is None or confirm_wildfire == 0:
599+
raise PreventUpdate
583600
client.label_sequence(sequence_id_on_display, True)
584601
return modal_hidden_style, sequence_id_on_display
585602

586603
elif triggered_id == "confirm-non-wildfire":
587-
# Send non-wildfire confirmation to the API
588-
client.token = user_token
604+
if confirm_non_wildfire is None or confirm_non_wildfire == 0:
605+
raise PreventUpdate
589606
client.label_sequence(sequence_id_on_display, False)
590607
return modal_hidden_style, sequence_id_on_display
591608

592609
elif triggered_id == "cancel-confirmation":
593-
# Cancel action
610+
if cancel is None or cancel == 0:
611+
raise PreventUpdate
594612
return modal_hidden_style, dash.no_update
595613

596-
raise dash.exceptions.PreventUpdate
614+
raise PreventUpdate
597615

598616

599617
# Modal issue let's add this later
@@ -740,12 +758,15 @@ def update_datepicker(open_clicks, selected_date):
740758
def pick_live_stream_camera(n_clicks, azimuth, camera_label, azimuth_label):
741759
logger.info("pick_live_stream_camera")
742760

761+
if n_clicks is None or n_clicks == 0:
762+
raise PreventUpdate
763+
743764
if not camera_label or not azimuth_label:
744765
raise PreventUpdate
766+
745767
try:
746768
cam_name, _, _ = camera_label.split(" ")
747769
azimuth = int(azimuth.replace("°", ""))
748-
# detection_azimuth = int(azimuth_label.replace("°", "").strip()) Need azimuth refine first
749770
except Exception as e:
750771
logger.warning(f"[pick_live_stream_camera] Failed to parse camera info: {e}")
751772
raise PreventUpdate
@@ -1032,7 +1053,7 @@ def hide_button_callback(sub_api_sequences, sequence_id, event_id_table_json, se
10321053
if df_sequences.empty:
10331054
return hide_style, hide_style, hide_style
10341055
except Exception as e:
1035-
print(f"[hide_button_callback] Failed to read sub_api_sequences: {e}")
1056+
logger.error(f"[hide_button_callback] Failed to read sub_api_sequences: {e}")
10361057
return hide_style, hide_style, hide_style
10371058

10381059
# Default: show stream & mask buttons
@@ -1054,6 +1075,6 @@ def hide_button_callback(sub_api_sequences, sequence_id, event_id_table_json, se
10541075
if isinstance(sequences, list) and len(sequences) > 1:
10551076
unmatch_style = show_style
10561077
except Exception as e:
1057-
print(f"[hide_button_callback] Failed to process event_id_table: {e}")
1078+
logger.error(f"[hide_button_callback] Failed to process event_id_table: {e}")
10581079

10591080
return stream_style, mask_style, unmatch_style

app/index.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,11 @@ def display_page(
5858
if triggered == "selected-camera-info" and selected_camera_info:
5959
return live_stream_layout(user_token, api_cameras, available_stream, selected_camera_info, lang=lang)
6060

61-
if (pathname == "/" or pathname is None) or triggered == "my-date-picker-single":
62-
return homepage_layout(user_token, api_cameras, lang=lang)
61+
if triggered == "my-date-picker-single":
62+
return homepage_layout(user_token, api_cameras, lang=lang, descending_order=False)
63+
64+
if pathname in ["/", None]:
65+
return homepage_layout(user_token, api_cameras, lang=lang, descending_order=True)
6366

6467
if pathname == "/cameras-status":
6568
return cameras_status_layout(user_token, api_cameras, lang=lang)

app/layouts/main_layout.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,5 @@ def get_main_layout():
8686
dcc.Store(id="selected-camera-info", storage_type="session"),
8787
dcc.Store(id="language", storage_type="session", data="fr"),
8888
dcc.Store(id="selected_event_id", storage_type="session", data=None),
89+
dcc.Store(id="detection_fetch_limit", storage_type="session", data=10),
8990
])

0 commit comments

Comments
 (0)