001// Copyright (c) FIRST and other WPILib contributors.
002// Open Source Software; you can modify and/or share it under the terms of
003// the WPILib BSD license file in the root directory of this project.
004
005package edu.wpi.first.wpilibj2.command;
006
007import edu.wpi.first.util.sendable.SendableBuilder;
008import java.util.Collections;
009import java.util.LinkedHashMap;
010import java.util.Map;
011
012/**
013 * A command composition that runs a set of commands in parallel, ending only when a specific
014 * command (the "deadline") ends, interrupting all other commands that are still running at that
015 * point.
016 *
017 * <p>The rules for command compositions apply: command instances that are passed to it cannot be
018 * added to any other composition or scheduled individually, and the composition requires all
019 * subsystems its components require.
020 *
021 * <p>This class is provided by the NewCommands VendorDep
022 */
023public class ParallelDeadlineGroup extends Command {
024  // maps commands in this composition to whether they are still running
025  // LinkedHashMap guarantees we iterate over commands in the order they were added (Note that
026  // changing the value associated with a command does NOT change the order)
027  private final Map<Command, Boolean> m_commands = new LinkedHashMap<>();
028  private boolean m_runWhenDisabled = true;
029  private boolean m_finished = true;
030  private Command m_deadline;
031  private InterruptionBehavior m_interruptBehavior = InterruptionBehavior.kCancelIncoming;
032
033  /**
034   * Creates a new ParallelDeadlineGroup. The given commands, including the deadline, will be
035   * executed simultaneously. The composition will finish when the deadline finishes, interrupting
036   * all other still-running commands. If the composition is interrupted, only the commands still
037   * running will be interrupted.
038   *
039   * @param deadline the command that determines when the composition ends
040   * @param otherCommands the other commands to be executed
041   * @throws IllegalArgumentException if the deadline command is also in the otherCommands argument
042   */
043  @SuppressWarnings("this-escape")
044  public ParallelDeadlineGroup(Command deadline, Command... otherCommands) {
045    setDeadline(deadline);
046    addCommands(otherCommands);
047  }
048
049  /**
050   * Sets the deadline to the given command. The deadline is added to the group if it is not already
051   * contained.
052   *
053   * @param deadline the command that determines when the group ends
054   * @throws IllegalArgumentException if the deadline command is already in the composition
055   */
056  public final void setDeadline(Command deadline) {
057    @SuppressWarnings("PMD.CompareObjectsWithEquals")
058    boolean isAlreadyDeadline = deadline == m_deadline;
059    if (isAlreadyDeadline) {
060      return;
061    }
062    if (m_commands.containsKey(deadline)) {
063      throw new IllegalArgumentException(
064          "The deadline command cannot also be in the other commands!");
065    }
066    addCommands(deadline);
067    m_deadline = deadline;
068  }
069
070  /**
071   * Adds the given commands to the group.
072   *
073   * @param commands Commands to add to the group.
074   */
075  public final void addCommands(Command... commands) {
076    if (!m_finished) {
077      throw new IllegalStateException(
078          "Commands cannot be added to a composition while it's running");
079    }
080
081    CommandScheduler.getInstance().registerComposedCommands(commands);
082
083    for (Command command : commands) {
084      if (!Collections.disjoint(command.getRequirements(), getRequirements())) {
085        throw new IllegalArgumentException(
086            "Multiple commands in a parallel group cannot require the same subsystems");
087      }
088      m_commands.put(command, false);
089      addRequirements(command.getRequirements());
090      m_runWhenDisabled &= command.runsWhenDisabled();
091      if (command.getInterruptionBehavior() == InterruptionBehavior.kCancelSelf) {
092        m_interruptBehavior = InterruptionBehavior.kCancelSelf;
093      }
094    }
095  }
096
097  @Override
098  public final void initialize() {
099    for (Map.Entry<Command, Boolean> commandRunning : m_commands.entrySet()) {
100      commandRunning.getKey().initialize();
101      commandRunning.setValue(true);
102    }
103    m_finished = false;
104  }
105
106  @Override
107  public final void execute() {
108    for (Map.Entry<Command, Boolean> commandRunning : m_commands.entrySet()) {
109      if (!commandRunning.getValue()) {
110        continue;
111      }
112      commandRunning.getKey().execute();
113      if (commandRunning.getKey().isFinished()) {
114        commandRunning.getKey().end(false);
115        commandRunning.setValue(false);
116        if (commandRunning.getKey().equals(m_deadline)) {
117          m_finished = true;
118        }
119      }
120    }
121  }
122
123  @Override
124  public final void end(boolean interrupted) {
125    for (Map.Entry<Command, Boolean> commandRunning : m_commands.entrySet()) {
126      if (commandRunning.getValue()) {
127        commandRunning.getKey().end(true);
128      }
129    }
130  }
131
132  @Override
133  public final boolean isFinished() {
134    return m_finished;
135  }
136
137  @Override
138  public boolean runsWhenDisabled() {
139    return m_runWhenDisabled;
140  }
141
142  @Override
143  public InterruptionBehavior getInterruptionBehavior() {
144    return m_interruptBehavior;
145  }
146
147  @Override
148  public void initSendable(SendableBuilder builder) {
149    super.initSendable(builder);
150
151    builder.addStringProperty("deadline", m_deadline::getName, null);
152  }
153}