# Social Event Voting

Last updated:

<!-- -->

October 18, 2026

<!-- -->

\| 6 minutes read

<br />

<br />

This guide will teach you how to confirm social reports submitted by other users.

## How it works[​](#how-it-works "Direct link to 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](/docs/flutter/assets/images/example_flutter_social_event_voting1-b30355c885ffd35fe07105c47f026527.png)

**Initial screen**

![Social report panel](/docs/flutter/assets/images/example_flutter_social_event_voting2-37cdb0f4a3370bc1ff86708976ebb9d9.png)

**Social report panel**

### UI and Map Integration[​](#ui-and-map-integration "Direct link to 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.dart[View on Github](https://github.com/magiclane/magiclane-maps-sdk-examples-for-flutter/tree/main/social_event_voting/lib/main.dart?ref_type=heads#L14)

```dart
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 "Direct link to Social Event Panel")

social\_event\_panel.dart[View on Github](https://github.com/magiclane/magiclane-maps-sdk-examples-for-flutter/tree/main/social_event_voting/lib/social_event_panel.dart?ref_type=heads#L10)

```dart
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[​](#calculate-a-route-containing-a-report "Direct link to 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.<br /><!-- -->Since reports may disappear from the map over time, these methods ensure the reliability and consistency of the example.

main.dart[View on Github](https://github.com/magiclane/magiclane-maps-sdk-examples-for-flutter/tree/main/social_event_voting/lib/main.dart?ref_type=heads#L258)

```dart
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;
}

```
