traffic-light/multi-traffic-light.js

  1. const {Light, TrafficLight} = require('./traffic-light');
  2. ///////////////
  3. /**
  4. * A composite light that combines all composed lights.
  5. * @memberof trafficLight
  6. * @extends trafficLight.Light
  7. */
  8. class MultiLight extends Light {
  9. /**
  10. * @param {trafficLight.Light[]} lights - Lights composed.
  11. */
  12. constructor(lights) {
  13. super();
  14. this.lights = lights;
  15. // this.on and this.off might not reflect the underlying lights,
  16. // just what the multi-light has been through
  17. }
  18. /** Toggles the lights. */
  19. toggle() {
  20. super.toggle();
  21. this.lights.forEach(l => l.toggle());
  22. }
  23. /** Turns the lights on. */
  24. turnOn() {
  25. super.turnOn();
  26. this.lights.forEach(l => l.turnOn());
  27. }
  28. /** Turns the lights off. */
  29. turnOff() {
  30. super.turnOff();
  31. this.lights.forEach(l => l.turnOff());
  32. }
  33. }
  34. ///////////////
  35. let dummyLight = new Light();
  36. ///////////////
  37. /**
  38. * A composite traffic light that combines all composed traffic lights.
  39. * Does not track or raise any `enabled` or `disabled` events for the composed
  40. * traffic lights.
  41. * @memberof trafficLight
  42. * @extends trafficLight.TrafficLight
  43. */
  44. class MultiTrafficLight extends TrafficLight {
  45. /**
  46. * @param {trafficLight.TrafficLight[]} trafficLights - Traffic lights composed.
  47. */
  48. constructor(trafficLights) {
  49. super(dummyLight, dummyLight, dummyLight);
  50. this.trafficLights = trafficLights;
  51. }
  52. get trafficLights() {
  53. return this._trafficLights;
  54. }
  55. set trafficLights(trafficLights) {
  56. this._trafficLights = trafficLights;
  57. this.red = new MultiLight(trafficLights.map(tl => tl.red)); // eslint-disable-line no-multi-spaces
  58. this.yellow = new MultiLight(trafficLights.map(tl => tl.yellow));
  59. this.green = new MultiLight(trafficLights.map(tl => tl.green)); // eslint-disable-line no-multi-spaces
  60. }
  61. /**
  62. * If any of the composed traffic lights is enabled.
  63. * @type {boolean}
  64. */
  65. get isEnabled() {
  66. return this._trafficLights.some(tl => tl.isEnabled);
  67. }
  68. }
  69. ///////////////
  70. function unique(a) {
  71. return [...new Set(a)];
  72. }
  73. ///////////////
  74. /**
  75. * A composite traffic light with a flexible way to select which composed
  76. * traffic lights are active or in use.
  77. * @memberof trafficLight
  78. * @extends trafficLight.TrafficLight
  79. */
  80. class FlexMultiTrafficLight extends TrafficLight {
  81. /**
  82. * Creates a new instance of this class.
  83. * Starts off using the first traffic light in the provided `trafficLights`.
  84. * Tries to check out the provided traffic lights.
  85. * @param {trafficLight.TrafficLight[]} trafficLights - Traffic lights composed.
  86. */
  87. constructor(trafficLights) {
  88. super(dummyLight, dummyLight, dummyLight);
  89. this.activeMultiTrafficLight = new MultiTrafficLight([]);
  90. this.allTrafficLights = trafficLights.filter(tl => tl.checkOut());
  91. this.allTrafficLights.forEach(tl => this._subscribe(tl));
  92. this.use([0]);
  93. }
  94. /**
  95. * Adds a traffic light to the composite.
  96. * Tries to exclusively check it out first and because of that won't add
  97. * any duplicates.
  98. * @param {trafficLight.TrafficLight} trafficLight - Traffic light to add.
  99. * Must not be null.
  100. */
  101. add(trafficLight) {
  102. if (!trafficLight.checkOut()) return;
  103. let wasEnabled = this.isEnabled;
  104. this.allTrafficLights.push(trafficLight);
  105. this._subscribe(trafficLight);
  106. if (this.activeTrafficLights.length === 0) {
  107. this.use([0]);
  108. }
  109. if (!wasEnabled && this.isEnabled) {
  110. this.emit('enabled');
  111. }
  112. }
  113. // returns an array of the tuple: (traffic light, original index)
  114. get enabledTrafficLights() {
  115. return (
  116. this.allTrafficLights
  117. .map((tl, i) => [tl, i])
  118. .filter(([tl, _]) => tl.isEnabled));
  119. }
  120. // returns an array of the tuple: (traffic light, original index)
  121. get activeTrafficLights() {
  122. return (
  123. this.enabledTrafficLights
  124. .filter(([tl, _], i) => this.activeIndexes.indexOf(i) >= 0));
  125. }
  126. /**
  127. * Selects which traffic lights to use given their indexes (0-based),
  128. * only considering enabled traffic lights.
  129. * Indexes wrap around from the last to the first.
  130. * @param {number[]} activeIndexes - Traffic light indexes to use.
  131. * Must not be empty.
  132. */
  133. use(activeIndexes) {
  134. this._setIndexes(activeIndexes);
  135. this.activeMultiTrafficLight.trafficLights = this.activeTrafficLights.map(([tl, _]) => tl);
  136. this.red = this.activeMultiTrafficLight.red;
  137. this.yellow = this.activeMultiTrafficLight.yellow;
  138. this.green = this.activeMultiTrafficLight.green;
  139. }
  140. _setIndexes(activeIndexes) {
  141. let tlsEnabled = this.enabledTrafficLights.map(([tl, _]) => tl);
  142. let l = tlsEnabled.length;
  143. if (l > 0) {
  144. activeIndexes = unique(activeIndexes.map(i => i < 0 ? l + i : i % l));
  145. } else {
  146. activeIndexes = [];
  147. }
  148. activeIndexes.sort();
  149. this.activeIndexes = activeIndexes;
  150. this.indexes = this.activeTrafficLights.map(([_, i]) => i);
  151. }
  152. _subscribe(tl) {
  153. tl.on('enabled', () => this._enabled(tl));
  154. tl.on('disabled', () => this._disabled(tl));
  155. }
  156. _enabled(tl) {
  157. if (this.enabledTrafficLights.length === 1) {
  158. // the first traffic light is enabled; all were disabled before
  159. this.use([0]);
  160. this.emit('enabled');
  161. } else {
  162. // recalculate indexes
  163. let tlIndex = this.allTrafficLights.indexOf(tl);
  164. let newActiveIndexes = this.indexes.map((i, j) => this.activeIndexes[j] + (tlIndex < i ? 1 : 0));
  165. this.use(newActiveIndexes);
  166. }
  167. }
  168. _disabled(tl) {
  169. if (!this.isEnabled) {
  170. // the only enabled traffic light was disabled
  171. this.use([]);
  172. this.emit('disabled'); // 'disabled' instead of 'interrupted'
  173. return;
  174. }
  175. // recalculate indexes
  176. let tlIndex = this.allTrafficLights.indexOf(tl);
  177. let activeTrafficLightWasDisabled = this.indexes.indexOf(tlIndex) >= 0;
  178. let newActiveIndexes = this.indexes
  179. .map((i, j) => tlIndex === i ? -1 : (this.activeIndexes[j] - (tlIndex < i ? 1 : 0)))
  180. .filter(i => i >= 0);
  181. if (newActiveIndexes.length === 0) {
  182. newActiveIndexes = [0]; // re-assign
  183. }
  184. this.use(newActiveIndexes);
  185. if (activeTrafficLightWasDisabled) {
  186. /**
  187. * Interrupted event. In a `FlexMultiTrafficLight`, if an active traffic
  188. * light gets disabled, and there are still enabled traffic lights left,
  189. * this event is raised. If no more traffic lights are enabled,
  190. * then the `disabled` event is raised.
  191. * @event trafficLight.FlexMultiTrafficLight#interrupted
  192. */
  193. this.emit('interrupted');
  194. }
  195. }
  196. /**
  197. * Gets the traffic light indexes that are in use.
  198. * If there are no traffic lights in use, or no traffic lights are useable,
  199. * returns an empty array.
  200. * @returns {number[]} The traffic light indexes that are in use.
  201. */
  202. using() {
  203. return this.activeIndexes;
  204. }
  205. /**
  206. * Selects the next traffic light to use, going back to the first one if
  207. * the currently selected one is the last.
  208. * Also works with multiple selected traffic lights, moving all to the next.
  209. */
  210. next() {
  211. this._move(+1);
  212. }
  213. /**
  214. * Selects the previous traffic light to use, going to the last one if
  215. * the currently selected one is the first.
  216. * Also works with multiple selected traffic lights, moving all to the previous.
  217. */
  218. previous() {
  219. this._move(-1);
  220. }
  221. /**
  222. * Selects the nearest traffic light to use, remembering the direction
  223. * of movement (forwards or backwards).
  224. * Also works with multiple selected traffic lights, moving all to the nearest,
  225. * following a single direction (so it's possible to wrap around at the last
  226. * if both the first and last indexes are in use).
  227. */
  228. near() {
  229. if (this.activeIndexes.length === 0) {
  230. this.use([0]);
  231. return;
  232. }
  233. let lastIndex = this.enabledTrafficLights.length - 1;
  234. if (this.activeIndexes.indexOf(0) >= 0) {
  235. this.direction = +1;
  236. } else if (this.activeIndexes.indexOf(lastIndex) >= 0) {
  237. this.direction = -1;
  238. }
  239. this._move(this.direction || +1);
  240. }
  241. _move(direction) {
  242. if (this.activeIndexes.length > 0) {
  243. this.use(this.activeIndexes.map(i => i + direction));
  244. } else {
  245. this.use([0]);
  246. }
  247. }
  248. /**
  249. * Selects the last traffic light to use.
  250. */
  251. last() {
  252. this.use([this.enabledTrafficLights.length - 1]);
  253. }
  254. /**
  255. * Selects all traffic lights to use simultaneously.
  256. */
  257. useAll() {
  258. this.use(this.enabledTrafficLights.map((_, i) => i));
  259. }
  260. /**
  261. * Resets all active traffic lights.
  262. */
  263. reset() {
  264. this.activeMultiTrafficLight.reset();
  265. }
  266. /**
  267. * If there are composed traffic lights and any of them is enabled.
  268. * @type {boolean}
  269. */
  270. get isEnabled() {
  271. return this.allTrafficLights.length > 0 &&
  272. this.allTrafficLights.some(tl => tl.isEnabled);
  273. }
  274. toString() {
  275. return `multi (${this.enabledTrafficLights.length};${this.activeTrafficLights.length})`;
  276. }
  277. }
  278. ///////////////
  279. module.exports = {
  280. MultiLight, MultiTrafficLight, FlexMultiTrafficLight
  281. };