Spaces:
Running
Running
Fix drone map playback with Gradio slider plus tested play controls
Browse files- README.md +1 -1
- dataset_bundle/public_release_manifest.json +1 -1
- public_copy.json +1 -1
- public_space_app.py +155 -29
README.md
CHANGED
|
@@ -15,6 +15,6 @@ python_version: 3.11
|
|
| 15 |
|
| 16 |
This private preview maps reported drone sightings from news stories over official covered military and civilian areas.
|
| 17 |
|
| 18 |
-
Use the
|
| 19 |
|
| 20 |
Each point is tied to one public event row and a linked source list. This release tracks reported drone sightings and does not prove intent, threat, wrongdoing, or a verified security breach.
|
|
|
|
| 15 |
|
| 16 |
This private preview maps reported drone sightings from news stories over official covered military and civilian areas.
|
| 17 |
|
| 18 |
+
Use the time slider plus the Play and Pause controls to watch red dots appear across time, then match the `event_id` on the map to the row data directly below it.
|
| 19 |
|
| 20 |
Each point is tied to one public event row and a linked source list. This release tracks reported drone sightings and does not prove intent, threat, wrongdoing, or a verified security breach.
|
dataset_bundle/public_release_manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
{
|
| 2 |
"public_version": "drone-sightings-slice-2026-04-v1-smoke",
|
| 3 |
"title": "Drone Sightings Near Covered U.S. Military and Civilian Areas",
|
| 4 |
-
"release_date": "2026-04-20T18:
|
| 5 |
"source_run_name": "drone_sightings_smoke_20260420",
|
| 6 |
"slice_description": "A small, review-oriented slice of U.S. news-reported drone sightings mapped against official covered-area registries.",
|
| 7 |
"source_window": {
|
|
|
|
| 1 |
{
|
| 2 |
"public_version": "drone-sightings-slice-2026-04-v1-smoke",
|
| 3 |
"title": "Drone Sightings Near Covered U.S. Military and Civilian Areas",
|
| 4 |
+
"release_date": "2026-04-20T18:15:50.018651+00:00",
|
| 5 |
"source_run_name": "drone_sightings_smoke_20260420",
|
| 6 |
"slice_description": "A small, review-oriented slice of U.S. news-reported drone sightings mapped against official covered-area registries.",
|
| 7 |
"source_window": {
|
public_copy.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
{
|
| 2 |
"title": "Drone Sightings Near Covered U.S. Military and Civilian Areas",
|
| 3 |
"dataset_bundle_prefix": "dataset_bundle",
|
| 4 |
-
"landing_markdown": "# Drone Sightings Near Covered U.S. Military and Civilian Areas\n\nThis private preview maps reported drone sightings from news stories over official covered military and civilian areas.\n\nUse the
|
| 5 |
}
|
|
|
|
| 1 |
{
|
| 2 |
"title": "Drone Sightings Near Covered U.S. Military and Civilian Areas",
|
| 3 |
"dataset_bundle_prefix": "dataset_bundle",
|
| 4 |
+
"landing_markdown": "# Drone Sightings Near Covered U.S. Military and Civilian Areas\n\nThis private preview maps reported drone sightings from news stories over official covered military and civilian areas.\n\nUse the time slider plus the Play and Pause controls to watch red dots appear across time, then match the `event_id` on the map to the row data directly below it.\n\nEach point is tied to one public event row and a linked source list. This release tracks reported drone sightings and does not prove intent, threat, wrongdoing, or a verified security breach."
|
| 5 |
}
|
public_space_app.py
CHANGED
|
@@ -29,6 +29,30 @@ def load_release_data(public_copy_path: Path) -> dict:
|
|
| 29 |
}
|
| 30 |
|
| 31 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
def _point_table(events: pd.DataFrame) -> pd.DataFrame:
|
| 33 |
ordered = events.copy()
|
| 34 |
ordered["event_date"] = ordered["event_date"].astype(str)
|
|
@@ -52,12 +76,9 @@ def _point_table(events: pd.DataFrame) -> pd.DataFrame:
|
|
| 52 |
]
|
| 53 |
|
| 54 |
|
| 55 |
-
def _build_map(events: pd.DataFrame):
|
| 56 |
if events.empty:
|
| 57 |
return px.scatter_mapbox(pd.DataFrame({"lat": [], "lon": []}), lat="lat", lon="lon", zoom=3, height=560)
|
| 58 |
-
frame_values = sorted(str(value) for value in events["event_date"].astype(str).unique())
|
| 59 |
-
events = events.copy()
|
| 60 |
-
events["animation_frame"] = events["event_date"].astype(str)
|
| 61 |
fig = px.scatter_mapbox(
|
| 62 |
events,
|
| 63 |
lat="lat",
|
|
@@ -76,30 +97,16 @@ def _build_map(events: pd.DataFrame):
|
|
| 76 |
"lon": False,
|
| 77 |
},
|
| 78 |
custom_data=["event_id"],
|
| 79 |
-
animation_frame="animation_frame",
|
| 80 |
zoom=3,
|
| 81 |
height=560,
|
| 82 |
)
|
| 83 |
fig.update_layout(
|
| 84 |
mapbox_style="open-street-map",
|
| 85 |
margin={"l": 0, "r": 0, "t": 40, "b": 0},
|
| 86 |
-
title="Reported drone sightings
|
| 87 |
showlegend=False,
|
| 88 |
)
|
| 89 |
fig.update_traces(marker={"size": 12, "opacity": 0.85})
|
| 90 |
-
if frame_values:
|
| 91 |
-
fig.update_layout(
|
| 92 |
-
updatemenus=[
|
| 93 |
-
{
|
| 94 |
-
"type": "buttons",
|
| 95 |
-
"showactive": False,
|
| 96 |
-
"buttons": [
|
| 97 |
-
{"label": "Play", "method": "animate", "args": [None, {"frame": {"duration": 600, "redraw": True}, "fromcurrent": True}]},
|
| 98 |
-
{"label": "Pause", "method": "animate", "args": [[None], {"frame": {"duration": 0, "redraw": False}, "mode": "immediate"}]},
|
| 99 |
-
],
|
| 100 |
-
}
|
| 101 |
-
]
|
| 102 |
-
)
|
| 103 |
return fig
|
| 104 |
|
| 105 |
|
|
@@ -144,30 +151,149 @@ def _detail_markdown(event_sources: pd.DataFrame, events: pd.DataFrame, event_id
|
|
| 144 |
return "\n".join(lines)
|
| 145 |
|
| 146 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
def build_app(public_copy_path: str | Path):
|
| 148 |
public_copy_path = Path(public_copy_path)
|
| 149 |
payload = json.loads(public_copy_path.read_text(encoding="utf-8"))
|
| 150 |
data = load_release_data(public_copy_path)
|
| 151 |
events = data["events"]
|
| 152 |
event_sources = data["event_sources"]
|
| 153 |
-
|
|
|
|
| 154 |
|
| 155 |
with gr.Blocks(title=payload["title"]) as app:
|
| 156 |
gr.Markdown(payload["landing_markdown"])
|
|
|
|
| 157 |
plot = gr.Plot(label="Drone sightings over time")
|
| 158 |
-
gr.
|
| 159 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
table = gr.Dataframe(label="Point data used to draw the map", interactive=False)
|
| 161 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
|
| 163 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
if not evt or evt.index is None:
|
| 165 |
-
return "Select a row in the point-data table to inspect the exact public data and source URLs used for
|
| 166 |
row_index = evt.index[0] if isinstance(evt.index, (list, tuple)) else evt.index
|
| 167 |
-
if row_index >= len(
|
| 168 |
return "No event details available."
|
| 169 |
-
|
|
|
|
| 170 |
|
| 171 |
-
|
| 172 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
return app
|
|
|
|
| 29 |
}
|
| 30 |
|
| 31 |
|
| 32 |
+
def _frame_dates(events: pd.DataFrame) -> list[str]:
|
| 33 |
+
if events.empty:
|
| 34 |
+
return []
|
| 35 |
+
return sorted(str(value) for value in events["event_date"].astype(str).unique())
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def _frame_index(value, max_index: int) -> int:
|
| 39 |
+
if max_index <= 0:
|
| 40 |
+
return 0
|
| 41 |
+
try:
|
| 42 |
+
numeric = int(float(value))
|
| 43 |
+
except (TypeError, ValueError):
|
| 44 |
+
numeric = 0
|
| 45 |
+
return max(0, min(max_index, numeric))
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def _visible_events(events: pd.DataFrame, frame_dates: list[str], frame_index: int) -> pd.DataFrame:
|
| 49 |
+
if events.empty or not frame_dates:
|
| 50 |
+
return events.iloc[0:0].copy()
|
| 51 |
+
frame_value = frame_dates[_frame_index(frame_index, len(frame_dates) - 1)]
|
| 52 |
+
visible = events[events["event_date"].astype(str) <= frame_value].copy()
|
| 53 |
+
return visible.sort_values(["event_date", "event_id", "headline"], ascending=[True, True, True]).reset_index(drop=True)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
def _point_table(events: pd.DataFrame) -> pd.DataFrame:
|
| 57 |
ordered = events.copy()
|
| 58 |
ordered["event_date"] = ordered["event_date"].astype(str)
|
|
|
|
| 76 |
]
|
| 77 |
|
| 78 |
|
| 79 |
+
def _build_map(events: pd.DataFrame, frame_label: str):
|
| 80 |
if events.empty:
|
| 81 |
return px.scatter_mapbox(pd.DataFrame({"lat": [], "lon": []}), lat="lat", lon="lon", zoom=3, height=560)
|
|
|
|
|
|
|
|
|
|
| 82 |
fig = px.scatter_mapbox(
|
| 83 |
events,
|
| 84 |
lat="lat",
|
|
|
|
| 97 |
"lon": False,
|
| 98 |
},
|
| 99 |
custom_data=["event_id"],
|
|
|
|
| 100 |
zoom=3,
|
| 101 |
height=560,
|
| 102 |
)
|
| 103 |
fig.update_layout(
|
| 104 |
mapbox_style="open-street-map",
|
| 105 |
margin={"l": 0, "r": 0, "t": 40, "b": 0},
|
| 106 |
+
title=f"Reported drone sightings through {frame_label}",
|
| 107 |
showlegend=False,
|
| 108 |
)
|
| 109 |
fig.update_traces(marker={"size": 12, "opacity": 0.85})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
return fig
|
| 111 |
|
| 112 |
|
|
|
|
| 151 |
return "\n".join(lines)
|
| 152 |
|
| 153 |
|
| 154 |
+
def _detail_placeholder(frame_label: str) -> str:
|
| 155 |
+
return (
|
| 156 |
+
"Select a row in the point-data table to inspect the exact public data and source URLs used for a plotted point. "
|
| 157 |
+
f"The map and table currently show all events through `{frame_label}`."
|
| 158 |
+
)
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def _frame_payload(events: pd.DataFrame, event_sources: pd.DataFrame, frame_dates: list[str], frame_index: int):
|
| 162 |
+
if not frame_dates:
|
| 163 |
+
empty_table = _point_table(events.iloc[0:0].copy())
|
| 164 |
+
return (
|
| 165 |
+
_build_map(events.iloc[0:0].copy(), "no data"),
|
| 166 |
+
empty_table,
|
| 167 |
+
"No dated events available.",
|
| 168 |
+
"## Point data",
|
| 169 |
+
"Time slider unavailable because there are no dated events in this release.",
|
| 170 |
+
empty_table.to_dict("records"),
|
| 171 |
+
"No event details available.",
|
| 172 |
+
)
|
| 173 |
+
visible = _visible_events(events, frame_dates, frame_index)
|
| 174 |
+
point_table = _point_table(visible)
|
| 175 |
+
frame_label = frame_dates[_frame_index(frame_index, len(frame_dates) - 1)]
|
| 176 |
+
return (
|
| 177 |
+
_build_map(visible, frame_label),
|
| 178 |
+
point_table,
|
| 179 |
+
f"### Showing events through `{frame_label}`",
|
| 180 |
+
"## Point data",
|
| 181 |
+
"Every row below is a point currently visible on the map. The `event_id` shown on hover matches the `event_id` in the table and detail panel.",
|
| 182 |
+
point_table.to_dict("records"),
|
| 183 |
+
_detail_placeholder(frame_label),
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
|
| 187 |
def build_app(public_copy_path: str | Path):
|
| 188 |
public_copy_path = Path(public_copy_path)
|
| 189 |
payload = json.loads(public_copy_path.read_text(encoding="utf-8"))
|
| 190 |
data = load_release_data(public_copy_path)
|
| 191 |
events = data["events"]
|
| 192 |
event_sources = data["event_sources"]
|
| 193 |
+
frame_dates = _frame_dates(events)
|
| 194 |
+
max_frame_index = max(0, len(frame_dates) - 1)
|
| 195 |
|
| 196 |
with gr.Blocks(title=payload["title"]) as app:
|
| 197 |
gr.Markdown(payload["landing_markdown"])
|
| 198 |
+
date_markdown = gr.Markdown()
|
| 199 |
plot = gr.Plot(label="Drone sightings over time")
|
| 200 |
+
with gr.Row():
|
| 201 |
+
play_button = gr.Button("Play")
|
| 202 |
+
pause_button = gr.Button("Pause")
|
| 203 |
+
slider = gr.Slider(
|
| 204 |
+
minimum=0,
|
| 205 |
+
maximum=max_frame_index,
|
| 206 |
+
step=1,
|
| 207 |
+
value=max_frame_index,
|
| 208 |
+
label="Time slider",
|
| 209 |
+
)
|
| 210 |
+
point_heading = gr.Markdown("## Point data")
|
| 211 |
+
point_caption = gr.Markdown("Every row below is a point currently visible on the map.")
|
| 212 |
table = gr.Dataframe(label="Point data used to draw the map", interactive=False)
|
| 213 |
+
table_rows = gr.State([])
|
| 214 |
+
detail = gr.Markdown("Select a row in the point-data table to inspect the exact public data and source URLs used for a plotted point.")
|
| 215 |
+
timer = gr.Timer(value=1.0, active=False)
|
| 216 |
+
|
| 217 |
+
def _render_frame(frame_value):
|
| 218 |
+
return _frame_payload(events, event_sources, frame_dates, _frame_index(frame_value, max_frame_index))
|
| 219 |
+
|
| 220 |
+
def _start_playback(frame_value):
|
| 221 |
+
next_index = _frame_index(frame_value, max_frame_index)
|
| 222 |
+
if next_index >= max_frame_index and frame_dates:
|
| 223 |
+
next_index = 0
|
| 224 |
+
plot_value, table_value, date_value, heading_value, caption_value, rows_value, detail_value = _render_frame(next_index)
|
| 225 |
+
return (
|
| 226 |
+
gr.Timer(active=True),
|
| 227 |
+
next_index,
|
| 228 |
+
plot_value,
|
| 229 |
+
table_value,
|
| 230 |
+
date_value,
|
| 231 |
+
heading_value,
|
| 232 |
+
caption_value,
|
| 233 |
+
rows_value,
|
| 234 |
+
detail_value,
|
| 235 |
+
)
|
| 236 |
|
| 237 |
+
def _pause_playback():
|
| 238 |
+
return gr.Timer(active=False)
|
| 239 |
+
|
| 240 |
+
def _advance_frame(frame_value):
|
| 241 |
+
if not frame_dates:
|
| 242 |
+
plot_value, table_value, date_value, heading_value, caption_value, rows_value, detail_value = _render_frame(0)
|
| 243 |
+
return (
|
| 244 |
+
gr.Timer(active=False),
|
| 245 |
+
0,
|
| 246 |
+
plot_value,
|
| 247 |
+
table_value,
|
| 248 |
+
date_value,
|
| 249 |
+
heading_value,
|
| 250 |
+
caption_value,
|
| 251 |
+
rows_value,
|
| 252 |
+
detail_value,
|
| 253 |
+
)
|
| 254 |
+
current_index = _frame_index(frame_value, max_frame_index)
|
| 255 |
+
next_index = min(max_frame_index, current_index + 1)
|
| 256 |
+
plot_value, table_value, date_value, heading_value, caption_value, rows_value, detail_value = _render_frame(next_index)
|
| 257 |
+
return (
|
| 258 |
+
gr.Timer(active=(next_index < max_frame_index)),
|
| 259 |
+
next_index,
|
| 260 |
+
plot_value,
|
| 261 |
+
table_value,
|
| 262 |
+
date_value,
|
| 263 |
+
heading_value,
|
| 264 |
+
caption_value,
|
| 265 |
+
rows_value,
|
| 266 |
+
detail_value,
|
| 267 |
+
)
|
| 268 |
+
|
| 269 |
+
def _table_detail(current_rows, evt: gr.SelectData):
|
| 270 |
if not evt or evt.index is None:
|
| 271 |
+
return "Select a row in the point-data table to inspect the exact public data and source URLs used for a plotted point."
|
| 272 |
row_index = evt.index[0] if isinstance(evt.index, (list, tuple)) else evt.index
|
| 273 |
+
if row_index >= len(current_rows):
|
| 274 |
return "No event details available."
|
| 275 |
+
event_id = str(current_rows[row_index]["event_id"])
|
| 276 |
+
return _detail_markdown(event_sources, events, event_id)
|
| 277 |
|
| 278 |
+
slider.change(
|
| 279 |
+
_render_frame,
|
| 280 |
+
inputs=slider,
|
| 281 |
+
outputs=[plot, table, date_markdown, point_heading, point_caption, table_rows, detail],
|
| 282 |
+
)
|
| 283 |
+
play_button.click(
|
| 284 |
+
_start_playback,
|
| 285 |
+
inputs=slider,
|
| 286 |
+
outputs=[timer, slider, plot, table, date_markdown, point_heading, point_caption, table_rows, detail],
|
| 287 |
+
)
|
| 288 |
+
pause_button.click(_pause_playback, outputs=timer)
|
| 289 |
+
timer.tick(
|
| 290 |
+
_advance_frame,
|
| 291 |
+
inputs=slider,
|
| 292 |
+
outputs=[timer, slider, plot, table, date_markdown, point_heading, point_caption, table_rows, detail],
|
| 293 |
+
)
|
| 294 |
+
table.select(_table_detail, inputs=table_rows, outputs=detail)
|
| 295 |
+
app.load(
|
| 296 |
+
lambda: _render_frame(max_frame_index),
|
| 297 |
+
outputs=[plot, table, date_markdown, point_heading, point_caption, table_rows, detail],
|
| 298 |
+
)
|
| 299 |
return app
|