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.apriltag;
006
007import com.fasterxml.jackson.annotation.JsonAutoDetect;
008import com.fasterxml.jackson.annotation.JsonCreator;
009import com.fasterxml.jackson.annotation.JsonIgnore;
010import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
011import com.fasterxml.jackson.annotation.JsonProperty;
012import com.fasterxml.jackson.databind.ObjectMapper;
013import edu.wpi.first.math.geometry.Pose3d;
014import edu.wpi.first.math.geometry.Rotation3d;
015import edu.wpi.first.math.geometry.Translation3d;
016import java.io.IOException;
017import java.io.InputStream;
018import java.io.InputStreamReader;
019import java.io.UncheckedIOException;
020import java.nio.charset.StandardCharsets;
021import java.nio.file.Path;
022import java.util.ArrayList;
023import java.util.HashMap;
024import java.util.List;
025import java.util.Map;
026import java.util.Objects;
027import java.util.Optional;
028
029/**
030 * Class for representing a layout of AprilTags on a field and reading them from a JSON format.
031 *
032 * <p>The JSON format contains two top-level objects, "tags" and "field". The "tags" object is a
033 * list of all AprilTags contained within a layout. Each AprilTag serializes to a JSON object
034 * containing an ID and a Pose3d. The "field" object is a descriptor of the size of the field in
035 * meters with "width" and "length" values. This is to account for arbitrary field sizes when
036 * transforming the poses.
037 *
038 * <p>Pose3ds in the JSON are measured using the normal FRC coordinate system, NWU with the origin
039 * at the bottom-right corner of the blue alliance wall. {@link #setOrigin(OriginPosition)} can be
040 * used to change the poses returned from {@link AprilTagFieldLayout#getTagPose(int)} to be from the
041 * perspective of a specific alliance.
042 *
043 * <p>Tag poses represent the center of the tag, with a zero rotation representing a tag that is
044 * upright and facing away from the (blue) alliance wall (that is, towards the opposing alliance).
045 */
046@JsonIgnoreProperties(ignoreUnknown = true)
047@JsonAutoDetect(getterVisibility = JsonAutoDetect.Visibility.NONE)
048public class AprilTagFieldLayout {
049  /** Common origin positions for the AprilTag coordinate system. */
050  public enum OriginPosition {
051    /** Blue alliance wall, right side. */
052    kBlueAllianceWallRightSide,
053    /** Red alliance wall, right side. */
054    kRedAllianceWallRightSide,
055  }
056
057  private final Map<Integer, AprilTag> m_apriltags = new HashMap<>();
058
059  @JsonProperty(value = "field")
060  private FieldDimensions m_fieldDimensions;
061
062  private Pose3d m_origin;
063
064  /**
065   * Construct a new AprilTagFieldLayout with values imported from a JSON file.
066   *
067   * @param path Path of the JSON file to import from.
068   * @throws IOException If reading from the file fails.
069   */
070  public AprilTagFieldLayout(String path) throws IOException {
071    this(Path.of(path));
072  }
073
074  /**
075   * Construct a new AprilTagFieldLayout with values imported from a JSON file.
076   *
077   * @param path Path of the JSON file to import from.
078   * @throws IOException If reading from the file fails.
079   */
080  public AprilTagFieldLayout(Path path) throws IOException {
081    AprilTagFieldLayout layout =
082        new ObjectMapper().readValue(path.toFile(), AprilTagFieldLayout.class);
083    m_apriltags.putAll(layout.m_apriltags);
084    m_fieldDimensions = layout.m_fieldDimensions;
085    setOrigin(OriginPosition.kBlueAllianceWallRightSide);
086  }
087
088  /**
089   * Construct a new AprilTagFieldLayout from a list of {@link AprilTag} objects.
090   *
091   * @param apriltags List of {@link AprilTag}.
092   * @param fieldLength Length of the field the layout is representing in meters.
093   * @param fieldWidth Width of the field the layout is representing in meters.
094   */
095  public AprilTagFieldLayout(List<AprilTag> apriltags, double fieldLength, double fieldWidth) {
096    this(apriltags, new FieldDimensions(fieldLength, fieldWidth));
097  }
098
099  @JsonCreator
100  private AprilTagFieldLayout(
101      @JsonProperty(required = true, value = "tags") List<AprilTag> apriltags,
102      @JsonProperty(required = true, value = "field") FieldDimensions fieldDimensions) {
103    // To ensure the underlying semantics don't change with what kind of list is passed in
104    for (AprilTag tag : apriltags) {
105      m_apriltags.put(tag.ID, tag);
106    }
107    m_fieldDimensions = fieldDimensions;
108    setOrigin(OriginPosition.kBlueAllianceWallRightSide);
109  }
110
111  /**
112   * Returns a List of the {@link AprilTag AprilTags} used in this layout.
113   *
114   * @return The {@link AprilTag AprilTags} used in this layout.
115   */
116  @JsonProperty("tags")
117  public List<AprilTag> getTags() {
118    return new ArrayList<>(m_apriltags.values());
119  }
120
121  /**
122   * Returns the length of the field the layout is representing in meters.
123   *
124   * @return length, in meters
125   */
126  @JsonIgnore
127  public double getFieldLength() {
128    return m_fieldDimensions.fieldLength;
129  }
130
131  /**
132   * Returns the length of the field the layout is representing in meters.
133   *
134   * @return width, in meters
135   */
136  @JsonIgnore
137  public double getFieldWidth() {
138    return m_fieldDimensions.fieldWidth;
139  }
140
141  /**
142   * Sets the origin based on a predefined enumeration of coordinate frame origins. The origins are
143   * calculated from the field dimensions.
144   *
145   * <p>This transforms the Pose3d objects returned by {@link #getTagPose(int)} to return the
146   * correct pose relative to a predefined coordinate frame.
147   *
148   * @param origin The predefined origin
149   */
150  @JsonIgnore
151  public final void setOrigin(OriginPosition origin) {
152    var pose =
153        switch (origin) {
154          case kBlueAllianceWallRightSide -> Pose3d.kZero;
155          case kRedAllianceWallRightSide -> new Pose3d(
156              new Translation3d(m_fieldDimensions.fieldLength, m_fieldDimensions.fieldWidth, 0),
157              new Rotation3d(0, 0, Math.PI));
158        };
159    setOrigin(pose);
160  }
161
162  /**
163   * Sets the origin for tag pose transformation.
164   *
165   * <p>This transforms the Pose3d objects returned by {@link #getTagPose(int)} to return the
166   * correct pose relative to the provided origin.
167   *
168   * @param origin The new origin for tag transformations
169   */
170  @JsonIgnore
171  public final void setOrigin(Pose3d origin) {
172    m_origin = origin;
173  }
174
175  /**
176   * Returns the origin used for tag pose transformation.
177   *
178   * @return the origin
179   */
180  @JsonIgnore
181  public Pose3d getOrigin() {
182    return m_origin;
183  }
184
185  /**
186   * Gets an AprilTag pose by its ID.
187   *
188   * @param ID The ID of the tag.
189   * @return The pose corresponding to the ID passed in or an empty optional if a tag with that ID
190   *     was not found.
191   */
192  @SuppressWarnings("ParameterName")
193  public Optional<Pose3d> getTagPose(int ID) {
194    AprilTag tag = m_apriltags.get(ID);
195    if (tag == null) {
196      return Optional.empty();
197    }
198    return Optional.of(tag.pose.relativeTo(m_origin));
199  }
200
201  /**
202   * Serializes a AprilTagFieldLayout to a JSON file.
203   *
204   * @param path The path to write to.
205   * @throws IOException If writing to the file fails.
206   */
207  public void serialize(String path) throws IOException {
208    serialize(Path.of(path));
209  }
210
211  /**
212   * Serializes a AprilTagFieldLayout to a JSON file.
213   *
214   * @param path The path to write to.
215   * @throws IOException If writing to the file fails.
216   */
217  public void serialize(Path path) throws IOException {
218    new ObjectMapper().writeValue(path.toFile(), this);
219  }
220
221  /**
222   * Get an official {@link AprilTagFieldLayout}.
223   *
224   * @param field The loadable AprilTag field layout.
225   * @return AprilTagFieldLayout of the field.
226   * @throws UncheckedIOException If the layout does not exist.
227   */
228  public static AprilTagFieldLayout loadField(AprilTagFields field) {
229    if (field.m_fieldLayout == null) {
230      try {
231        field.m_fieldLayout = loadFromResource(field.m_resourceFile);
232      } catch (IOException e) {
233        throw new UncheckedIOException(
234            "Could not load AprilTagFieldLayout from " + field.m_resourceFile, e);
235      }
236    }
237    // Copy layout because the layout's origin is mutable
238    return new AprilTagFieldLayout(
239        field.m_fieldLayout.getTags(),
240        field.m_fieldLayout.getFieldLength(),
241        field.m_fieldLayout.getFieldWidth());
242  }
243
244  /**
245   * Deserializes a field layout from a resource within a internal jar file.
246   *
247   * <p>Users should use {@link AprilTagFields#loadAprilTagLayoutField()} to load official layouts
248   * and {@link #AprilTagFieldLayout(String)} for custom layouts.
249   *
250   * @param resourcePath The absolute path of the resource
251   * @return The deserialized layout
252   * @throws IOException If the resource could not be loaded
253   */
254  public static AprilTagFieldLayout loadFromResource(String resourcePath) throws IOException {
255    InputStream stream = AprilTagFieldLayout.class.getResourceAsStream(resourcePath);
256    if (stream == null) {
257      // Class.getResourceAsStream() returns null if the resource does not exist.
258      throw new IOException("Could not locate resource: " + resourcePath);
259    }
260    InputStreamReader reader = new InputStreamReader(stream, StandardCharsets.UTF_8);
261    try {
262      return new ObjectMapper().readerFor(AprilTagFieldLayout.class).readValue(reader);
263    } catch (IOException e) {
264      throw new IOException("Failed to load AprilTagFieldLayout: " + resourcePath);
265    }
266  }
267
268  @Override
269  public boolean equals(Object obj) {
270    return obj instanceof AprilTagFieldLayout layout
271        && m_apriltags.equals(layout.m_apriltags)
272        && m_origin.equals(layout.m_origin);
273  }
274
275  @Override
276  public int hashCode() {
277    return Objects.hash(m_apriltags, m_origin);
278  }
279
280  @JsonIgnoreProperties(ignoreUnknown = true)
281  @JsonAutoDetect(getterVisibility = JsonAutoDetect.Visibility.NONE)
282  private static class FieldDimensions {
283    @SuppressWarnings("MemberName")
284    @JsonProperty(value = "length")
285    public final double fieldLength;
286
287    @SuppressWarnings("MemberName")
288    @JsonProperty(value = "width")
289    public final double fieldWidth;
290
291    @JsonCreator()
292    FieldDimensions(
293        @JsonProperty(required = true, value = "length") double fieldLength,
294        @JsonProperty(required = true, value = "width") double fieldWidth) {
295      this.fieldLength = fieldLength;
296      this.fieldWidth = fieldWidth;
297    }
298  }
299}