-
Notifications
You must be signed in to change notification settings - Fork 2
/
client.py
248 lines (220 loc) · 10.2 KB
/
client.py
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
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
"""
AirSchedule Client
2020
This file includes the client. The client communicates with the server to view and interact with the simulation state
todo - finish handlers on server side for client requests to move flights
"""
import time
import asyncio
import datetime
import websockets
import logging
from graphics import Gui, FlightElement, scroll_handler, selection_handler, drag_handler
from vec2 import Vec2
logging.basicConfig(level=logging.INFO)
# controls how wide the timeline is
MINUTES_WIDTH = 4
# Aircraft and Flight are client-side objects for holding information related to specific simObjects on the server side
# They Also contain elements, which are their representation that the user sees
class Aircraft:
def __init__(self, string):
data = string.split(",")
self.name = data[1]
self.tail_number = data[2]
self.element = None
class Flight:
def __init__(self, string):
data = string.split(",")
self.name = data[1]
self.dept_loc = data[2]
self.arri_loc = data[3]
self.dept_time = datetime.datetime.fromisoformat(data[4])
self.arri_time = datetime.datetime.fromisoformat(data[5])
self.aircraft = data[6]
self.status = data[7]
self.element = None
def resolve_reference(self, aircraft):
# Replaces self.aircraft with a reference to the Aircraft Object with the same name
for item in aircraft:
if item.name == self.aircraft:
self.aircraft = item
def get_color(self):
# Determines the color of this flight's element
if self.status == "scheduled":
return 50, 160, 160
elif self.status == "outgate":
return 50, 200, 200
elif self.status == "offground":
return 50, 220, 68
elif self.status == "onground":
return 50, 109, 160
elif self.status == "ingate":
return 78, 104, 128
else:
return 0, 0, 0
def create_element(self, gui, index):
# Returns an element that the gui can use that visualizes this flight on the screen
box_width = (self.arri_time - self.dept_time).total_seconds() / 60
box_start = (gui.default_time - self.dept_time).total_seconds() / 60
self.element = FlightElement(
size=Vec2(box_width * MINUTES_WIDTH, 35),
padding=Vec2(100 - (box_start * MINUTES_WIDTH), (1 + index * 35)),
color=self.get_color(),
border=3,
border_color=(255, 0, 0),
text=self.name,
font=gui.font_25,
dept_text=self.dept_loc,
dept_time=str(self.dept_time.time())[0:5],
arri_text=self.arri_loc,
arri_time=str(self.arri_time.time())[0:5],
side_font=gui.font_15,
handlers=[scroll_handler, selection_handler, drag_handler],
object=self
)
return self.element
def update_element(self, client):
# If the server issues an update for the flight, the flight's element has to be updated to reflect the changes
self.element.dept_text = self.dept_loc
# Formats time into a 4 digit string. ex: 08:30, 22:15, etc
self.element.dept_time = str(self.dept_time.time())[0:5]
self.element.arri_text = self.arri_loc
self.element.arri_time = str(self.arri_time.time())[0:5]
self.element.color = self.get_color()
box_start = (client.gui.default_time - self.dept_time).total_seconds() / 60
self.element.padding = Vec2(100 - (box_start * MINUTES_WIDTH),
client.objects["aircraft"].index(self.aircraft) * 35)
self.element.loc_mod = Vec2(0, 0)
self.element.location = None # Clear the location so that it gets recalculated the next time we ask for it
self.element.rendered_text, self.element.text_location = self.element.prep_text()
self.element.prep_side_text()
# The Client object interacts with the server and utilizes a GUI for the user to interact with
class Client:
def __init__(self):
# New events are requests generated by the GUI and sent to the server
self.new_events = []
# Pending events are requests we are waiting for the server to reply about
self.pending_events = []
# Events are any of our requests that were granted by the server. Keeping track of these allows 'undo' to be
# supported in the future
self.events = []
self.running = True
# Like the server, the client has a dict of object types
self.objects = {"aircraft": [], "flight": []}
# The current time of the simulation
self.time = None
# Whether or not we have received the simulation state from the server
self.need_setup = True
# How wide a minute appears in the gui. Default is 4, ie one minute is 4 pixels wide
self.MINUTES_WIDTH = MINUTES_WIDTH
self.gui = Gui()
# Sets the gui up to display the "connecting" page
self.gui.connecting_page()
self.gui.update(self)
# The first thing received from the server is a simulation state. It is decoded here.
async def setup(self, state):
# Each object from the server is split into a list. The first object is the time
blocks = state.split("`")
self.time = datetime.datetime.fromisoformat(blocks[0])
for i in range(1, len(blocks)):
if blocks[i].startswith("aircraft"):
self.objects["aircraft"].append(Aircraft(blocks[i]))
elif blocks[i].startswith("flight"):
self.objects["flight"].append(Flight(blocks[i]))
for flight in self.objects["flight"]:
flight.resolve_reference(self.objects["aircraft"])
# Printing a sanity check to make sure all objects were received and that the flights resolved their aircraft
logging.info("parsed %s aircraft and %s flights" % (len(self.objects["aircraft"]), len(self.objects["flight"])))
first = self.objects["flight"][0]
logging.info("flight %s has linked to aircraft %s" % (first.name, first.aircraft.name))
self.need_setup = False
# GUI needs a start time to use as a reference, so we use the current sim time
self.gui.default_time = self.time
# Switching the GUI from the "connecting" page to the schedule page
self.gui.schedule_page(self)
# This gets used when we have to change the attribute of an object but don't have a direct reference to it.
# Instead we search by its type and name
async def set_attr(self, object_type, object_name, attribute_name, value):
for obj in self.objects[object_type]:
if obj.name == object_name:
print(obj.__getattribute__(attribute_name), value)
obj.__setattr__(attribute_name, value)
if object_type == "flight":
# updates the flight's element to reflect the new data
obj.update_element(self)
break
# Handles events from the server
# Can't handle all new_value types, see long comment in event.py for a full decoder
async def handle_event(self, event):
data = event.split(",")
event_id = int(data[0])
object_type = data[1]
object_name = data[2]
attribute_name = data[3]
reference_type = None
reference_name = None
new_value = None
if len(data) > 5:
reference_type = data[4]
reference_name = data[5]
else:
new_value = data[4]
# todo - check pending_events to see if we can remove any of those. Required in order to support undo
if object_type == "scenario":
if attribute_name == "date":
self.time = datetime.datetime.fromisoformat(new_value)
elif object_type == "flight":
if attribute_name == "status":
await self.set_attr("flight", object_name, attribute_name, new_value)
elif attribute_name == "arri_time" or attribute_name == "dept_time":
await self.set_attr("flight", object_name, attribute_name, datetime.datetime.fromisoformat(new_value))
elif attribute_name == "aircraft":
for item in self.objects["aircraft"]:
if item.name == reference_name:
aircraft = item
break
await self.set_attr("flight", object_name, attribute_name, aircraft)
# Handles messages coming from the server
async def consume(self, ws):
while self.running:
try:
data = await ws.recv()
if self.need_setup:
await self.setup(data)
else:
await self.handle_event(data)
logging.info(data)
except websockets.exceptions.ConnectionClosed:
logging.info("Connection Closed")
break
await asyncio.sleep(.001)
# Handles the gui and sending messages back to the server
async def produce(self, ws):
while self.running:
self.gui.update(self)
if self.gui.quit:
self.running = False
while len(self.new_events) > 0:
event = self.new_events.pop()
self.pending_events.append(event)
await ws.send(event)
await asyncio.sleep(.001)
# Connects to the server and configures the async tasks.
async def run(self):
try:
async with websockets.connect("ws://localhost:51010") as ws:
logging.info("Connected!")
consume_task = asyncio.create_task(self.consume(ws))
produce_task = asyncio.create_task(self.produce(ws))
await asyncio.wait([consume_task, produce_task], return_when=asyncio.FIRST_COMPLETED)
except OSError as e:
# 99% of the time this will be triggered by a failed websocket connection
logging.info(e)
self.gui.connecting_failed_page()
self.gui.update(self)
time.sleep(1.75)
except Exception as e:
logging.info(e)
if __name__ == "__main__":
x = Client()
asyncio.run(x.run())