My little world of sharing

Jan24

Leafletjs interactive map and clustering with ASP.NET MVC 5

Leafletjs interactive map and clustering with ASP.NET MVC 5

Leaflet JS is a very popular lightweight js library for displaying data in a map and creating an interactive map. It is quite flexible in terms to adding custom layers, styling controls, and integrating events. In this post, I will try to demonstrate how it can be integrated with your MVC application instead using Google map API. I will also integrate the clustering since, in my application, there is a possibility to have multiple records with the same latitude and longitude. I guess it will be easier to understand if you have some basic knowledge on map marker clustering. You can read more about map clustering at https://developers.google.com/maps/documentation/javascript/marker-clustering .

The finished product will be like the image below –

map_1

Setup Model

In the MVC application, I have created a model which holds the GPS coordinates along with other fields. Just to make it simple, I have omitted unnecessary codes to make it simple. Here is my model.

namespace my_app.Models
{
    public class Location
    {
        [Key]
        public int LocId { get; set; }

      [Display(Name = "Description"), MaxLength(8000, ErrorMessage = "Title cannot be more than 8000 characters long."), Required(ErrorMessage ="You must enter a description in {0} field")]
        [AllowHtml]
        [DataType(DataType.MultilineText)]
        public string Description { get; set; }       

        [Display(Name = "Park/Location Name"), MaxLength(120, ErrorMessage = "Park or Location name is too long"), Required]
        public string Name { get; set; }
        
        [DisplayFormat(DataFormatString = "{0:N10}", ApplyFormatInEditMode = true), Required]
        public decimal Longitude { get; set; }

        [DisplayFormat(DataFormatString = "{0:N10}", ApplyFormatInEditMode = true), Required]
        public decimal Latitude { get; set; }
                
       //other codes omitted
       
    }
}
Optional Title
In my application I have another model which links with this model. Basically, I am using a relational database. But just to make this demonstration simple, I am just using the relevant model.

Setup Controller

In my controller, I am using a get request to fetch data in JSON format so that I can bind them in the Leaflet map. Here is the sample code of the controller. The Map action result produces the JSON data which is accessible through GET request.

// GET: Locations
        [HttpGet]
        public ActionResult map()
        {

var q = (from a in db.Locations 
select new { a.Title, a.Description, a.Latitude, a.Longitude, a.Name, a.alertType.IconUrl }) 
.OrderBy(a=>a.Name); 
// return PartialView("_map", q.ToList()); 
return Json(q, JsonRequestBehavior.AllowGet); 
}

map_2

I have used Linq query in the above example however you can use simply EF instead like below.

// GET: Locations
        [HttpGet]
        public ActionResult map()
        {
           
            var q= db.Alerts.Where(a => a.Published == "Yes").ToList();
//return the result in json
            return Json(q, JsonRequestBehavior.AllowGet);
        }

 

Setup View

Again, I have omitted irrelevant codes from the view. I am interested in the map view section of the page where the Map will be loaded through ajax call. The basic HTML code has been used in the bootstrap tab. In the below code block, you might have noticed an overlay block which has been used to show loading indicator to end user while fetching the data from the remote server. It will hide through CSS class switcher once data has been successfully loaded.

 

 <div class="tab-pane fade" id="mapview">

            <br />
           
            <div class="loadingOverlay">
                <div class="loading-spinner">
                    <img src="~/img/googleballs.gif" title="Loading" />
                    <span class="loading-text">Loading...</span>
                </div>
            </div>
            <!-- This is the div that will contain the Google Map -->
            <div id="lmap" style="width:100%; height:600px"></div>
        </div>

 

In the header section, let’s call the all associated plugins. Remember you must require jQuery and Bootstrap js libraries and style sheets to work with this demo.

<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyDaJlkDXFC39432fsdH9PQo7j3C9RmUAFkXAEQY" async defer></script>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.0.2/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.0.2/dist/leaflet.js"></script>

<script src='~/Scripts/Leaflet.GoogleMutant.js'></script>
<script src="~/Scripts/leaflet.markercluster-src.js"></script>
<link href="~/Content/MarkerCluster.css" rel="stylesheet" />
<link href="~/Content/MarkerCluster.Default.css" rel="stylesheet" />

I have included Google API key since I am using Google layer instead of  OpenStreet map in this example. Same output of the json data.

json_data

The idea is to load the map only when a user clicks on the map tab. I don’t want to load all the content on initial page load since it can increase the payload and user might have to wait longer to load the page. To give better user experience, the page will load data on demand means when user requests for the additional data.

$(function(){

        var latlng = L.latLng(-30.81881, 116.16596);
        var map = L.map('lmap', { center: latlng, zoom: 6 });
        var lcontrol = new L.control.layers();
        
    	//clear map first
        clearMap();
    	//resize the map
        map.invalidateSize(true);
    	//load the map once all layers cleared
        loadMap();
        //reset the map size on dom ready
        map.invalidateSize(true);
        function loadMap() {

            $(".loadingOverlay").show();

            var roadMutant = L.gridLayer.googleMutant({
                type: 'roadmap' // valid values are 'roadmap', 'satellite', 'terrain' and 'hybrid'
            }).addTo(map);
            var satMutant = L.gridLayer.googleMutant({
                maxZoom: 24,
                type: 'satellite'
            });

            var terrainMutant = L.gridLayer.googleMutant({
                maxZoom: 24,
                type: 'terrain'
            });

            var hybridMutant = L.gridLayer.googleMutant({
                maxZoom: 24,
                type: 'hybrid'
            });

            //add the control on the map
            
           lcontrol= L.control.layers({
                Roadmap: roadMutant,
                Aerial: satMutant,
                Terrain: terrainMutant,
                Hybrid: hybridMutant //,Styles: styleMutant

            }, {}, {
                collapsed: false
            }).addTo(map);

        
        var markers = L.markerClusterGroup({ chunkedLoading: true, spiderfyOnMaxZoom: true, maxClusterRadius: 80 });

        
        //clear markers and remove all layers
        markers.clearLayers();
        
        

        $.ajax({
            type: "GET",
            url: '@Url.Action("map", "Home")',
           // data: {'atype': st},
            dataType: 'json',
            contentType: 'application/x-www-form-urlencoded',
            success: function (data) {

                $.each(data, function (i, item) {
                    var img = (item.IconUrl).replace("~", "");
                    var dpawIcon = L.icon({ iconUrl: img, iconSize: [42, 42] });

                    var marker = L.marker(L.latLng(item.Latitude, item.Longitude), { icon: dpawIcon }, { title: item.Name });
                    var content = "<div class='infoDiv'><h3><img src='" + appUrl + img + "' width='24' />" + item.Name + "</h3><p>" + item.Title + "</p><a href='#' data-value='" + item.AlertId + "' class='btn btn-success btn-sm alertInfo' data-toggle='modal' data-target='#alertDetails'>Details</a></div>";
                    marker.bindPopup(content);
                    markers.addLayer(marker);

                });


            }

        })
       .done(function () {
           $(".loadingOverlay").hide();
           map.invalidateSize(true);
       });

        //add the markers layer to the map
        map.addLayer(markers);

        
        }

       

    $('#gmap').on('shown.bs.tab', function (e) {
        //clear map first
        clearMap();
        //resize the map
       map.invalidateSize(true);
       //load the map once all layers cleared
       loadMap();
    })

    //this function to clear the map before another ajax call
    function clearMap()
    {
        // clear all layers before it reloads;
        map.eachLayer(function (layer) {
            map.removeLayer(layer);
        });
        map.removeControl(lcontrol);
        map.removeControl(eb);
    }

    map.on('focus', function () { map.scrollWheelZoom.enable(); });
    map.on('blur', function () { map.scrollWheelZoom.disable(); });
});

Place the above codes inside the script block. Remember  I have not shown the jQuery and Bootstrap js files in the above script reference since I have added them in the layout file.

<script type="text/javascript">
//place your code here

</script>

In the above javascript, I have to create another function to clear the map. It is a bit tricky to display map inside a bootstrap tab or Modal. If you are using this map in a single page then you don’t have to do this. You can clean up your code.

map_3

 

 

2 Comments

  1. Tony DiPollina

    Excellent article, just what I need.

    Could you please the visual studio solution source code available for this example?

  2. Mark

    Very good article,

    Where I can get the visual studio solution source code ?

Leave a Comment

Your email address will not be published. Required fields are marked *