Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
83 changes: 83 additions & 0 deletions demo_status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""Demo: attention-lifecycle styling (style.status) on survey stations.

Simulates the EOGPT Cawndilla-2 pixel-drill flow: drop six stations, then
walk them one by one — the station being processed PULSES (status=active),
finished stations flip to a solid success look (status=done), and when the
analysis pivots away, abandoned stations GRAY OUT (status=muted).

Run: python demo_status.py (server on http://localhost:8000)
Open the printed URL in a browser to watch.
"""

import time

import httpx

BASE = "http://localhost:8000"

STATIONS = [
("P1", -143.20, 32.10), ("P2", -143.05, 32.18), ("P3", -143.12, 31.98),
("C1", -143.45, 32.30), ("C2", -142.80, 32.35), ("C3", -142.90, 31.85),
]


def point_geojson(name, lon, lat):
return (
'{"type":"Feature","properties":{"name":"%s"},'
'"geometry":{"type":"Point","coordinates":[%f,%f]}}' % (name, lon, lat)
)


def main():
c = httpx.Client(base_url=BASE, timeout=30)
map_id = c.post("/api/maps").json()["map_id"]
print(f"Map: {BASE}/map/{map_id}")

def event(type_, data):
r = c.post(f"/api/maps/{map_id}/events", json={"type": type_, "data": data})
r.raise_for_status()
return r.json()

# Drop the stations (labeled points)
ids = {}
for name, lon, lat in STATIONS:
resp = event("add_point", {
"geojson": point_geojson(name, lon, lat), "name": name,
"style": {"fill_color": "#38bdf8", "stroke_color": "#e0f2fe",
"stroke_width": 2, "label": True},
})
ids[name] = resp["asset_id"]
event("zoom_to_bbox", {"bbox": [-143.7, 31.7, -142.6, 32.5]})
print("Stations placed. Starting the drill loop...")
time.sleep(3)

# Drill P1-P3: pulse while "processing", then mark done
for name in ("P1", "P2", "P3"):
print(f" {name}: processing (pulse)...")
event("update_style", {"asset_id": ids[name], "style": {"status": "active"}})
time.sleep(5) # pretend to compute
print(f" {name}: done.")
event("update_style", {"asset_id": ids[name], "style": {"status": "done"}})
time.sleep(1)

# Pivot: the control stations are no longer under consideration
print("Pivoting — muting control stations C1-C3...")
time.sleep(2)
for name in ("C1", "C2", "C3"):
event("update_style", {"asset_id": ids[name], "style": {"status": "muted"}})

# Finale: raw ripple spec — a big slow sonar ping with a custom color
time.sleep(3)
print("Finale: raw ripple on P2 (halo 8->40px, 2.2s period, amber)...")
event("update_style", {"asset_id": ids["P2"], "style": {
"animate": [{"property": "ripple", "from": 8, "to": 40,
"period": 2.2, "color": "#f59e0b"}],
}})
time.sleep(10)
event("update_style", {"asset_id": ids["P2"], "style": {"status": "done"}})

print("Demo complete: P1-P3 solid green, C1-C3 grayed out.")


if __name__ == "__main__":
main()
31 changes: 31 additions & 0 deletions sdk/mapcontrol/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,31 @@ class Style:
glow: True → defaults, or {"period": 2.0 (seconds per cycle),
"min_opacity": 0.15, "max_opacity": 0.85, "stroke": True} —
the asset slowly fades between translucent and opaque.
(Back-compat sugar over `animate`.)

Static opacity:
opacity: flat 0..1 opacity on fills, lines, and circles.
None = renderer defaults. An active animation overrides it
while running.

Animate (generic property animation):
animate: list of effects driven by one shared client rAF loop:
[{"property": "opacity"|"circle_radius"|"stroke_width",
"from": 0.35, "to": 1.0, "period": 1.2}]
Each effect oscillates the paint property between `from` and
`to` over `period` seconds. [] stops any running animation.
Special effect "ripple": a sonar-ping halo ring that expands
outward from point markers (radius `from`→`to` px) while
fading to transparent, then restarts (sawtooth). Optional
"color" (hex) tints the halo:
[{"property": "ripple", "from": 8, "to": 26, "period": 1.6}]

Status (attention-lifecycle sugar):
status: "active" (attention pulse: opacity + marker-size + ripple halo),
"done" (stop animation, full opacity, success stroke), or
"muted" (stop animation, grayed out). Expanded server-side
into concrete animate/opacity/color fields; explicit fields
you set alongside it always win.
"""
fill_color: str | None = None
stroke_color: str | None = None
Expand All @@ -37,6 +62,9 @@ class Style:
label_placement: str | None = None
color_by: dict[str, Any] | None = None
glow: bool | dict[str, Any] | None = None
opacity: float | None = None
animate: list[dict[str, Any]] | None = None
status: str | None = None

def to_dict(self) -> dict:
return {k: v for k, v in {
Expand All @@ -50,6 +78,9 @@ def to_dict(self) -> dict:
"label_placement": self.label_placement,
"color_by": self.color_by,
"glow": self.glow,
"opacity": self.opacity,
"animate": self.animate,
"status": self.status,
}.items() if v is not None}


Expand Down
Loading
Loading