From 22599efefc46ccf3568788e93e5a7fd91ab67639 Mon Sep 17 00:00:00 2001 From: Seth Bourget Date: Mon, 30 Sep 2024 08:20:55 -0700 Subject: [PATCH] Updated IsoChrone API with latest features/options. (#1596) --- CHANGELOG.md | 3 +- .../api/isochrone/IsochroneCriteria.java | 9 +- .../api/isochrone/IsochroneService.java | 16 +- .../mapbox/api/isochrone/MapboxIsochrone.java | 193 ++++++++++++++- .../api/isochrone/MapboxIsochroneTest.java | 228 +++++++++++++++++- 5 files changed, 437 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5b3dc087..aaee40412 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,7 @@ Mapbox welcomes participation and contributions from everyone. ### main -### v7.3.0 - September 23, 2024 - +- Updated IsoChrone API to support new query parameters including contours_meters, road/route exclusions and departure time. [#1596](https://github.com/mapbox/mapbox-java/pull/1596) - Bumped `okhttp` version to `4.10.0`. [#1595](https://github.com/mapbox/mapbox-java/pull/1595) ### v7.2.0 - August 28, 2024 diff --git a/services-isochrone/src/main/java/com/mapbox/api/isochrone/IsochroneCriteria.java b/services-isochrone/src/main/java/com/mapbox/api/isochrone/IsochroneCriteria.java index 39404fe21..a87f70f2d 100644 --- a/services-isochrone/src/main/java/com/mapbox/api/isochrone/IsochroneCriteria.java +++ b/services-isochrone/src/main/java/com/mapbox/api/isochrone/IsochroneCriteria.java @@ -44,6 +44,12 @@ public class IsochroneCriteria { */ public static final String PROFILE_CYCLING = "cycling"; + /** + * For fastest travel by car using current and historic traffic conditions. + * Traffic information is available for the supported geographies listed in our Traffic Data page. + */ + public static final String PROFILE_DRIVING_TRAFFIC = "driving-traffic"; + /** * Queries for a specific geometry type selector. * @@ -53,7 +59,8 @@ public class IsochroneCriteria { @StringDef( { PROFILE_WALKING, PROFILE_DRIVING, - PROFILE_CYCLING + PROFILE_CYCLING, + PROFILE_DRIVING_TRAFFIC }) public @interface IsochroneProfile { } diff --git a/services-isochrone/src/main/java/com/mapbox/api/isochrone/IsochroneService.java b/services-isochrone/src/main/java/com/mapbox/api/isochrone/IsochroneService.java index 62586c7c5..b2808b1fd 100644 --- a/services-isochrone/src/main/java/com/mapbox/api/isochrone/IsochroneService.java +++ b/services-isochrone/src/main/java/com/mapbox/api/isochrone/IsochroneService.java @@ -51,6 +51,17 @@ public interface IsochroneService { * optimized generalization to use for the request. Note that the * generalization of contours can lead to self-intersections, as well * as intersections of adjacent contours. + * @param contoursMeters A single String which has a comma-separated integers in meters + * to use for each isochrone contour. + * @param exclude Exclude certain road types and custom locations from routing. Default + * is to not exclude anything from the list below. You can specify + * multiple values as a comma-separated list. + * @param depart The departure time from the given coordinates. Formatted in one of + * three ISO 8601 formats. + * See https://docs.mapbox.com/api/navigation/isochrone/ + * If not provided then depart_at is considered to be the present time + * in the local timezone of the coordinates. The isochrone contours will + * traffic conditions at the time provided. * @return a {@link FeatureCollection} in a Call wrapper * @since 4.7.0 */ @@ -64,5 +75,8 @@ Call getCall( @Query("contours_colors") String contoursColors, @Query("polygons") Boolean polygons, @Query("denoise") Float denoise, - @Query("generalize") Float generalize); + @Query("generalize") Float generalize, + @Query("contours_meters") String contoursMeters, + @Query("exclude") String exclude, + @Query("depart_at") String depart); } diff --git a/services-isochrone/src/main/java/com/mapbox/api/isochrone/MapboxIsochrone.java b/services-isochrone/src/main/java/com/mapbox/api/isochrone/MapboxIsochrone.java index 6ec9bce3e..653a73393 100644 --- a/services-isochrone/src/main/java/com/mapbox/api/isochrone/MapboxIsochrone.java +++ b/services-isochrone/src/main/java/com/mapbox/api/isochrone/MapboxIsochrone.java @@ -17,7 +17,9 @@ import com.mapbox.geojson.Point; import com.mapbox.geojson.gson.GeoJsonAdapterFactory; +import java.util.HashSet; import java.util.Locale; +import java.util.Set; import retrofit2.Call; @@ -50,6 +52,8 @@ protected MapboxIsochrone() { @Override protected GsonBuilder getGsonBuilder() { + MapboxIsochrone.Builder b = MapboxIsochrone.builder(); + return new GsonBuilder() .registerTypeAdapterFactory(GeoJsonAdapterFactory.create()) .registerTypeAdapterFactory(GeometryAdapterFactory.create()); @@ -66,7 +70,10 @@ protected Call initializeCall() { contoursColors(), polygons(), denoise(), - generalize() + generalize(), + contoursMeters(), + exclusions(), + departAt() ); } @@ -80,6 +87,8 @@ protected Call initializeCall() { public static Builder builder() { return new AutoValue_MapboxIsochrone.Builder() .baseUrl(Constants.BASE_API_URL) + .contoursMinutes("") + .contoursMeters("") .user(IsochroneCriteria.PROFILE_DEFAULT_USER); } @@ -114,6 +123,15 @@ public static Builder builder() { @Nullable abstract Float generalize(); + @Nullable + abstract String contoursMeters(); + + @Nullable + abstract String exclusions(); + + @Nullable + abstract String departAt(); + /** * This builder is used to create a new request to the Mapbox Isochrone API. At a bare minimum, * your request must include an access token, a directions routing profile (driving, walking, @@ -131,9 +149,11 @@ public abstract static class Builder { private Integer[] contoursMinutes; private String[] contoursColors; + private Integer[] contoursMeters; + private Set exclusions = new HashSet<>(); /** - * Optionally change the APIs base URL to something other then the default Mapbox one. + * Optionally change the APIs base URL to something other than the default Mapbox one. * * @param baseUrl base url used as end point * @return this builder for chaining options together @@ -164,6 +184,8 @@ public abstract static class Builder { /** * A Mapbox Directions routing profile ID. Options are * {@link IsochroneCriteria#PROFILE_DRIVING} for travel times by car, + * {@link IsochroneCriteria#PROFILE_DRIVING_TRAFFIC} for fastest travel by car using + * current and historic traffic, * {@link IsochroneCriteria#PROFILE_WALKING} for pedestrian and hiking travel times, * and {@link IsochroneCriteria#PROFILE_CYCLING} for travel times by bicycle. * @@ -217,6 +239,22 @@ public Builder addContoursMinutes(@NonNull @IntRange(from = 0, to = 60) return this; } + /** + * An integer list of meter values to use for each isochrone contour. + * The distances, in meters, to use for each isochrone contour. You + * must be in increasing order. The default maximum distance that can be specified + * is 100000 meters (100km), if you need a bigger range contact support. + * + * @param listOfMeterValues an integer list with at least one number + * for the meters which represent each contour + * @return this builder for chaining options together + * @since 7.3.0 + */ + public Builder addContoursMeters(Integer... listOfMeterValues) { + this.contoursMeters = listOfMeterValues; + return this; + } + /** * A single String which is a comma-separated list of time(s) in minutes * to use for each isochrone contour. You must pass in at least one minute @@ -226,12 +264,25 @@ public Builder addContoursMinutes(@NonNull @IntRange(from = 0, to = 60) * * @param stringListOfMinuteValues a String of at least one number for the * minutes which represent each contour - * @return this builder for chaining optio.ns together + * @return this builder for chaining options together */ // Required for matching with MapboxIsochrone addContoursMinutes() method. @SuppressWarnings("WeakerAccess") abstract Builder contoursMinutes(@NonNull String stringListOfMinuteValues); + /** + * A single String which is a comma-separated list of values(s) in meters + * to use for each isochrone contour. The distances, in meters, to use for each + * isochrone contour. You can specify up to four contours. Distances must be in + * increasing order. The default maximum distance that can be specified + * is 100000 meters (100km), if you need a bigger range contact support. + * + * @param stringListOfMeterValues a String of at least one number for the + * meters which represent each contour + * @return this builder for chaining options together + */ + abstract Builder contoursMeters(@NonNull String stringListOfMeterValues); + /** * A list of separate String which has a list of comma-separated * HEX color values to use for each isochrone contour. @@ -300,6 +351,107 @@ public Builder addContoursColors(@Nullable String... contoursColors) { */ public abstract Builder generalize(@Nullable @FloatRange(from = 0.0) Float generalize); + abstract Builder exclusions(@Nullable String exclusions); + + /** + * Exclude highways or motorways. Available in mapbox/driving and + * mapbox/driving-traffic profiles. + * @param exclude indicates whether motorways should be excluded + * @return this builder for chaining options together + * + * @since 7.3.0 + */ + public Builder excludeMotorways(Boolean exclude) { + if (exclude != null && exclude) { + exclusions.add("motorway"); + } else { + exclusions.remove("motorway"); + } + return this; + } + + /** + * Exclude tolls. Available in mapbox/driving and + * mapbox/driving-traffic profiles. + * @param exclude indicates whether tolls should be excluded + * @return this builder for chaining options together + * + * @since 7.3.0 + */ + public Builder excludeTolls(Boolean exclude) { + if (exclude != null && exclude) { + exclusions.add("toll"); + } else { + exclusions.remove("toll"); + } + return this; + } + + /** + * Exclude ferries. Available in mapbox/driving and + * mapbox/driving-traffic profiles. + * @param exclude indicates whether ferries should be excluded + * @return this builder for chaining options together + * + * @since 7.3.0 + */ + public Builder excludeFerries(Boolean exclude) { + if (exclude != null && exclude) { + exclusions.add("ferry"); + } else { + exclusions.remove("ferry"); + } + return this; + } + + /** + * Exclude unpaved roads. Available in mapbox/driving and + * mapbox/driving-traffic profiles. + * @param exclude indicates whether unpaved roads should be excluded + * @return this builder for chaining options together + * + * @since 7.3.0 + */ + public Builder excludeUnpavedRoads(Boolean exclude) { + if (exclude != null && exclude) { + exclusions.add("unpaved"); + } else { + exclusions.remove("unpaved"); + } + return this; + } + + /** + * Exclude cash only toll roads. Available in mapbox/driving and + * mapbox/driving-traffic profiles. + * @param exclude indicates whether cash only toll roads should be excluded + * @return this builder for chaining options together + * + * @since 7.3.0 + */ + public Builder excludeCashOnlyTollRoads(Boolean exclude) { + if (exclude != null && exclude) { + exclusions.add("cash_only_tolls"); + } else { + exclusions.remove("cash_only_tolls"); + } + return this; + } + + /** + * The departure time from the given coordinates. + * One of three formats.. + * If not provided then 'depart at' is considered to be the present time in the local + * timezone of the coordinates. The isochrone contours will reflect traffic + * conditions at the time provided. + * + * @param depart the departure date/time + * @return this builder for chaining options together + * + * @since 7.3.0 + */ + public abstract Builder departAt(@NonNull String depart); + /** * @return this builder for chaining options together * @since 4.6.0 @@ -314,6 +466,8 @@ public Builder addContoursColors(@Nullable String... contoursColors) { */ public MapboxIsochrone build() { + exclusions(TextUtils.join(",", exclusions.toArray())); + if (contoursMinutes != null) { if (contoursMinutes.length < 1) { throw new ServicesException("A query with at least one specified " @@ -331,6 +485,23 @@ public MapboxIsochrone build() { contoursMinutes(TextUtils.join(",", contoursMinutes)); } + if (contoursMeters != null) { + if (contoursMeters.length < 1) { + throw new ServicesException("A query with at least one specified " + + "meter value is required."); + } + + if (contoursMeters.length >= 2) { + for (int x = 0; x < contoursMeters.length - 1; x++) { + if (contoursMeters[x] > contoursMeters[x + 1]) { + throw new ServicesException("The meters must be listed" + + " in order from the lowest number to the highest number."); + } + } + contoursMeters(TextUtils.join(",", contoursMeters)); + } + } + if (contoursColors != null) { contoursColors(TextUtils.join(",", contoursColors)); } @@ -342,6 +513,17 @@ public MapboxIsochrone build() { + "must match number of minute elements provided."); } + if (contoursColors != null + && contoursMeters != null + && contoursColors.length != contoursMeters.length) { + throw new ServicesException("Number of color elements " + + "must match number of meter elements provided."); + } + + if (contoursMinutes != null && contoursMeters != null) { + throw new ServicesException("Cannot specify both contoursMinutes and contoursMeters."); + } + MapboxIsochrone isochrone = autoBuild(); if (!MapboxUtils.isAccessTokenValid(isochrone.accessToken())) { @@ -359,9 +541,10 @@ public MapboxIsochrone build() { + " walking, or driving) is required."); } - if (TextUtils.isEmpty(isochrone.contoursMinutes())) { + if (TextUtils.isEmpty(isochrone.contoursMinutes()) + && TextUtils.isEmpty(isochrone.contoursMeters())) { throw new ServicesException("A query with at least one specified minute amount" - + " is required."); + + " or meter value is required."); } if (isochrone.contoursColors() != null) { diff --git a/services-isochrone/src/test/java/com/mapbox/api/isochrone/MapboxIsochroneTest.java b/services-isochrone/src/test/java/com/mapbox/api/isochrone/MapboxIsochroneTest.java index 33835fa44..deef49440 100644 --- a/services-isochrone/src/test/java/com/mapbox/api/isochrone/MapboxIsochroneTest.java +++ b/services-isochrone/src/test/java/com/mapbox/api/isochrone/MapboxIsochroneTest.java @@ -13,6 +13,7 @@ import retrofit2.Response; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @@ -234,9 +235,9 @@ public void build_noQueryExceptionThrown() throws Exception { } @Test - public void build_noCountourMinutesExceptionThrown() throws Exception { - thrown.expect(IllegalStateException.class); - thrown.expectMessage("Missing required properties: contoursMinutes"); + public void build_noCountourMinutesOrMetersExceptionThrown() throws Exception { + thrown.expect(ServicesException.class); + thrown.expectMessage("A query with at least one specified minute amount or meter value is required."); MapboxIsochrone.builder() .accessToken(ACCESS_TOKEN) .profile(testProfile) @@ -256,6 +257,18 @@ public void build_invalidCountourMinutesExceptionThrown() throws Exception { .build(); } + @Test + public void build_invalidCountourMetersExceptionThrown() throws Exception { + thrown.expect(ServicesException.class); + thrown.expectMessage("A query with at least one specified meter value is required."); + MapboxIsochrone.builder() + .accessToken(ACCESS_TOKEN) + .profile(testProfile) + .coordinates(testPoint) + .addContoursMeters() + .build(); + } + @Test public void build_optionalParameters() throws Exception { MapboxIsochrone client = MapboxIsochrone.builder() @@ -319,6 +332,20 @@ public void build_colorAndMinuteAmountMismatchThrowsException() throws Exception .build(); } + @Test + public void build_colorAndMeterAmountMismatchThrowsException() throws Exception { + thrown.expect(ServicesException.class); + thrown.expectMessage("Number of color elements must match number of meter elements provided."); + MapboxIsochrone.builder() + .accessToken(ACCESS_TOKEN) + .coordinates(testPoint) + .profile(testProfile) + .addContoursColors("4286f4") + .addContoursMeters(5,30,55) + .baseUrl(mockUrl.toString()) + .build(); + } + @Test public void build_multipleColorsGetsAddedToListCorrectly() throws Exception { MapboxIsochrone client = MapboxIsochrone.builder() @@ -336,6 +363,23 @@ public void build_multipleColorsGetsAddedToListCorrectly() throws Exception { + "04e813" + commaEquivalent + "4286f4")); } + @Test + public void build_multipleColorsGetsAddedToListCorrectlyWithMeters() throws Exception { + MapboxIsochrone client = MapboxIsochrone.builder() + .accessToken(ACCESS_TOKEN) + .coordinates(testPoint) + .profile(testProfile) + .addContoursColors("6706ce","04e813","4286f4") + .addContoursMeters(5,30,55) + .baseUrl(mockUrl.toString()) + .build(); + + + String requestUrlString = client.cloneCall().request().url().toString(); + assertTrue(requestUrlString.contains("contours_colors=6706ce" + commaEquivalent + + "04e813" + commaEquivalent + "4286f4")); + } + @Test public void build_colorWithHexValuePoundSymbolExceptionThrown() throws Exception { thrown.expect(ServicesException.class); @@ -365,4 +409,182 @@ public void build_minutesOutOfOrderExceptionThrown() throws Exception { .baseUrl(mockUrl.toString()) .build(); } + + @Test + public void sanityUsingIntegerListForMeters() throws ServicesException, IOException { + MapboxIsochrone client = MapboxIsochrone.builder() + .accessToken(ACCESS_TOKEN) + .coordinates(testPoint) + .addContoursMeters(5,30,55) + .profile(testProfile) + .baseUrl(mockUrl.toString()) + .build(); + Response response = client.executeCall(); + assertEquals(200, response.code()); + assertNotNull(response.body()); + assertNotNull(response.body().features()); + } + + @Test + public void cannotSpecifyContourMinutesAndMeters() throws ServicesException, IOException { + thrown.expect(ServicesException.class); + thrown.expectMessage("Cannot specify both contoursMinutes and contoursMeters."); + MapboxIsochrone client = MapboxIsochrone.builder() + .accessToken(ACCESS_TOKEN) + .coordinates(testPoint) + .addContoursMeters(15,35,75) + .addContoursMinutes(5,30,55) + .profile(testProfile) + .baseUrl(mockUrl.toString()) + .build(); + Response response = client.executeCall(); + assertEquals(200, response.code()); + assertNotNull(response.body()); + assertNotNull(response.body().features()); + } + + @Test + public void excludeMotorways() { + MapboxIsochrone client = MapboxIsochrone.builder() + .accessToken(ACCESS_TOKEN) + .coordinates(testPoint) + .profile(testProfile) + .addContoursMeters(5,30,55) + .excludeMotorways(true) + .baseUrl(mockUrl.toString()) + .build(); + + assertTrue(client.cloneCall().request().url().toString() + .contains("exclude=motorway")); + } + + @Test + public void excludeTolls() { + MapboxIsochrone client = MapboxIsochrone.builder() + .accessToken(ACCESS_TOKEN) + .coordinates(testPoint) + .profile(testProfile) + .addContoursMeters(5,30,55) + .excludeTolls(true) + .baseUrl(mockUrl.toString()) + .build(); + + assertTrue(client.cloneCall().request().url().toString() + .contains("exclude=toll")); + } + + @Test + public void excludeFerries() { + MapboxIsochrone client = MapboxIsochrone.builder() + .accessToken(ACCESS_TOKEN) + .coordinates(testPoint) + .profile(testProfile) + .addContoursMeters(5,30,55) + .excludeFerries(true) + .baseUrl(mockUrl.toString()) + .build(); + + assertTrue(client.cloneCall().request().url().toString() + .contains("exclude=ferry")); + } + + @Test + public void excludeUnpavedRoads() { + MapboxIsochrone client = MapboxIsochrone.builder() + .accessToken(ACCESS_TOKEN) + .coordinates(testPoint) + .profile(testProfile) + .addContoursMeters(5,30,55) + .excludeUnpavedRoads(true) + .baseUrl(mockUrl.toString()) + .build(); + + assertTrue(client.cloneCall().request().url().toString() + .contains("exclude=unpaved")); + } + + @Test + public void excludeCashOnlyTollRoads() { + MapboxIsochrone client = MapboxIsochrone.builder() + .accessToken(ACCESS_TOKEN) + .coordinates(testPoint) + .profile(testProfile) + .addContoursMeters(5,30,55) + .excludeCashOnlyTollRoads(true) + .baseUrl(mockUrl.toString()) + .build(); + + assertTrue(client.cloneCall().request().url().toString() + .contains("exclude=cash_only_tolls")); + } + + @Test + public void allExclusions() { + MapboxIsochrone client = MapboxIsochrone.builder() + .accessToken(ACCESS_TOKEN) + .coordinates(testPoint) + .profile(testProfile) + .addContoursMeters(5,30,55) + .excludeMotorways(true) + .excludeTolls(true) + .excludeFerries(true) + .excludeUnpavedRoads(true) + .excludeCashOnlyTollRoads(true) + .baseUrl(mockUrl.toString()) + .build(); + + assertTrue(client.cloneCall().request().url().toString() + .contains("exclude=ferry%2Cmotorway%2Ctoll%2Cunpaved%2Ccash_only_tolls")); + } + + @Test + public void exclusionsOmitted() { + MapboxIsochrone.Builder builder = MapboxIsochrone.builder() + .accessToken(ACCESS_TOKEN) + .coordinates(testPoint) + .profile(testProfile) + .addContoursMeters(5,30,55) + .excludeMotorways(true) + .excludeTolls(true) + .excludeFerries(true) + .excludeUnpavedRoads(true) + .excludeCashOnlyTollRoads(true) + .baseUrl(mockUrl.toString()); + + MapboxIsochrone client = builder.excludeMotorways(false) + .excludeMotorways(false) + .excludeTolls(false) + .excludeFerries(false) + .excludeUnpavedRoads(false) + .excludeCashOnlyTollRoads(false) + .build(); + + assertFalse(client.cloneCall().request().url().toString() + .contains("motorway")); + assertFalse(client.cloneCall().request().url().toString() + .contains("toll")); + assertFalse(client.cloneCall().request().url().toString() + .contains("ferry")); + assertFalse(client.cloneCall().request().url().toString() + .contains("unpaved")); + assertFalse(client.cloneCall().request().url().toString() + .contains("cash_only_tolls")); + } + + @Test + public void departureTest() { + MapboxIsochrone client = MapboxIsochrone.builder() + .accessToken(ACCESS_TOKEN) + .coordinates(testPoint) + .profile(testProfile) + .addContoursMeters(5,30,55) + .departAt("2000-11-21T13:33") + .baseUrl(mockUrl.toString()) + .build(); + + System.out.println(client.cloneCall().request().url().toString()); + + assertTrue(client.cloneCall().request().url().toString() + .contains("depart_at=2000-11-21T13%3A33")); + } }