Simple User Interface

Introduction

This Example shows how to display simple mapview and perform basic map actions:

  1. Display MapView Widget
  2. User Interface on MapView
    1. Customize MapView Size
    2. MapView UI Settings
    3. MapView GestureDetector
Android snapshot iOS snapshot

For all code examples, refer to Flutter Maps SDK Code Examples

MapUiPage view source

1import 'dart:math';
2
3import 'package:flutter/material.dart';
4import 'package:collection/collection.dart';
5import 'package:nb_maps_flutter/nb_maps_flutter.dart';
6
7import 'main.dart';
8import 'page.dart';
9
10final LatLngBounds sydneyBounds = LatLngBounds(
11  southwest: const LatLng(-34.022631, 150.620685),
12  northeast: const LatLng(-33.571835, 151.325952),
13);
14
15class MapUiPage extends ExamplePage {
16  MapUiPage() : super(const Icon(Icons.map), 'User interface');
17
18  
19  Widget build(BuildContext context) {
20    return const MapUiBody();
21  }
22}
23
24class MapUiBody extends StatefulWidget {
25  const MapUiBody();
26
27  
28  State<StatefulWidget> createState() => MapUiBodyState();
29}
30
31class MapUiBodyState extends State<MapUiBody> {
32  MapUiBodyState();
33
34  static final CameraPosition _kInitialPosition = const CameraPosition(
35    target: LatLng(-33.852, 151.211),
36    zoom: 11.0,
37  );
38
39  NextbillionMapController? mapController;
40  CameraPosition _position = _kInitialPosition;
41  bool _isMoving = false;
42  bool _compassEnabled = true;
43  bool _mapExpanded = true;
44  CameraTargetBounds _cameraTargetBounds = CameraTargetBounds.unbounded;
45  MinMaxZoomPreference _minMaxZoomPreference = MinMaxZoomPreference.unbounded;
46  int _styleStringIndex = 0;
47  // Style string can a reference to a local or remote resources.
48  // On Android the raw JSON can also be passed via a styleString, on iOS this is not supported.
49  List<String> _styleStrings = [
50    "https://api.nextbillion.io/maps/streets/style.json",
51    "https://api.nextbillion.io/maps/streets/style.json",
52    "https://api.nextbillion.io/maps/streets/style.json"
53  ];
54  List<String> _styleStringLabels = [
55    "NBMAP_STREETS",
56    "SATELLITE",
57    "LOCAL_ASSET"
58  ];
59  bool _rotateGesturesEnabled = true;
60  bool _scrollGesturesEnabled = true;
61  bool? _doubleClickToZoomEnabled;
62  bool _tiltGesturesEnabled = true;
63  bool _zoomGesturesEnabled = true;
64  bool _myLocationEnabled = true;
65  bool _telemetryEnabled = true;
66  MyLocationTrackingMode _myLocationTrackingMode = MyLocationTrackingMode.None;
67  List<Object>? _featureQueryFilter;
68  Fill? _selectedFill;
69
70  
71  void initState() {
72    super.initState();
73  }
74
75  void _onMapChanged() {
76    setState(() {
77      _extractMapInfo();
78    });
79  }
80
81  void _extractMapInfo() {
82    final position = mapController!.cameraPosition;
83    if (position != null) _position = position;
84    _isMoving = mapController!.isCameraMoving;
85  }
86
87  
88  void dispose() {
89    mapController?.removeListener(_onMapChanged);
90    super.dispose();
91  }
92
93  Widget _myLocationTrackingModeCycler() {
94    final MyLocationTrackingMode nextType = MyLocationTrackingMode.values[
95        (_myLocationTrackingMode.index + 1) %
96            MyLocationTrackingMode.values.length];
97    return TextButton(
98      child: Text('change to $nextType'),
99      onPressed: () {
100        setState(() {
101          _myLocationTrackingMode = nextType;
102        });
103      },
104    );
105  }
106
107  Widget _queryFilterToggler() {
108    return TextButton(
109      child: Text(
110          'filter zoo on click ${_featureQueryFilter == null ? 'disabled' : 'enabled'}'),
111      onPressed: () {
112        setState(() {
113          if (_featureQueryFilter == null) {
114            _featureQueryFilter = [
115              "==",
116              ["get", "type"],
117              "zoo"
118            ];
119          } else {
120            _featureQueryFilter = null;
121          }
122        });
123      },
124    );
125  }
126
127  Widget _mapSizeToggler() {
128    return TextButton(
129      child: Text('${_mapExpanded ? 'shrink' : 'expand'} map'),
130      onPressed: () {
131        setState(() {
132          _mapExpanded = !_mapExpanded;
133        });
134      },
135    );
136  }
137
138  Widget _compassToggler() {
139    return TextButton(
140      child: Text('${_compassEnabled ? 'disable' : 'enable'} compasss'),
141      onPressed: () {
142        setState(() {
143          _compassEnabled = !_compassEnabled;
144        });
145      },
146    );
147  }
148
149  Widget _latLngBoundsToggler() {
150    return TextButton(
151      child: Text(
152        _cameraTargetBounds.bounds == null
153            ? 'bound camera target'
154            : 'release camera target',
155      ),
156      onPressed: () {
157        setState(() {
158          _cameraTargetBounds = _cameraTargetBounds.bounds == null
159              ? CameraTargetBounds(sydneyBounds)
160              : CameraTargetBounds.unbounded;
161        });
162      },
163    );
164  }
165
166  Widget _zoomBoundsToggler() {
167    return TextButton(
168      child: Text(_minMaxZoomPreference.minZoom == null
169          ? 'bound zoom'
170          : 'release zoom'),
171      onPressed: () {
172        setState(() {
173          _minMaxZoomPreference = _minMaxZoomPreference.minZoom == null
174              ? const MinMaxZoomPreference(12.0, 16.0)
175              : MinMaxZoomPreference.unbounded;
176        });
177      },
178    );
179  }
180
181  Widget _setStyleToSatellite() {
182    return TextButton(
183      child: Text(
184          'change map style to ${_styleStringLabels[(_styleStringIndex + 1) % _styleStringLabels.length]}'),
185      onPressed: () {
186        setState(() {
187          _styleStringIndex = (_styleStringIndex + 1) % _styleStrings.length;
188        });
189      },
190    );
191  }
192
193  Widget _rotateToggler() {
194    return TextButton(
195      child: Text('${_rotateGesturesEnabled ? 'disable' : 'enable'} rotate'),
196      onPressed: () {
197        setState(() {
198          _rotateGesturesEnabled = !_rotateGesturesEnabled;
199        });
200      },
201    );
202  }
203
204  Widget _scrollToggler() {
205    return TextButton(
206      child: Text('${_scrollGesturesEnabled ? 'disable' : 'enable'} scroll'),
207      onPressed: () {
208        setState(() {
209          _scrollGesturesEnabled = !_scrollGesturesEnabled;
210        });
211      },
212    );
213  }
214
215  Widget _doubleClickToZoomToggler() {
216    final stateInfo = _doubleClickToZoomEnabled == null
217        ? "disable"
218        : _doubleClickToZoomEnabled!
219            ? 'unset'
220            : 'enable';
221    return TextButton(
222      child: Text('$stateInfo double click to zoom'),
223      onPressed: () {
224        setState(() {
225          if (_doubleClickToZoomEnabled == null) {
226            _doubleClickToZoomEnabled = false;
227          } else if (!_doubleClickToZoomEnabled!) {
228            _doubleClickToZoomEnabled = true;
229          } else {
230            _doubleClickToZoomEnabled = null;
231          }
232        });
233      },
234    );
235  }
236
237  Widget _tiltToggler() {
238    return TextButton(
239      child: Text('${_tiltGesturesEnabled ? 'disable' : 'enable'} tilt'),
240      onPressed: () {
241        setState(() {
242          _tiltGesturesEnabled = !_tiltGesturesEnabled;
243        });
244      },
245    );
246  }
247
248  Widget _zoomToggler() {
249    return TextButton(
250      child: Text('${_zoomGesturesEnabled ? 'disable' : 'enable'} zoom'),
251      onPressed: () {
252        setState(() {
253          _zoomGesturesEnabled = !_zoomGesturesEnabled;
254        });
255      },
256    );
257  }
258
259  Widget _myLocationToggler() {
260    return TextButton(
261      child: Text('${_myLocationEnabled ? 'disable' : 'enable'} my location'),
262      onPressed: () {
263        setState(() {
264          _myLocationEnabled = !_myLocationEnabled;
265        });
266      },
267    );
268  }
269
270  Widget _telemetryToggler() {
271    return TextButton(
272      child: Text('${_telemetryEnabled ? 'disable' : 'enable'} telemetry'),
273      onPressed: () {
274        setState(() {
275          _telemetryEnabled = !_telemetryEnabled;
276        });
277        mapController?.setTelemetryEnabled(_telemetryEnabled);
278      },
279    );
280  }
281
282  Widget _visibleRegionGetter() {
283    return TextButton(
284      child: Text('get currently visible region'),
285      onPressed: () async {
286        var result = await mapController!.getVisibleRegion();
287        ScaffoldMessenger.of(context).showSnackBar(SnackBar(
288          content: Text(
289              "SW: ${result.southwest.toString()} NE: ${result.northeast.toString()}"),
290        ));
291      },
292    );
293  }
294
295  _clearFill() {
296    if (_selectedFill != null) {
297      mapController!.removeFill(_selectedFill!);
298      setState(() {
299        _selectedFill = null;
300      });
301    }
302  }
303
304  _drawFill(List<dynamic> features) async {
305    Map<String, dynamic>? feature =
306        features.firstWhereOrNull((f) => f['geometry']['type'] == 'Polygon');
307
308    if (feature != null) {
309      List<List<LatLng>> geometry = feature['geometry']['coordinates']
310          .map(
311              (ll) => ll.map((l) => LatLng(l[1], l[0])).toList().cast<LatLng>())
312          .toList()
313          .cast<List<LatLng>>();
314      Fill fill = await mapController!.addFill(FillOptions(
315        geometry: geometry,
316        fillColor: "#FF0000",
317        fillOutlineColor: "#FF0000",
318        fillOpacity: 0.6,
319      ));
320      setState(() {
321        _selectedFill = fill;
322      });
323    }
324  }
325
326  
327  Widget build(BuildContext context) {
328    final NBMap nbMap = NBMap(
329      onMapCreated: onMapCreated,
330      initialCameraPosition: _kInitialPosition,
331      trackCameraPosition: true,
332      compassEnabled: _compassEnabled,
333      cameraTargetBounds: _cameraTargetBounds,
334      minMaxZoomPreference: _minMaxZoomPreference,
335      styleString: _styleStrings[_styleStringIndex],
336      rotateGesturesEnabled: _rotateGesturesEnabled,
337      scrollGesturesEnabled: _scrollGesturesEnabled,
338      tiltGesturesEnabled: _tiltGesturesEnabled,
339      zoomGesturesEnabled: _zoomGesturesEnabled,
340      doubleClickZoomEnabled: _doubleClickToZoomEnabled,
341      myLocationEnabled: _myLocationEnabled,
342      myLocationTrackingMode: _myLocationTrackingMode,
343      myLocationRenderMode: MyLocationRenderMode.GPS,
344      onMapClick: (point, latLng) async {
345        print(
346            "Map click: ${point.x},${point.y}   ${latLng.latitude}/${latLng.longitude}");
347        print("Filter $_featureQueryFilter");
348        List features = await mapController!
349            .queryRenderedFeatures(point, ["landuse"], _featureQueryFilter);
350        print('# features: ${features.length}');
351        _clearFill();
352        if (features.isEmpty && _featureQueryFilter != null) {
353          ScaffoldMessenger.of(context).showSnackBar(SnackBar(
354              content: Text('QueryRenderedFeatures: No features found!')));
355        } else if (features.isNotEmpty) {
356          _drawFill(features);
357        }
358      },
359      onMapLongClick: (point, latLng) async {
360        print(
361            "Map long press: ${point.x},${point.y}   ${latLng.latitude}/${latLng.longitude}");
362        Point convertedPoint = await mapController!.toScreenLocation(latLng);
363        LatLng convertedLatLng = await mapController!.toLatLng(point);
364        print(
365            "Map long press converted: ${convertedPoint.x},${convertedPoint.y}   ${convertedLatLng.latitude}/${convertedLatLng.longitude}");
366        double metersPerPixel =
367            await mapController!.getMetersPerPixelAtLatitude(latLng.latitude);
368
369        print(
370            "Map long press The distance measured in meters at latitude ${latLng.latitude} is $metersPerPixel m");
371
372        List features =
373            await mapController!.queryRenderedFeatures(point, [], null);
374        if (features.length > 0) {
375          print(features[0]);
376        }
377      },
378      onCameraTrackingDismissed: () {
379        this.setState(() {
380          _myLocationTrackingMode = MyLocationTrackingMode.None;
381        });
382      },
383      onUserLocationUpdated: (location) {
384        print(
385            "new location: ${location.position}, alt.: ${location.altitude}, bearing: ${location.bearing}, speed: ${location.speed}, horiz. accuracy: ${location.horizontalAccuracy}, vert. accuracy: ${location.verticalAccuracy}");
386      },
387    );
388
389    final List<Widget> listViewChildren = <Widget>[];
390
391    if (mapController != null) {
392      listViewChildren.addAll(
393        <Widget>[
394          Text('camera bearing: ${_position.bearing}'),
395          Text('camera target: ${_position.target.latitude.toStringAsFixed(4)},'
396              '${_position.target.longitude.toStringAsFixed(4)}'),
397          Text('camera zoom: ${_position.zoom}'),
398          Text('camera tilt: ${_position.tilt}'),
399          Text(_isMoving ? '(Camera moving)' : '(Camera idle)'),
400          _mapSizeToggler(),
401          _queryFilterToggler(),
402          _compassToggler(),
403          _myLocationTrackingModeCycler(),
404          _latLngBoundsToggler(),
405          _setStyleToSatellite(),
406          _zoomBoundsToggler(),
407          _rotateToggler(),
408          _scrollToggler(),
409          _doubleClickToZoomToggler(),
410          _tiltToggler(),
411          _zoomToggler(),
412          _myLocationToggler(),
413          _telemetryToggler(),
414          _visibleRegionGetter(),
415        ],
416      );
417    }
418    return Column(
419      mainAxisSize: MainAxisSize.min,
420      children: [
421        Center(
422          child: SizedBox(
423            width: _mapExpanded ? null : 300.0,
424            height: 200.0,
425            child: nbMap,
426          ),
427        ),
428        Expanded(
429          child: ListView(
430            children: listViewChildren,
431          ),
432        )
433      ],
434    );
435  }
436
437  void onMapCreated(NextbillionMapController controller) {
438    mapController = controller;
439    mapController!.addListener(_onMapChanged);
440    _extractMapInfo();
441
442    mapController!.getTelemetryEnabled().then((isEnabled) => setState(() {
443          _telemetryEnabled = isEnabled;
444        }));
445  }
446}

Code summary

The above code snippet defines a MapUiBody widget that displays a map using the NBMap widget from the nb_maps_flutter package. The widget allows users to interact with the map and control various aspects of its user interface.

  1. Display MapView Widget:

    1. The NBMap widget is used to display the map on the screen.
    2. The onMapCreated callback is used to get access to the NextbillionMapController, which allows interaction with the map.
    3. The initialCameraPosition property sets the initial position of the camera when the map is first displayed.
  2. User Interface on MapView: The MapUiBody widget provides several buttons and toggles that allow users to interact with the map and customize its behavior:

    1. Camera Information: Displays the current camera bearing, target latitude and longitude, zoom level, and tilt. Also shows whether the camera is moving or idle.
    2. Map Size Toggler: Toggles between expanding and shrinking the map view.
    3. Query Filter Toggler: Toggles the filter for querying features on the map.
    4. Compass Toggler: Toggles the compass display on the map.
    5. My Location Tracking Mode Cycler: Cycles through different tracking modes for user location.
    6. Camera Target Bounds Toggler: Toggles between bounding and releasing the camera target within specific bounds.
    7. Set Style To Satellite: Cycles through different map styles (NBMAP_STREETS, SATELLITE, LOCAL_ASSET).
    8. Zoom Bounds Toggler: Toggles between bounding and releasing the zoom level within specific bounds.
    9. Rotate Toggler: Toggles the rotate gesture on the map.
    10. Scroll Toggler: Toggles the scroll gesture on the map.
    11. Double Click To Zoom Toggler: Toggles the double click to zoom gesture on the map.
    12. Tilt Toggler: Toggles the tilt gesture on the map.
    13. Zoom Toggler: Toggles the zoom gesture on the map.
    14. My Location Toggler: Toggles the display of the user's location on the map.
    15. Telemetry Toggler: Toggles the telemetry tracking on the map.
    16. Visible Region Getter: Retrieves the currently visible region on the map and displays it in a snackbar.

To use this widget in your Flutter app, make sure you have installed the nb_maps_flutter package and added the necessary dependencies to your pubspec.yaml file.

Overall, this code provides a comprehensive example of how to use the NBMap widget and interact with the map's user interface in a Flutter app using the NextbillionMap SDK.

DIDN'T FIND WHAT YOU LOOKING FOR?