Overview
The Rodando app integrates with Mapbox APIs for routing and geocoding services. Two main services handle these operations:
- MapboxDirectionsService: Route calculations and turn-by-turn directions
- MapboxPlacesService: Place search, geocoding, and reverse geocoding
MapboxDirectionsService
Handles route calculations between two points using the Mapbox Directions API.
Location: src/app/core/services/http/mapbox-directions.service.ts
Configuration
private token = environment.mapbox.accessToken;
API Endpoint: https://api.mapbox.com/directions/v5/mapbox/driving/
getRoute()
Calculates the optimal driving route between origin and destination.
getRoute(
origin: LatLng,
destination: LatLng
): Observable<RouteResult>
Starting point coordinates
Destination point coordinates
Route calculation resultShow RouteResult properties
Total route distance in kilometers (e.g., 12.3)
Estimated travel time in minutes (e.g., 24)
feature
GeoJSON.FeatureCollection<GeoJSON.LineString>
required
GeoJSON FeatureCollection containing the route geometry
Optional raw Mapbox API response
Request Parameters
The method automatically includes these Mapbox API parameters:
alternatives: false (only best route)
geometries: geojson (GeoJSON format)
overview: full (complete route geometry)
annotations: distance,duration (detailed metrics)
Validation
The method validates input coordinates:
- Ensures
lat and lng are finite numbers
- Normalizes coordinates to 6 decimal places
- Throws error if coordinates are invalid
Usage Example
import { MapboxDirectionsService } from '@/app/core/services/http/mapbox-directions.service';
import type { LatLng, RouteResult } from '@/app/core/models/trip/place-suggestion.model';
const directionsService = inject(MapboxDirectionsService);
const origin: LatLng = { lat: 20.0238, lng: -75.8274 }; // Santiago de Cuba
const destination: LatLng = { lat: 20.0513, lng: -75.8152 };
directionsService.getRoute(origin, destination).subscribe({
next: (route: RouteResult) => {
console.log(`Distance: ${route.distanceKm.toFixed(2)} km`);
console.log(`Duration: ${route.durationMin.toFixed(0)} min`);
// Use the GeoJSON feature to display route on map
const routeGeoJson = route.feature;
map.addSource('route', {
type: 'geojson',
data: routeGeoJson
});
map.addLayer({
id: 'route',
type: 'line',
source: 'route',
paint: {
'line-color': '#3b82f6',
'line-width': 4
}
});
},
error: (err) => {
console.error('Route calculation failed:', err.message);
}
});
MapboxPlacesService
Handles place search, geocoding, and reverse geocoding using the Mapbox Geocoding API.
Location: src/app/core/services/http/mapbox-places.service.ts
Configuration
private token = environment.mapbox.accessToken;
// Santiago de Cuba province bounding box
private SCU_PROV_BBOX: BBox = [-76.30, 19.60, -75.10, 20.60];
private SCU_PROV_CENTER: LatLng = { lng: -75.82, lat: 20.02 };
API Endpoint: https://api.mapbox.com/geocoding/v5/mapbox.places/
search()
Searches for places matching a query string.
search(
query: string,
opts?: SearchOpts
): Observable<PlaceSuggestion[]>
Search query (e.g., “Parque Céspedes”, “Avenida Garzón”)
Optional search configurationShow SearchOpts properties
Bias results towards a specific location
Bounding box to limit results: [minLng, minLat, maxLng, maxLat]
Country code (e.g., 'cu' for Cuba)
Feature types: 'poi', 'address', 'place', 'locality', etc.
Response language (e.g., 'es')
Filter results to Santiago de Cuba province only
Array of matching placesShow PlaceSuggestion properties
Short place name (e.g., “Parque Céspedes”)
Full formatted address (cleaned of “Santiago de Cuba, Cuba” suffix)
Default Behavior
- Clamping: By default, results are filtered to Santiago de Cuba province
- Proximity: Results are biased towards Santiago city center
- Bounding Box: Limited to Santiago province coordinates
- Types: Searches POIs, landmarks, addresses, places, localities, and neighborhoods
Usage Example
import { MapboxPlacesService } from '@/app/core/services/http/mapbox-places.service';
import type { PlaceSuggestion } from '@/app/core/models/trip/place-suggestion.model';
const placesService = inject(MapboxPlacesService);
// Search for a place
placesService.search('Parque Céspedes').subscribe({
next: (suggestions: PlaceSuggestion[]) => {
suggestions.forEach(place => {
console.log(`${place.text} - ${place.placeName}`);
console.log(`Coords: ${place.coords.lat}, ${place.coords.lng}`);
});
}
});
// Search with custom options
const userLocation: LatLng = { lat: 20.0238, lng: -75.8274 };
placesService.search('hospital', {
proximity: userLocation,
limit: 5,
types: 'poi',
clampToSantiagoProvince: true
}).subscribe({
next: (suggestions) => {
// Display suggestions in autocomplete
}
});
// Search without province clamping (all of Cuba)
placesService.search('Havana', {
clampToSantiagoProvince: false,
country: 'cu'
}).subscribe({
next: (suggestions) => {
// Results from entire country
}
});
reverse()
Converts coordinates to a human-readable address (reverse geocoding).
reverse(
lng: number,
lat: number,
opts?: {
clampToSantiagoProvince?: boolean;
language?: string;
}
): Observable<ReverseResult | null>
Optional configuration
Filter results to Santiago province
Response language (e.g., 'es')
Reverse geocoding result (or null if no match)Show ReverseResult properties
Human-readable address label for display
Coordinates of the matched feature
Feature Type Priority
The method prioritizes feature types in this order:
- POI
- POI landmark
- Address
- Place
- Locality
- Neighborhood
- First result (fallback)
Usage Example
import { MapboxPlacesService } from '@/app/core/services/http/mapbox-places.service';
const placesService = inject(MapboxPlacesService);
// Reverse geocode coordinates
const lng = -75.8274;
const lat = 20.0238;
placesService.reverse(lng, lat).subscribe({
next: (result) => {
if (result) {
console.log('Address:', result.label);
console.log('Coords:', result.coords);
// Display address in UI
} else {
console.log('No address found for this location');
}
},
error: (err) => {
console.error('Reverse geocoding failed:', err);
}
});
// Reverse geocode with options
placesService.reverse(lng, lat, {
clampToSantiagoProvince: false,
language: 'es'
}).subscribe({
next: (result) => {
// Handle result
}
});
// Use with drag-and-drop pin on map
map.on('dragend', (e) => {
const { lng, lat } = e.target.getCenter();
placesService.reverse(lng, lat).subscribe({
next: (result) => {
if (result) {
updateAddressInput(result.label);
}
}
});
});
TypeScript Interfaces
Common Types
export type LatLng = {
lat: number;
lng: number;
};
export type BBox = [
number, // minLng
number, // minLat
number, // maxLng
number // maxLat
];
MapboxDirectionsService Types
export interface RouteResult {
distanceKm: number; // e.g., 12.3
durationMin: number; // e.g., 24
feature: GeoJSON.FeatureCollection<GeoJSON.LineString>;
raw?: any; // optional: full Mapbox response
}
MapboxPlacesService Types
export interface PlaceSuggestion {
id: string; // Mapbox feature.id
text: string; // short title (main text)
placeName: string; // "street, city, country"
coords: LatLng; // center [lat,lng]
}
export interface ReverseResult {
label: string; // Human-readable text for UI
coords: {
lat: number;
lng: number;
};
}
export interface SearchOpts {
proximity?: LatLng;
bbox?: BBox;
country?: string; // 'cu'
limit?: number; // <= 10 (Mapbox limit)
types?: string; // 'poi,address,place,...'
language?: string; // 'es'
clampToSantiagoProvince?: boolean;
}
Best Practices
1. Coordinate Validation
Always validate coordinates before passing to Mapbox:
function isValidLatLng(coords: LatLng): boolean {
return (
typeof coords.lat === 'number' &&
typeof coords.lng === 'number' &&
isFinite(coords.lat) &&
isFinite(coords.lng) &&
coords.lat >= -90 && coords.lat <= 90 &&
coords.lng >= -180 && coords.lng <= 180
);
}
2. Error Handling
directionsService.getRoute(origin, destination).subscribe({
next: (route) => { /* success */ },
error: (err) => {
if (err.message.includes('invalid')) {
console.error('Invalid coordinates');
} else if (err.message.includes('Sin rutas')) {
console.error('No route found between points');
} else {
console.error('Route error:', err);
}
}
});
Debounce user input when using the search method:
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
searchInput$.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(query => {
if (query.length < 3) return of([]);
return placesService.search(query);
})
).subscribe(suggestions => {
// Update autocomplete list
});
4. Cache Results
Consider caching route results for common trips:
private routeCache = new Map<string, RouteResult>();
getCachedRoute(origin: LatLng, dest: LatLng): Observable<RouteResult> {
const key = `${origin.lat},${origin.lng}-${dest.lat},${dest.lng}`;
if (this.routeCache.has(key)) {
return of(this.routeCache.get(key)!);
}
return this.directionsService.getRoute(origin, dest).pipe(
tap(route => this.routeCache.set(key, route))
);
}
Santiago de Cuba Bounding Box
The service includes a pre-configured bounding box for Santiago de Cuba province:
private SCU_PROV_BBOX: BBox = [
-76.30, // minLng (west)
19.60, // minLat (south)
-75.10, // maxLng (east)
20.60 // maxLat (north)
];
private SCU_PROV_CENTER: LatLng = {
lng: -75.82,
lat: 20.02
};
This ensures search results are relevant to the app’s primary service area.
Mapbox API Limits
- Directions API: 300,000 requests/month (free tier)
- Geocoding API: 100,000 requests/month (free tier)
- Search limit: Maximum 10 results per request
Monitor usage in the Mapbox Dashboard.