/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.car.app.navigation.model;
import static java.util.Objects.requireNonNull;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.car.app.annotations.CarProtocol;
import androidx.car.app.model.CarIcon;
import androidx.car.app.model.CarText;
import androidx.car.app.model.DistanceSpan;
import androidx.car.app.model.DurationSpan;
import androidx.car.app.model.constraints.CarIconConstraints;
import androidx.car.app.model.constraints.CarTextConstraints;
import androidx.car.app.utils.CollectionUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
/**
* Represents a step that the driver should take in order to remain on the current navigation route.
*
* <p>Example of steps are turning onto a street, taking a highway exit and merging onto a different
* highway, or continuing straight through a roundabout.
*/
@CarProtocol
public final class Step {
@Keep
@Nullable
private final Maneuver mManeuver;
@Keep
private final List<Lane> mLanes;
@Keep
@Nullable
private final CarIcon mLanesImage;
@Keep
@Nullable
private final CarText mCue;
@Keep
@Nullable
private final CarText mRoad;
/**
* Returns the maneuver to be performed on this step or {@code null} if this step doesn't
* involve a maneuver.
*
* @see Builder#setManeuver(Maneuver)
*/
@Nullable
public Maneuver getManeuver() {
return mManeuver;
}
/**
* Returns a list of {@link Lane} that contains information of the road lanes at the point
* where the driver should execute this step.
*
* @see Builder#addLane(Lane)
*/
@NonNull
public List<Lane> getLanes() {
return CollectionUtils.emptyIfNull(mLanes);
}
/**
* Returns the image representing all the lanes or {@code null} if not set.
*
* @see Builder#setLanesImage(CarIcon)
*/
@Nullable
public CarIcon getLanesImage() {
return mLanesImage;
}
/**
* Returns the text description of this maneuver or {@code null} if not set.
*
* @see Builder#setCue(CharSequence)
*/
@Nullable
public CarText getCue() {
return mCue;
}
/**
* Returns the text description of the road for the step or {@code null} if unknown.
*
* @see Builder#setRoad(CharSequence)
*/
@Nullable
public CarText getRoad() {
return mRoad;
}
@Override
@NonNull
public String toString() {
return "[maneuver: "
+ mManeuver
+ ", lane count: "
+ (mLanes != null ? mLanes.size() : 0)
+ ", lanes image: "
+ mLanesImage
+ ", cue: "
+ CarText.toShortString(mCue)
+ ", road: "
+ CarText.toShortString(mRoad)
+ "]";
}
@Override
public int hashCode() {
return Objects.hash(mManeuver, mLanes, mLanesImage, mCue, mRoad);
}
@Override
public boolean equals(@Nullable Object other) {
if (this == other) {
return true;
}
if (!(other instanceof Step)) {
return false;
}
Step otherStep = (Step) other;
return Objects.equals(mManeuver, otherStep.mManeuver)
&& Objects.equals(mLanes, otherStep.mLanes)
&& Objects.equals(mLanesImage, otherStep.mLanesImage)
&& Objects.equals(mCue, otherStep.mCue)
&& Objects.equals(mRoad, otherStep.mRoad);
}
Step(
@Nullable Maneuver maneuver,
List<Lane> lanes,
@Nullable CarIcon lanesImage,
@Nullable CarText cue,
@Nullable CarText road) {
mManeuver = maneuver;
mLanes = CollectionUtils.unmodifiableCopy(lanes);
CarIconConstraints.DEFAULT.validateOrThrow(lanesImage);
mLanesImage = lanesImage;
mCue = cue;
mRoad = road;
}
/** Constructs an empty instance, used by serialization code. */
private Step() {
mManeuver = null;
mLanes = Collections.emptyList();
mLanesImage = null;
mCue = null;
mRoad = null;
}
/** A builder of {@link Step}. */
public static final class Builder {
private final List<Lane> mLanes = new ArrayList<>();
@Nullable
private Maneuver mManeuver;
@Nullable
private CarIcon mLanesImage;
@Nullable
private CarText mCue;
@Nullable
private CarText mRoad;
/**
* Constructs a new builder of {@link Step}.
*/
public Builder() {
}
/**
* Constructs a new builder of {@link Step} with a cue.
*
* <p>A cue can be used as a fallback when {@link Maneuver} is not set or is unavailable.
*
* <p>Some cluster displays do not support UTF-8 encoded characters, in which case
* unsupported characters will not be displayed properly.
*
* <p>See {@link Builder#setCue} for details on span support in the input string.
*
* @throws NullPointerException if {@code cue} is {@code null}
* @throws IllegalArgumentException if {@code cue} contains unsupported spans
* @see Builder#setCue(CharSequence)
*/
public Builder(@NonNull CharSequence cue) {
mCue = CarText.create(requireNonNull(cue));
CarTextConstraints.TEXT_AND_ICON.validateOrThrow(mCue);
}
/**
* Constructs a new builder of {@link Step} with a cue, with support for multiple length
* variants.
*
* <p>See {@link Builder#setCue} for details on span support in the input string.
*
* @throws NullPointerException if {@code cue} is {@code null}
* @throws IllegalArgumentException if {@code cue} contains unsupported spans
* @see Builder#Builder(CharSequence)
*/
public Builder(@NonNull CarText cue) {
mCue = requireNonNull(cue);
CarTextConstraints.TEXT_AND_ICON.validateOrThrow(mCue);
}
/**
* Sets the maneuver to be performed on this step.
*
* @throws NullPointerException if {@code maneuver} is {@code null}
*/
@NonNull
public Builder setManeuver(@NonNull Maneuver maneuver) {
mManeuver = requireNonNull(maneuver);
return this;
}
/**
* Adds the information of a single road lane at the point where the driver should
* execute this step.
*
* <p>Lane information is primarily used when the step is passed to the vehicle cluster
* or heads up displays. Some vehicles may not use the information at all. The navigation
* template primarily uses the lanes image provided in {@link #setLanesImage}.
*
* <p>Lanes are displayed from left to right.
*
* @throws NullPointerException if {@code lane} is {@code null}
*/
@NonNull
public Builder addLane(@NonNull Lane lane) {
mLanes.add(requireNonNull(lane));
return this;
}
/**
* Sets an image representing all the lanes.
*
* <p>This image takes priority over {@link Lane}s that may have been added with {@link
* #addLane}. If an image is added for the lanes with this method then corresponding lane
* data using {@link #addLane} must also have been added in case it is shown on a display
* with limited resources such as the car cluster or heads-up display (HUD).
*
* <p>This image should ideally have a transparent background.
*
* <h4>Image Sizing Guidance</h4>
*
* To minimize scaling artifacts across a wide range of car screens, apps should provide
* images targeting a 500 x 74 dp bounding box. If the image exceeds this maximum size in
* either one of the dimensions, it will be scaled down to be centered inside the
* bounding box while preserving its aspect ratio.
*
* <p>See {@link CarIcon} for more details related to providing icon and image resources
* that work with different car screen pixel densities.
*
* @throws NullPointerException if {@code lanesImage} is {@code null}
*/
@NonNull
public Builder setLanesImage(@NonNull CarIcon lanesImage) {
mLanesImage = requireNonNull(lanesImage);
return this;
}
/**
* Sets a text description of this maneuver.
*
* <p>A cue can be used as a fallback when {@link Maneuver} is not set or is unavailable.
*
* <p>For example "Turn left", "Make a U-Turn", "Sharp Right", or "Take the exit using
* the left lane"
*
* <p>The {@code cue} string can contain {@link androidx.car.app.model.CarIconSpan}s,
* {@link androidx.car.app.model.DistanceSpan}s, and
* {@link androidx.car.app.model.DurationSpan}s.
*
* <p>In the following example, the "520" text is replaced with an icon:
*
* <pre>{@code
* SpannableString string = new SpannableString("Turn right on 520 East");
* string.setSpan(textWithImage.setSpan(
* CarIconSpan.create(new CarIcon.Builder(
* IconCompat.createWithResource(getCarContext(), R.drawable.ic_520_highway))),
* 14, 17, SPAN_INCLUSIVE_EXCLUSIVE));
* }</pre>
*
* <p>The host may choose to display the string without the images, so it is important
* that the string content is readable without the images. This may be the case, for
* example, if the string is sent to a cluster display that does not support images, or
* if the host limits the number of images that may be allowed for one line of text.
*
* <h4>Image Sizing Guidance</h4>
*
* The size these images will be displayed at varies depending on where the {@link Step}
* object is used. Refer to the documentation of those APIs for details.
*
* <p>See {@link CarIcon} for more details related to providing icon and image resources
* that work with different car screen pixel densities.
*
* @throws NullPointerException if {@code cue} is {@code null}
* @throws IllegalArgumentException if {@code cue} contains unsupported spans
* @see CarText
*/
@NonNull
public Builder setCue(@NonNull CharSequence cue) {
mCue = CarText.create(requireNonNull(cue));
CarTextConstraints.TEXT_AND_ICON.validateOrThrow(mCue);
return this;
}
/**
* Sets a text description of the road for the step.
*
* <p>This value is primarily used for vehicle cluster and heads-up displays and may not
* appear in the navigation template.
*
* <p>For example, a {@link Step} for a left turn might provide "State Street" for the road.
*
* <p>Only {@link DistanceSpan}s and {@link DurationSpan}s are supported in the input
* string.
*
* @throws NullPointerException if {@code road} is {@code null}
* @throws IllegalArgumentException if {@code road} contains unsupported spans
* @see CarText
*/
@NonNull
public Builder setRoad(@NonNull CharSequence road) {
mRoad = CarText.create(requireNonNull(road));
CarTextConstraints.TEXT_ONLY.validateOrThrow(mRoad);
return this;
}
/**
* Constructs the {@link Step} defined by this builder.
*
* @throws IllegalStateException if {@code lanesImage} was set but no lanes were added
*/
@NonNull
public Step build() {
if (mLanesImage != null && mLanes.isEmpty()) {
throw new IllegalStateException(
"A step must have lane data when the lanes image is set");
}
return new Step(mManeuver, mLanes, mLanesImage, mCue, mRoad);
}
}
}