Social Event Voting
This guide will teach you how to confirm social reports submitted by other users.
How it works
The example app demonstrates the following key features:
- Route calculation.
- Simulated navigation along a route.
- Detection of nearby social reports.
- Confirmation of a social report.

Initial screen

Social report panel
UI and Map Integration
The following code builds the UI with a GemMap widget and an app bar containing a compute route button. Once the route is calculated, simulated navigation starts along the route with a social event reported. As the simulated position approaches the social report, a bottom panel appears with a confirm report button.
main.dartView on Github
const projectApiToken = String.fromEnvironment("YOUR_API_TOKEN_HERE");
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Social Event Voting',
debugShowCheckedModeBanner: false,
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
late GemMapController _mapController;
AlarmService? _alarmService;
AlarmListener? _alarmListener;
bool _areRoutesBuilt = false;
bool _isSimulationActive = false;
// The closest alarm and with its associated distance and image
OverlayItemPosition? _closestOverlayItem;
TaskHandler? _navigationHandler;
@override
void dispose() {
GemKit.release();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.deepPurple[900],
title: const Text(
'Social Event Voting',
style: TextStyle(color: Colors.white),
),
actions: [
if (_navigationHandler == null && !_areRoutesBuilt)
IconButton(
onPressed: () => _onRouteButtonPressed(),
icon: const Icon(Icons.route, color: Colors.white),
),
if (_navigationHandler == null &&
!_isSimulationActive &&
_areRoutesBuilt)
IconButton(
onPressed: _startSimulation,
icon: const Icon(Icons.play_arrow, color: Colors.white),
),
if (_isSimulationActive && _navigationHandler != null)
IconButton(
onPressed: _stopSimulation,
icon: const Icon(Icons.stop, color: Colors.white),
),
],
),
body: Stack(
children: [
GemMap(
key: ValueKey("GemMap"),
onMapCreated: _onMapCreated,
appAuthorization: projectApiToken,
),
if (_closestOverlayItem != null)
Padding(
padding: const EdgeInsets.all(8.0),
child: Align(
alignment: Alignment.bottomCenter,
child: SocialEventPanel(
overlayItem: _closestOverlayItem!.overlayItem,
onClose: () {
setState(() {
_closestOverlayItem = null;
});
},
),
),
),
],
),
);
}
// The callback for when map is ready to use.
void _onMapCreated(GemMapController controller) async {
// Save controller for further usage.
_mapController = controller;
_mapController.registerOnTouch((point) {
_mapController.setCursorScreenPosition(point);
});
_registerSocialEventListener();
}
Future<void> _onBuildRouteButtonPressed(BuildContext context) async {
Route? routeWithReport = await _getRouteWithReport();
if (routeWithReport != null) {
_mapController.preferences.routes.add(
routeWithReport,
true,
// Do not show intermediate waypoints as they may cover the report displayed on the map
routeRenderSettings: RouteRenderSettings(
options: {
RouteRenderOptions.showTraffic,
RouteRenderOptions.showHighlights,
},
),
);
} else {
// ignore: use_build_context_synchronously
_showSnackBar(context, message: "No route available");
}
}
Future<void> _onRouteButtonPressed() async {
_showSnackBar(
context,
message: "Route is being calculated, please wait...",
duration: const Duration(seconds: 2),
);
await _onBuildRouteButtonPressed(context);
if (_mapController.preferences.routes.mainRoute == null) {
// ignore: use_build_context_synchronously
_showSnackBar(context, message: "No main route available");
return;
}
_mapController.centerOnRoute(_mapController.preferences.routes.mainRoute!);
setState(() {
_areRoutesBuilt = true;
});
}
Future<void> _startSimulation() async {
_navigationHandler = NavigationService.startSimulation(
_mapController.preferences.routes.mainRoute!,
onNavigationInstruction: (instruction, events) {},
onDestinationReached: (landmark) => _stopSimulation(),
);
setState(() {
_isSimulationActive = true;
});
_mapController.startFollowingPosition();
}
void _stopSimulation() {
// Cancel the navigation.
if (_navigationHandler != null) {
NavigationService.cancelNavigation(_navigationHandler);
}
setState(() {
_navigationHandler = null;
_isSimulationActive = false;
});
_navigationHandler = null;
_cancelRoute();
}
// Method for removing the routes from display,
void _cancelRoute() {
// Remove the routes from map.
_mapController.preferences.routes.clear();
_areRoutesBuilt = false;
}
void _registerSocialEventListener() {
_alarmListener = AlarmListener(
onOverlayItemAlarmsUpdated: () {
// The overlay item alarm list containing the overlay items that are to be intercepted
OverlayItemAlarmsList overlayItemAlarms =
_alarmService!.overlayItemAlarms;
// The overlay items and their distance from the reference position
// Sorted ascending by distance from the current position
List<OverlayItemPosition> items = overlayItemAlarms.items;
if (items.isEmpty) {
return;
}
// The closest overlay item and its associated distance
OverlayItemPosition closestOverlayItem = items.first;
setState(() {
_closestOverlayItem = closestOverlayItem;
});
},
// When the overlay item alarms are passed over
onOverlayItemAlarmsPassedOver: () {
setState(() {
_closestOverlayItem = null;
});
},
);
// Set the alarms service with the listener
_alarmService = AlarmService(_alarmListener!);
_alarmService!.alarmDistance = 400;
_alarmService!.monitorWithoutRoute = true;
// Add social reports id in order to receive desired notifications via alarm listener
_alarmService!.overlays.add(CommonOverlayId.socialReports.id);
setState(() {});
}
void _showSnackBar(
BuildContext context, {
required String message,
Duration duration = const Duration(hours: 1),
}) {
final snackBar = SnackBar(content: Text(message), duration: duration);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
}
Social Event Panel
social_event_panel.dartView on Github
class SocialEventPanel extends StatelessWidget {
final OverlayItem overlayItem;
final VoidCallback onClose;
const SocialEventPanel({
super.key,
required this.overlayItem,
required this.onClose,
});
@override
Widget build(BuildContext context) {
return Container(
width: MediaQuery.of(context).size.width - 40,
color: Colors.white,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Image.memory(
overlayItem.img
.getRenderableImage(size: Size(50, 50))!
.bytes,
gaplessPlayback: true,
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(overlayItem.name),
Text(
"Upvotes: ${overlayItem.previewDataJson["parameters"]["score"]}",
),
],
),
],
),
IconButton(onPressed: onClose, icon: Icon(Icons.close)),
],
),
TextButton(
onPressed: () => _onVoteButtonPressed(context),
child: Text("Confirm report"),
),
],
),
);
}
void _onVoteButtonPressed(BuildContext context) {
SocialOverlay.confirmReport(
overlayItem,
onComplete: (err) {
_showSnackBar(
context,
message: "Confirm report status: $err",
duration: Duration(seconds: 3),
);
},
);
}
void _showSnackBar(
BuildContext context, {
required String message,
Duration duration = const Duration(hours: 1),
}) {
final snackBar = SnackBar(content: Text(message), duration: duration);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
}
Calculate a route containing a report
The following methods are used to locate a report and compute a short route that includes the corresponding social report.
Since reports may disappear from the map over time, these methods ensure the reliability and consistency of the example.
main.dartView on Github
Future<Landmark?> _getReportFromMap() async {
final area = RectangleGeographicArea(
topLeft: Coordinates.fromLatLong(52.59310690528571, 7.524257524882292),
bottomRight: Coordinates.fromLatLong(
48.544623829072655,
12.815748995947535,
),
);
Completer<Landmark?> completer = Completer<Landmark?>();
// Allow to search only for social reports
final searchPreferences = SearchPreferences(
searchAddresses: false,
searchMapPOIs: false,
);
searchPreferences.overlays.add(CommonOverlayId.socialReports.id);
SearchService.searchInArea(
area,
Coordinates.fromLatLong(51.02858483954893, 10.29982567727901),
(err, results) {
if (err == GemError.success) {
completer.complete(results.first);
} else {
completer.complete(null);
}
},
preferences: searchPreferences,
);
return completer.future;
}
// Get a route which contains a social report as an intermediate waypoint
// Used for demo, should not be used in a production application
Future<Route?> _getRouteWithReport() async {
// Create an initial route with a social report
// This route will stretch accross Germany, containing a social report as an intermediate waypoint
// It will be cropped to a few hundred meters around the social report
final initalStart = Landmark.withCoordinates(
Coordinates.fromLatLong(51.48345483353617, 6.851883736746337),
);
final initalEnd = Landmark.withCoordinates(
Coordinates.fromLatLong(49.01867442442069, 12.061988113314802),
);
final report = await _getReportFromMap();
if (report == null) {
return null;
}
final initialRoute = await _calculateRoute([initalStart, report, initalEnd]);
if (initialRoute == null) {
return null;
}
// Crop the route to a few hundred meters around the social report
final reportDistanceInInitialRoute = initialRoute.getDistanceOnRoute(
report.coordinates,
true,
);
final newStartCoords = initialRoute.getCoordinateOnRoute(
reportDistanceInInitialRoute - 600,
);
final newEndCoords = initialRoute.getCoordinateOnRoute(
reportDistanceInInitialRoute + 200,
);
final newStart = Landmark.withCoordinates(newStartCoords);
final newEnd = Landmark.withCoordinates(newEndCoords);
// Make a route containing both directions as the report can be on the opposite direction
return await _calculateRoute([newStart, report, newEnd, report, newStart]);
}
Future<Route?> _calculateRoute(List<Landmark> waypoints) async {
Completer<Route?> croppedRouteCompleter = Completer<Route?>();
RoutingService.calculateRoute(waypoints, RoutePreferences(), (err, routes) {
if (err == GemError.success) {
croppedRouteCompleter.complete(routes.first);
} else {
croppedRouteCompleter.complete(null);
}
});
return await croppedRouteCompleter.future;
}