/
Controller.js
227 lines (203 loc) · 6.04 KB
/
Controller.js
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
import Action from './Action.js';
import SignalTarget from './SignalTarget.js';
import Doors from '../hardware/Doors.js';
import LightButton from '../hardware/LightButton.js';
// private properties are handled by this WeakMap
const privates = new WeakMap;
// trap utilities to avoid external
// interferences/polyfills/pollution
const {assign, freeze} = Object;
// a controller is in charge of orchestrating
// the elevator functionality as a whole.
export default class Controller extends SignalTarget {
constructor(elevator, doors, panels) {
super();
// let's assume by contract doorsPanel
// is mandatory at index 0
const floor = Math.min.apply(
Math,
panels[0].buttons
.filter(isFloor)
.map(Action.asFloor)
);
// store private variables
privates.set(this, {
doors,
elevator,
panels,
queue: [],
state: {floor, moving: false},
timer: 0
});
// listen to all panels buttons
panels.forEach(addPanelPress, this);
// listen to doors too and propagate events
doors
.on('changed', this)
.on('changed', propagate(this, 'doors'))
.on('moving', propagate(this, 'doors'));
// same goes for the elevator
elevator
.on('changed', this)
.on('changed', propagate(this, 'elevator'))
.on('moving', propagate(this, 'elevator'));
}
// a state is exposed as read-only (unique) object
get state() {
return freeze(assign({}, privates.get(this).state));
}
// every event should eventually clear the timer
// that scheduled doors closing and next action
// this is why handleEvent is redefined,
// to intercept all possible registered events.
handleEvent(event) {
const info = privates.get(this);
// clear any waiting timer
if (info.timer) {
clearTimeout(info.timer);
info.timer = 0;
}
// then analyze the action
switch (event.type) {
case 'press':
this.onButtonPress(event);
break;
case 'changed':
if (event.currentTarget === info.doors) {
this.onDoorsChanged(event);
} else {
this.onElevatorChanged(event);
}
break;
}
}
onButtonPress(event) {
const button = event.detail;
const info = privates.get(this);
// if the button is about changing level/floor
if (isFloor(button)) {
// find out which one
const floor = Action.asFloor(button.symbol);
// be sure it's not already in the queue
if (!info.queue.includes(floor)) {
// in such case push it through
info.queue.push(floor);
// light eah button related to this floor on
switchButton(info.panels, button.symbol, LightButton.ON);
// verify doors state
switch (info.doors.status) {
// if opened, close them and let the event follow up
case Doors.OPENED:
info.doors.close();
break;
// if the lift has closed doors
case Doors.CLOSED:
// and the elevator is not moving
if (!info.state.moving) {
// signal a doors change to trigger doors close logic
info.doors.signal('changed');
}
break;
}
}
} else {
// turn on the light for an instant
if (button instanceof LightButton) {
button.switch(LightButton.ON);
setTimeout(() => button.switch(LightButton.OFF), 300);
}
// find out what to do
switch (button.symbol) {
case Action.ALARM:
alert('ALARM ALARM');
break;
// if it's about opening doors
case Action.OPEN_DOORS:
// in case these are already opened
if (info.doors.status === Doors.OPENED) {
// prepare for the next action, if any
prepareNextAction(info);
}
// otherwise if the elevator is not moving
else if (!info.state.moving) {
// ask to open doors
info.doors.open();
}
break;
// if it's about clsing doors
case Action.CLOSE_DOORS:
// just invoke it and let the rest
// of the events flow
info.doors.close();
break;
}
}
}
onDoorsChanged(event) {
const doors = event.currentTarget;
const info = privates.get(this);
switch (doors.status) {
case Doors.OPENED:
prepareNextAction(info);
break;
case Doors.CLOSED:
if (info.queue.length) {
const level = info.queue[0];
if (level === Action.asFloor(info.state.floor.symbol)) {
info.queue.shift();
info.doors.open();
} else {
info.state.moving = true;
info.elevator.reach(level);
}
}
break;
}
}
onElevatorChanged(event) {
const info = privates.get(this);
info.state.moving = false;
info.state.floor = info.queue.shift();
switchButton(
info.panels,
Action.asSymbol(info.state.floor),
LightButton.OFF
);
info.doors.open();
}
}
function addPanelPress(panel) {
// attach all panel events to this controller
panel.on('press', this);
}
function isFloor(button) {
// true if a button is associated to a floor
return Action.asFloor(button.symbol) !== -1;
}
function prepareNextAction(info) {
// if there is something to do
if (info.queue.length) {
// setup a timer to do it once doors are closed
info.timer = setTimeout(() => info.doors.close(), 1000);
}
}
function propagate(controller, prefix) {
// re-signal events for UI sake. Pass parts around too
return event => {
controller.signal(`${prefix}:${event.type}`, event.currentTarget);
};
}
// find every button of every panel
// that is related to a certain symbol
// and switch its state on or off
function switchButton(panels, symbol, state) {
for (const panel of panels) {
for (const button of panel.buttons) {
if (button.symbol === symbol) {
if (button instanceof LightButton) {
button.switch(state);
}
}
}
}
}