Rails With SpineJS and Google Maps
I wanted to create a simple project that used ruby on rails, spinejs, and google maps together. I came up with the following features:
- Dragging a marker icon from outside of a map onto a map creates a marker on the map and in the database
- Dragging a marker from one position to another updates the marker on the map and updates its position in the database
- Right-clicking on a map marker removes it from the map and the database
The end result can be seen here and the source code is on github.
Let’s get started.
First, create a new rails project. I prefer to use rails 3.1 because the asset pipeline makes it easy to use coffeescript with your application.
rails new dragmarkers --database=postgresql
I used PostgreSQL because it’s awesome and I wanted to put an example up on Heroku. For this project, the database doesn’t matter. ignore the database option if you’d like. Remember you’ll need to edit config/database.yml accordingly.
Take care of the usual cleanup.
cd dragmarkers
rm public/index.html
Now that we have our rails project, let’s add spinejs. I think that the spine-rails gem is great (I’m even a minor contributor), but I’d like to write everything from scratch for this project. Instead, download the latest version of spine from the spinejs website. There will be several directories. Create a directory in your project for the spine files
mkdir -p vendor/assets/javascripts/spine
and copy all of the files from the lib directory of your spinejs download into the spine directory you created.
Now let’s add our standard libraries. For this project I decided to use CDN’s for the standard javascript libraries, rather than the jquery-rails gem. Open the file app/views/layouts/application.html.erb and add the libraries to the <head> section of your layout. The file should look like this:
Note that we’ll be using jquery-ui to make the marker icons draggable from outside of the map onto the map. I also added a div for page styling.
The foundation is in place, let’s start writing our own code. First we want to scaffold a Marker.
rails generate scaffold Marker
Edit config/routes.rb to make generated route the root route, for convenience. The file should look like this when you’re done:
Now let’s set up the marker model on the rails side. Edit the migration file db/migrate/[datestamp]_create_markers.rb to add columns for latitude, longitude, and icon. The file should look like this when you’re done:
Run the migration.
rake db:migrate
The rails model is done. Let’s turn to the views for a bit. Most of the scaffolded views are unnecessary and you can remove them.
rm app/assets/stylesheets/scaffolds.css.scss
rm app/views/markers/_form.html.erb
rm app/views/markers/edit.html.erb
rm app/views/markers/new.html.erb
rm app/views/markers/show.html.erb
Only the index.html.erb will be used.
We don’t need to touch the controller. Spine requires a REST interface and json, which our controller is already doing beautifully.
The rails part is done, so now for the spinejs portion. I like to do everything with coffeescript so let’s rename the application.js file to application.js.coffee (I wish this were a default).
mv app/assets/javascripts/application.js app/assets/javascripts/application.js.coffee
Open the application.js.coffee file for editing. We aren’t using the jquery gem, so you can remove the jquery-related require statements at the top. One of the nice features of spinejs is its modularity. For this project we’re using the core spinejs module and the ajax module (to work with the rails REST).
#= require spine/spine #= require spine/ajax
I like to create my directory structure for spinejs similar to what I’d find in rails application (or any MVC) and require them accordingly:
#= require_tree ./lib #= require_self #= require_tree ./models #= require_tree ./controllers #= require_tree ./views #= require_tree .
Create the corresponding directories:
mkdir app/assets/javascripts/lib
mkdir app/assets/javascripts/models
mdkir app/assets/javascripts/controllers
mkdir app/assets/javascripts/views
Let’s create a base spinejs controller and expose it. So far, your application.js.coffee file should look like this:
We want our index file to set up html for the base controller to hook in to and create the base controller. Edit app/views/markers/index.html.erb by taking out the scaffolding and making it look like this:
A div with an “app” id and create a new App and pass that div in as the root element. At this point you could actually run your rails application and have a single-page javascript application, even though you’d only see a blank page.
Let’s create a spinejs model to correspond to our rails model. First let’s create the file
touch app/assets/javascripts/models/marker.js.coffee
Open it up and create a Marker model that inherits from Spine.Model, is configured with the same fields as our rails model, and extends the Spine.Model.Ajax functions. The file should look like this for now:
At this point, if you ran the rails application, you’d be able to do CRUD operations with the database through the javascript console in your browser with commands like:
App.Marker.create({latitude: 0, longitude: 0}) (create a marker entry in the database), marker = App.Marker.find(id) (get the marker from the database that was just created, using its id), marker.updateAttributes({latitude: 1, longitude: 2}) (update the marker’s fields in the database), marker.destroy() (delete the marker from the database)
Things are shaping up pretty well, but we still have a blank-page application. Let’s remedy that. We need to show a map a marker images that can be dragged onto the map. Let’s make a javascript template (JST) view for it. I like to use eco, but there are other nice options like jQuery templates and mustache. to use eco, let’s add the line gem 'eco' to our Gemfile and install it.
bundle install
Now let’s create a new directory for our marker views and create the view file.
mkdir app/assets/javascripts/views/markers
touch app/assets/javascripts/views/markers/index.jst.eco
Note the jst and eco extensions. Edit the file to contain a div with a “map” id and a bunch of image tags. Your file should look like this when you’re done.
Notice that it references 0.png through 9.png in the app/assets/images directory. You can copy the images I used into that directory or use your own. I found this website to be a great resource for map marker images. Just make sure that the images you reference in the javascript template are there.
Now let’s create a spinejs controller for the template and actually display it. I like to alias $ for jQuery and Marker for the App.Marker model. Also, we want references to the elements in our view and to display our view. Create the new spine controller file.
touch app/assets/javascripts/controllers/markers_controllers.js.coffee
Edit markers_controller.js.coffee to include that code. The file should look like this so far:
Remember in our rails view index, we’re actually creating a new App so we need to add our spinejs markers controller to that one. Edit the app/assets/javascripts/application.js.coffee file and have the constructor append our new controller. When you’re done the file should look like this.
This is the last time we’ll edit that file. If you were to run your rails application now, you would see it working because the marker icons would show up, but there still isn’t a map yet. We still need to create our google map. First, however, let’s add some style to the div that will hold the map and a bit of CSS for the marker icons too. Edit your app/assets/stylesheets/application.css file to look something like this
and edit your app/assets/stylesheets/markers.css.scss file to look something like this
If you run your rails application again, you’ll see a border around the div where the map will go and spacing for the marker icons.
Let’s return to our markers controller (app/assets/javascripts/controllers/markers_controller.js.coffee) and add the rest of the code to it. We want it to create our google map, create an overlay for the map (so that we can calculate the drag position on the map), make our marker icons draggable (and droppable), and, lastly, add any existing markers to the map. Let’s write the code for it in our controller’s constructor, then create each function in turn. Edit the file to look like this.
The first function is to create the map. I initialize it to the middle of the United States at a reasonable zoom level and use the roadmap view. Our elements declaration for our map div returns a jQuery wrapped set, the same as $('#map') and we need to get the first (and only) member to pass to the google map constructor. The function for creating the map is pretty straightforward:
The second function is to create an overlay for the map. It is simple. Note that we’re using the variable @overlay because we’ll be using it later.
The third function uses jquery-ui to make the icons draggable. We need another function to handle the icon when it is dropped (we’ll call it placeNewMarker). Note that I’m using the draggable option clone to make a copy of the image and containment of parent to keep it within the map area. Placing a new marker takes some calculations to figure out where the dropped location is within the page and uses our overlay to figure out what the equivalent longitude and latitude on the map would be. Finally, we create the new marker and put it on the map (the setMap function doesn’t exist yet, we’ll get to that in a bit). The two functions look like this
Lastly, we want to make sure the existing markers show up on the map. A fetch will retrieve the marker information from rails which triggers a refresh event. Let’s bind that even to display our markers, which will be triggered when we fetch. Our last function looks like this
Altogether, our markers_controller.js.coffee file should look like this.
Our controller is done, but we have a few more things to do to finish up. Our spinejs model for markers has no concept of a map to set a map to, and, for that matter, has no concept of a google marker for the map. Lastly, we need to make the markers draggable once they are on the map, and provide a means to remove the marker from the map.
Let’s edit our spinejs marker model (app/assets/javascripts/models/marker.js.coffee) and add these finishing touches. First, let’s have our constructor create a corresponding google map marker
add add the setMap function we were using in our controller. It’s a simple 1-liner
When a marker has been dragged on the map we want it to save its new position to the database
and right-click will remove the marker. We want to remove it from the map and the database
If we add the code to listen to the events and respond with those functions, we’re done. The file should look like this when you’re done.
The project is now complete and does everything I set out to do. However, I think there is still some room for improvement. This is nice for a small set of points, but if you were dealing with a large number of markers you’d want to fetch only the markers within the map’s currently viewable area. You may want to use a GIS system (like postgis). Instead of having right-click delete the marker, perhaps you could have a context menu with delete as one of the options.
Acknowledgements:
Thanks to Alex MacCaw for creating spinejs and all of the great documentation to go with it.
Thanks to kwicher who’s example code helped me debug some frustrating bugs.
Notes