Skip to main content

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>
origin
LatLng
required
Starting point coordinates
destination
LatLng
required
Destination point coordinates
RouteResult
object
Route calculation result

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/
Searches for places matching a query string.
search(
  query: string, 
  opts?: SearchOpts
): Observable<PlaceSuggestion[]>
query
string
required
Search query (e.g., “Parque Céspedes”, “Avenida Garzón”)
opts
SearchOpts
Optional search configuration
PlaceSuggestion[]
array
Array of matching places

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>
lng
number
required
Longitude coordinate
lat
number
required
Latitude coordinate
opts
object
Optional configuration
ReverseResult | null
object | null
Reverse geocoding result (or null if no match)

Feature Type Priority

The method prioritizes feature types in this order:
  1. POI
  2. POI landmark
  3. Address
  4. Place
  5. Locality
  6. Neighborhood
  7. 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);
    }
  }
});

3. Debounce Search Input

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.

Build docs developers (and LLMs) love