Meteor and marker filtering with Leaflet

I’m currently working on a Leaflet map that’s being ported from vanilla JS (with a dash of Mapbox.js) to Meteor. Each Point in the collection has a category and I want to implement client-side filtering for the markers based on this.

The approach I was leaning towards is using a session to capture the selected checkboxes and iterating over the collection to add Layers to the map based on this. But I’m having issues joining the pieces together and adapting this from a single layer showing all markers to one that only shows the selected category markers.

Any suggestions for the best way to go about this? Is there a better way to manage this sort of filtering UI in Meteor (or Leaflet.js)?

Hi Kate,

I am on similar project as yours - gps tracker based on leaflet maps in meteor (and android app). What I have started with, was adding native leaflet,js library, however I had issues with that. The solution was to add meteor package - bevanhunt:leaflet. Worth trying.

Good luck !

reminds me of my fun with google maps polymer Fun with Polymer Google maps

So for your usecase, I would probably work with cursor observe/observechanges representing all markers currently on map. And pair add/remove/update operation with leaflet api.

Paste code here afterwards, I think many ppl would like to use it.

Thanks. Yep, I’m using the bevanhunt:leafletpackage and also found some other packages like leaflet.groupedlayercontrol and leaflet-awesome-markers quite useful.

Thanks for the suggestion, I haven’t used observe/observechanges much but sounds like it could be the way to go. Not quite sure how (or if) observechanges works in conjunction with sessions but will report back any progress.

Ok, after a bit of a timeout from this, still not having much luck.

I’ve created a session for the selected checkboxes (that align with the categories field in the Points collection). Basically, I want to clear and update the map markers based on any changes to this array.

Initially, it shows all markers by default but not sure the best way to integrate the category selection in a reactive way (and I’m pretty sure there are quite a few better ways to go about this).

Points.find().forEach(function (point) {
    var lat, lng, category, popup
    lat = point.lat;
    lng = point.lng;
    category = point.category
    popup = point.name;

    var categories = Session.get('categories')

        function showCategories(categories) {

            if (categories) {
                for (var i = 0; i < categories.length; i++) {
                    if (point.category == categories[i]) {
                        return markers.addLayer(new L.marker([lat, lng], {
                            icon: icon,
                        }).bindPopup(popup));
                    }
                }
            }
        }
}

}
})

Any suggestions and/or examples much appreciated. Apologies for the bad formatting/spaghetti code example.

I am slightly lost in all this marker magic, but why not Points.find({category: $in { Session.get(‘categories’)}}).map and some function directly adding it ?

I am not sure if I am using $in there in correct way tho, check docs :smiley:

I’m using observe to populate the map.

The code sample below integrate also cluster handling but you should get the idea. Custom functions not documented are most used to decorate/augment markers.

Cordage.Markers.observe = function() {

    var curMarkers = Cordage.Collections.Markers.find(Cordage.Markers.selector.get());

    _.each(Cordage.Markers.markers, function(marker) {
        if (marker.document._id !== 'New') {
            Cordage.Markers.remove(marker.document._id, 'cluster');
        }
    });

    curMarkers.observe({
        added: function(document) {
            Cordage.Markers.create(document, 'cluster');
        },

        changed: function(newDocument, oldDocument) {
            if (_.isEqual(newDocument, oldDocument) === false) {
                Cordage.Markers.markers[newDocument._id].document = newDocument;
                Cordage.Markers.markers[newDocument._id].setLatLng([newDocument.loc.lat, newDocument.loc.lng]);
                Cordage.Markers.list.set(newDocument._id);
            }
        },

        removed: function(document) {
            Cordage.Markers.remove(document._id, 'cluster');
        }
    });
};


Cordage.Markers.create = function(document, to) {
    var layer = (to === 'cluster') ? Cordage.Markers.cluster : Cordage.Markers.map;

    if (_.isUndefined(Cordage.Markers.markers[document._id]) === true &&
        _.isUndefined(layer) === false) {


        // augment tags with extra information
        document.tags = Cordage.Markers.tagsByMarker(document.tags, Cordage.Markers.tagslist());

        // set the icon and color of marker according first tag
        document.iconTag = Cordage.Markers.iconByTag(document);

        // set the marker for leaflet
        var marker = new L.marker([document.loc.lat, document.loc.lng], {
            draggable: false,
            icon: L.divIcon(document.iconTag)
        });

        marker.addTo(layer);
        marker.on('click', Cordage.Markers.onMarkerClick);

        // add the marker to the markers list
        marker.document = document;
        Cordage.Markers.markers[document._id] = marker;

        Cordage.Markers.list.set(document._id);
    }
};

Cordage.Markers.remove = function(_id, from) {
    var layer = (from === 'cluster') ? Cordage.Markers.cluster : Cordage.Markers.map;

    if (_.isUndefined(Cordage.Markers.markers[_id]) === false) {
        Cordage.Markers.markers[_id].off('click');
        Cordage.Markers.markers[_id].closePopup();
        Cordage.Markers.markers[_id].unbindPopup();
        layer.removeLayer(Cordage.Markers.markers[_id]);
        delete Cordage.Markers.markers[_id];
        Cordage.Markers.list.set(_id);
    }
};

The selector (a reactiveVar not a session) is built by combining keywords and tags (your categories) and by moving/zooming the map. Here the related code.

   Cordage.Markers.setSelector = function() {
    //    Cordage.Markers.selector.set({
    //        $and: [
    //            Cordage.Markers.boundsSelector || {},
    //            Cordage.Markers.searchSelector || {}
    //        ]
    //    });

    Cordage.Markers.selector.set(_.extend({},
        Cordage.Markers.searchSelector || {},
        Cordage.Markers.tagSelector || {},
        Cordage.Markers.boundsSelector || {}));
};

Cordage.Markers.setBoundsSelector = function(bounds) {

    if (_.isObject(bounds)) {
        // geoloc not yet supported by minimongo
        //        Cordage.Markers.boundsSelector = {
        //            'loc': {
        //                '$within': {
        //                    '$box': [bounds.southWest, bounds.northEast]
        //                }
        //            }
        //        };

        Cordage.Markers.boundsSelector = {
            'loc.lat': {
                $gt: bounds.getSouth(),
                $lt: bounds.getNorth()
            },
            'loc.lng': {
                $gt: bounds.getWest(),
                $lt: bounds.getEast()
            }
        };
    }
    Cordage.Markers.setSelector();
};

Cordage.Markers.setSearchSelector = function(search) {

    Cordage.Markers.searchSelectorValue = search;
    Cordage.Markers.searchValue.set(search);
    Cordage.Markers.searchSelector = {};
    if (search) {
        search = '.*' + search.replace(' ', '.*|.*') + '.*';
        Cordage.Markers.searchSelector = {
            $or: [{
                title: {
                    $regex: search,
                    $options: 'i'
                }
            }, {
                description: {
                    $regex: search,
                    $options: 'i'
                }
            }]
        };
    }

    Cordage.Markers.setSelector();
};


Cordage.Markers.setTagSelector = function(taglist) {

    Cordage.Markers.tagSelectorValue = taglist;
    Cordage.Markers.tagValue.set(taglist);
    Cordage.Markers.tagSelector = {};
    if (taglist) {
        Cordage.Markers.tagSelector = {
             tags: { $in: taglist }
        };
    }

        Cordage.Markers.setSelector();
};

Cordage.Markers.getSelector = function() {
    return Cordage.Markers.selector.get();
};

Thanks, you’re right $in is a much better way to go there.

Hey appshore, thanks for sharing your code. Thinking of switching to a similar approach using observe to capture changes to the marker selections but need to get my head around how you’ve done it with a selector rather than a session (as I think my requirements are much simpler so still might work best with a session but happy to be proved wrong).

Here a bit more of code that might help regarding reactiveVars.

The following template called Markers displays the map (template MarkersMap) and on the right side a yield to switch between the marker list with search field, a marker editor or detail depending of the user’s choice.

Cordage.Markers is the sub namespace of the module Markers in the project namespace Cordage. You have to adapt to your own context.

<template name="Markers">
    <div id="splitterMarkers">
        <div id="paneMarkers">
            {{> MarkersMap}}
        </div>
        <div id="entriesMarkers">
            {{> yield "Entries"}}
        </div>
    </div>
</template>

Here the associated code with this Markers template

   Template.Markers.onCreated(function () {
        // reactive vars for search filter
        Cordage.Markers.selector = new ReactiveVar({});
        Cordage.Markers.searchValue = new ReactiveVar('');
        Cordage.Markers.setSearchSelector();
    
        // reactive var for boundaries and zoomof map
        Cordage.Markers.setBoundsSelector();
        Cordage.Markers.zoom = new ReactiveVar({});
    
        // reactive var for tag filter
        Cordage.Markers.tagValue = new ReactiveVar({});
    
        // reactive var for list of markers
        Cordage.Markers.list = new ReactiveVar({});
    });
    
    Template.Markers.onRendered(function () {
        // Tracker is required to react on map and query change
        Cordage.Markers.Tracker = Tracker.autorun(function () {
            // start to observe when templates ready
            Cordage.Markers.observe();
        });
    });
    
    Template.Markers.onDestroyed(function () {
        // we stop the main tracker to reset cursor
        Cordage.Markers.Tracker.stop();
    });

I’m not sure that this is the “best” Meteor way to do it but it works so far.