com.mapbox.api.matching.v5.MapboxMapMatching Maven / Gradle / Ivy
package com.mapbox.api.matching.v5;
import androidx.annotation.FloatRange;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.auto.value.AutoValue;
import com.google.gson.GsonBuilder;
import com.mapbox.api.directions.v5.DirectionsAdapterFactory;
import com.mapbox.api.directions.v5.DirectionsCriteria;
import com.mapbox.api.directions.v5.DirectionsCriteria.AnnotationCriteria;
import com.mapbox.api.directions.v5.DirectionsCriteria.GeometriesCriteria;
import com.mapbox.api.directions.v5.DirectionsCriteria.OverviewCriteria;
import com.mapbox.api.directions.v5.DirectionsCriteria.ProfileCriteria;
import com.mapbox.api.directions.v5.utils.FormatUtils;
import com.mapbox.api.matching.v5.models.MapMatchingAdapterFactory;
import com.mapbox.api.matching.v5.models.MapMatchingResponse;
import com.mapbox.core.MapboxService;
import com.mapbox.core.constants.Constants;
import com.mapbox.core.exceptions.ServicesException;
import com.mapbox.core.utils.ApiCallHelper;
import com.mapbox.core.utils.MapboxUtils;
import com.mapbox.core.utils.TextUtils;
import com.mapbox.geojson.Point;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
/**
* The Mapbox map matching interface (v5)
*
* The Mapbox Map Matching API snaps fuzzy, inaccurate traces from a GPS unit or a phone to the
* OpenStreetMap road and path network using the Directions API. This produces clean paths that can
* be displayed on a map or used for other analysis.
*
* @see Map matching API
* documentation
* @since 2.0.0
*/
@AutoValue
public abstract class MapboxMapMatching extends
MapboxService {
protected MapboxMapMatching() {
super(MapMatchingService.class);
}
@Override
protected GsonBuilder getGsonBuilder() {
return new GsonBuilder()
.registerTypeAdapterFactory(MapMatchingAdapterFactory.create())
.registerTypeAdapterFactory(DirectionsAdapterFactory.create());
}
@Override
protected Call initializeCall() {
if (usePostMethod() == null) {
return callForUrlLength();
}
if (usePostMethod()) {
return post();
}
return get();
}
private Call callForUrlLength() {
Call get = get();
if (get.request().url().toString().length() < MAX_URL_SIZE) {
return get;
}
return post();
}
private Call get() {
return getService().getCall(
ApiCallHelper.getHeaderUserAgent(clientAppName()),
user(),
profile(),
coordinates(),
accessToken(),
geometries(),
radiuses(),
steps(),
overview(),
timestamps(),
annotations(),
language(),
tidy(),
roundaboutExits(),
bannerInstructions(),
voiceInstructions(),
voiceUnits(),
waypointIndices(),
waypointNames(),
approaches());
}
private Call post() {
return getService().postCall(
ApiCallHelper.getHeaderUserAgent(clientAppName()),
user(),
profile(),
coordinates(),
accessToken(),
geometries(),
radiuses(),
steps(),
overview(),
timestamps(),
annotations(),
language(),
tidy(),
roundaboutExits(),
bannerInstructions(),
voiceInstructions(),
voiceUnits(),
waypointIndices(),
waypointNames(),
approaches());
}
/**
* Wrapper method for Retrofits {@link Call#execute()} call returning a response specific to the
* Map Matching API.
*
* @return the Map Matching v5 response once the call completes successfully
* @throws IOException Signals that an I/O exception of some sort has occurred
* @since 1.0.0
*/
@Override
public Response executeCall() throws IOException {
Response response = getCall().execute();
MatchingResponseFactory factory = new MatchingResponseFactory(MapboxMapMatching.this);
return factory.generate(response);
}
/**
* Wrapper method for Retrofits {@link Call#enqueue(Callback)} call returning a response specific
* to the Map Matching API. Use this method to make a directions request on the Main Thread.
*
* @param callback a {@link Callback} which is used once the {@link MapMatchingResponse} is
* created.
* @since 1.0.0
*/
@Override
public void enqueueCall(final Callback callback) {
getCall().enqueue(new Callback() {
@Override
public void onResponse(Call call,
Response response) {
MatchingResponseFactory factory = new MatchingResponseFactory(MapboxMapMatching.this);
Response generatedResponse = factory.generate(response);
callback.onResponse(call, generatedResponse);
}
@Override
public void onFailure(Call call, Throwable throwable) {
callback.onFailure(call, throwable);
}
});
}
@Nullable
abstract Boolean usePostMethod();
@Nullable
abstract String clientAppName();
@NonNull
abstract String accessToken();
@Nullable
abstract Boolean tidy();
@NonNull
abstract String user();
@NonNull
abstract String profile();
@NonNull
abstract String coordinates();
@Nullable
abstract String geometries();
@Nullable
abstract String radiuses();
@Nullable
abstract Boolean steps();
@Nullable
abstract String overview();
@Nullable
abstract String timestamps();
@Nullable
abstract String annotations();
@Nullable
abstract String language();
@Nullable
abstract Boolean roundaboutExits();
@Nullable
abstract Boolean bannerInstructions();
@Nullable
abstract Boolean voiceInstructions();
@Nullable
abstract String voiceUnits();
@Nullable
abstract String waypointIndices();
@Nullable
abstract String waypointNames();
@Nullable
abstract String approaches();
@NonNull
@Override
protected abstract String baseUrl();
/**
* Build a new {@link MapboxMapMatching} object with the initial values set for
* {@link #baseUrl()}, {@link #profile()}, {@link #geometries()}, and {@link #user()}.
*
* @return a {@link Builder} object for creating this object
* @since 3.0.0
*/
public static Builder builder() {
return new AutoValue_MapboxMapMatching.Builder()
.baseUrl(Constants.BASE_API_URL)
.profile(DirectionsCriteria.PROFILE_DRIVING)
.geometries(DirectionsCriteria.GEOMETRY_POLYLINE6)
.user(DirectionsCriteria.PROFILE_DEFAULT_USER);
}
/**
* Builds your map matching query by adding parameters.
*
* @since 2.0.0
*/
@AutoValue.Builder
public abstract static class Builder {
private List coordinates = new ArrayList<>();
private String[] annotations;
private String[] timestamps;
private Double[] radiuses;
private Integer[] waypointIndices;
private String[] waypointNames;
private String[] approaches;
/**
* Use POST method to request data.
* The default is to use GET.
* @return this builder for chaining options together
* @since 4.4.0
*/
public Builder post() {
usePostMethod(true);
return this;
}
/**
* Use GET method to request data.
* @return this builder for chaining options together
* @since 4.4.0
*/
public Builder get() {
usePostMethod(false);
return this;
}
abstract Builder usePostMethod(@NonNull Boolean usePost);
/**
* Required to call when this is being built. If no access token provided,
* {@link ServicesException} will be thrown.
*
* @param accessToken Mapbox access token, You must have a Mapbox account inorder to use
* the Map Matching API
* @return this builder for chaining options together
* @since 2.1.0
*/
public abstract Builder accessToken(@NonNull String accessToken);
/**
* Whether or not to transparently remove clusters and re-sample traces for improved map
* matching results. Pass in null to reset to the APIs default setting.
*
* @param tidy true if you'd like the API to remove coordinates clustered together, otherwise
* false
* @return this builder for chaining options together
* @since 3.0.0
*/
public abstract Builder tidy(@Nullable Boolean tidy);
/**
* The username for the account that the directions engine runs on. In most cases, this should
* always remain the default value of {@link DirectionsCriteria#PROFILE_DEFAULT_USER}.
*
* @param user a non-null string which will replace the default user used in the map matching
* request
* @return this builder for chaining options together
* @since 2.1.0
*/
public abstract Builder user(@NonNull String user);
/**
* This selects which mode of transportation the user will be using to accurately give the
* map matching route. The options include driving, driving considering traffic, walking, and
* cycling. Using each of these profiles will result in different durations
*
* @param profile required to be one of the String values found in the {@link ProfileCriteria}
* @return this builder for chaining options together
* @since 2.1.0
*/
public abstract Builder profile(@NonNull @ProfileCriteria String profile);
/**
* alter the default geometry being returned for the map matching route. A null value will reset
* this field to the APIs default value vs this SDKs default value of
* {@link DirectionsCriteria#GEOMETRY_POLYLINE6}.
*
* Note that while the API supports GeoJson as an option for geometry, this SDK intentionally
* removes this as an option since an encoded string for the geometry significantly reduces
* bandwidth on mobile devices and speeds up response time.
*
*
* @param geometries null if you'd like the default geometry, else one of the options found in
* {@link GeometriesCriteria}.
* @return this builder for chaining options together
* @since 2.0.0
*/
public abstract Builder geometries(@Nullable @GeometriesCriteria String geometries);
/**
* Optionally, set the maximum distance in meters that each coordinate is allowed to move when
* snapped to a nearby road segment. There must be as many radiuses as there are coordinates in
* the request. Values can be any number greater than 0 or they can be unlimited simply by
* passing {@link Double#POSITIVE_INFINITY}.
*
* If no routable road is found within the radius, a {@code NoSegment} error is returned.
*
*
* @param radiuses double array containing the radiuses defined in unit meters.
* @return this builder for chaining options together
* @since 1.0.0
*/
public Builder radiuses(@Nullable @FloatRange(from = 0) Double... radiuses) {
this.radiuses = radiuses;
return this;
}
// Required for matching with MapboxMapMatching radiuses() method.
abstract Builder radiuses(@Nullable String radiuses);
/**
* Optionally, set which input coordinates should be treated as waypoints.
*
* Most useful in combination with steps=true and requests based on traces
* with high sample rates. Can be an index corresponding to any of the input coordinates,
* but must contain the first ( 0 ) and last coordinates' index separated by ; .
* {@link #steps()}
*
*
* @param waypoints integer array of coordinate indices to be used as waypoints
* @return this builder for chaining options together
* @since 3.0.0
* @deprecated you should now use {@link #waypointIndices(Integer[])}
*/
@Deprecated
public Builder waypoints(@Nullable @IntRange(from = 0) Integer... waypoints) {
this.waypointIndices = waypoints;
return this;
}
/**
* Optionally, set which input coordinates should be treated as waypoints / separate legs.
* Note: coordinate indices not added here act as silent waypoints
*
* Most useful in combination with steps=true and requests based on traces
* with high sample rates. Can be an index corresponding to any of the input coordinates,
* but must contain the first ( 0 ) and last coordinates' index separated by ; .
* {@link #steps()}
*
*
* @param waypointIndices integer array of coordinate indices to be used as waypoints
* @return this builder for chaining options together
* @since 3.0.0
*/
public Builder waypointIndices(@Nullable @IntRange(from = 0) Integer... waypointIndices) {
this.waypointIndices = waypointIndices;
return this;
}
abstract Builder waypointIndices(@Nullable String waypointIndices);
/**
* Setting this will determine whether to return steps and turn-by-turn instructions. Can be
* set to either true or false to enable or disable respectively. null can also optionally be
* passed in to set the default behavior to match what the API does by default.
*
* @param steps true if you'd like step information
* @return this builder for chaining options together
* @since 1.0.0
*/
public abstract Builder steps(@Nullable Boolean steps);
/**
* Type of returned overview geometry. Can be {@link DirectionsCriteria#OVERVIEW_FULL} (the most
* detailed geometry available), {@link DirectionsCriteria#OVERVIEW_SIMPLIFIED} (a simplified
* version of the full geometry), or {@link DirectionsCriteria#OVERVIEW_FALSE} (no overview
* geometry). The default is simplified. Passing in null will use the APIs default setting for
* the overview field.
*
* @param overview null or one of the options found in {@link OverviewCriteria}
* @return this builder for chaining options together
* @since 1.0.0
*/
public abstract Builder overview(@Nullable @OverviewCriteria String overview);
/**
* Setting this will determine Whether or not to return banner objects associated with
* the `routeSteps`. Should be used in conjunction with `steps`.
* Can be set to either true or false to enable or
* disable respectively. null can also optionally be
* passed in to set the default behavior to match what the API does by default.
*
* @param bannerInstructions true if you'd like step information
* @return this builder for chaining options together
* @since 3.0.0
*/
public abstract Builder bannerInstructions(@Nullable Boolean bannerInstructions);
/**
* Setting this will determine whether to return steps and turn-by-turn instructions. Can be
* set to either true or false to enable or disable respectively. null can also optionally be
* passed in to set the default behavior to match what the API does by default.
*
* @param voiceInstructions true if you'd like step information
* @return this builder for chaining options together
* @since 3.0.0
*/
public abstract Builder voiceInstructions(@Nullable Boolean voiceInstructions);
/**
* Specify what unit you'd like voice and banner instructions to use.
*
* @param voiceUnits either Imperial (default) or Metric
* @return this builder for chaining options together
* @since 3.0.0
*/
public abstract Builder voiceUnits(
@Nullable @DirectionsCriteria.VoiceUnitCriteria String voiceUnits
);
/**
* Setting this will determine whether to return steps and turn-by-turn instructions. Can be
* set to either true or false to enable or disable respectively. null can also optionally be
* passed in to set the default behavior to match what the API does by default.
*
* @param roundaboutExits true if you'd like step information
* @return this builder for chaining options together
* @since 3.0.0
*/
public abstract Builder roundaboutExits(@Nullable Boolean roundaboutExits);
/**
* Whether or not to return additional metadata along the route. Possible values are:
* {@link DirectionsCriteria#ANNOTATION_DISTANCE},
* {@link DirectionsCriteria#ANNOTATION_DURATION},
* {@link DirectionsCriteria#ANNOTATION_DURATION} and
* {@link DirectionsCriteria#ANNOTATION_CONGESTION}.
*
* @param annotations string referencing one of the annotation direction criteria's. The strings
* restricted to one or multiple values inside the {@link AnnotationCriteria}
* or null which will result in no annotations being used
* @return this builder for chaining options together
* @see RouteLeg object
* documentation
* @since 2.1.0
*/
public Builder annotations(@Nullable @AnnotationCriteria String... annotations) {
this.annotations = annotations;
return this;
}
// Required for matching with MapboxMapMatching annotations() method.
@SuppressWarnings("WeakerAccess")
protected abstract Builder annotations(@Nullable String annotations);
/**
* Timestamps corresponding to each coordinate provided in the request; must be numbers in Unix
* time (seconds since the Unix epoch) converted to a String. There must be as many timestamps
* as there are coordinates in the request.
*
* @param timestamps timestamp corresponding to the coordinate added at the identical index
* @return this builder for chaining options together
* @since 2.1.0
*/
public Builder timestamps(@Nullable String... timestamps) {
this.timestamps = timestamps;
return this;
}
// Required for matching with MapboxMapMatching timestamps() method.
@SuppressWarnings("WeakerAccess")
protected abstract Builder timestamps(@Nullable String timestamps);
/**
* Add a list of {@link Point}'s which define the points to perform the map matching on. The
* minimum points is 2 and the maximum points allowed in totals 100. You can use this method in
* conjunction with {@link #coordinate(Point)}.
*
* @param coordinates a List full of {@link Point}s which define the points to perform the map
* matching on
* @return this builder for chaining options together
* @since 2.1.0
*/
public Builder coordinates(@NonNull List coordinates) {
this.coordinates.addAll(coordinates);
return this;
}
// Required for matching with MapboxMapMatching coordinates() method.
@SuppressWarnings("WeakerAccess")
protected abstract Builder coordinates(@NonNull String coordinates);
/**
* This will add a single {@link Point} to the coordinate list which is used to determine the
* duration between points. This can be called up to 100 times until you hit the maximum allowed
* points. You can use this method in conjunction with {@link #coordinates(List)}.
*
* @param coordinate a {@link Point} which you'd like the map matching APi to perform on
* @return this builder for chaining options together
* @since 3.0.0
*/
public Builder coordinate(@NonNull Point coordinate) {
this.coordinates.add(coordinate);
return this;
}
/**
* Set the instruction language for the map matching request, the default is english. Only a
* select number of languages are currently supported, reference the table provided in the see
* link below.
*
* @param language a Locale value representing the language you'd like the instructions to be
* written in when returned
* @return this builder for chaining options together
* @see Supported
* Languages
* @since 3.0.0
*/
public Builder language(@Nullable Locale language) {
if (language != null) {
language(language.getLanguage());
}
return this;
}
/**
* Set the instruction language for the map matching request, the default is english. Only a
* select number of languages are currently supported, reference the table provided in the see
* link below.
*
* @param language a String value representing the language you'd like the instructions to be
* written in when returned
* @return this builder for chaining options together
* @see Supported
* Languages
* @since 2.2.0
*/
public abstract Builder language(String language);
/**
* Base package name or other simple string identifier. Used inside the calls user agent header.
*
* @param clientAppName base package name or other simple string identifier
* @return this builder for chaining options together
* @since 1.0.0
*/
public abstract Builder clientAppName(@NonNull String clientAppName);
/**
* Optionally used to indicate how map matched routes consider
* rom which side of the road to approach a waypoint.
* Accepts unrestricted (default), curb or null.
* If set to unrestricted , the map matched route can approach waypoints
* from either side of the road. If set to curb , the map matched route will be returned
* so that on arrival, the waypoint will be found on the side that corresponds with the
* driving_side of the region in which the returned route is located.
* If provided, the list of approaches must be the same length as the list of waypoints.
*
* @param approaches null if you'd like the default approaches,
* else one of the options found in
* {@link DirectionsCriteria.ApproachesCriteria}.
* @return this builder for chaining options together
* @since 3.2.0
*/
public Builder addApproaches(@Nullable String... approaches) {
this.approaches = approaches;
return this;
}
abstract Builder approaches(@Nullable String approaches);
/**
* Custom names for waypoints used for the arrival instruction,
* each separated by ; . Values can be any string and total number of all characters cannot
* exceed 500. If provided, the list of waypointNames must be the same length as the list of
* waypoints, but you can skip a coordinate and show its position with the ; separator.
*
* @param waypointNames Custom names for waypoints used for the arrival instruction.
* @return this builder for chaining options together
* @since 3.3.0
*/
public Builder addWaypointNames(@Nullable String... waypointNames) {
this.waypointNames = waypointNames;
return this;
}
abstract Builder waypointNames(@Nullable String waypointNames);
/**
* Optionally change the APIs base URL to something other then the default Mapbox one.
*
* @param baseUrl base url used as end point
* @return this builder for chaining options together
* @since 2.1.0
*/
public abstract Builder baseUrl(String baseUrl);
@SuppressWarnings("WeakerAccess")
protected abstract MapboxMapMatching autoBuild();
/**
* This uses the provided parameters set using the {@link Builder} and first checks that all
* values are valid, formats the values as strings for easier consumption by the API, and lastly
* creates a new {@link MapboxMapMatching} object with the values provided.
*
* @return a new instance of Mapbox Map Matching
* @throws ServicesException when a provided parameter is detected to be incorrect
* @since 2.1.0
*/
public MapboxMapMatching build() {
if (coordinates == null || coordinates.size() < 2) {
throw new ServicesException("At least two coordinates must be provided with your API"
+ " request.");
}
if (radiuses != null && radiuses.length != coordinates.size()) {
throw new ServicesException(
"There must be as many radiuses as there are coordinates.");
}
if (timestamps != null && timestamps.length != coordinates.size()) {
throw new ServicesException(
"There must be as many timestamps as there are coordinates.");
}
if (waypointIndices != null) {
if (waypointIndices.length < 2) {
throw new ServicesException(
"Waypoints must be a list of at least two indexes separated by ';'");
}
if (waypointIndices[0] != 0
|| waypointIndices[waypointIndices.length - 1] != coordinates.size() - 1) {
throw new ServicesException(
"Waypoints must contain indices of the first and last coordinates"
);
}
for (int i = 1; i < waypointIndices.length - 1; i++) {
if (waypointIndices[i] < 0 || waypointIndices[i] >= coordinates.size()) {
throw new ServicesException(
"Waypoints index too large (no corresponding coordinate)");
}
}
}
if (waypointNames != null) {
final String waypointNamesStr
= FormatUtils.formatWaypointNames(Arrays.asList(waypointNames));
waypointNames(waypointNamesStr);
}
if (approaches != null) {
if (approaches.length != coordinates.size()) {
throw new ServicesException("Number of approach elements must match "
+ "number of coordinates provided.");
}
String formattedApproaches = FormatUtils.formatApproaches(Arrays.asList(approaches));
if (formattedApproaches == null) {
throw new ServicesException("All approaches values must be one of curb, unrestricted");
}
approaches(formattedApproaches);
}
coordinates(formatCoordinates(coordinates));
timestamps(TextUtils.join(";", timestamps));
annotations(TextUtils.join(",", annotations));
radiuses(TextUtils.join(";", radiuses));
waypointIndices(TextUtils.join(";", waypointIndices));
// Generate build so that we can check that values are valid.
MapboxMapMatching mapMatching = autoBuild();
if (!MapboxUtils.isAccessTokenValid(mapMatching.accessToken())) {
throw new ServicesException("Using Mapbox Services requires setting a valid access token.");
}
return mapMatching;
}
private static String formatCoordinates(List coordinates) {
List coordinatesFormatted = new ArrayList<>();
for (Point point : coordinates) {
coordinatesFormatted.add(String.format(Locale.US, "%s,%s",
FormatUtils.formatCoordinate(point.longitude()),
FormatUtils.formatCoordinate(point.latitude())));
}
return TextUtils.join(";", coordinatesFormatted.toArray());
}
}
}