Mapbox Directions API Integration in Java: Complete Guide

Mapbox Directions API provides routing and navigation capabilities with support for driving, walking, cycling, and multiple waypoints. This guide covers complete integration with Mapbox Directions API using Java.


Setup and Dependencies

Maven Dependencies
<properties>
<spring-boot.version>3.1.0</spring-boot.version>
<okhttp.version>4.12.0</okhttp.version>
<gson.version>2.10.1</gson.version>
<jackson.version>2.15.2</jackson.version>
</properties>
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- HTTP Client -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>${okhttp.version}</version>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>${gson.version}</version>
</dependency>
<!-- Jackson for JSON -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- Caching -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
<version>${spring-boot.version}</version>
</dependency>
</dependencies>
Configuration
@Configuration
public class MapboxConfig {
@Value("${mapbox.access-token:}")
private String accessToken;
@Value("${mapbox.base-url:https://api.mapbox.com}")
private String baseUrl;
@Value("${mapbox.max-waypoints:25}")
private int maxWaypoints;
@Bean
public OkHttpClient okHttpClient() {
return new OkHttpClient.Builder()
.connectTimeout(Duration.ofSeconds(30))
.readTimeout(Duration.ofSeconds(30))
.writeTimeout(Duration.ofSeconds(30))
.build();
}
@Bean
public MapboxClient mapboxClient(OkHttpClient okHttpClient) {
return new MapboxClient(accessToken, baseUrl, okHttpClient);
}
}
# application.yml
mapbox:
access-token: ${MAPBOX_ACCESS_TOKEN}
base-url: https://api.mapbox.com
max-waypoints: 25
cache:
duration: 3600 # 1 hour in seconds
app:
rate-limit:
requests-per-minute: 60

Domain Models

1. Core Models
public class Coordinate {
private double longitude;
private double latitude;
// constructors, getters, setters
public Coordinate() {}
public Coordinate(double longitude, double latitude) {
this.longitude = longitude;
this.latitude = latitude;
}
public String toString() {
return String.format("%.6f,%.6f", longitude, latitude);
}
public static Coordinate fromString(String coordString) {
String[] parts = coordString.split(",");
if (parts.length != 2) {
throw new IllegalArgumentException("Invalid coordinate format: " + coordString);
}
return new Coordinate(
Double.parseDouble(parts[0].trim()),
Double.parseDouble(parts[1].trim())
);
}
}
public class Waypoint {
private Coordinate coordinate;
private String name;
private boolean isStopover = true;
private Integer bearing;
private Integer radius; // in meters
private String approach; // unrestricted, curb
// constructors, getters, setters
}
public class RouteRequest {
private List<Waypoint> waypoints;
private RoutingProfile profile = RoutingProfile.DRIVING;
private List<Annotation> annotations = Arrays.asList(Annotation.DURATION, Annotation.DISTANCE);
private boolean alternatives = false;
private boolean steps = false;
private boolean overview = true;
private boolean continueStraight = true;
private String language = "en";
private String roundaboutExits = false;
private List<VoiceInstruction> voiceInstructions;
private String bannerInstructions = false;
private List<String> exclude;
// constructors, getters, setters
public void validate() {
if (waypoints == null || waypoints.size() < 2) {
throw new IllegalArgumentException("At least 2 waypoints are required");
}
if (waypoints.size() > 25) {
throw new IllegalArgumentException("Maximum 25 waypoints allowed");
}
}
}
public enum RoutingProfile {
DRIVING("mapbox/driving"),
DRIVING_TRAFFIC("mapbox/driving-traffic"),
WALKING("mapbox/walking"),
CYCLING("mapbox/cycling");
private final String path;
RoutingProfile(String path) {
this.path = path;
}
public String getPath() {
return path;
}
}
public enum Annotation {
DURATION("duration"),
DISTANCE("distance"),
SPEED("speed"),
CONGESTION("congestion");
private final String value;
Annotation(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
2. Response Models
public class RouteResponse {
private List<Route> routes;
private List<Waypoint> waypoints;
private String code;
private String uuid;
private String message;
// constructors, getters, setters
}
public class Route {
private String geometry; // Polyline encoded
private List<Leg> legs;
private double distance; // meters
private double duration; // seconds
private double weight;
private String weightName;
private String voiceLocale;
private List<RouteStep> steps;
// constructors, getters, setters
}
public class Leg {
private List<Step> steps;
private String summary;
private double distance; // meters
private double duration; // seconds
private List<Annotation> annotation;
private List<ViaWaypoint> viaWaypoints;
// constructors, getters, setters
}
public class Step {
private String geometry; // Polyline encoded
private Maneuver maneuver;
private String name;
private String ref;
private String destinations;
private String mode;
private double distance; // meters
private double duration; // seconds
private String drivingSide;
private String pronunciation;
private List<Intersection> intersections;
private List<VoiceInstruction> voiceInstructions;
private List<BannerInstruction> bannerInstructions;
// constructors, getters, setters
}
public class Maneuver {
private String type;
private String instruction;
private Integer bearingAfter;
private Integer bearingBefore;
private List<Double> location;
private String modifier;
// constructors, getters, setters
}
public class Intersection {
private List<Double> location;
private List<Integer> bearings;
private List<String> classes;
private List<String> entry;
private Integer in;
private Integer out;
private List<Lane> lanes;
// constructors, getters, setters
}
public class Lane {
private List<String> indications;
private Boolean valid;
private String active;
// constructors, getters, setters
}
public class VoiceInstruction {
private Double distanceAlongGeometry;
private String announcement;
private String ssmlAnnouncement;
// constructors, getters, setters
}
public class BannerInstruction {
private Double distanceAlongGeometry;
private String primary;
private String secondary;
private List<BannerComponent> components;
private String type;
private String modifier;
// constructors, getters, setters
}
public class BannerComponent {
private String text;
private String type;
private String imageBaseURL;
private Map<String, Object> map;
// constructors, getters, setters
}

Core Mapbox Service

1. HTTP Client Service
@Service
public class MapboxDirectionsService {
private static final Logger logger = LoggerFactory.getLogger(MapboxDirectionsService.class);
private final String accessToken;
private final String baseUrl;
private final OkHttpClient httpClient;
private final ObjectMapper objectMapper;
public MapboxDirectionsService(
@Value("${mapbox.access-token}") String accessToken,
@Value("${mapbox.base-url}") String baseUrl,
OkHttpClient httpClient,
ObjectMapper objectMapper) {
this.accessToken = accessToken;
this.baseUrl = baseUrl;
this.httpClient = httpClient;
this.objectMapper = objectMapper;
}
/**
* Get directions between waypoints
*/
public RouteResponse getDirections(RouteRequest request) {
request.validate();
try {
String url = buildDirectionsUrl(request);
logger.debug("Making Mapbox Directions API request: {}", url);
Request httpRequest = new Request.Builder()
.url(url)
.get()
.addHeader("Accept", "application/json")
.build();
try (Response response = httpClient.newCall(httpRequest).execute()) {
if (!response.isSuccessful()) {
handleErrorResponse(response);
}
String responseBody = response.body().string();
RouteResponse routeResponse = objectMapper.readValue(responseBody, RouteResponse.class);
if (!"Ok".equals(routeResponse.getCode())) {
throw new MapboxException("Directions API error: " + 
routeResponse.getMessage() + " (Code: " + routeResponse.getCode() + ")");
}
logger.info("Retrieved directions with {} routes, total distance: {} meters", 
routeResponse.getRoutes().size(), 
routeResponse.getRoutes().get(0).getDistance());
return routeResponse;
}
} catch (IOException e) {
logger.error("Failed to get directions from Mapbox API", e);
throw new MapboxException("Failed to retrieve directions", e);
}
}
/**
* Get directions with coordinates as input
*/
public RouteResponse getDirections(List<Coordinate> coordinates, RoutingProfile profile) {
RouteRequest request = new RouteRequest();
request.setProfile(profile);
List<Waypoint> waypoints = coordinates.stream()
.map(coord -> {
Waypoint wp = new Waypoint();
wp.setCoordinate(coord);
return wp;
})
.collect(Collectors.toList());
request.setWaypoints(waypoints);
return getDirections(request);
}
/**
* Get optimized route (Traveling Salesman Problem)
*/
public RouteResponse getOptimizedRoute(List<Coordinate> coordinates, RoutingProfile profile) {
if (coordinates.size() < 2 || coordinates.size() > 12) {
throw new IllegalArgumentException("Optimized route requires 2-12 coordinates");
}
String coordinatesString = coordinates.stream()
.map(Coordinate::toString)
.collect(Collectors.joining(";"));
String url = String.format("%s/directions/v5/mapbox/%s/%s?access_token=%s&roundtrip=true&source=first&destination=last",
baseUrl, profile.getPath(), coordinatesString, accessToken);
return makeApiCall(url);
}
/**
* Get matrix of travel times between multiple points
*/
public MatrixResponse getMatrix(List<Coordinate> coordinates, RoutingProfile profile) {
if (coordinates.size() > 25) {
throw new IllegalArgumentException("Matrix API supports maximum 25 coordinates");
}
String coordinatesString = coordinates.stream()
.map(Coordinate::toString)
.collect(Collectors.joining(";"));
String url = String.format("%s/directions-matrix/v1/mapbox/%s/%s?access_token=%s&annotations=duration,distance",
baseUrl, profile.getPath(), coordinatesString, accessToken);
return makeMatrixApiCall(url);
}
private String buildDirectionsUrl(RouteRequest request) {
String coordinates = request.getWaypoints().stream()
.map(wp -> wp.getCoordinate().toString())
.collect(Collectors.joining(";"));
StringBuilder urlBuilder = new StringBuilder()
.append(baseUrl).append("/directions/v5/mapbox/")
.append(request.getProfile().getPath()).append("/")
.append(coordinates)
.append("?access_token=").append(accessToken);
// Add annotations
if (request.getAnnotations() != null && !request.getAnnotations().isEmpty()) {
String annotations = request.getAnnotations().stream()
.map(Annotation::getValue)
.collect(Collectors.joining(","));
urlBuilder.append("&annotations=").append(annotations);
}
// Add other parameters
if (request.isAlternatives()) {
urlBuilder.append("&alternatives=true");
}
if (request.isSteps()) {
urlBuilder.append("&steps=true");
}
if (request.getLanguage() != null) {
urlBuilder.append("&language=").append(request.getLanguage());
}
if (!request.isContinueStraight()) {
urlBuilder.append("&continue_straight=false");
}
return urlBuilder.toString();
}
private RouteResponse makeApiCall(String url) {
try {
Request request = new Request.Builder()
.url(url)
.get()
.build();
try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
handleErrorResponse(response);
}
String responseBody = response.body().string();
return objectMapper.readValue(responseBody, RouteResponse.class);
}
} catch (IOException e) {
logger.error("Mapbox API call failed", e);
throw new MapboxException("Mapbox API call failed", e);
}
}
private MatrixResponse makeMatrixApiCall(String url) {
try {
Request request = new Request.Builder()
.url(url)
.get()
.build();
try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
handleErrorResponse(response);
}
String responseBody = response.body().string();
return objectMapper.readValue(responseBody, MatrixResponse.class);
}
} catch (IOException e) {
logger.error("Mapbox Matrix API call failed", e);
throw new MapboxException("Mapbox Matrix API call failed", e);
}
}
private void handleErrorResponse(Response response) throws IOException {
String errorBody = response.body().string();
logger.error("Mapbox API error: {} - {}", response.code(), errorBody);
switch (response.code()) {
case 400:
throw new MapboxBadRequestException("Bad request: " + errorBody);
case 401:
throw new MapboxAuthException("Invalid access token");
case 403:
throw new MapboxForbiddenException("Access forbidden");
case 404:
throw new MapboxNotFoundException("Resource not found");
case 429:
throw new MapboxRateLimitException("Rate limit exceeded");
case 500:
throw new MapboxServerException("Mapbox server error");
default:
throw new MapboxException("Mapbox API error: " + response.code() + " - " + errorBody);
}
}
}
2. Cached Directions Service
@Service
@CacheConfig(cacheNames = "directions")
public class CachedDirectionsService {
private static final Logger logger = LoggerFactory.getLogger(CachedDirectionsService.class);
private final MapboxDirectionsService directionsService;
public CachedDirectionsService(MapboxDirectionsService directionsService) {
this.directionsService = directionsService;
}
@Cacheable(value = "directions", key = "#cacheKey", unless = "#result == null")
public RouteResponse getDirectionsWithCache(RouteRequest request, String cacheKey) {
logger.debug("Cache miss for directions, making API call");
return directionsService.getDirections(request);
}
@Cacheable(value = "matrix", key = "#cacheKey", unless = "#result == null")
public MatrixResponse getMatrixWithCache(List<Coordinate> coordinates, 
RoutingProfile profile, String cacheKey) {
logger.debug("Cache miss for matrix, making API call");
return directionsService.getMatrix(coordinates, profile);
}
/**
* Generate cache key from request parameters
*/
public String generateCacheKey(RouteRequest request) {
String waypointsKey = request.getWaypoints().stream()
.map(wp -> wp.getCoordinate().toString())
.collect(Collectors.joining("|"));
return String.format("%s|%s|%s|%s",
waypointsKey,
request.getProfile(),
request.getAnnotations().stream().map(Annotation::getValue).collect(Collectors.joining(",")),
request.isSteps());
}
public String generateMatrixCacheKey(List<Coordinate> coordinates, RoutingProfile profile) {
String coordsKey = coordinates.stream()
.map(Coordinate::toString)
.collect(Collectors.joining("|"));
return String.format("matrix|%s|%s", coordsKey, profile);
}
}
3. Matrix Response Models
public class MatrixResponse {
private List<List<Double>> durations;
private List<List<Double>> distances;
private List<MatrixWaypoint> destinations;
private List<MatrixWaypoint> sources;
private String code;
// constructors, getters, setters
}
public class MatrixWaypoint {
private String name;
private List<Double> location;
// constructors, getters, setters
}

Advanced Routing Features

1. Route Optimization Service
@Service
public class RouteOptimizationService {
private static final Logger logger = LoggerFactory.getLogger(RouteOptimizationService.class);
private final MapboxDirectionsService directionsService;
private final CachedDirectionsService cachedDirectionsService;
public RouteOptimizationService(MapboxDirectionsService directionsService,
CachedDirectionsService cachedDirectionsService) {
this.directionsService = directionsService;
this.cachedDirectionsService = cachedDirectionsService;
}
/**
* Solve Traveling Salesman Problem using Mapbox Optimized Trips API
*/
public RouteResponse solveTSP(List<Coordinate> coordinates, RoutingProfile profile) {
if (coordinates.size() < 2 || coordinates.size() > 12) {
throw new IllegalArgumentException("TSP requires 2-12 coordinates");
}
logger.info("Solving TSP for {} locations", coordinates.size());
return directionsService.getOptimizedRoute(coordinates, profile);
}
/**
* Calculate distance matrix for multiple points
*/
public MatrixResponse getDistanceMatrix(List<Coordinate> coordinates, RoutingProfile profile) {
logger.info("Calculating distance matrix for {} points", coordinates.size());
String cacheKey = cachedDirectionsService.generateMatrixCacheKey(coordinates, profile);
return cachedDirectionsService.getMatrixWithCache(coordinates, profile, cacheKey);
}
/**
* Find nearest point from a reference point
*/
public Coordinate findNearestPoint(Coordinate reference, List<Coordinate> candidates, 
RoutingProfile profile) {
if (candidates.isEmpty()) {
throw new IllegalArgumentException("Candidate list cannot be empty");
}
List<Coordinate> allPoints = new ArrayList<>();
allPoints.add(reference);
allPoints.addAll(candidates);
MatrixResponse matrix = getDistanceMatrix(allPoints, profile);
// Find minimum duration from reference (index 0) to other points
int nearestIndex = -1;
double minDuration = Double.MAX_VALUE;
for (int i = 1; i < matrix.getDurations().get(0).size(); i++) {
double duration = matrix.getDurations().get(0).get(i);
if (duration < minDuration) {
minDuration = duration;
nearestIndex = i - 1; // Adjust for reference point at index 0
}
}
return candidates.get(nearestIndex);
}
/**
* Calculate isochrone - reachable areas within time/distance limit
*/
public List<Coordinate> calculateIsochrone(Coordinate center, double maxMinutes, 
RoutingProfile profile) {
// This is a simplified implementation
// In practice, you'd use Mapbox Isochrone API or generate multiple routes
logger.info("Calculating isochrone for {} minutes from {}", maxMinutes, center);
// Generate points in a circle around center
List<Coordinate> boundaryPoints = generateCirclePoints(center, maxMinutes * 1000); // Convert to meters
// For each point, check if it's reachable within time limit
List<Coordinate> reachablePoints = new ArrayList<>();
for (Coordinate point : boundaryPoints) {
try {
RouteResponse route = directionsService.getDirections(
Arrays.asList(center, point), profile);
double durationMinutes = route.getRoutes().get(0).getDuration() / 60.0;
if (durationMinutes <= maxMinutes) {
reachablePoints.add(point);
}
} catch (Exception e) {
logger.debug("Could not calculate route to point: {}", point, e);
}
}
return reachablePoints;
}
private List<Coordinate> generateCirclePoints(Coordinate center, double radiusMeters) {
List<Coordinate> points = new ArrayList<>();
int pointsCount = 36; // 10-degree intervals
for (int i = 0; i < pointsCount; i++) {
double angle = Math.toRadians(i * 10);
double[] offset = calculateOffset(radiusMeters, angle);
Coordinate point = new Coordinate(
center.getLongitude() + offset[0],
center.getLatitude() + offset[1]
);
points.add(point);
}
return points;
}
private double[] calculateOffset(double distanceMeters, double angle) {
// Convert meters to degrees (approximate)
double metersPerDegreeLat = 111320.0; // meters per degree latitude
double metersPerDegreeLon = 111320.0 * Math.cos(Math.toRadians(angle)); // varies with latitude
double latOffset = (distanceMeters * Math.cos(angle)) / metersPerDegreeLat;
double lonOffset = (distanceMeters * Math.sin(angle)) / metersPerDegreeLon;
return new double[]{lonOffset, latOffset};
}
}
2. Route Analysis Service
@Service
public class RouteAnalysisService {
/**
* Analyze route for statistics and insights
*/
public RouteAnalysis analyzeRoute(RouteResponse routeResponse) {
if (routeResponse.getRoutes().isEmpty()) {
throw new IllegalArgumentException("No routes in response");
}
Route route = routeResponse.getRoutes().get(0);
RouteAnalysis analysis = new RouteAnalysis();
analysis.setTotalDistance(route.getDistance());
analysis.setTotalDuration(route.getDuration());
analysis.setAverageSpeed(calculateAverageSpeed(route));
analysis.setTollRoads(countTollRoads(route));
analysis.setTurns(analyzeTurns(route));
analysis.setElevationChanges(analyzeElevation(route));
analysis.setTrafficConditions(analyzeTraffic(route));
return analysis;
}
/**
* Compare multiple routes
*/
public RouteComparison compareRoutes(List<RouteResponse> routeResponses) {
RouteComparison comparison = new RouteComparison();
for (int i = 0; i < routeResponses.size(); i++) {
RouteResponse response = routeResponses.get(i);
if (!response.getRoutes().isEmpty()) {
Route route = response.getRoutes().get(0);
RouteAnalysis analysis = analyzeRoute(response);
comparison.addRoute("Route " + (i + 1), route, analysis);
}
}
comparison.calculateBestRoutes();
return comparison;
}
/**
* Calculate ETA with traffic considerations
*/
public ETA calculateETA(RouteResponse routeResponse, LocalDateTime departureTime) {
Route route = routeResponse.getRoutes().get(0);
ETA eta = new ETA();
// Base ETA without traffic
Duration baseDuration = Duration.ofSeconds((long) route.getDuration());
LocalDateTime baseArrival = departureTime.plus(baseDuration);
eta.setBaseArrivalTime(baseArrival);
eta.setBaseDuration(baseDuration);
// Adjust for traffic if available
if (hasTrafficData(route)) {
Duration trafficAdjustedDuration = adjustForTraffic(route, departureTime);
eta.setTrafficAdjustedDuration(trafficAdjustedDuration);
eta.setTrafficAdjustedArrivalTime(departureTime.plus(trafficAdjustedDuration));
eta.setTrafficDelay(trafficAdjustedDuration.minus(baseDuration));
}
return eta;
}
private double calculateAverageSpeed(Route route) {
return route.getDistance() / route.getDuration() * 3.6; // km/h
}
private int countTollRoads(Route route) {
// Count toll roads in route steps
return (int) route.getSteps().stream()
.filter(step -> step.getName() != null && 
(step.getName().toLowerCase().contains("toll") ||
step.getName().toLowerCase().contains("turnpike")))
.count();
}
private TurnAnalysis analyzeTurns(Route route) {
TurnAnalysis turns = new TurnAnalysis();
for (RouteStep step : route.getSteps()) {
if (step.getManeuver() != null) {
String type = step.getManeuver().getType();
switch (type) {
case "turn":
case "sharp turn":
turns.addTurn(type);
break;
case "roundabout":
turns.incrementRoundabouts();
break;
case "fork":
case "off ramp":
turns.incrementExits();
break;
}
}
}
return turns;
}
private boolean hasTrafficData(Route route) {
return route.getLegs().stream()
.anyMatch(leg -> leg.getAnnotation() != null && 
leg.getAnnotation().contains(Annotation.CONGESTION));
}
private Duration adjustForTraffic(Route route, LocalDateTime departureTime) {
// Simplified traffic adjustment
// In practice, you'd use historical traffic data and congestion annotations
double baseDuration = route.getDuration();
double trafficFactor = calculateTrafficFactor(departureTime);
long adjustedSeconds = (long) (baseDuration * trafficFactor);
return Duration.ofSeconds(adjustedSeconds);
}
private double calculateTrafficFactor(LocalDateTime time) {
// Simple traffic model - peak hours have higher factors
int hour = time.getHour();
if ((hour >= 7 && hour <= 9) || (hour >= 16 && hour <= 18)) {
return 1.3; // 30% longer during rush hours
} else if (hour >= 22 || hour <= 5) {
return 0.9; // 10% shorter during night
} else {
return 1.0; // Normal traffic
}
}
// Placeholder methods for elevation and traffic analysis
private ElevationAnalysis analyzeElevation(Route route) { return new ElevationAnalysis(); }
private TrafficAnalysis analyzeTraffic(Route route) { return new TrafficAnalysis(); }
}
3. Analysis Models
public class RouteAnalysis {
private double totalDistance;
private double totalDuration;
private double averageSpeed;
private int tollRoads;
private TurnAnalysis turns;
private ElevationAnalysis elevationChanges;
private TrafficAnalysis trafficConditions;
// constructors, getters, setters
}
public class TurnAnalysis {
private int leftTurns;
private int rightTurns;
private int uTurns;
private int roundabouts;
private int exits;
// constructors, getters, setters
public void addTurn(String type) {
switch (type) {
case "turn left":
case "sharp left":
leftTurns++;
break;
case "turn right":
case "sharp right":
rightTurns++;
break;
case "uturn":
uTurns++;
break;
}
}
public void incrementRoundabouts() { roundabouts++; }
public void incrementExits() { exits++; }
}
public class ETA {
private LocalDateTime baseArrivalTime;
private Duration baseDuration;
private LocalDateTime trafficAdjustedArrivalTime;
private Duration trafficAdjustedDuration;
private Duration trafficDelay;
// constructors, getters, setters
}
public class RouteComparison {
private Map<String, Route> routes = new HashMap<>();
private Map<String, RouteAnalysis> analyses = new HashMap<>();
private String fastestRoute;
private String shortestRoute;
private String mostEfficientRoute;
// constructors, getters, setters
public void addRoute(String name, Route route, RouteAnalysis analysis) {
routes.put(name, route);
analyses.put(name, analysis);
}
public void calculateBestRoutes() {
// Find fastest route (shortest duration)
fastestRoute = analyses.entrySet().stream()
.min(Comparator.comparingDouble(entry -> entry.getValue().getTotalDuration()))
.map(Map.Entry::getKey)
.orElse(null);
// Find shortest route (shortest distance)
shortestRoute = analyses.entrySet().stream()
.min(Comparator.comparingDouble(entry -> entry.getValue().getTotalDistance()))
.map(Map.Entry::getKey)
.orElse(null);
// Find most efficient (best speed/distance ratio)
mostEfficientRoute = analyses.entrySet().stream()
.max(Comparator.comparingDouble(entry -> entry.getValue().getAverageSpeed()))
.map(Map.Entry::getKey)
.orElse(null);
}
}

REST Controllers

1. Directions Controller
@RestController
@RequestMapping("/api/directions")
@Validated
public class DirectionsController {
private final MapboxDirectionsService directionsService;
private final CachedDirectionsService cachedDirectionsService;
private final RouteOptimizationService optimizationService;
private final RouteAnalysisService analysisService;
public DirectionsController(MapboxDirectionsService directionsService,
CachedDirectionsService cachedDirectionsService,
RouteOptimizationService optimizationService,
RouteAnalysisService analysisService) {
this.directionsService = directionsService;
this.cachedDirectionsService = cachedDirectionsService;
this.optimizationService = optimizationService;
this.analysisService = analysisService;
}
@PostMapping("/route")
public ResponseEntity<RouteResponse> getDirections(@Valid @RequestBody RouteRequest request) {
String cacheKey = cachedDirectionsService.generateCacheKey(request);
RouteResponse response = cachedDirectionsService.getDirectionsWithCache(request, cacheKey);
return ResponseEntity.ok(response);
}
@GetMapping("/simple")
public ResponseEntity<RouteResponse> getSimpleDirections(
@RequestParam @NotBlank String start,
@RequestParam @NotBlank String end,
@RequestParam(defaultValue = "DRIVING") RoutingProfile profile) {
Coordinate startCoord = Coordinate.fromString(start);
Coordinate endCoord = Coordinate.fromString(end);
RouteResponse response = directionsService.getDirections(
Arrays.asList(startCoord, endCoord), profile);
return ResponseEntity.ok(response);
}
@PostMapping("/optimize")
public ResponseEntity<RouteResponse> optimizeRoute(
@RequestBody @Valid OptimizeRouteRequest request) {
RouteResponse response = optimizationService.solveTSP(
request.getCoordinates(), request.getProfile());
return ResponseEntity.ok(response);
}
@PostMapping("/matrix")
public ResponseEntity<MatrixResponse> getDistanceMatrix(
@RequestBody @Valid MatrixRequest request) {
MatrixResponse response = optimizationService.getDistanceMatrix(
request.getCoordinates(), request.getProfile());
return ResponseEntity.ok(response);
}
@PostMapping("/analyze")
public ResponseEntity<RouteAnalysis> analyzeRoute(@RequestBody RouteResponse routeResponse) {
RouteAnalysis analysis = analysisService.analyzeRoute(routeResponse);
return ResponseEntity.ok(analysis);
}
@PostMapping("/eta")
public ResponseEntity<ETA> calculateETA(@RequestBody ETARequest request) {
ETA eta = analysisService.calculateETA(request.getRouteResponse(), request.getDepartureTime());
return ResponseEntity.ok(eta);
}
}
2. Request Models
public class OptimizeRouteRequest {
@NotEmpty
@Size(min = 2, max = 12)
private List<Coordinate> coordinates;
private RoutingProfile profile = RoutingProfile.DRIVING;
// constructors, getters, setters
}
public class MatrixRequest {
@NotEmpty
@Size(max = 25)
private List<Coordinate> coordinates;
private RoutingProfile profile = RoutingProfile.DRIVING;
// constructors, getters, setters
}
public class ETARequest {
@NotNull
private RouteResponse routeResponse;
@NotNull
private LocalDateTime departureTime;
// constructors, getters, setters
}

GeoJSON Utilities

@Service
public class GeoJsonService {
/**
* Convert route to GeoJSON for mapping
*/
public String routeToGeoJson(RouteResponse routeResponse) {
JsonObject featureCollection = new JsonObject();
featureCollection.addProperty("type", "FeatureCollection");
JsonArray features = new JsonArray();
// Add route as LineString
if (!routeResponse.getRoutes().isEmpty()) {
Route route = routeResponse.getRoutes().get(0);
features.add(createRouteFeature(route));
}
// Add waypoints as Points
for (Waypoint waypoint : routeResponse.getWaypoints()) {
features.add(createWaypointFeature(waypoint));
}
featureCollection.add("features", features);
return new Gson().toJson(featureCollection);
}
/**
* Convert multiple routes to GeoJSON for comparison
*/
public String routesToGeoJson(List<RouteResponse> routeResponses) {
JsonObject featureCollection = new JsonObject();
featureCollection.addProperty("type", "FeatureCollection");
JsonArray features = new JsonArray();
for (int i = 0; i < routeResponses.size(); i++) {
RouteResponse response = routeResponses.get(i);
if (!response.getRoutes().isEmpty()) {
features.add(createRouteFeature(response.getRoutes().get(0), "Route " + (i + 1)));
}
}
featureCollection.add("features", features);
return new Gson().toJson(featureCollection);
}
private JsonObject createRouteFeature(Route route) {
return createRouteFeature(route, "Route");
}
private JsonObject createRouteFeature(Route route, String name) {
JsonObject feature = new JsonObject();
feature.addProperty("type", "Feature");
// Geometry
JsonObject geometry = new JsonObject();
geometry.addProperty("type", "LineString");
try {
// Decode polyline geometry
List<Coordinate> coordinates = decodePolyline(route.getGeometry());
JsonArray coordsArray = new JsonArray();
for (Coordinate coord : coordinates) {
JsonArray coordArray = new JsonArray();
coordArray.add(coord.getLongitude());
coordArray.add(coord.getLatitude());
coordsArray.add(coordArray);
}
geometry.add("coordinates", coordsArray);
} catch (Exception e) {
// Fallback: create simple line from waypoints
geometry.add("coordinates", new JsonArray());
}
feature.add("geometry", geometry);
// Properties
JsonObject properties = new JsonObject();
properties.addProperty("name", name);
properties.addProperty("distance", route.getDistance());
properties.addProperty("duration", route.getDuration());
feature.add("properties", properties);
return feature;
}
private JsonObject createWaypointFeature(Waypoint waypoint) {
JsonObject feature = new JsonObject();
feature.addProperty("type", "Feature");
// Geometry
JsonObject geometry = new JsonObject();
geometry.addProperty("type", "Point");
JsonArray coordinates = new JsonArray();
coordinates.add(waypoint.getCoordinate().getLongitude());
coordinates.add(waypoint.getCoordinate().getLatitude());
geometry.add("coordinates", coordinates);
feature.add("geometry", geometry);
// Properties
JsonObject properties = new JsonObject();
if (waypoint.getName() != null) {
properties.addProperty("name", waypoint.getName());
}
feature.add("properties", properties);
return feature;
}
/**
* Decode Mapbox polyline geometry
*/
public List<Coordinate> decodePolyline(String polyline) {
List<Coordinate> coordinates = new ArrayList<>();
int index = 0;
int lat = 0;
int lon = 0;
while (index < polyline.length()) {
int b;
int shift = 0;
int result = 0;
do {
b = polyline.charAt(index++) - 63;
result |= (b & 0x1f) << shift;
shift += 5;
} while (b >= 0x20);
int dlat = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
lat += dlat;
shift = 0;
result = 0;
do {
b = polyline.charAt(index++) - 63;
result |= (b & 0x1f) << shift;
shift += 5;
} while (b >= 0x20);
int dlng = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
lon += dlng;
coordinates.add(new Coordinate(
lon / 1e5,  // Convert to degrees
lat / 1e5   // Convert to degrees
));
}
return coordinates;
}
}

Error Handling

@ControllerAdvice
public class MapboxExceptionHandler {
@ExceptionHandler(MapboxAuthException.class)
public ResponseEntity<ErrorResponse> handleAuthException(MapboxAuthException ex) {
logger.error("Mapbox authentication error", ex);
ErrorResponse error = new ErrorResponse("MAPBOX_AUTH_ERROR", ex.getMessage());
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(error);
}
@ExceptionHandler(MapboxRateLimitException.class)
public ResponseEntity<ErrorResponse> handleRateLimitException(MapboxRateLimitException ex) {
logger.error("Mapbox rate limit exceeded", ex);
ErrorResponse error = new ErrorResponse("MAPBOX_RATE_LIMIT", ex.getMessage());
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body(error);
}
@ExceptionHandler(MapboxBadRequestException.class)
public ResponseEntity<ErrorResponse> handleBadRequestException(MapboxBadRequestException ex) {
logger.error("Mapbox bad request", ex);
ErrorResponse error = new ErrorResponse("MAPBOX_BAD_REQUEST", ex.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGenericException(Exception ex) {
logger.error("Unexpected error in Mapbox integration", ex);
ErrorResponse error = new ErrorResponse("INTERNAL_ERROR", "An unexpected error occurred");
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
}
public class MapboxException extends RuntimeException {
public MapboxException(String message) {
super(message);
}
public MapboxException(String message, Throwable cause) {
super(message, cause);
}
}
public class MapboxAuthException extends MapboxException {
public MapboxAuthException(String message) {
super(message);
}
}
public class MapboxRateLimitException extends MapboxException {
public MapboxRateLimitException(String message) {
super(message);
}
}
public class MapboxBadRequestException extends MapboxException {
public MapboxBadRequestException(String message) {
super(message);
}
}

Testing

1. Unit Tests
@ExtendWith(MockitoExtension.class)
class MapboxDirectionsServiceTest {
@Mock
private OkHttpClient httpClient;
@Mock
private Call call;
@InjectMocks
private MapboxDirectionsService directionsService;
@Test
void shouldGetDirections() throws Exception {
// Given
RouteRequest request = createTestRouteRequest();
String mockResponse = createMockDirectionsResponse();
Response response = new Response.Builder()
.request(new Request.Builder().url("http://test.com").build())
.protocol(Protocol.HTTP_1_1)
.code(200)
.message("OK")
.body(ResponseBody.create(mockResponse, MediaType.parse("application/json")))
.build();
when(httpClient.newCall(any())).thenReturn(call);
when(call.execute()).thenReturn(response);
// When
RouteResponse result = directionsService.getDirections(request);
// Then
assertThat(result).isNotNull();
assertThat(result.getRoutes()).hasSize(1);
assertThat(result.getRoutes().get(0).getDistance()).isEqualTo(1000.0);
}
private RouteRequest createTestRouteRequest() {
RouteRequest request = new RouteRequest();
request.setWaypoints(Arrays.asList(
createWaypoint(-74.0059, 40.7128), // NYC
createWaypoint(-118.2437, 34.0522) // LA
));
request.setProfile(RoutingProfile.DRIVING);
return request;
}
private Waypoint createWaypoint(double lng, double lat) {
Waypoint wp = new Waypoint();
wp.setCoordinate(new Coordinate(lng, lat));
return wp;
}
private String createMockDirectionsResponse() {
return """
{
"routes": [
{
"geometry": "mockGeometry",
"legs": [
{
"steps": [],
"summary": "",
"distance": 1000.0,
"duration": 600.0
}
],
"distance": 1000.0,
"duration": 600.0,
"weight": 600.0,
"weight_name": "duration"
}
],
"waypoints": [
{
"location": [-74.0059, 40.7128]
},
{
"location": [-118.2437, 34.0522]
}
],
"code": "Ok",
"uuid": "test-uuid"
}
""";
}
}
2. Integration Test
@SpringBootTest
@TestPropertySource(locations = "classpath:application-test.properties")
class MapboxIntegrationTest {
@Autowired
private MapboxDirectionsService directionsService;
@Test
@Disabled("Requires actual Mapbox access token")
void shouldConnectToMapbox() {
// Given
Coordinate nyc = new Coordinate(-74.0060, 40.7128);
Coordinate la = new Coordinate(-118.2437, 34.0522);
// When
RouteResponse response = directionsService.getDirections(
Arrays.asList(nyc, la), RoutingProfile.DRIVING);
// Then
assertThat(response).isNotNull();
assertThat(response.getCode()).isEqualTo("Ok");
assertThat(response.getRoutes()).isNotEmpty();
}
}

Best Practices

  1. Caching: Cache directions and matrix results to reduce API calls
  2. Rate Limiting: Implement rate limiting to respect Mapbox quotas
  3. Error Handling: Comprehensive error handling for API failures
  4. Validation: Validate coordinates and request parameters
  5. Monitoring: Monitor API usage and performance
  6. Fallbacks: Implement fallback strategies for API failures
@Component
public class MapboxHealthCheck {
@Scheduled(fixedRate = 300000) // 5 minutes
public void checkMapboxConnectivity() {
try {
// Test with simple coordinates
Coordinate test1 = new Coordinate(-74.0060, 40.7128);
Coordinate test2 = new Coordinate(-73.9352, 40.7306);
RouteResponse response = directionsService.getDirections(
Arrays.asList(test1, test2), RoutingProfile.DRIVING);
if ("Ok".equals(response.getCode())) {
logger.debug("Mapbox connectivity check passed");
} else {
logger.error("Mapbox connectivity check failed: {}", response.getMessage());
}
} catch (Exception e) {
logger.error("Mapbox connectivity check failed with exception", e);
}
}
}

Conclusion

This comprehensive Mapbox Directions API integration provides:

  • Complete directions between multiple waypoints
  • Route optimization for traveling salesman problems
  • Distance matrices for multiple points
  • Route analysis and comparison tools
  • GeoJSON support for mapping visualization
  • Caching and performance optimization

The implementation supports all Mapbox routing profiles (driving, walking, cycling) and includes advanced features like traffic-aware ETAs, isochrone calculations, and route optimization, making it suitable for navigation apps, delivery services, and logistics planning.

Java Observability, Logging Intelligence & AI-Driven Monitoring (APM, Tracing, Logs & Anomaly Detection)

https://macronepal.com/blog/beyond-metrics-observing-serverless-and-traditional-java-applications-with-thundra-apm/
Explains using Thundra APM to observe both serverless and traditional Java applications by combining tracing, metrics, and logs into a unified observability platform for faster debugging and performance insights.

https://macronepal.com/blog/dynatrace-oneagent-in-java-2/
Explains Dynatrace OneAgent for Java, which automatically instruments JVM applications to capture metrics, traces, and logs, enabling full-stack monitoring and root-cause analysis with minimal configuration.

https://macronepal.com/blog/lightstep-java-sdk-distributed-tracing-and-observability-implementation/
Explains Lightstep Java SDK for distributed tracing, helping developers track requests across microservices and identify latency issues using OpenTelemetry-based observability.

https://macronepal.com/blog/honeycomb-io-beeline-for-java-complete-guide-2/
Explains Honeycomb Beeline for Java, which provides high-cardinality observability and deep query capabilities to understand complex system behavior and debug distributed systems efficiently.

https://macronepal.com/blog/lumigo-for-serverless-in-java-complete-distributed-tracing-guide-2/
Explains Lumigo for Java serverless applications, offering automatic distributed tracing, log correlation, and error tracking to simplify debugging in cloud-native environments. (Lumigo Docs)

https://macronepal.com/blog/from-noise-to-signals-implementing-log-anomaly-detection-in-java-applications/
Explains how to detect anomalies in Java logs using behavioral patterns and machine learning techniques to separate meaningful incidents from noisy log data and improve incident response.

https://macronepal.com/blog/ai-powered-log-analysis-in-java-from-reactive-debugging-to-proactive-insights/
Explains AI-driven log analysis for Java applications, shifting from manual debugging to predictive insights that identify issues early and improve system reliability using intelligent log processing.

https://macronepal.com/blog/titliel-java-logging-best-practices/
Explains best practices for Java logging, focusing on structured logs, proper log levels, performance optimization, and ensuring logs are useful for debugging and observability systems.

https://macronepal.com/blog/seeking-a-loguru-for-java-the-quest-for-elegant-and-simple-logging/
Explains the search for simpler, more elegant logging frameworks in Java, comparing modern logging approaches that aim to reduce complexity while improving readability and developer experience.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper