A Simple OpenLayers App with Yeoman, Sinatra, MongoDB and Backbone - Part 3
In the previous posts, we created a simple Sinatra API for CRUD operations on points of interest (POIs) then used Yeoman and OpenLayers to create a web application to render these POIs on a map.
In this third and final post, we’ll add the functionality for creating and editing the POIs.
Note: This post sat around half finished for some months during which time new major versions of both Yeoman and OpenLayers were released. Significant parts of it are now dated, but I recently decided to put a bookend on the series for completeness - mostly because my current job is all server side and web development is a novelty for me at the moment.
The first thing we’ll do is add a some hover-over text to our map icons so we know what any given POI actually is. For simplicity, we’ll just show the name of the POI.
To indicate that a given POI has our focus, we need a new style for selection. We’ll just change it from blue to green. So our style from the previous part becomes this.
Then we need to create an OpenLayers control to handle the hover events. We’ll create more than one control, so let’s add a setupControls method that gets called in the init method of our mapping module. In that method goes this code. We’re indicating that the control is activated by hovering over the feature and that we want to apply the select style we defined.
We’ve also hooked up two event listeners in our control definition, so we’d better add those handlers to our mapping module too. They look like this.
The popup that this affects is a simple box without styling. OpenLayers has different types of popups as well as properties to control appearance. If that isn’t enough, you can simply apply CSS to the element it creates. In the interest of brevity I’ll leave this as an exercise for the reader.
Creating a View
This example only requires one view. In its first form, we’ll just get it to show us the summary of a POI that we select from the map.
$ yeoman init backbone:view poi
This creates a Backbone view and a template. Because we’re keeping it simple, we’ll just define the template directly in index.html. I deleted the one that Yeoman generated.
Let’s look at the template first. What we want to do is show a Bootstrap modal when we create or click POIs. For this example we’ll use the templating provided by Underscore. Again, for expediency I included the delete button in this modal, though in the wild this functionality might be separate - perhaps invoked via another OpenLayers control. This is what we add to index.html.
Backbone views need a render method. Ours gets the template, compiles it using a PoiModel instance, appends the view to the page body, then shows the modal. When we’re adding a POI we don’t want to show the delete button. We can detect this situation based on whether the model has the MongoDB generated ID. This is how it looks.
OpenLayers - Handling Feature Selection
Now that we have our POI view, we need another OpenLayers control to handle feature selection. All we’re doing is specifiying that selected POIs use the select style we defined above, and designating the handler for this event.
The handler simply closes the hover popup if it is active and renders the Backbone view with the model for the selected POI. It looks like this.
Here’s is what our rendered view looks like.
So now we have a view that will be rendered with the data for any POI that we click on. Unfortunately, as it is, none of the buttons do anything, so we can’t even close the modal. It’s time to hookup some events for our view.
We can easily map events on our controls to methods by defining the mappings in the view’s events object. We need events for closing the modal, for deleting the POI and for saving the POI, which is handled automatically for us by Backbone as an upsert. We also need to manage our map based on these actions, so we fire externally visible events using the trigger method. Here are the mappings and the methods.
Closing the Modal
When we click the close button we want to close the modal and remove the view from the DOM. As we’ll see later, we also want to do some conditional handling depending on the purpose for which the view was opened; so we’ll also trigger an event. When opening the view for an existing POI, the handler for this event simply ensures no features are selected.
Deleting a Point of Interest
From the view render method, we can see that the delete button is not visible when opened in add mode. When this button is clicked we want to:
- Delete our POI from the database by calling model.destroy().
- Remove the feature from our vector layer.
- Invoke the close functionality above to clean up the view.
Here is our selection method with the view events handled.
Adding a Point of Interest
Adding a feature to a vector layer requires a DrawFeature control. When activated, a click on the map will add a new feature at that location. If we’ve registered a handler with the event, it will fire. This is what we add to our setupControls method.
The handler looks like this.
I had a minor issue here where no matter what I tried, the feature drawn by the add control ended up on the unrenderedFeatures list for the layer. In the end I actually used less code to just remove the drawn feature and add a fresh one generated from the POI in the same fashion as when initialising the layer.
Naturally we need a mechanism for switching between the modes of (1) selecting POIs on the map to view or delete, and (2) adding new ones. This is the public method I added to the mapping module. It just toggles the OpenLayers controls we created above.
Going for brevity again, I used plain old Bootstrap navigation list items and jQuery click bindings to switch between the modes.
That’s it! A simple-as-can-be UI for adding/updating/deleting points of interest on a map with OpenLayers and Backbone, and persisting the data to MongoDB via a Sinatra API. I’ve dumped the code as it is into a GitHub repository here. One enhancement that comes to mind is the ability to relocate POIs by dragging them around the map. Someone might find a use for it as a template for a more sophisticated application, or maybe just make the code better. I’d certainly appreciate the feedback.