File size: 10,400 Bytes
5e5efc0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
#!/usr/bin/env python3
"""
Live Supply Chain Map — visual dashboard for judges.

Shows a world map with:
  - Ports (green=open, red=closed, yellow=reduced)
  - Shipping routes (blue=open, red=blocked)
  - Shipments moving along routes
  - Disruptions highlighted
  - Real-time stats panel

Generates an HTML file that opens in browser.
Run: python3 visualize.py
"""

import json
import webbrowser
import os
from world import SupplyChainWorld
from real_data import REAL_PORTS, REAL_ROUTES


def generate_map_html(world: SupplyChainWorld, filename: str = "supply_chain_map.html") -> str:
    """Generate an interactive HTML map of the supply chain."""

    # Port data with status colors
    ports_js = []
    for p in REAL_PORTS:
        status = world.ports.get(p["id"], {}).get("status", "open")
        color = {"open": "#22c55e", "closed": "#ef4444", "reduced": "#eab308"}.get(status, "#666")
        throughput = p.get("throughput_teu", 0)
        ports_js.append({
            "id": p["id"], "name": p["name"], "lat": p["lat"], "lon": p["lon"],
            "status": status, "color": color, "throughput": throughput,
            "country": p.get("country", ""),
        })

    # Route data with status
    routes_js = []
    for r in REAL_ROUTES:
        src_port = next((p for p in REAL_PORTS if p["id"] == r["from"]), None)
        dst_port = next((p for p in REAL_PORTS if p["id"] == r["to"]), None)
        if not src_port or not dst_port:
            continue
        route_info = world.routes.get(r["from"], {}).get(r["to"], {})
        status = route_info.get("status", "open")
        color = "#3b82f6" if status == "open" else "#ef4444"
        routes_js.append({
            "from_lat": src_port["lat"], "from_lon": src_port["lon"],
            "to_lat": dst_port["lat"], "to_lon": dst_port["lon"],
            "cost": r["cost_per_teu"], "days": r["transit_days"],
            "mode": r.get("mode", "ocean"), "status": status, "color": color,
            "carrier": r.get("carrier", ""),
        })

    # Shipment positions
    shipments_js = []
    for s in world.shipments.values():
        port = world.ports.get(s.current_location, {})
        p_data = next((p for p in REAL_PORTS if p["id"] == s.current_location), None)
        if p_data:
            color = {"pending": "#fbbf24", "in_transit": "#3b82f6", "delivered": "#22c55e", "lost": "#ef4444"}.get(s.status, "#666")
            shipments_js.append({
                "id": s.id, "product": s.product, "value": s.value_usd,
                "lat": p_data["lat"], "lon": p_data["lon"],
                "status": s.status, "color": color,
            })

    # Disruptions
    disruptions_js = [
        {"type": d.type, "severity": d.severity, "desc": d.description,
         "active": d.active, "start": d.start_day, "end": d.end_day}
        for d in world.disruptions
    ]

    # Stats
    total_value = sum(s.value_usd for s in world.shipments.values())
    stats = {
        "day": world.day, "total_days": world.total_days,
        "pending": sum(1 for s in world.shipments.values() if s.status == "pending"),
        "in_transit": sum(1 for s in world.shipments.values() if s.status == "in_transit"),
        "delivered": sum(1 for s in world.shipments.values() if s.status == "delivered"),
        "lost": sum(1 for s in world.shipments.values() if s.status == "lost"),
        "delivered_value": world.delivered_value,
        "lost_value": world.lost_value,
        "shipping_cost": world.total_shipping_cost,
        "total_value": total_value,
    }

    html = f"""<!DOCTYPE html>
<html><head>
<meta charset="utf-8">
<title>SupplyChainEnv — Live Map</title>
<style>
  * {{ margin:0; padding:0; box-sizing:border-box; }}
  body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f172a; color: #e2e8f0; }}
  .header {{ padding: 16px 24px; background: #1e293b; border-bottom: 1px solid #334155; display: flex; justify-content: space-between; align-items: center; }}
  .header h1 {{ font-size: 20px; color: #f8fafc; }}
  .header .badge {{ background: #22c55e; color: #000; padding: 4px 12px; border-radius: 12px; font-size: 12px; font-weight: 600; }}
  .container {{ display: flex; height: calc(100vh - 56px); }}
  .map {{ flex: 1; position: relative; background: #1e293b; overflow: hidden; }}
  .sidebar {{ width: 360px; background: #0f172a; border-left: 1px solid #334155; overflow-y: auto; padding: 16px; }}
  svg {{ width: 100%; height: 100%; }}
  .stat-card {{ background: #1e293b; border-radius: 8px; padding: 12px 16px; margin-bottom: 8px; }}
  .stat-label {{ font-size: 11px; color: #94a3b8; text-transform: uppercase; letter-spacing: 1px; }}
  .stat-value {{ font-size: 24px; font-weight: 700; margin-top: 2px; }}
  .stat-value.green {{ color: #22c55e; }}
  .stat-value.red {{ color: #ef4444; }}
  .stat-value.blue {{ color: #3b82f6; }}
  .stat-value.yellow {{ color: #eab308; }}
  .disruption {{ background: #1e293b; border-left: 3px solid #ef4444; border-radius: 4px; padding: 8px 12px; margin-bottom: 6px; font-size: 13px; }}
  .disruption.active {{ border-color: #ef4444; background: #291515; }}
  .disruption.upcoming {{ border-color: #eab308; }}
  .section-title {{ font-size: 13px; font-weight: 600; color: #94a3b8; margin: 16px 0 8px; text-transform: uppercase; letter-spacing: 1px; }}
  .port-label {{ font-size: 10px; fill: #cbd5e1; text-anchor: middle; pointer-events: none; }}
  .tooltip {{ position: absolute; background: #1e293b; border: 1px solid #475569; border-radius: 6px; padding: 8px 12px; font-size: 12px; pointer-events: none; display: none; z-index: 10; }}
</style>
</head><body>
<div class="header">
  <h1>SupplyChainEnv — Global Trade Network</h1>
  <span class="badge">Day {stats['day']}/{stats['total_days']} | {world.difficulty.upper()}</span>
</div>
<div class="container">
  <div class="map">
    <svg viewBox="-180 -90 360 180" preserveAspectRatio="xMidYMid meet">
      <rect x="-180" y="-90" width="360" height="180" fill="#0c1222"/>
      <!-- Simplified world outline -->
      <ellipse cx="0" cy="0" rx="170" ry="80" fill="none" stroke="#1e293b" stroke-width="0.5"/>
      <!-- Routes -->
      {"".join(f'<line x1="{r["from_lon"]}" y1="{-r["from_lat"]}" x2="{r["to_lon"]}" y2="{-r["to_lat"]}" stroke="{r["color"]}" stroke-width="{"0.8" if r["status"]=="open" else "1.2"}" opacity="{"0.4" if r["status"]=="open" else "0.8"}" stroke-dasharray="{"" if r["status"]=="open" else "2,2"}"/>' for r in routes_js)}
      <!-- Ports -->
      {"".join(f'<circle cx="{p["lon"]}" cy="{-p["lat"]}" r="2.5" fill="{p["color"]}" stroke="#fff" stroke-width="0.3"/><text x="{p["lon"]}" y="{-p["lat"]-4}" class="port-label">{p["name"]}</text>' for p in ports_js)}
      <!-- Shipments -->
      {"".join(f'<circle cx="{s["lon"]}" cy="{-s["lat"]}" r="1.5" fill="{s["color"]}" opacity="0.9"><animate attributeName="r" values="1;2.5;1" dur="2s" repeatCount="indefinite"/></circle>' for s in shipments_js)}
    </svg>
  </div>
  <div class="sidebar">
    <div class="stat-card"><div class="stat-label">Shipments Pending</div><div class="stat-value yellow">{stats['pending']}</div></div>
    <div class="stat-card"><div class="stat-label">In Transit</div><div class="stat-value blue">{stats['in_transit']}</div></div>
    <div class="stat-card"><div class="stat-label">Delivered</div><div class="stat-value green">{stats['delivered']} (${stats['delivered_value']:,.0f})</div></div>
    <div class="stat-card"><div class="stat-label">Lost</div><div class="stat-value red">{stats['lost']} (${stats['lost_value']:,.0f})</div></div>
    <div class="stat-card"><div class="stat-label">Shipping Cost</div><div class="stat-value">${stats['shipping_cost']:,.0f}</div></div>
    <div class="stat-card"><div class="stat-label">Total Cargo Value</div><div class="stat-value">${stats['total_value']:,.0f}</div></div>

    <div class="section-title">Active Disruptions</div>
    {"".join(f'<div class="disruption {"active" if d["active"] else "upcoming"}"><strong>{d["type"].upper()}</strong> [{d["severity"]}]<br/>{d["desc"][:80]}<br/>Day {d["start"]}-{d["end"]}</div>' for d in disruptions_js)}

    <div class="section-title">Legend</div>
    <div style="font-size:12px; line-height:1.8;">
      <span style="color:#22c55e;">&#9679;</span> Port Open &nbsp;
      <span style="color:#ef4444;">&#9679;</span> Port Closed &nbsp;
      <span style="color:#eab308;">&#9679;</span> Reduced<br/>
      <span style="color:#3b82f6;">&#9644;</span> Route Open &nbsp;
      <span style="color:#ef4444;">&#9644;</span> Route Blocked<br/>
      <span style="color:#fbbf24;">&#9679;</span> Pending &nbsp;
      <span style="color:#3b82f6;">&#9679;</span> In Transit &nbsp;
      <span style="color:#22c55e;">&#9679;</span> Delivered
    </div>

    <div class="section-title" style="margin-top:20px;">Data Sources</div>
    <div style="font-size:11px; color:#64748b; line-height:1.6;">
      Port throughput: UNCTAD 2023<br/>
      Shipping rates: Freightos Baltic Index Q1 2024<br/>
      Disruptions: Lloyd's List, WHO, USGS 2017-2024<br/>
      LPI scores: World Bank 2023
    </div>
  </div>
</div>
</body></html>"""

    with open(filename, "w") as f:
        f.write(html)
    return os.path.abspath(filename)


def main():
    """Run the demo agent and generate map at each stage."""
    from server.supply_chain_environment import SupplyChainEnvironment
    from models import SupplyChainAction

    def tool(name, args=None):
        return SupplyChainAction(action_type="ToolCallAction", tool_name=name, arguments=args or {})

    env = SupplyChainEnvironment()
    env.reset(seed=42, difficulty="hard")

    # Route shipments
    obs = env.step(tool("view_shipments"))
    for s in obs.tool_result["shipments"]:
        if s["status"] == "pending":
            path_obs = env.step(tool("find_path", {"from_port": s["current_location"], "to_warehouse": s["destination"]}))
            path = path_obs.tool_result.get("path")
            if path:
                env.step(tool("route_shipment", {"shipment_id": s["id"], "route": path}))

    # Advance 15 days for interesting state
    for _ in range(15):
        obs = env.step(tool("advance_day"))
        if obs.done:
            break

    # Generate map
    path = generate_map_html(env._world)
    print(f"Map generated: {path}")
    webbrowser.open(f"file://{path}")


if __name__ == "__main__":
    main()