lucid-hf's picture
CI: deploy Docker/PDM Space
7735c3d verified
import base64
import streamlit as st
from datetime import datetime, timedelta
import time
import random
from pathlib import Path
import os
# Choose one of these:
# A) Env var: APP_ENV=prod on your server / cloud
APP_ENV = os.getenv("natsar", "local").lower()
# B) Or secrets: put env="prod" in .streamlit/secrets.toml on your server
# APP_ENV = st.secrets.get("env", "local").lower()
IS_LOCAL = True
# … later, where you currently render your Deploy controls …
if IS_LOCAL:
st.markdown("""
<style>
/* Hide the toolbar and header (which creates the black bar) */
[data-testid="stToolbar"] { display: none !important; }
header[data-testid="stHeader"] { display: none !important; }
/* (Legacy fallbacks) */
#MainMenu { visibility: hidden; }
footer { visibility: hidden; }
/* Remove the empty space the header leaves behind */
[data-testid="stAppViewContainer"] { padding-top: 0 !important; }
[data-testid="stAppViewContainer"] .main .block-container { padding-top: 0 !important; }
/* Give the sidebar a little breathing room at the top */
section[data-testid="stSidebar"] > div:first-child { padding-top: 0.5rem; }
</style>
""", unsafe_allow_html=True)
def _image_to_data_url(path: str) -> str:
p = Path(path)
if not p.is_absolute():
p = Path(__file__).parent / p
mime = "image/png" if p.suffix.lower() == ".png" else "image/jpeg"
b64 = base64.b64encode(p.read_bytes()).decode()
return f"data:{mime};base64,{b64}"
# =============== Page setup ===============
st.markdown("""
<style>
.block-container { padding-top: 1.2rem; padding-bottom: 0.6rem; max-width: 1400px; }
h2, h3, h4 { margin: 0.4rem 0; }
textarea { min-height: 70px !important; }
/* Light outline on inputs */
.stSelectbox > div, .stTextInput > div, .stNumberInput > div, .stDateInput > div, .stTextArea > div {
border: 1px solid #D0D0D0; border-radius: 6px;
}
</style>
""", unsafe_allow_html=True)
st.set_page_config(
page_title="Satellite Tasking",
layout="wide",
initial_sidebar_state="expanded", # keep sidebar open
)
# =============== Compact styling & header spacing (avoid cropping) ===============
st.markdown("""
<style>
.block-container { padding-top: 1.2rem; padding-bottom: 0.6rem; max-width: 1400px; }
h2, h3, h4 { margin: 0.4rem 0; }
textarea { min-height: 70px !important; }
/* Light outline on inputs */
.stSelectbox > div, .stTextInput > div, .stNumberInput > div, .stDateInput > div, .stTextArea > div {
border: 1px solid #D0D0D0; border-radius: 6px;
}
</style>
""", unsafe_allow_html=True)
# =============== Sidebar navigation ===============
with st.sidebar:
logo_data_url = _image_to_data_url("../resources/images/lucid_insights_logo.png")
st.markdown(
f"""
<div style="text-align:left; margin: 0 0 0.8rem 0.25rem;">
<img src="{logo_data_url}" alt="Lucid Insights" style="width:160px; height:auto; margin-bottom:0.5rem;">
</div>
""",
unsafe_allow_html=True,
)
st.markdown("---")
st.page_link("app.py", label="Home")
#st.page_link("pages/lost_at_sea.py", label="Lost at Sea")
st.page_link("pages/signal_watch.py", label="Signal Watch")
st.page_link("pages/bushland_beacon.py", label="Bushland Beacon")
st.page_link("pages/misc_find.py", label="Misc Finder")
st.markdown("---")
st.page_link("pages/task_drone.py", label="Task Drone")
st.page_link("pages/task_satellite.py", label="Satellite Tasking")
st.page_link("pages/information.py", label="Survival Information")
st.markdown("---")
st.caption("SAR-X AI • Satellite Tasking Module")
# =============== Header ===============
st.markdown(
"<h2 style='text-align:center;margin-top:0;font-size:4.0em;color:yellow'>SAR-X<sup>ai</h2>"
"<h2 style='text-align:center;margin-top:0.0rem;'>Satellite Tasking 🛰️</h2>"
"</br>",
unsafe_allow_html=True,
)
# =============== Top Row: Provider & Notes (single row) ===============
satellite_providers = [
"Auto Select Lowest Cost",
"Auto Select Fastest Turnaround",
"BlackSky",
"Planet Labs",
"Maxar Technologies",
"Airbus Defence and Space",
"ICEYE",
"Capella Space",
"Satellogic",
"Earth-i",
"Umbra Space",
"Spire Global"
]
# Metadata for cost/turnaround heuristics
provider_cost_multiplier = {
"BlackSky": 1.00, "Planet Labs": 0.85, "Maxar Technologies": 1.40,
"Airbus Defence and Space": 1.25, "ICEYE": 1.10, "Capella Space": 1.15,
"Satellogic": 0.80, "Earth-i": 0.95, "Umbra Space": 1.05, "Spire Global": 0.90
}
provider_turnaround_hours = {
"BlackSky": 6, "Planet Labs": 8, "Maxar Technologies": 6,
"Airbus Defence and Space": 7, "ICEYE": 5, "Capella Space": 5,
"Satellogic": 7, "Earth-i": 8, "Umbra Space": 6, "Spire Global": 8
}
# ===== Utility Functions =====
def resolve_provider(choice: str) -> str:
if choice == "Auto Select Lowest Cost":
return min(provider_cost_multiplier, key=provider_cost_multiplier.get)
if choice == "Auto Select Fastest Turnaround":
return min(provider_turnaround_hours, key=provider_turnaround_hours.get)
return choice
def estimate_cost(resolution: str, capture_type: str, priority: str, provider_choice: str) -> float:
base_by_res = {"0.3 m": 3500, "0.5 m": 2500, "1 m": 1500, "3 m": 800, "10 m": 300}
type_add = {"Optical": 0, "SAR (Radar)": 600, "Multispectral": 400, "Infrared": 500}
priority_mult = {"Low": 1.00, "Medium": 1.10, "High": 1.25}
prov = resolve_provider(provider_choice)
prov_mult = provider_cost_multiplier.get(prov, 1.0)
cost = (base_by_res[resolution] + type_add[capture_type]) * priority_mult[priority] * prov_mult
return round(cost, 2)
# ACST utilities
def now_acst() -> datetime:
return datetime.utcnow() + timedelta(hours=9, minutes=30)
def eta_time_acst(hours_from_now: int) -> datetime:
return now_acst() + timedelta(hours=hours_from_now)
# =============== Provider & Notes Row ===============
top_left, top_right = st.columns([0.32, 0.68])
with top_left:
selected_provider = st.selectbox("Satellite Provider", satellite_providers, index=0)
with top_right:
additional_notes = st.text_area(
"Provider Notes / Special Instructions",
placeholder="Any special imaging parameters or constraints…"
)
st.markdown("---")
# =============== Bottom: Left (Form) / Right (Response) ===============
left_col, right_col = st.columns(2)
with left_col:
st.subheader("Tasking Parameters", divider=False)
# ---- FORM with specific row layout order ----
with st.form("task_satellite_form", clear_on_submit=False):
# Row 1: Target area name
target_name = st.text_input("Target Area Name", placeholder="e.g., Porepunkah, Victoria")
# Row 2: Latitude & Longitude
r2c1, r2c2 = st.columns(2)
with r2c1:
latitude = st.number_input("Latitude", format="%.6f", value=0.0)
with r2c2:
longitude = st.number_input("Longitude", format="%.6f", value=0.0)
# Row 3: Capture Type & Resolution
r3c1, r3c2 = st.columns(2)
with r3c1:
capture_type = st.selectbox("Capture Type", ["Optical", "SAR (Radar)", "Multispectral", "Infrared"])
with r3c2:
resolution = st.selectbox("Desired Resolution", ["0.3 m", "0.5 m", "1 m", "3 m", "10 m"], index=1)
# Row 4: Capture Date & Priority Level
r4c1, r4c2 = st.columns(2)
with r4c1:
request_date = st.date_input("Preferred Capture Date", datetime.now().date())
with r4c2:
priority = st.select_slider("Priority Level", ["Low", "Medium", "High"], value="Medium")
# Row 5: Buttons
b_l, b_r = st.columns([0.5, 0.5])
with b_l:
estimate_clicked = st.form_submit_button("Estimate Time & Cost", use_container_width=True)
with b_r:
submitted = st.form_submit_button("Submit Satellite Task", use_container_width=True)
# =============== Right Side: Response Section ===============
with right_col:
st.subheader("Tasking Information Response", divider=False)
# --- Estimate Cost ---
if 'estimate_clicked' in locals() and estimate_clicked and not submitted:
resolved = resolve_provider(selected_provider)
est = estimate_cost(resolution, capture_type, priority, selected_provider)
indicative_hours = provider_turnaround_hours.get(resolved, 6)
indicative_eta = eta_time_acst(indicative_hours).strftime("%Y-%m-%d %H:%M")
st.info(
f"💰 Estimated cost with **{resolved}**: **${est:,.2f}** \n"
f"⏱️ Indicative delivery by **{indicative_eta} ACST (UTC+9:30)** "
f"(**+{indicative_hours} hours**)."
)
a, b = st.columns(2)
with a:
st.markdown(f"**Target:** {target_name or '—'}")
st.markdown(f"**Resolution:** {resolution}")
st.markdown(f"**Type:** {capture_type}")
with b:
st.markdown(f"**Priority:** {priority}")
st.markdown(f"**Provider Choice:** {selected_provider} → **{resolved}**")
if additional_notes.strip():
st.markdown("**Notes:**")
st.caption(additional_notes)
# --- Submit Task ---
if 'submitted' in locals() and submitted:
resolved = resolve_provider(selected_provider)
st.success(f"✅ Task submitted to **{resolved}**")
a, b = st.columns(2)
with a:
st.markdown(f"**Target:** {target_name or '—'}")
st.markdown(f"**Date:** {request_date.strftime('%Y-%m-%d')}")
st.markdown(f"**Coordinates:** ({latitude:.6f}, {longitude:.6f})")
with b:
st.markdown(f"**Resolution:** {resolution}")
st.markdown(f"**Type:** {capture_type}")
st.markdown(f"**Priority:** {priority}")
if additional_notes.strip():
st.markdown("**Notes:**")
st.caption(additional_notes)
est = estimate_cost(resolution, capture_type, priority, selected_provider)
st.markdown(f"**Estimated Cost:** ${est:,.2f}")
with st.spinner("Contacting satellite provider…"):
time.sleep(3)
# Calculate ETA hours and timestamp
if selected_provider == "Auto Select Fastest Turnaround":
eta_hours = provider_turnaround_hours.get(resolved, 4)
else:
eta_hours = random.randint(4, 8)
eta_local = eta_time_acst(eta_hours).strftime("%Y-%m-%d %H:%M")
st.info(
f"📩 {resolved} advises an ETA on image delivery **within {eta_hours} hours** — "
f"approximately **{eta_local} ACST (UTC+9:30)** "
f"(**+{eta_hours} hours**). \n"
f"🕒 Current time (ACST): **{now_acst().strftime('%Y-%m-%d %H:%M')}**."
)
elif not ('estimate_clicked' in locals() and estimate_clicked):
st.caption("Use **Estimate Cost** to preview pricing or **Submit** to create the task and get an ETA here (shown in **ACST**).")
# =============== Footer ===============
st.markdown("---")
st.caption("Task Satellite • SAR-X AI Satellite Imagery Request Interface · Times shown in **ACST (UTC+9:30)**")