Skip to content

Commit fbfac2f

Browse files
authored
Merge pull request #7 from ESIPFed/feat/style-animate
feat(style): animate + opacity + status lifecycle (pulse, ripple halo, done, muted)
2 parents b2dd8dc + 684a05a commit fbfac2f

7 files changed

Lines changed: 564 additions & 71 deletions

File tree

demo_status.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
"""Demo: attention-lifecycle styling (style.status) on survey stations.
2+
3+
Simulates the EOGPT Cawndilla-2 pixel-drill flow: drop six stations, then
4+
walk them one by one — the station being processed PULSES (status=active),
5+
finished stations flip to a solid success look (status=done), and when the
6+
analysis pivots away, abandoned stations GRAY OUT (status=muted).
7+
8+
Run: python demo_status.py (server on http://localhost:8000)
9+
Open the printed URL in a browser to watch.
10+
"""
11+
12+
import time
13+
14+
import httpx
15+
16+
BASE = "http://localhost:8000"
17+
18+
STATIONS = [
19+
("P1", -143.20, 32.10), ("P2", -143.05, 32.18), ("P3", -143.12, 31.98),
20+
("C1", -143.45, 32.30), ("C2", -142.80, 32.35), ("C3", -142.90, 31.85),
21+
]
22+
23+
24+
def point_geojson(name, lon, lat):
25+
return (
26+
'{"type":"Feature","properties":{"name":"%s"},'
27+
'"geometry":{"type":"Point","coordinates":[%f,%f]}}' % (name, lon, lat)
28+
)
29+
30+
31+
def main():
32+
c = httpx.Client(base_url=BASE, timeout=30)
33+
map_id = c.post("/api/maps").json()["map_id"]
34+
print(f"Map: {BASE}/map/{map_id}")
35+
36+
def event(type_, data):
37+
r = c.post(f"/api/maps/{map_id}/events", json={"type": type_, "data": data})
38+
r.raise_for_status()
39+
return r.json()
40+
41+
# Drop the stations (labeled points)
42+
ids = {}
43+
for name, lon, lat in STATIONS:
44+
resp = event("add_point", {
45+
"geojson": point_geojson(name, lon, lat), "name": name,
46+
"style": {"fill_color": "#38bdf8", "stroke_color": "#e0f2fe",
47+
"stroke_width": 2, "label": True},
48+
})
49+
ids[name] = resp["asset_id"]
50+
event("zoom_to_bbox", {"bbox": [-143.7, 31.7, -142.6, 32.5]})
51+
print("Stations placed. Starting the drill loop...")
52+
time.sleep(3)
53+
54+
# Drill P1-P3: pulse while "processing", then mark done
55+
for name in ("P1", "P2", "P3"):
56+
print(f" {name}: processing (pulse)...")
57+
event("update_style", {"asset_id": ids[name], "style": {"status": "active"}})
58+
time.sleep(5) # pretend to compute
59+
print(f" {name}: done.")
60+
event("update_style", {"asset_id": ids[name], "style": {"status": "done"}})
61+
time.sleep(1)
62+
63+
# Pivot: the control stations are no longer under consideration
64+
print("Pivoting — muting control stations C1-C3...")
65+
time.sleep(2)
66+
for name in ("C1", "C2", "C3"):
67+
event("update_style", {"asset_id": ids[name], "style": {"status": "muted"}})
68+
69+
# Finale: raw ripple spec — a big slow sonar ping with a custom color
70+
time.sleep(3)
71+
print("Finale: raw ripple on P2 (halo 8->40px, 2.2s period, amber)...")
72+
event("update_style", {"asset_id": ids["P2"], "style": {
73+
"animate": [{"property": "ripple", "from": 8, "to": 40,
74+
"period": 2.2, "color": "#f59e0b"}],
75+
}})
76+
time.sleep(10)
77+
event("update_style", {"asset_id": ids["P2"], "style": {"status": "done"}})
78+
79+
print("Demo complete: P1-P3 solid green, C1-C3 grayed out.")
80+
81+
82+
if __name__ == "__main__":
83+
main()

sdk/mapcontrol/models.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,31 @@ class Style:
2626
glow: True → defaults, or {"period": 2.0 (seconds per cycle),
2727
"min_opacity": 0.15, "max_opacity": 0.85, "stroke": True} —
2828
the asset slowly fades between translucent and opaque.
29+
(Back-compat sugar over `animate`.)
30+
31+
Static opacity:
32+
opacity: flat 0..1 opacity on fills, lines, and circles.
33+
None = renderer defaults. An active animation overrides it
34+
while running.
35+
36+
Animate (generic property animation):
37+
animate: list of effects driven by one shared client rAF loop:
38+
[{"property": "opacity"|"circle_radius"|"stroke_width",
39+
"from": 0.35, "to": 1.0, "period": 1.2}]
40+
Each effect oscillates the paint property between `from` and
41+
`to` over `period` seconds. [] stops any running animation.
42+
Special effect "ripple": a sonar-ping halo ring that expands
43+
outward from point markers (radius `from`→`to` px) while
44+
fading to transparent, then restarts (sawtooth). Optional
45+
"color" (hex) tints the halo:
46+
[{"property": "ripple", "from": 8, "to": 26, "period": 1.6}]
47+
48+
Status (attention-lifecycle sugar):
49+
status: "active" (attention pulse: opacity + marker-size + ripple halo),
50+
"done" (stop animation, full opacity, success stroke), or
51+
"muted" (stop animation, grayed out). Expanded server-side
52+
into concrete animate/opacity/color fields; explicit fields
53+
you set alongside it always win.
2954
"""
3055
fill_color: str | None = None
3156
stroke_color: str | None = None
@@ -37,6 +62,9 @@ class Style:
3762
label_placement: str | None = None
3863
color_by: dict[str, Any] | None = None
3964
glow: bool | dict[str, Any] | None = None
65+
opacity: float | None = None
66+
animate: list[dict[str, Any]] | None = None
67+
status: str | None = None
4068

4169
def to_dict(self) -> dict:
4270
return {k: v for k, v in {
@@ -50,6 +78,9 @@ def to_dict(self) -> dict:
5078
"label_placement": self.label_placement,
5179
"color_by": self.color_by,
5280
"glow": self.glow,
81+
"opacity": self.opacity,
82+
"animate": self.animate,
83+
"status": self.status,
5384
}.items() if v is not None}
5485

5586

0 commit comments

Comments
 (0)