Categories
Code

Better Marker Controls with LeafletJS

This is a follow up post to Custom Marker Controls with Mapbox and LeafletJS.

As with many projects, things changed and I was asked to add more controls to the map.

Each marker had it’s own category, and now each had its own language, Welsh or English.

Adding another custom control to show or hide markers based on two different taxonomies means a proper system for checking if a marker should be displayed or not. Much of the code I previously wrote was refactored.

First of all the language had to be added to the GeoJSON object.

language = { language : plentyn.language, code : plentyn.lang_code };

If the video had no language (i.e. deemed to be bilingual) it was an empty object. This property was added just like the category.

// the marker. all values are for demonstration
var geoObj = {
    type: 'Feature',
    geometry : { type : 'Point', coordinates : [ 53, -4 ] },
    properties : { 
        title : 'Video Title',
        video : 'http://vimeo.com/123',
        category : category,
        language : language
    },

To keep track of which markers should be shown or hidden, I had two arrays – categoryHide and langHide – to store them.

When a category or language checkbox was clicked, the code would add or remove the marker from either array.

// each checkbox had a data-df-type attribute to store the category or language

// value of the checkbox
var Tvalue = input.value;

// for the category marker...
if(input.dataset.dfType == 'category')
{
    if(input.checked)
    {
        categoryHide.splice(categoryHide.indexOf(Tvalue), 1); // remove (to show)
    }
    else
    {
        categoryHide.push(Tvalue); // add (to hide)
    }
}

After each array was prepared, I called a toggleMarker() function. This function would loop through each marker in the main array. If the marker was hidden it would be shown, then hidden if it was in the either of the hide arrays.

// pass the main map object
toggleMarker = function(map) 
{
    // loop through each marker
    for(var i in Dymafi.marks)
    {
         var mrkr = Dymafi.marks[i];

        // show all hidden markers
        if(!map.hasLayer(mrkr))
            map.addLayer(mrkr);

        // check if marker language is in hide array
        if(languageHide.indexOf(mrkr.feature.properties.language.code) != -1)
        {                        
            // hide this marker
            if(map.hasLayer(mrkr))
                map.removeLayer(mrkr)
        }

        // check if marker category is in hide array
        if(categoryHide.indexOf(mrkr.feature.properties.category.id.toString()) != -1)
        {                        
            // hide this marker
            if(map.hasLayer(mrkr))
                map.removeLayer(mrkr)
        }
    }
}

This is an improvement on the old solution, and ready for any more checkboxes that may need to be added.

UI

Although it worked well, a new problem was created. The control box was growing and was getting in the way on smaller screens.

I looked at Leaflet’s documentation and tutorials for ideas. It had a nice built in show hide ability when controls were created for its own layers, but were unavailable (for whatever reason) for custom controls.

So with a bit of HTML, jQuery and CSS I had my own system working:

I added an anchor with a background icon to the control <div>:

<a href="#" class="menuclick"><i class="icon-align-justify"></i></a>

The jQuery adds or removes a class from the parent <div>:

// set a variable if touch is enabled
var clickevent = 'mousedown';
if ('ontouchstart' in document.documentElement) {
    clickevent = 'touchstart';
}

// show/hide the categories on hover
$('a.menuclick').on('mouseenter', function(){
    $(this).parent().addClass('expanded');
});
$('.cats').on('mouseleave', function(){
    $(this).removeClass('expanded');
});
// the same for touchscreens
if(L.Browser.touch) // LeafletJS utility 
{
    $('a.menuclick').on(clickevent, function(){
        $(this).parent().addClass('expanded');
    });

    // hide the div if the map is moved
    map.on('movestart', function(){
        $('.cats').removeClass('expanded');
    });
}  

And the CSS:

/* .cats is the main wrapper */
/* show hide on touch/mouseover */
.cats .options{
display:none;
}
.cats.expanded .options{
display:block;
min-width: 130px;
padding:15px;
}
.cats a{
display:inline;
padding:10px;
}
.cats.expanded a{
display:none;
}

This need some clearing up, but works great.

You can see the finished result on the Dyma Fi map page.

Categories
Code

Custom Marker Controls with Mapbox and LeafletJS

I’m working on a simple mapping application to display user videos on a map. I’ve opted to use Mapbox (which is built on LeafletJS) mainly because of the styling options available.

My problem was showing and hiding markers based on their categories. Each marker has this geoJson structure:

// the marker's category object
category = { id : 1, name: 'category name' };

// the marker. all values are for demonstration
var geoObj = {
    type: 'Feature',
    geometry : { type : 'Point', coordinates : [ 53, -4 ] },
    properties : { 
        title : 'Video Title',
        video : 'http://vimeo.com/123',
        category : category
    },
};

Each category was in it’s own array, each an object with an id and name attribute as above:

maincats = [category1, category2, category3];

As far as I could google, I couldn’t find a method of adding map controls to show or hide these markers based on a property. So I rolled my own.

This will create a div of checkboxes for each category, which will show or hide all markers of that category.

Here I have the on ready event for the marker layer, which then loops through each of its markers (layers, in this case). After adding each marker to an array I build the control structure.

map.markerLayer.on('ready', function(e) 
{        
    marks = []; // array of markers
    this.eachLayer(function(marker) // loop
    {
        marker.bindPopup('pop up content etc');
        marks.push(marker); // add the marker to the array
    }

     var cats = L.control();

     cats.onAdd = function (map) {
         this._div = L.DomUtil.create('div', 'cats'); // create the control div

         // create the html for the checkboxes
         var html = '<h6>Categories</h6>';
         for(var i in maincats)
         {
             html += '<div><input type="checkbox" name="category-'+maincats[i].id+'" value="'+maincats[i].id+'" checked />'+maincats[i].name+'</div>';
         }

         // add the html to the div
         this._div.innerHTML = html;

         // add the click event listener
         L.DomEvent.addListener(this._div, 'click', function(e){

            var input = e.srcElement;
            var catid = input.value;

            if(typeof(input.value) == 'undefined') return false; // ignore random clicks on the div

            // loop through each marker, show or hide if in category
            for(var i in marks)
            {
                if(marks[i].feature.properties.category.id == catid)
                {                        
                    // check if box is checked, then show or hide accordingly
                    (input.checked) ? map.addLayer(marks[i]) : map.removeLayer(marks[i]); // each marker is a single layer
                }
            }

        });
     }
}

This works well enough, although I am yet to browser test it properly.

I suspect there may be better ways of doing this with LayerGroups, but I couldn’t get them to work at all. But this method is as good as any.