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.command3; 006 007import static org.wpilib.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_optionalCommands = 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_optionalCommands.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 return this; 066 } 067 068 /** 069 * Adds a command to the group. The command must complete for the group to exit. 070 * 071 * @param command The command to add to the group 072 * @return The builder object, for chaining 073 */ 074 // Note: this primarily exists so users can cleanly chain .alongWith calls to build a 075 // parallel group, eg `fooCommand().alongWith(barCommand()).alongWith(bazCommand())` 076 public ParallelGroupBuilder alongWith(Command command) { 077 return requiring(command); 078 } 079 080 /** 081 * Adds an end condition to the command group. If this condition is met before all required 082 * commands have completed, the group will exit early. If multiple end conditions are added (e.g. 083 * {@code .until(() -> conditionA()).until(() -> conditionB())}), then the last end condition 084 * added will be used and any previously configured condition will be overridden. 085 * 086 * @param condition The end condition for the group. May be null. 087 * @return The builder object, for chaining 088 */ 089 public ParallelGroupBuilder until(BooleanSupplier condition) { 090 m_endCondition = condition; 091 return this; 092 } 093 094 /** 095 * Creates the group, using the provided named. The group will require everything that the 096 * commands in the group require, and will have a priority level equal to the highest priority 097 * among those commands. 098 * 099 * @param name The name of the parallel group 100 * @return The built group 101 */ 102 public ParallelGroup named(String name) { 103 requireNonNullParam(name, "name", "ParallelGroupBuilder.named"); 104 105 var group = new ParallelGroup(name, m_requiredCommands, m_optionalCommands); 106 if (m_endCondition == null) { 107 // No custom end condition, return the group as is 108 return group; 109 } 110 111 // We have a custom end condition, so we need to wrap the group in a race 112 return new ParallelGroupBuilder() 113 .optional(group, Command.waitUntil(m_endCondition).named("Until Condition")) 114 .named(name); 115 } 116 117 /** 118 * Creates the group, giving it a name based on the commands within the group. 119 * 120 * @return The built group 121 */ 122 public ParallelGroup withAutomaticName() { 123 // eg "(CommandA & CommandB & CommandC)" 124 String required = 125 m_requiredCommands.stream().map(Command::name).collect(Collectors.joining(" & ", "(", ")")); 126 127 // eg "(CommandA | CommandB | CommandC)" 128 String optional = 129 m_optionalCommands.stream().map(Command::name).collect(Collectors.joining(" | ", "(", ")")); 130 131 if (m_requiredCommands.isEmpty()) { 132 // No required commands, pure race 133 return named(optional); 134 } else if (m_optionalCommands.isEmpty()) { 135 // Everything required 136 return named(required); 137 } else { 138 // Some form of deadline 139 // eg "[(CommandA & CommandB) * (CommandX | CommandY | ...)]" 140 String name = "[%s * %s]".formatted(required, optional); 141 return named(name); 142 } 143 } 144}