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 org.wpilib.commands3; 006 007import static edu.wpi.first.util.ErrorMessages.requireNonNullParam; 008 009import java.util.Arrays; 010import java.util.LinkedHashSet; 011import java.util.Set; 012import java.util.function.BooleanSupplier; 013import java.util.stream.Collectors; 014import org.wpilib.annotation.NoDiscard; 015 016/** 017 * A builder class to configure and then create a {@link ParallelGroup}. Like {@link 018 * StagedCommandBuilder}, the final command is created by calling the terminal {@link 019 * #named(String)} method, or with an automatically generated name using {@link 020 * #withAutomaticName()}. 021 */ 022@NoDiscard 023public class ParallelGroupBuilder { 024 private final Set<Command> m_commands = new LinkedHashSet<>(); 025 private final Set<Command> m_requiredCommands = new LinkedHashSet<>(); 026 private BooleanSupplier m_endCondition; 027 028 /** 029 * Creates a new parallel group builder. The builder will have no commands and have no preapplied 030 * configuration options. 031 */ 032 public ParallelGroupBuilder() {} 033 034 /** 035 * Adds one or more optional commands to the group. They will not be required to complete for the 036 * parallel group to exit, and will be canceled once all required commands have finished. 037 * 038 * @param commands The optional commands to add to the group 039 * @return The builder object, for chaining 040 */ 041 public ParallelGroupBuilder optional(Command... commands) { 042 requireNonNullParam(commands, "commands", "ParallelGroupBuilder.optional"); 043 for (int i = 0; i < commands.length; i++) { 044 requireNonNullParam(commands[i], "commands[" + i + "]", "ParallelGroupBuilder.optional"); 045 } 046 047 m_commands.addAll(Arrays.asList(commands)); 048 return this; 049 } 050 051 /** 052 * Adds one or more required commands to the group. All required commands will need to complete 053 * for the group to exit. 054 * 055 * @param commands The required commands to add to the group 056 * @return The builder object, for chaining 057 */ 058 public ParallelGroupBuilder requiring(Command... commands) { 059 requireNonNullParam(commands, "commands", "ParallelGroupBuilder.requiring"); 060 for (int i = 0; i < commands.length; i++) { 061 requireNonNullParam(commands[i], "commands[" + i + "]", "ParallelGroupBuilder.requiring"); 062 } 063 064 m_requiredCommands.addAll(Arrays.asList(commands)); 065 m_commands.addAll(m_requiredCommands); 066 return this; 067 } 068 069 /** 070 * Adds a command to the group. The command must complete for the group to exit. 071 * 072 * @param command The command to add to the group 073 * @return The builder object, for chaining 074 */ 075 // Note: this primarily exists so users can cleanly chain .alongWith calls to build a 076 // parallel group, eg `fooCommand().alongWith(barCommand()).alongWith(bazCommand())` 077 public ParallelGroupBuilder alongWith(Command command) { 078 return requiring(command); 079 } 080 081 /** 082 * Adds an end condition to the command group. If this condition is met before all required 083 * commands have completed, the group will exit early. If multiple end conditions are added (e.g. 084 * {@code .until(() -> conditionA()).until(() -> conditionB())}), then the last end condition 085 * added will be used and any previously configured condition will be overridden. 086 * 087 * @param condition The end condition for the group. May be null. 088 * @return The builder object, for chaining 089 */ 090 public ParallelGroupBuilder until(BooleanSupplier condition) { 091 m_endCondition = condition; 092 return this; 093 } 094 095 /** 096 * Creates the group, using the provided named. The group will require everything that the 097 * commands in the group require, and will have a priority level equal to the highest priority 098 * among those commands. 099 * 100 * @param name The name of the parallel group 101 * @return The built group 102 */ 103 public ParallelGroup named(String name) { 104 requireNonNullParam(name, "name", "ParallelGroupBuilder.named"); 105 106 var group = new ParallelGroup(name, m_commands, m_requiredCommands); 107 if (m_endCondition == null) { 108 // No custom end condition, return the group as is 109 return group; 110 } 111 112 // We have a custom end condition, so we need to wrap the group in a race 113 return new ParallelGroupBuilder() 114 .optional(group, Command.waitUntil(m_endCondition).named("Until Condition")) 115 .named(name); 116 } 117 118 /** 119 * Creates the group, giving it a name based on the commands within the group. 120 * 121 * @return The built group 122 */ 123 public ParallelGroup withAutomaticName() { 124 // eg "(CommandA & CommandB & CommandC)" 125 String required = 126 m_requiredCommands.stream().map(Command::name).collect(Collectors.joining(" & ", "(", ")")); 127 128 var optionalCommands = new LinkedHashSet<>(m_commands); 129 optionalCommands.removeAll(m_requiredCommands); 130 // eg "(CommandA | CommandB | CommandC)" 131 String optional = 132 optionalCommands.stream().map(Command::name).collect(Collectors.joining(" | ", "(", ")")); 133 134 if (m_requiredCommands.isEmpty()) { 135 // No required commands, pure race 136 return named(optional); 137 } else if (optionalCommands.isEmpty()) { 138 // Everything required 139 return named(required); 140 } else { 141 // Some form of deadline 142 // eg "[(CommandA & CommandB) * (CommandX | CommandY | ...)]" 143 String name = "[%s * %s]".formatted(required, optional); 144 return named(name); 145 } 146 } 147}