Realtime center, polyline and markers

Hi…I have a robot which streams GPS data every second to the Syncfusion Map in our Flutter app. The map is initially zoomed out (zoom level 1) to show the whole world. When the app begins to receive data from the robots, the map then:


  1. Centers on the robot’s location with a zoom level of 15
  2. Displays the continuous path of the robot using a polyline
  3. Adds markers in specific locations along the robots path


While all this is successfully working, I’m 100% sure my code is wrong and inefficient. To be honest, I actually had to turn off MapZoomPanBehavior features because after a zoom button is pressed, the map would reset back to it's original zoom when new data streams in. In addition, openstreetmap.org would throw a 400 error. Which makes me wonder if my code is retrieving a new map from openstreetmap.org every second as data from my streams in. This seems inefficient.

Can anyone who’s more familiar with this library take a look at my code and offer suggestions to improve it? If I’m doing something incorrect, please explain why my approach is incorrect so I can learn. Thanks in advance!


This is the code that calls my map (MapSyncfusionWidget). The viewModel.currentDeviceLocation, viewModel.hits and viewModel.polyline parameters are all data Streams 

return SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Container(
color: Colors.grey.shade200,
height: 200.h,
width: double.infinity,
child: MapSyncfusionWidget(
center: viewModel.currentDeviceLocation,
markers: viewModel.hits,
polyline: viewModel.polyline,
title: 'map',
),
),
],
),
);


This is my map widget

class PolylineModel {
PolylineModel(this.points, this.color, this.width);
List points = const [];
final Color color;
final double width;
}


class MapSyncfusionWidget extends StatefulWidget {
const MapSyncfusionWidget({
Key? key,
required this.center,
this.markers = const [],
this.polyline = const [],
required this.title,
}) : super(key: key);


final MapLatLng? center;
final List markers;
final List polyline;
final String title;


@override
_MapSyncfusionWidgetState createState() => _MapSyncfusionWidgetState();
}


class _MapSyncfusionWidgetState extends State
with SingleTickerProviderStateMixin {
late MapTileLayerController _layerController;
late MapZoomPanBehavior _zoomPanBehavior;
late List markers;
late List polyline;
late List polylines;
late AnimationController animationController;
late Animation animation;
late MapLatLng center;


@override
void initState() {
print('MapSyncfusionWidgetState, initState');


_layerController = MapTileLayerController();
center = const MapLatLng(0, 0);
polyline = widget.polyline;
polylines = [
PolylineModel(
polyline,
const Color.fromARGB(255, 4, 155, 248),
7,
),
];
markers = [];


_zoomPanBehavior = MapZoomPanBehavior(
enableDoubleTapZooming: true,
enableMouseWheelZooming: true,
enablePanning: true,
enablePinching: true,
zoomLevel: 2,
minZoomLevel: 2,
maxZoomLevel: 18,
showToolbar: true,
);


animationController = AnimationController(
duration: const Duration(seconds: 1),
vsync: this,
);


animation = CurvedAnimation(
parent: animationController,
curve: Curves.bounceIn,
);
animationController.forward(from: 0);
super.initState();
}


@override
void dispose() {
_layerController.dispose();
animationController.dispose();
super.dispose();
}


// getInfo creates the markers and also centers the map. I don't like having
// to create markers and recenter in the same function but I couldn't figure
// out how to split them apart
Future> getInfo() async {
print('MapSyncfusion, getInfo');
markers.clear();
for (int i = 0; i < widget.markers.length; i++) {
markers.add(widget.markers[i]);
_layerController.insertMarker(i);
}


// center the map
if (widget.center != null) {
print('MapSyncfusion, getInfo, centering');
setState(() {
center = widget.center!;
});
_zoomPanBehavior
..focalLatLng = center
..zoomLevel = 15;
}


print('MapSyncfusion, getInfo, length: ${markers.length}');
return markers;
}


@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: BoxDecoration(
border: Border.all(
color: Colors.grey,
width: 0.5,
),
color: Colors.grey.shade200,
),
child: FutureBuilder(
future: getInfo(),
builder: (context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
return SfMaps(
layers: [
MapTileLayer(
controller: _layerController,
urlTemplate:
'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
initialMarkersCount: markers.length,
zoomPanBehavior: _zoomPanBehavior,
markerBuilder: (BuildContext context, int index) {
return MapMarker(
latitude: markers[index].latitude,
longitude: markers[index].longitude,
child: Icon(
Icons.circle,
color: const Color.fromARGB(255, 237, 52, 39),
size: 15.r,
),
);
},
sublayers: [
polyline.isEmpty
? MapPolylineLayer(polylines: [].toSet())
: MapPolylineLayer(
polylines: List.generate(
polylines.length,
(int index) => MapPolyline(
width: polylines[index].width,
points: polylines[index].points,
color: polylines[index].color,
strokeCap: StrokeCap.round,
),
).toSet(),
animation: animation,
)
],
)
],
);
}
return CircularProgressIndicator();
},
),
),
floatingActionButtonLocation: FloatingActionButtonLocation.miniEndFloat,
floatingActionButton: Padding(
padding: EdgeInsets.all(1.r),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
GestureDetector(
onTap: () {
print('Zooming in');
if (_zoomPanBehavior.zoomLevel <
_zoomPanBehavior.maxZoomLevel) {
_zoomPanBehavior.zoomLevel = _zoomPanBehavior.zoomLevel + 2;
}
},
child: Container(
height: 32.r,
width: 32.r,
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
border: Border.all(
width: 0.5,
color: Colors.grey,
),
borderRadius: BorderRadius.circular(5),
color: Colors.grey.shade100),
child: Align(
alignment: Alignment.center,
child: Icon(
CupertinoIcons.zoom_in,
color: Colors.black,
size: 20.r,
),
),
),
),
verticalSpaceTiny,
GestureDetector(
onTap: () {
print('Zooming out');
if (_zoomPanBehavior.zoomLevel >
_zoomPanBehavior.minZoomLevel) {
_zoomPanBehavior.zoomLevel = _zoomPanBehavior.zoomLevel - 2;
}
},
child: Container(
height: 32.r,
width: 32.r,
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
border: Border.all(
width: 0.5,
color: Colors.grey,
),
borderRadius: BorderRadius.circular(5),
color: Colors.grey.shade100),
child: Align(
alignment: Alignment.center,
child: Icon(
CupertinoIcons.zoom_out,
color: Colors.black,
size: 20.r,
),
),
),
),
],
),
),
);
}
}

4 Replies

PB Pieter Bergmans October 19, 2023 03:46 PM UTC

Any suggestions on how to improve this?  I would be grateful for any help.



HK Hariharasudhan Kanagaraj Syncfusion Team October 19, 2023 04:07 PM UTC

Hi Pieter,


We are currently working on the sample and tried to render the polyline based on the coordinates fetched from the data per second to achieve the possible solution and to improve it from our end. But facing some difficulties to achieve the expected output and working on resolving it. Will share further details within one business day on October 23, 2023 without fail. We appreciate your patience until then.


Regards,
Hari Hara Sudhan. K.



PB Pieter Bergmans October 19, 2023 04:16 PM UTC

If you need more time beyond Oct 23, just let me know. I have some flexibility.

Also, I just wanted to let you know that the support we have received from Syncfusion has been great and worth the price of the license! 




HK Hariharasudhan Kanagaraj Syncfusion Team October 24, 2023 02:43 AM UTC

Hi Pieter,


We have prepared a sample by initializing fixed random coordinates to locate the marker at specific coordinates fetched from the given data per second and to render the polyline to track the location on the map. In this sample, we used the StreamBuilder widget instead of the FutureBuilder widget because the StreamBuilder widget can be used when you want to listen to a stream of data and update your UI continuously as data arrives. Whereas the FutureBuilder widget can be used when you have a one-time asynchronous operation that will complete and return a result as a future. If you need more details regarding the StreamBuilder Widget, refer to the shared StreamBuilder link below.


StreamBuilder - https://api.flutter.dev/flutter/widgets/StreamBuilder-class.html.


When the data has been fetched from the stream per second, we used the MapLayerController.updateMarker method to update the marker to its latest position based on the coordinates fetched from the stream per second. We passed the latest MapLatLng positions to the focalLatLng property of the MapZoomPanBehavior to always display the marker at the center position of the screen. As we are rendering the markers and polyline of the map with the fixed random coordinates, we have given the zoomLevel property of the MapZoomPanBehavior as 5 to zoom in the chart when the data has been fetched, and you can change the value of the zoomLevel property according to your needs. Additionally, we disabled the showToolbar property to prevent the chart from resetting to its initial zoom level when the reset button is pressed. The polyline is drawn using the MapPolyLineLayer of the sublayers property, and we passed the list of MapLatLng positions which is fetched from the stream per second, to the polyline property to render the continuous path of polyline based on the given coordinates.


It is important to note that currently, we do not have caching support to store all the fetched data at once from OpenStreetMap. Therefore, it retrieves the new map from the given URL each time the data stream is fetched.


Kindly refer the code snippet below :

import 'dart:async';

import 'package:flutter/material.dart';

import 'package:syncfusion_flutter_maps/maps.dart';

 

void main(List<String> args) {

  runApp(

    const MaterialApp(

      home: MapWithLiveUpdate(),

    ),

  );

}

 

class MapWithLiveUpdate extends StatefulWidget {

  const MapWithLiveUpdate({super.key});

 

  @override

  State<MapWithLiveUpdate> createState() => _MapWithLiveUpdateState();

}

 

class _MapWithLiveUpdateState extends State<MapWithLiveUpdate> {

  // Stream controller to send and listen for new map coordinates

  final StreamController<MapLatLng> _coordinatesController =

      StreamController<MapLatLng>();

 

  // ValueNotifier to track the count of updated markers

  final ValueNotifier<int> _count = ValueNotifier(0);

  final List<MapLatLng> _markerCoordinates = [];

 

  late MapTileLayerController _layerController;

  late MapZoomPanBehavior _zoomPanBehavior;

  late List<MapLatLng> _coordinates;

  Timer? _updateTimer;

 

  @override

  void initState() {

    // Fixed coordinates to render the polyline and markers.

    _coordinates = [

      const MapLatLng(05.837450799474553, 12.494009507787759),

      const MapLatLng(10.912268914713209, 20.321936346975782),

      const MapLatLng(18.811086460285722, 28.506198025032404),

      const MapLatLng(27.208891636068572, 39.994394639613709),

      const MapLatLng(35.176128777995501, 46.483779812137705),

      const MapLatLng(41.458446486325997, 57.61816437431272),

      const MapLatLng(49.738894484374001, 63.515906073527056),

      const MapLatLng(52.921366815107089, 76.882714748654724),

      const MapLatLng(56.11125613802929, 83.83480943313398),

      const MapLatLng(59.19125613802929, 91.183480943313398),

      const MapLatLng(63.4125613802929, 99.254480943313398),

      const MapLatLng(68.76525613802929, 106.183480943313398),

      const MapLatLng(71.11125613802929, 114.183480943313398),

      const MapLatLng(77.11125613802929, 121.729865220655334),

      const MapLatLng(56.476721225917373, 83.729865220655334),

      const MapLatLng(53.088424513676259, 48.794470119740136),

      const MapLatLng(41.057202589851997, 36.416095282499036),

      const MapLatLng(36.999987420584958, 25.925479234319987),

      const MapLatLng(28.304248895199535, 12.364505723575554),

      const MapLatLng(21.675017938173767, 01.234901231387528),

      const MapLatLng(14.25177859898335, -15.726198431906337),

      const MapLatLng(11.120440708369841, -13.876541652939586),

      const MapLatLng(-186.024392938179147, -86.630984605408567),

      const MapLatLng(-167.22747195836628, -46.39789805755607),

      const MapLatLng(-158.060237664749806, -26.414615383402484),

      const MapLatLng(-130.174804315140904, -16.810405178325944),

      const MapLatLng(-114.043132765661198, -07.445818589786818),

    ];

 

    // Initialize map behavior

    _zoomPanBehavior = MapZoomPanBehavior(

      // Disabled tool bar to prevent resetting the chart.

      showToolbar: false,

      zoomLevel: 0,

    );

 

    _layerController = MapTileLayerController();

    super.initState();

  }

 

  // Function to start updating coordinates and return a stream

  Stream<MapLatLng> _startUpdatingCoordinates() {

    int index = 0;

    _updateTimer = Timer.periodic(

      const Duration(seconds: 1),

      (Timer timer) {

        if (index < _coordinates.length) {

          _markerCoordinates.add(_coordinates[index]);

          _coordinatesController.add(_markerCoordinates[index]);

          // Update the markers to the recent position.

          _layerController.updateMarkers([0]);

          // Uncomment the below line to render multiple markers at specified coordinates.

          // _layerController.insertMarker(index);

          index++;

        } else {

          timer.cancel();

        }

        _count.value++;

      },

    );

    return _coordinatesController.stream;

  }

 

  @override

  Widget build(BuildContext context) {

    return Scaffold(

      body: StreamBuilder(

        // Listens to the stream of coordinates

        stream: _startUpdatingCoordinates(),

        builder: (BuildContext context, AsyncSnapshot snapshot) {

          if (snapshot.hasData) {

            // Rebuild the UI when _count changes

            return ValueListenableBuilder(

              valueListenable: _count,

              builder: (BuildContext context, int value, Widget? child) {

                _zoomPanBehavior.focalLatLng = _markerCoordinates.last;

                _zoomPanBehavior.zoomLevel = 5;

                return SfMaps(

                  layers: [

                    MapTileLayer(

                      controller: _layerController,

                      initialMarkersCount: _markerCoordinates.length,

                      zoomPanBehavior: _zoomPanBehavior,

                      sublayers: [

                        MapPolylineLayer(

                          polylines: <MapPolyline>{

                            MapPolyline(

                              points: _markerCoordinates,

                              color: Colors.blue,

                              width: 5,

                            ),

                          },

                        ),

                      ],

                      markerBuilder: (BuildContext context, int index) {

                        return MapMarker(

                          iconColor: Colors.red,

                          iconType: MapIconType.circle,

                          size: const Size(12, 12),

                          latitude: _markerCoordinates.last.latitude,

                          longitude: _markerCoordinates.last.longitude,

                        );

                      },

                      urlTemplate:

                          'https://tile.openstreetmap.org/{z}/{x}/{y}.png',

                    ),

                  ],

                );

              },

            );

          } else {

            return const SfMaps(

              layers: [

                MapTileLayer(

                  urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',

                ),

              ],

            );

          }

        },

      ),

    );

  }

 

  @override

  void dispose() {

    _coordinatesController.close();

    _updateTimer!.cancel();

    _markerCoordinates.clear();

    super.dispose();

  }

}


Attached the recording below for your reference and you can modify the above shared code snippet according to your needs. If you have further queries, please get back to us.


Regards,
Hari Hara Sudhan. K.


Attachment: 184968_585e1e0c.zip

Loader.
Up arrow icon