“Marionette Maps” is an on-going series on building a loosely coupled, configuration driven map viewer using Marionette.js. Read Part 1, Part 2, Part 3, Part 4 and Part 5.
Displaying Wildfires
So, this is where it all comes together… first, here is a shot of the app showing 2000+ wildfires from 2012
Recall from Part 4 that the heart of this application is showing wildfires from Geomac.gov. Their main map service (http://wildfire.cr.usgs.gov/ArcGIS/rest/services/geomac_dyn/MapServer) breaks out the fires across a number of layers.
- Current fires (aka “burning”) are in layer 0, with the associated perimeters (if any) in layer 1.
- All fires for the current year (active and inactive) are in layer 4.
- All perimeters from all previous years, are in layer 5,
- Layers 7 through 17 hold historic fires (points) from 2012 back to 2007.
In our app we want to have the following options:
a) View Current Fires and Perimeters
b) View Historic Fires, by year, back to 2005.
The layout of this service is pretty good for some uses, but for our app it would have been much easier if they had all the wildfires in one layer, and all the perimeters in another. Then switching between “years” would simply mean changing the “definition query” (essentially a “where-clause”) used when requesting the data. But, that’s not the case, and we don’t control the data, so we have to live with it.
Additionally, the cartography likely makes sense for fire fighters, but it’s not too great for the public, so using the MapServices directly is not going to work for us. We need more control.
Fetching Data
In order to keep things separated, I put all the logic for fetching the data into FireManagerModule.js, and it’s job is to listen to one event: ‘FireLayer:ConfigChanged’.
That event is raised from the HistoricFiresModule when an item is clicked, and essentially we pass over the following structure:
The FireManagerModule, then takes this and the fun begins. The simplest thing would be to just load up the layer as a feature layer and assign a custom renderer, but we are trying to be decoupled here and if we wanted to swap out the map to Leaflet, we’d have a bunch more hoops to jump through to “teach” Leaflet how to work with a feature layer. So, we will use jQuery to fetch all the features into an array and throw that to the map and let it sort out how to handle things.
Sounds good right? Except the ArcGIS server can return a maximum of 1000 records. Hmmm… that makes it more interesting. The basic process boils down to this:
- call query on the layer specifying to only return the Ids (no maximum on this)
- loop over the id’s in set batches, and query to get the features
- accumulate the features in an array
- handle the fact we launch multiple async requests, and need to do something when they have ALL completed
- raise the Map:ShowFires event, and pass along the array of features
Sounds much worse than it is, and I’m sure the current implementation could use some love, but it’s working. Feel free to hack at this jsfiddle which is doing much the same thing, or fix it up and hit me with a pull request.
Server-less FeatureLayers
When the EsriMapModule gets the array of features, the next step is to stuff it into the map. This is where the esri.layers.FeatureLayer’s constructor overload really helped. Usually a FeatureLayer is connected to a FeatureService, but that’s not always needed – you can construct a FeatureLayer by just padding it a FeatureCollectionObject, which has a definition of the layer (attributes, renderer etc), and a featureSet. Conveniently a featureSet is an array of Features. Shazam! Ok, this is not very “backboney”, or “marionetteish”, but stuffing a collection in the middle of this was only going to make things more complex.
To get this done, I created a function that would return the layer Definition object:
With that in hand, I could just setup the rest of the layer like so:
Adding Year into the Router
This was more complex than I’d hoped, and I’m not thrilled with the current solution, so I’ll likely continue to hack on this over time. Anyhow – the easy part was adding an optional parameter to the route, which allowed the app to handle urls like
http://localhost/marionette-map/#map/-92.27/34.34/5
or
http://localhost/marionette-map/#map/-92.27/34.34/5/2006
What was more complex was working out how to “set” the year in the router, and then how to correctly “re-hydrate” the map to the specified “state”.
I ended up having the router listen for the FireLayer:ConfigChanged event, as that’s what’s used to inform the rest of the application about a change in what’s shown on the map, and then the router held on to this value.
To get the map to re-hydrate correctly, I needed to wait until the map was loaded, and then raise the appropriate events. It was easy enough to add a Map:Loaded event into the mix, and have the router listen for that, and then, if it had values parsed from the url, raise the needed events as shown below…
The app is now live over at github, so here are some example Urls to play with:
High Park fire near Fort Collins, in 2012 http://dbouwman.github.com/geomacmapper/#map/-105.3/40.65/10/2012
Border 127 fire near San Diego in 2007 http://dbouwman.github.com/geomacmapper/#map/-116.82/32.69/11/2007
To Production…
At this point we have a non-trivial application, but we are a ways from “production” yet. This needs to be tested on non-webkit browsers, particularly Internet Explorer. Theoretically it should work “ok” in IE9 & 10, but below that, who knows. We would also want to combine and minify the css and javascript. When using a backend like Rails, ASP.NET MVC, or Node (+Express), this work is part of what you get from using the framework. However, for a completely static app like this one, we would need to add a formal build step to do this. From the little research I’ve done on this, it sounds like gruntjs and it’s uglify plug-in is the tooling de jour for this task.
A Word about Collections
In looking back over this series, one thing that I did not cover was working with Backbone collections – specifically how they work with GET/PUT/POST/DELETE HTTP services. Since we did not have such a service, it was going to be a hack to work it into the mix, but if you are interested, here are some links to posts which cover this.
http://liquidmedia.org/blog/2011/01/backbone-js-part-1/
http://coenraets.org/blog/2011/12/backbone-js-wine-cellar-tutorial-part-1-getting-started/
Lessons Learned
I learned a couple conceptual level things along the way which I’ll be sure to plan for moving forward. Here they are in no particular order…
Make a List of Events
This app is right at the edge of something that you can keep in your head easily. Moving forward, I’ll be keeping a list of the events and a description of what they are supposed to do in the project.
Plan your Routes
Plan ahead for how you are going to handle the router, and holding the context of the map in the url. When I first added the router, I just stored the center of the map and the zoom level, which was easy to tie into things as the map is all cleanly separated, and it’s easy to raise events from the map and use that to update the url. Adding in the “Year” was troublesome in that the value of the year is set as a result of selecting a historical year to view. Ok, not too bad for updating the url via Router.navigate, but parsing out the values and correctly restoring the map state was more complex than I’d have liked.
Data Consistency
This also came up at the end. I had planned to show another “window” with details about a wildfire, which would be relatively simple – EXCEPT – the attributes for the current fires layer are DIFFERENT from those on the historical fire layers, which borked things up royally as all the rest of the code assumes that all the wildfire layers are the same. This certainly could be worked around, but since no one is paying for this app, I let it ride with a mouse-over that shows the fire name. I have some ideas about how to harvest all the data into a consistent format, hosted on ArcGIS Server 10.1 and then use some of the new Stats options to show interesting info about the data, but that will have to wait for a while.
Code
The code for this drop is tagged v0.0.6 on github
and the latest version can be found at http://dbouwman.github.com/geomacmapper/