/*jshint multistr:true */ this.recline = this.recline || {}; this.recline.View = this.recline.View || {}; (function($, my) { // ## Map view for a Dataset using Leaflet mapping library. // // This view allows to plot gereferenced records on a map. The location // information can be provided either via a field with // [GeoJSON](http://geojson.org) objects or two fields with latitude and // longitude coordinates. // // Initialization arguments are as standard for Dataset Views. State object may // have the following (optional) configuration options: // //
// {
// // geomField if specified will be used in preference to lat/lon
// geomField: {id of field containing geometry in the dataset}
// lonField: {id of field containing longitude in the dataset}
// latField: {id of field containing latitude in the dataset}
// }
//
my.Map = Backbone.View.extend({
tagName: 'div',
className: 'recline-map',
template: ' \
\
',
// These are the default (case-insensitive) names of field that are used if found.
// If not found, the user will need to define the fields via the editor.
latitudeFieldNames: ['lat','latitude'],
longitudeFieldNames: ['lon','longitude'],
geometryFieldNames: ['geojson', 'geom','the_geom','geometry','spatial','location', 'geo', 'lonlat'],
initialize: function(options) {
var self = this;
this.el = $(this.el);
// Listen to changes in the fields
this.model.fields.bind('change', function() {
self._setupGeometryField()
self.render()
});
// Listen to changes in the records
this.model.currentRecords.bind('add', function(doc){self.redraw('add',doc)});
this.model.currentRecords.bind('change', function(doc){
self.redraw('remove',doc);
self.redraw('add',doc);
});
this.model.currentRecords.bind('remove', function(doc){self.redraw('remove',doc)});
this.model.currentRecords.bind('reset', function(){self.redraw('reset')});
this.bind('view:show',function(){
// If the div was hidden, Leaflet needs to recalculate some sizes
// to display properly
if (self.map){
self.map.invalidateSize();
if (self._zoomPending && self.state.get('autoZoom')) {
self._zoomToFeatures();
self._zoomPending = false;
}
}
self.visible = true;
});
this.bind('view:hide',function(){
self.visible = false;
});
var stateData = _.extend({
geomField: null,
lonField: null,
latField: null,
autoZoom: true
},
options.state
);
this.state = new recline.Model.ObjectState(stateData);
this.menu = new my.MapMenu({
model: this.model,
state: this.state.toJSON()
});
this.menu.state.bind('change', function() {
self.state.set(self.menu.state.toJSON());
self.redraw();
});
this.elSidebar = this.menu.el;
this.mapReady = false;
this.render();
this.redraw();
},
// ### Public: Adds the necessary elements to the page.
//
// Also sets up the editor fields and the map if necessary.
render: function() {
var self = this;
htmls = Mustache.render(this.template, this.model.toTemplateJSON());
$(this.el).html(htmls);
this.$map = this.el.find('.panel.map');
return this;
},
// ### Public: Redraws the features on the map according to the action provided
//
// Actions can be:
//
// * reset: Clear all features
// * add: Add one or n features (records)
// * remove: Remove one or n features (records)
// * refresh: Clear existing features and add all current records
redraw: function(action, doc){
var self = this;
action = action || 'refresh';
// try to set things up if not already
if (!self._geomReady()){
self._setupGeometryField();
}
if (!self.mapReady){
self._setupMap();
}
if (this._geomReady() && this.mapReady){
if (action == 'reset' || action == 'refresh'){
this.features.clearLayers();
this._add(this.model.currentRecords.models);
} else if (action == 'add' && doc){
this._add(doc);
} else if (action == 'remove' && doc){
this._remove(doc);
}
if (this.state.get('autoZoom')){
if (this.visible){
this._zoomToFeatures();
} else {
this._zoomPending = true;
}
}
}
},
_geomReady: function() {
return Boolean(this.state.get('geomField') || (this.state.get('latField') && this.state.get('lonField')));
},
// Private: Add one or n features to the map
//
// For each record passed, a GeoJSON geometry will be extracted and added
// to the features layer. If an exception is thrown, the process will be
// stopped and an error notification shown.
//
// Each feature will have a popup associated with all the record fields.
//
_add: function(docs){
var self = this;
if (!(docs instanceof Array)) docs = [docs];
var count = 0;
var wrongSoFar = 0;
_.every(docs,function(doc){
count += 1;
var feature = self._getGeometryFromRecord(doc);
if (typeof feature === 'undefined' || feature === null){
// Empty field
return true;
} else if (feature instanceof Object){
// Build popup contents
// TODO: mustache?
html = ''
for (key in doc.attributes){
if (!(self.state.get('geomField') && key == self.state.get('geomField'))){
html += '
';
var bg = new L.TileLayer(mapUrl, {maxZoom: 18, attribution: osmAttribution ,subdomains: '1234'});
this.map.addLayer(bg);
this.features = new L.GeoJSON();
this.features.on('featureparse', function (e) {
if (e.properties && e.properties.popupContent){
e.layer.bindPopup(e.properties.popupContent);
}
if (e.properties && e.properties.cid){
e.layer.cid = e.properties.cid;
}
});
// This will be available in the next Leaflet stable release.
// In the meantime we add it manually to our layer.
this.features.getBounds = function(){
var bounds = new L.LatLngBounds();
this._iterateLayers(function (layer) {
if (layer instanceof L.Marker){
bounds.extend(layer.getLatLng());
} else {
if (layer.getBounds){
bounds.extend(layer.getBounds().getNorthEast());
bounds.extend(layer.getBounds().getSouthWest());
}
}
}, this);
return (typeof bounds.getNorthEast() !== 'undefined') ? bounds : null;
}
this.map.addLayer(this.features);
this.map.setView(new L.LatLng(0, 0), 2);
this.mapReady = true;
},
// Private: Helper function to select an option from a select list
//
_selectOption: function(id,value){
var options = $('.' + id + ' > select > option');
if (options){
options.each(function(opt){
if (this.value == value) {
$(this).attr('selected','selected');
return false;
}
});
}
}
});
my.MapMenu = Backbone.View.extend({
className: 'editor',
template: ' \
\
',
// Define here events for UI elements
events: {
'click .editor-update-map': 'onEditorSubmit',
'change .editor-field-type': 'onFieldTypeChange',
'click #editor-auto-zoom': 'onAutoZoomChange'
},
initialize: function(options) {
var self = this;
this.el = $(this.el);
_.bindAll(this, 'render');
this.model.fields.bind('change', this.render);
this.state = new recline.Model.ObjectState(options.state);
this.state.bind('change', this.render);
this.render();
},
// ### Public: Adds the necessary elements to the page.
//
// Also sets up the editor fields and the map if necessary.
render: function() {
var self = this;
htmls = Mustache.render(this.template, this.model.toTemplateJSON());
$(this.el).html(htmls);
if (this._geomReady() && this.model.fields.length){
if (this.state.get('geomField')){
this._selectOption('editor-geom-field',this.state.get('geomField'));
this.el.find('#editor-field-type-geom').attr('checked','checked').change();
} else{
this._selectOption('editor-lon-field',this.state.get('lonField'));
this._selectOption('editor-lat-field',this.state.get('latField'));
this.el.find('#editor-field-type-latlon').attr('checked','checked').change();
}
}
if (this.state.get('autoZoom')) {
this.el.find('#editor-auto-zoom').attr('checked', 'checked');
}
else {
this.el.find('#editor-auto-zoom').removeAttr('checked');
}
return this;
},
_geomReady: function() {
return Boolean(this.state.get('geomField') || (this.state.get('latField') && this.state.get('lonField')));
},
// ## UI Event handlers
//
// Public: Update map with user options
//
// Right now the only configurable option is what field(s) contains the
// location information.
//
onEditorSubmit: function(e){
e.preventDefault();
if (this.el.find('#editor-field-type-geom').attr('checked')){
this.state.set({
geomField: this.el.find('.editor-geom-field > select > option:selected').val(),
lonField: null,
latField: null
});
} else {
this.state.set({
geomField: null,
lonField: this.el.find('.editor-lon-field > select > option:selected').val(),
latField: this.el.find('.editor-lat-field > select > option:selected').val()
});
}
return false;
},
// Public: Shows the relevant select lists depending on the location field
// type selected.
//
onFieldTypeChange: function(e){
if (e.target.value == 'geom'){
this.el.find('.editor-field-type-geom').show();
this.el.find('.editor-field-type-latlon').hide();
} else {
this.el.find('.editor-field-type-geom').hide();
this.el.find('.editor-field-type-latlon').show();
}
},
onAutoZoomChange: function(e){
this.state.set({autoZoom: !this.state.get('autoZoom')});
},
// Private: Helper function to select an option from a select list
//
_selectOption: function(id,value){
var options = this.el.find('.' + id + ' > select > option');
if (options){
options.each(function(opt){
if (this.value == value) {
$(this).attr('selected','selected');
return false;
}
});
}
}
});
})(jQuery, recline.View);