Skip to main content
GuidesAPI ReferenceExamplesFAQ

Draw Roadblock

Estimated reading time: 4 minutes

This example showcases how to build a Flutter app featuring an interactive map. Users can draw paths on map, whose coordinates can be used to confirm a roadblock.

How it works

The example app includes the following features:

  • Display a map.
  • Draw a roadblock in one or multiple steps.
  • Display preview paths on map.
  • Confirm a persistent Roadblock based on the drawn path.
hello_maphello_map
Initial map screenStarted drawing preview path
hello_maphello_map
Confirmed previous path (in red)Confirmed roadblock on map

UI and Map Integration

The code below builds a user interface featuring an interactive GemMap and an app bar with action buttons to start drawing mode, confirm the drawn path, cancel drawing, and confirm a roadblock.

const projectApiToken = String.fromEnvironment('GEM_TOKEN');

void main() async {
await GemKit.initialize(appAuthorization: projectApiToken);
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({super.key});


Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Draw Roadblock',
home: MyHomePage(),
);
}
}

class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});


State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
late GemMapController _mapController;

// The coordinate where the preview currently ends (acts as a cursor for preview path)
UserRoadblockPathPreviewCoordinate? previewCursor;

// List of permanent coordinates (user confirmed)
List<Coordinates> permanentCoords = [];
// List of preview (temporary) coordinates
List<Coordinates> previewCoordsList = [];

// Draw mode
bool drawMode = false;

/// Debouncer in order to calculate preview only after movement is stopped
Timer? _debounce;


void initState() {
super.initState();
}


void dispose() {
GemKit.release();
super.dispose();
}


Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: true,
foregroundColor: Colors.white,
title: const Text(
"Draw Roadblock",
style: TextStyle(color: Colors.white),
),
backgroundColor: Colors.deepPurple[900],
actions: [
// Add temporary coordinates to permanent
if (drawMode)
IconButton(
icon: Icon(Icons.add),
onPressed: () {
permanentCoords = [...permanentCoords, ...previewCoordsList];
previewCoordsList = [];
_redrawPath();
},
),
// Draw mode activate button
if (!drawMode)
IconButton(
icon: Icon(Icons.draw),
onPressed: () {
setState(() {
drawMode = true;
permanentCoords = [];
previewCursor = null;
previewCoordsList = [];
});
_handlePreviewPathUpdate(allowRecursive: false);
},
),
// Add roadblock by permanent coordinates and reset the draw mode and coordinates lists
if (drawMode)
IconButton(
icon: Icon(Icons.check),
onPressed: () {
final roadblockResult = TrafficService.addPersistentRoadblockByCoordinates(
coords: permanentCoords,
startTime: DateTime.now(),
expireTime: DateTime.now().add(const Duration(days: 1)),
transportMode: RouteTransportMode.car,
id: DateTime.now().toIso8601String(), // Unique identifier
);

setState(() {
drawMode = false;
permanentCoords = [];
previewCursor = null;
previewCoordsList = [];
});

// In case of error, show snackbar
if (roadblockResult.second != GemError.success) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Error ${roadblockResult.second} when adding roadblock.',
),
backgroundColor: Colors.red,
duration: Duration(seconds: 3),
),
);
}

// Reset the coordinates
permanentCoords = [];
previewCoordsList = [];
_redrawPath();
},
),
// Cancel draw mode
if (drawMode)
IconButton(
icon: Icon(Icons.cancel),
onPressed: () {
setState(() {
drawMode = false;
permanentCoords = [];
previewCoordsList = [];
});
permanentCoords = [];
previewCoordsList = [];
previewCursor = null;
_redrawPath();
},
),
],
),
body: Stack(
children: [
GemMap(
appAuthorization: projectApiToken,
onMapCreated: _onMapCreated,
),
// Mark the center of the viewport
const Center(
child: Icon(
Icons.add,
size: 32,
color: Colors.black,
),
),
],
),
);
}

/// Called when the map is created. Registers the move callback for draw mode.
void _onMapCreated(GemMapController controller) async {
_mapController = controller;

_mapController.registerMoveCallback((_, __) {
if (!drawMode) return;

_debounce?.cancel();
_debounce = Timer(const Duration(milliseconds: 100), () {
_handlePreviewPathUpdate(allowRecursive: true);
});
});
}

/// Handles updating the preview path when the map moves.
void _handlePreviewPathUpdate({required bool allowRecursive}) {
final centerScreen = Point<int>(
_mapController.viewport.width ~/ 2,
_mapController.viewport.height ~/ 2,
);
final centerCoord = _mapController.transformScreenToWgs(centerScreen);

// On first call, initialize permanentCoords with the center coordinate
if (permanentCoords.isEmpty) {
permanentCoords.add(centerCoord);
}

// If previewCursor is null, set it and return
if (previewCursor == null) {
previewCursor = UserRoadblockPathPreviewCoordinate.fromCoordinates(centerCoord);
return;
}

// Compute route coordinates from previous coordinates to new coordinates
final (from, to, error) = TrafficService.getPersistentRoadblockPathPreview(
from: previewCursor!,
to: centerCoord,
transportMode: RouteTransportMode.car,
);

if (error != GemError.success) {
_resetPreviewPathOnError(allowRecursive);
return;
}

_updatePreviewPath(from, to);
}

/// Resets the preview path and optionally retries if an error occurs.
void _resetPreviewPathOnError(bool allowRecursive) {
previewCoordsList = [];
previewCursor = UserRoadblockPathPreviewCoordinate.fromCoordinates(permanentCoords.last);
_redrawPath();
if (allowRecursive) {
_handlePreviewPathUpdate(allowRecursive: false);
}
}

/// Updates the preview path and redraws the map.
void _updatePreviewPath(List<Coordinates> from, UserRoadblockPathPreviewCoordinate to) {
previewCoordsList = [...previewCoordsList, ...from];
previewCursor = to;
_redrawPath();
}

/// Redraws the permanent and preview paths on the map.
void _redrawPath() {
final previewPath = Path.fromCoordinates(previewCoordsList);
final permanentPath = Path.fromCoordinates(permanentCoords);
_mapController.preferences.paths.clear();
_mapController.preferences.paths.add(previewPath, colorInner: Colors.green);
_mapController.preferences.paths.add(permanentPath, colorInner: Colors.red);
}
}

Flutter Examples

Maps SDK for Flutter Examples can be downloaded or cloned with Git.