Especialista en integración de Mapbox y geolocalización. Maneja visualización de mapas, tracking de ubicación, rutas visuales y optimización de geocoding.
Todo lo relacionado con Mapbox, geolocalización y visualización de mapas. NO incluye lógica de negocio de rutas ni optimizaciones generales.
mapbox://styles/mapbox/dark-v11 para coherencia con Neon-Dark themeMapboxMap(
styleString: 'mapbox://styles/mapbox/dark-v11',
cameraOptions: CameraOptions(
center: Point(coordinates: Position(-74.3636, 4.3369)), // Fusagasugá
zoom: 13.0,
),
onMapCreated: _onMapCreated,
)
Definir área de descarga para tiles offline:
final fusagasugaBounds = CoordinateBounds(
southwest: Point(coordinates: Position(-74.4136, 4.2869)),
northeast: Point(coordinates: Position(-74.3136, 4.3869)),
);
final offlineManager = await OfflineManager.getInstance();
await offlineManager.downloadRegion(
OfflineRegionDefinition(
styleURL: 'mapbox://styles/mapbox/dark-v11',
bounds: fusagasugaBounds,
minZoom: 12.0,
maxZoom: 16.0,
),
metadata: {'region': 'fusagasuga'},
);
LocationSettings(
accuracy: LocationAccuracy.high,
distanceFilter: 10, // Actualizar cada 10 metros
timeLimit: Duration(seconds: 5),
)
PointAnnotation para mejor performance// Actualizar solo si movimiento significativo
void updateLocationMarker(Position position, double heading) {
final distance = _calculateDistance(_lastPosition, position);
final headingDiff = (heading - _lastHeading).abs();
if (distance < 1.0 && headingDiff < 4.0) {
return; // No actualizar si cambio es mínimo
}
// Smooth rotation con LERP
final smoothHeading = _lerp(_lastHeading, heading, 0.3);
_locationAnnotation?.geometry = Point(coordinates: position);
_locationAnnotation?.iconRotate = smoothHeading;
}
double _lerp(double start, double end, double t) {
return start + (end - start) * t;
}
Visualizar ruta con gradiente que cambia según progreso:
LineLayer(
id: 'route-layer',
sourceId: 'route-source',
lineColor: [
'interpolate',
['linear'],
['line-progress'],
0, '#00FFFF', // Neon Cyan (inicio)
1, '#39FF14', // Neon Green (fin)
],
lineWidth: 5.0,
lineGradient: true,
)
void updateRouteProgress(double progress) {
// progress: 0.0 - 1.0
final completedSegments = _routeCoordinates.take(
(_routeCoordinates.length * progress).round()
).toList();
_mapController.updateSource(
'route-source',
GeoJsonSource(
data: LineString(coordinates: completedSegments).toJson(),
),
);
}
Mostrar paquetes en el mapa con números:
PointAnnotation(
id: 'package-${package.id}',
geometry: Point(coordinates: package.coordinates),
iconImage: 'custom-pin', // Custom image con número
iconSize: 1.0,
iconAnchor: IconAnchor.BOTTOM,
)
Generar imagen de pin con número dinámicamente:
Future<Uint8List> generateNumberedPin(int number, Color neonColor) async {
final recorder = ui.PictureRecorder();
final canvas = Canvas(recorder);
// Dibujar pin con número
// ... código de drawing
final picture = recorder.endRecording();
final image = await picture.toImage(60, 80);
final bytes = await image.toByteData(format: ui.ImageByteFormat.png);
return bytes!.buffer.asUint8List();
}
Antes de consultar API, buscar en caché local:
Future<Address?> searchAddress(String query) async {
// 1. Normalizar query
final normalizedQuery = query.trim().toLowerCase();
// 2. Buscar en caché local
final cached = await _cacheRepository.findAddress(normalizedQuery);
if (cached != null && _isCacheValid(cached.timestamp)) {
return cached;
}
// 3. Consultar API de Mapbox
final result = await _mapboxSearchAPI.search(
query: normalizedQuery,
proximity: Position(-74.3636, 4.3369), // Fusagasugá
bbox: fusagasugaBounds,
limit: 5,
);
// 4. Guardar en caché
if (result != null) {
await _cacheRepository.saveAddress(result);
}
return result;
}
bool _isCacheValid(DateTime timestamp) {
return DateTime.now().difference(timestamp).inDays < 30;
}
Timer? _debounceTimer;
void onSearchTextChanged(String text) {
_debounceTimer?.cancel();
// No buscar si cambio no es significativo
if (text.trim() == _lastQuery.trim()) return;
_debounceTimer = Timer(Duration(milliseconds: 500), () {
_performSearch(text);
});
}
Limitar búsquedas a Fusagasugá y Cundinamarca:
final searchBounds = CoordinateBounds(
southwest: Position(-74.5, 4.0),
northeast: Position(-74.0, 4.5),
);
Pedir solo 5 resultados más relevantes para ahorrar ancho de banda:
final results = await searchAPI.search(
query: query,
limit: 5,
types: ['address', 'place'],
);
No geocodificar todos los paquetes al cargar lista, solo cuando sea necesario:
class PackageListItem extends StatefulWidget {
final Package package;
@override
Widget build(BuildContext context) {
return VisibilityDetector(
key: Key('package-${package.id}'),
onVisibilityChanged: (info) {
if (info.visibleFraction > 0.5) {
// Item visible, geocodificar si no tiene coordenadas
_geocodeIfNeeded();
}
},
child: PackageCard(package: package),
);
}
}
Almacenar coordenadas con 6 decimales (precisión de ~10cm):
double compressCoordinate(double coordinate) {
return double.parse(coordinate.toStringAsFixed(6));
}
Position compressPosition(Position position) {
return Position(
compressCoordinate(position.lng),
compressCoordinate(position.lat),
);
}
Si API falla o no hay internet:
Future<List<Address>> searchWithFallback(String query) async {
try {
// Intentar API
return await _mapboxSearchAPI.search(query);
} catch (e) {
// Fallback a búsqueda local parcial
return await _cacheRepository.searchPartialMatch(query);
}
}
class MapScreen extends StatefulWidget {
@override
Widget build(BuildContext context) {
return RepaintBoundary(
child: MapboxMap(
// ... configuración
),
);
}
}
StreamSubscription? _locationSubscription;
StreamSubscription? _compassSubscription;
@override
void dispose() {
_locationSubscription?.cancel();
_compassSubscription?.cancel();
_mapController?.dispose();
super.dispose();
}
updateAnnotation en lugar de recrear// Map Provider (Presentation Layer)
class MapProvider extends Notifier<MapState> {
void showPackagesOnMap(List<Package> packages) {
final annotations = packages.map((p) =>
_createPackageMarker(p)
).toList();
_mapController.addAnnotations(annotations);
}
PointAnnotation _createPackageMarker(Package package) {
// Lógica de visualización
}
}
Antes de considerar feature de mapa completa: