Overview
Rodando uses Mapbox GL JS for all map interactions, including:
Interactive base maps with geolocation
Route visualization with polylines
Draggable markers for origin and destination
Geocoding (forward and reverse)
Real-time location tracking
Mapbox Services
MapboxDirectionsService
Calculates driving routes between two points using the Mapbox Directions API.
Location: src/app/core/services/http/mapbox-directions.service.ts
getRoute Method
getRoute
(origin: LatLng, destination: LatLng) => Observable<RouteResult>
Fetches an optimized driving route with distance and duration. Parameters: interface LatLng {
lat : number ; // Latitude
lng : number ; // Longitude
}
Returns: interface RouteResult {
distanceKm : number ; // e.g., 12.3
durationMin : number ; // e.g., 24
feature : FeatureCollection < LineString >; // GeoJSON for rendering
raw ?: any ; // Full Mapbox response
}
Example: const origin = { lat: 20.0236 , lng: - 75.8286 };
const dest = { lat: 20.0400 , lng: - 75.8100 };
this . directionsService . getRoute ( origin , dest ). subscribe ( route => {
console . log ( `Distance: ${ route . distanceKm } km` );
console . log ( `Duration: ${ route . durationMin } min` );
// Draw route on map
map . addSource ( 'route' , {
type: 'geojson' ,
data: route . feature
});
map . addLayer ({
id: 'route-line' ,
type: 'line' ,
source: 'route' ,
paint: {
'line-width' : 5 ,
'line-color' : '#FF3B30'
}
});
});
Routes use the driving profile and include traffic data when available. The geometry is returned as GeoJSON for easy rendering.
MapboxPlacesService
Provides geocoding (address search) and reverse geocoding (coordinates to address).
Location: src/app/core/services/http/mapbox-places.service.ts
search Method
search
(query: string, opts?: SearchOpts) => Observable<PlaceSuggestion[]>
Searches for places matching a text query. Options: interface SearchOpts {
proximity ?: LatLng ; // Bias results near this point
bbox ?: BBox ; // Restrict to bounding box
country ?: string ; // ISO country code (e.g., 'cu')
limit ?: number ; // Max results (default: 10)
types ?: string ; // Feature types (see below)
language ?: string ; // Result language (e.g., 'es')
clampToSantiagoProvince ?: boolean ; // Filter to Santiago de Cuba
}
type BBox = [ minLng , minLat , maxLng , maxLat ];
Valid types:
poi - Points of interest
poi.landmark - Landmarks
address - Street addresses
place - Cities and towns
locality - Neighborhoods
neighborhood - Sub-neighborhoods
Example: this . placesService . search ( 'Parque Céspedes' , {
proximity: { lat: 20.0236 , lng: - 75.8286 },
clampToSantiagoProvince: true ,
language: 'es' ,
limit: 5
}). subscribe ( results => {
results . forEach ( place => {
console . log ( place . text ); // "Parque Céspedes"
console . log ( place . placeName ); // "Parque Céspedes, Santiago de Cuba"
console . log ( place . coords ); // { lat: 20.0244, lng: -75.8269 }
});
});
reverse Method
reverse
(lng: number, lat: number, opts?) => Observable<ReverseResult | null>
Converts coordinates to a human-readable address. Options: interface ReverseOpts {
clampToSantiagoProvince ?: boolean ;
language ?: string ;
}
Returns: interface ReverseResult {
label : string ; // "Calle Enramadas, Santiago de Cuba"
coords : { lat : number ; lng : number };
}
Example: this . placesService . reverse ( - 75.8269 , 20.0244 ). subscribe ( result => {
if ( result ) {
console . log ( result . label ); // "Parque Céspedes, Centro Histórico"
}
});
The service automatically strips redundant location suffixes like ”, Santiago de Cuba, Cuba” for cleaner UI labels.
Map Component Integration
Here’s how the MapComponent integrates Mapbox GL JS:
Initialization
src/app/features/tabs/map/map.component.ts
import mapboxgl from 'mapbox-gl' ;
import { environment } from '@/environments/environment' ;
export class MapComponent implements AfterViewInit {
@ ViewChild ( 'mapEl' , { static: true }) mapEl !: ElementRef < HTMLDivElement >;
map !: mapboxgl . Map ;
geolocate !: mapboxgl . GeolocateControl ;
ngAfterViewInit () {
this . initMap ();
}
private initMap () {
mapboxgl . accessToken = environment . mapbox . accessToken ;
this . map = new mapboxgl . Map ({
container: this . mapEl . nativeElement ,
style: 'mapbox://styles/mapbox/streets-v12' ,
center: [ - 75.83 , 20.02 ], // Santiago de Cuba
zoom: 12 ,
attributionControl: true ,
dragPan: true ,
scrollZoom: true ,
doubleClickZoom: true
});
// Add controls
this . map . addControl (
new mapboxgl . ScaleControl ({ maxWidth: 100 , unit: 'metric' })
);
this . geolocate = new mapboxgl . GeolocateControl ({
positionOptions: { enableHighAccuracy: true },
trackUserLocation: true ,
showUserHeading: true
});
this . map . addControl ( this . geolocate , 'top-left' );
// Wait for map to load
this . map . on ( 'load' , () => {
this . isLoaded . set ( true );
this . map . resize ();
});
}
}
Adding Markers
private originMarker ?: mapboxgl . Marker ;
private destMarker ?: mapboxgl . Marker ;
// Add origin marker (blue, non-draggable)
private placeOrMoveOrigin ( point : LatLng , title = 'Tu ubicación' ) {
if ( ! this . originMarker ) {
this . originMarker = new mapboxgl . Marker ({ color: '#0A84FF' })
. setLngLat ([ point . lng , point . lat ])
. setPopup ( new mapboxgl . Popup ({ offset: 10 }). setText ( title ))
. addTo ( this . map );
} else {
this . originMarker . setLngLat ([ point . lng , point . lat ]);
}
}
// Add destination marker (red, draggable)
private ensureDestPicker ( lng : number , lat : number , title = 'Destino' ) {
if ( ! this . destMarker ) {
this . destMarker = new mapboxgl . Marker ({
color: '#FF3B30' ,
draggable: true
})
. setLngLat ([ lng , lat ])
. setPopup ( new mapboxgl . Popup ({ offset: 10 }). setText ( title ))
. addTo ( this . map );
// Handle drag events
this . destMarker . on ( 'dragend' , () => {
const ll = this . destMarker ! . getLngLat ();
this . facade . recalcRouteAfterAdjust ({ lat: ll . lat , lng: ll . lng });
});
} else {
this . destMarker . setLngLat ([ lng , lat ]);
}
}
Drawing Routes
// Effect: Draw route when routeSummary changes
effect (() => {
if ( ! this . isLoaded ()) return ;
const rs = this . store . routeSummary ();
const srcId = 'trip-route' ;
const layerId = 'trip-route-line' ;
// Clear previous route
if ( ! rs ?. geometry ) {
if ( this . map . getLayer ( layerId )) this . map . removeLayer ( layerId );
if ( this . map . getSource ( srcId )) this . map . removeSource ( srcId );
return ;
}
// Add new route
if ( this . map . getLayer ( layerId )) this . map . removeLayer ( layerId );
if ( this . map . getSource ( srcId )) this . map . removeSource ( srcId );
this . map . addSource ( srcId , {
type: 'geojson' ,
data: rs . geometry
});
this . map . addLayer ({
id: layerId ,
type: 'line' ,
source: srcId ,
layout: {
'line-join' : 'round' ,
'line-cap' : 'round'
},
paint: {
'line-width' : 5 ,
'line-color' : '#FF3B30'
}
});
// Fit map to route bounds
const coords = rs . geometry . features [ 0 ]. geometry . coordinates ;
const bounds = coords . reduce (
( b , c ) => b . extend ( c as [ number , number ]),
new mapboxgl . LngLatBounds ( coords [ 0 ], coords [ 0 ])
);
this . map . fitBounds ( bounds , { padding: 60 , duration: 600 });
});
Handling User Interactions
// Click on map to set destination
this . map . on ( 'click' , ( e ) => {
const { lng , lat } = e . lngLat ;
this . ensureDestPicker ( lng , lat );
this . facade . recalcRouteAfterAdjust ({ lat , lng });
});
// Center on user location
centerOnMe () {
const o = this . store . originPoint ();
if ( ! o ) return ;
this . map . easeTo ({
center: [ o . lng , o . lat ],
zoom: 15 ,
duration: 600
});
}
Geolocation
The app uses mapboxgl.GeolocateControl for user location:
this . geolocate = new mapboxgl . GeolocateControl ({
positionOptions: {
enableHighAccuracy: true ,
timeout: 10000 ,
maximumAge: 10000
},
trackUserLocation: true , // Continuous tracking
showUserHeading: true // Show compass direction
});
this . map . addControl ( this . geolocate , 'top-left' );
// Listen for location updates
this . geolocate . on ( 'geolocate' , ( e : GeolocationPosition ) => {
const { longitude , latitude , accuracy } = e . coords ;
console . log ( `Location: ${ latitude } , ${ longitude } ` );
console . log ( `Accuracy: ${ accuracy } m` );
});
// Handle errors
this . geolocate . on ( 'error' , ( err ) => {
console . error ( 'Geolocation error:' , err );
});
On some Android devices, the geolocation provider may return 403 errors. Always handle these gracefully and provide manual location selection as a fallback.
Location Provider
For programmatic location access (without UI), use LocationProvider:
import { LocationProvider } from '@/app/core/providers/location.provider' ;
export class MyComponent {
private loc = inject ( LocationProvider );
async getCurrentLocation () {
try {
// One-time location
const sample : GeoSample = await this . loc . getOnceBalanced ();
console . log ( sample . lat , sample . lng , sample . accuracy );
} catch ( err ) {
console . error ( 'Failed to get location:' , err );
}
}
watchLocation () {
// Continuous tracking
this . loc . watchBalanced ()
. pipe ( sampleTime ( 4000 )) // Throttle to every 4s
. subscribe ( sample => {
console . log ( 'Updated location:' , sample );
});
}
}
GeoSample Interface:
interface GeoSample {
lat : number ;
lng : number ;
accuracy ?: number ; // Meters
altitude ?: number ; // Meters
heading ?: number ; // Degrees (0-360)
speed ?: number ; // m/s
timestamp : number ; // Epoch milliseconds
}
Santiago de Cuba Clamping
The app restricts searches to Santiago de Cuba province by default:
private SCU_PROV_BBOX : BBox = [ - 76.30 , 19.60 , - 75.10 , 20.60 ];
private SCU_PROV_CENTER : LatLng = { lng: - 75.82 , lat: 20.02 };
private isInSantiagoProvince ( f : MbFeature ): boolean {
const ctx = f . context ?? [];
const tail = f . place_name . toLowerCase ();
const texts = ctx . map ( c => c . text . toLowerCase ());
return tail . includes ( 'santiago de cuba' ) ||
texts . some ( t => t . includes ( 'santiago de cuba' ));
}
// Usage
this . placesService . search ( 'Parque' , {
clampToSantiagoProvince: true // Default: true
}). subscribe ( results => {
// Only results in Santiago de Cuba province
});
Set clampToSantiagoProvince: false if you need to search outside the province (e.g., for airport or inter-city trips).
Map Styling
The app uses mapbox://styles/mapbox/streets-v12 by default. To customize:
this . map = new mapboxgl . Map ({
container: this . mapEl . nativeElement ,
style: 'mapbox://styles/your-username/your-style-id' ,
// ... other options
});
Available Mapbox styles:
streets-v12 - Standard street map
outdoors-v12 - Topographic style
light-v11 - Light monochrome
dark-v11 - Dark monochrome
satellite-v9 - Satellite imagery
satellite-streets-v12 - Satellite + labels
Best Practices
Always resize map after layout changes Call map.resize() after showing/hiding modals or changing container size: setTimeout (() => this . map . resize (), 0 );
Clean up on destroy Remove the map instance to prevent memory leaks: ngOnDestroy () {
try {
if ( this . map ) this . map . remove ();
} catch {}
}
Validate coordinates Always validate lat/lng before passing to Mapbox: const isValid = ( n : any ) => typeof n === 'number' && isFinite ( n );
if ( ! isValid ( lat ) || ! isValid ( lng )) {
throw new Error ( 'Invalid coordinates' );
}
Mapbox GL JS requires mapboxgl.accessToken to be set before creating a map. Store the token in environment.ts.