Spaces:
Sleeping
Sleeping
| 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)**") | |