Implementing Flutter Maps With OSM

Implementing Flutter Maps With OSM

Introduction

With the surge in availability of access to the internet, we all have moved from remembering neighborhood locations to marking them on Maps.

So, as the users are getting quite comfortable with using maps on their devices, the necessity of embedding a map on an application has also increased. Maps/Geo-location can be used for a number of applications.

The first option that we think of when starting with maps is Google maps. But, Google maps are insanely expensive. So, a business/startup just starting out might not have the budget to get started with it.

Apart from the cost there is a second factor which we need to take into consideration while setting up a map, which is complexity. Setting up a map can be quite complex.

So, there are two main difficulties while setting up a map: Complexity & Expense

Let us see how we can tackle both Complexity & Expense of setting up a map with Flutter & OSM.

In this article, we will look into how to set up a completely working & functional map with Flutter using Flutter_Map & OSM.

Objectives

  • Set up a map.
  • Show custom pointers.
  • Handle clustering & de-clustering of pointers.
  • Show custom popup on click of any of the pointers.

Setting Up

First of all, let's add the following dependencies in our pubspec.yaml:

      latlong: ^0.6.1
      flutter_map: any
      flutter_map_marker_cluster: any

Now let's look into the flutter_map plugin.

Flutter Map

The flutter_map widget provided to us by the flutter_map plugin has three main parameters:

     FlutterMap(
         mapController: ...,
         options: ...,
         layers: ...
      ),
  • mapController: It will be used to handle the map programmatically. It can be used to move the map, zoom in & zoom out, etc.
  • options: This is the parameter which we can use to pass properties to the layers/map.
  • layers: This is the parameter where we pass a list of layers which we want to be built on the screen. It can be considered as the Stack widget, so layers will be laid out in the order they are mentioned.

Now, let's start looking into our objectives one by one:

Set a map

To set our map, we need to add a layer in the layers parameter of the FlutterMap widget.

This is going to be a TileLayerOptions layer, which is used to create a tile-based layer.

A tile is a square bitmap graphic displayed in a grid arrangement to show a map. It can also be described as an image bound to a specific geographical position.

     FlutterMap(
       mapController: ...,
       options: ...,
       layers: [
         TileLayerOptions(
           minZoom: 1,
           maxZoom: 18,
           backgroundColor: Colors.black,
           urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
           subdomains: ['a', 'b', 'c'],
         ),
       ]
    ),

Let's look at its parameters:

  • urlTemplate- Here we will mention the URL of our map tile provider (in this case, OSM).
  • minZoom/maxZoom- We can pass the minimum & maximum zoom we want to provide.
  • minNativeZoom/maxNativeZoom- These are the minimum native zoom (usually 1) & maximum native zoom (usually between 18-20), which is the zoom level provided by our map tile provider.
  • errorImage- If, because of any issue, our map is not able to get rendered, then this image will be shown.

That’s it! We have our map up & running.

Screenshot 2021-03-06 at 6.12.25 PM.png

Show custom pointers

First, let us have our list of points we want to build our markers on.

    List<LatLng> _latLngList = [
       LatLng(13, 77.5),
       LatLng(13.02, 77.51),
       LatLng(13.05, 77.53),
         ...
       LatLng(13.155, 77.54),
       LatLng(13.159, 77.55),
       LatLng(13.17, 77.55),
      ];

Let's create our list of custom markers for these points.

To build our custom marker, we will be using the Marker widget. Here, we will have a pin_drop icon as our marker. We can build anything as our marker using the builder of the Marker.

      List<Marker> _markers = _latLngList
       .map((point) => Marker(
             point: point,
             width: 60,
             height: 60,
             builder: (context) => Icon(
               Icons.pin_drop,
               size: 60,
               color: Colors.blueAccent,
             ),
           ))
       .toList();

To have this list of markers on top of our map, we will mention this just after our TileLayerOptions layer using the MarkerLayerOptions layer. This can also be achieved by using the MarkerClusterLayerOptions layer, but that is mainly used to cluster the markers.

      FlutterMap(
       mapController: ...,
       options: ...,
       layers: [
         TileLayerOptions(
          ...
         ),
         MarkerLayerOptions(markers: _markers)
        ]
       ),

We need to pass our markers list to the markers parameter and we will have our custom markers ready.

Screenshot 2021-03-06 at 6.14.42 PM.png

Handling clustering & de-clustering of pointers

To handle the clustering & de-clustering of the markers, we will replace the MarkerLayerOptions with MarkerClusterLayerOptions, pass some parameters & a builder function to the MarkerClusterLayerOptions for it to handle the clustering/de-clustering for us.

     FlutterMap(
       mapController: ...,
       options: ...,
       layers: [
         TileLayerOptions(
          ...
          ),
         MarkerClusterLayerOptions(
           maxClusterRadius: 190,
           disableClusteringAtZoom: 16,
           size: Size(50, 50),
           fitBoundsOptions: FitBoundsOptions(
             padding: EdgeInsets.all(50),
           ),
           markers: _markers,
           polygonOptions: PolygonOptions(
               borderColor: Colors.blueAccent,
               color: Colors.black12,
               borderStrokeWidth: 3),
           builder: (context, markers) {
             return Container(
               alignment: Alignment.center,
               decoration:
                   BoxDecoration(color: Colors.orange, shape: BoxShape.circle),
               child: Text('${markers.length}'),
             );
           },
         ),
       ],
     ),

Let's look at the parameters we just added and how they help us out:

  • maxClusterRadius- The clusters formed will cover these many pixels.
  • disableClusteringAtZoom- This parameter will be used for de-clustering markers. The markers will not be clustered from this zoom level (defaults to 20).
  • size- This is the size of our cluster (not the markers).
  • fitBoundsOptions- Option for fit bounds (padding, maxZoom & zoom).
  • polygonOptions- When a cluster is tapped, it gets de-clustered, and on de-clustering, they make a polygon based on the number of markers in the cluster. This property is an option for that polygon drawn (the showPolygon property needs to be true for this).
  • builder- We can pass any custom widget we want to be displayed as our
    marker-cluster. Also, this builder receives a ‘markers’ parameter, which includes the number of markers included in this cluster.

Now, all our markers will be clustered & de`clustered as we zoom in & out.

Screenshot 2021-03-06 at 6.16.00 PM.png

Custom popups on click of any pointer

To have a popup on click of any of our markers, we will need to add an extra parameter to our MarkerClusterLayerOptions, this parameter is the popupOptions parameter.

     FlutterMap(
       mapController: _mapController,
       options: MapOptions(
        ...
       ),
       layers: [
         TileLayerOptions(
           ...
         ),
         MarkerClusterLayerOptions(
           ...
           popupOptions: PopupOptions(
               popupSnap: PopupSnap.top,
               popupController: _popupController,
               popupBuilder: (_, marker) => Container(
                    alignment: Alignment.center,
                      height: 50,
                      width: 50,
                      decoration: BoxDecoration(
                          color: Colors.black, shape: BoxShape.rectangle),
                      child: Text(
                        'Popup',
                        style: TextStyle(color: Colors.white),
                      ),
                   )),
         ),
       ],
     ),

Let's understand the popupOptions that we used to have or popup:

  • popupController: Will be used to control the popup programmatically (mainly to close the popup, when any other part of the map is tapped).
  • popupSnap: It's an enum to tell the position of the popup.
  • popupBuilder: Here, we can return any custom widget we want to be displayed as our popup.

Now, on tap of any of our markers, we will have a popup.

Screenshot 2021-03-06 at 6.20.58 PM.png

**Apart from these objectives there are two more parameters of FlutterMap, mapController & options, that are also needed to have our map running.

Let's put values into them:

     FlutterMap(
       mapController: _mapController,
       options: MapOptions(
         center: _latLngList[0], 
         bounds: LatLngBounds.fromPoints(_latLngList),
         plugins: [
           MarkerClusterPlugin(),          ],
         onTap: (_) => _popupController
             .hidePopup(), 
       ),
       layers: ... ),
  • mapController: It will be used to handle the map programmatically. It can be used to move the map, zoom in & zoom out, etc.
  • options: This is the parameter which we can use to pass properties to the map.

Some of the important properties here are -

- `center`- Mention the center of the map, it will be the center when the map starts.
- `bounds`- It can take a list of geo-coordinates and show them all when the map starts. If both bounds & center are provided, then bounds will take preference.
- `zoom`- It is used to mention the initial zoom.
- `swPanBoundary`/`nePanBoundary`- These are two geocoordinate points, which can be used to have interactivity constraints.
- Callbacks such as `onTap`/`onLongPress`/`onPositionChanged` can also be used.

We have now successfully added a functional map in our Flutter project using OSM and Flutter_Maps.

Here’s the full project link.

Thanks for reading.