view-map.js

#

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 documents on a map. The location information can be provided either via a field with GeoJSON objects or two fields with latitude and longitude coordinates.

Initialization arguments:

  • options: initial options. They must contain a model:

    { model: {recline.Model.Dataset} }

  • config: (optional) map configuration hash (not yet used)

my.Map = Backbone.View.extend({

  tagName:  'div',
  className: 'data-map-container',

  template: ' \
  <div class="editor"> \
    <form class="form-stacked"> \
      <div class="clearfix"> \
        <div class="editor-field-type"> \
            <label class="radio"> \
              <input type="radio" id="editor-field-type-latlon" name="editor-field-type" value="latlon" checked="checked"/> \
              Latitude / Longitude fields</label> \
            <label class="radio"> \
              <input type="radio" id="editor-field-type-geom" name="editor-field-type" value="geom" /> \
              GeoJSON field</label> \
        </div> \
        <div class="editor-field-type-latlon"> \
          <label>Latitude field</label> \
          <div class="input editor-lat-field"> \
            <select> \
            <option value=""></option> \
            {{#fields}} \
            <option value="{{id}}">{{label}}</option> \
            {{/fields}} \
            </select> \
          </div> \
          <label>Longitude field</label> \
          <div class="input editor-lon-field"> \
            <select> \
            <option value=""></option> \
            {{#fields}} \
            <option value="{{id}}">{{label}}</option> \
            {{/fields}} \
            </select> \
          </div> \
        </div> \
        <div class="editor-field-type-geom" style="display:none"> \
          <label>Geometry field (GeoJSON)</label> \
          <div class="input editor-geom-field"> \
            <select> \
            <option value=""></option> \
            {{#fields}} \
            <option value="{{id}}">{{label}}</option> \
            {{/fields}} \
            </select> \
          </div> \
        </div> \
      </div> \
      <div class="editor-buttons"> \
        <button class="btn editor-update-map">Update</button> \
      </div> \
      <input type="hidden" class="editor-id" value="map-1" /> \
      </div> \
    </form> \
  </div> \
<div class="panel map"> \
</div> \
',
#

These are the default field names that will be used if found. If not found, the user will need to define the fields via the editor.

  latitudeFieldNames: ['lat','latitude'],
  longitudeFieldNames: ['lon','longitude'],
  geometryFieldNames: ['geom','the_geom','geometry','spatial','location'],
#

Define here events for UI elements

  events: {
    'click .editor-update-map': 'onEditorSubmit',
    'change .editor-field-type': 'onFieldTypeChange'
  },


  initialize: function(options, config) {
    var self = this;

    this.el = $(this.el);
#

Listen to changes in the fields

    this.model.bind('change', function() {
      self._setupGeometryField();
    });
    this.model.fields.bind('add', this.render);
    this.model.fields.bind('reset', function(){
      self._setupGeometryField()
      self.render()
    });
#

Listen to changes in the documents

    this.model.currentDocuments.bind('add', function(doc){self.redraw('add',doc)});
    this.model.currentDocuments.bind('remove', function(doc){self.redraw('remove',doc)});
    this.model.currentDocuments.bind('reset', function(){self.redraw('reset')});
#

If the div was hidden, Leaflet needs to recalculate some sizes to display properly

    this.bind('view:show',function(){
        self.map.invalidateSize();
    });

    this.mapReady = false;

    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(this.template, this.model.toTemplateJSON());

    $(this.el).html(htmls);
    this.$map = this.el.find('.panel.map');

    if (this.geomReady && this.model.fields.length){
      if (this._geomFieldName){
        this._selectOption('editor-geom-field',this._geomFieldName);
        $('#editor-field-type-geom').attr('checked','checked').change();
      } else{
        this._selectOption('editor-lon-field',this._lonFieldName);
        this._selectOption('editor-lat-field',this._latFieldName);
        $('#editor-field-type-latlon').attr('checked','checked').change();
      }
    }

    this.model.bind('query:done', function() {
      if (!self.geomReady){
        self._setupGeometryField();
      }

      if (!self.mapReady){
        self._setupMap();
      }
      self.redraw();
    });

    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 (documents)
  • remove: Remove one or n features (documents)
  • refresh: Clear existing features and add all current documents
  redraw: function(action,doc){

    var self = this;

    action = action || 'refresh';

    if (this.geomReady && this.mapReady){
      if (action == 'reset'){
        this.features.clearLayers();
      } else if (action == 'add' && doc){
        this._add(doc);
      } else if (action == 'remove' && doc){
        this._remove(doc);
      } else if (action == 'refresh'){
        this.features.clearLayers();
        this._add(this.model.currentDocuments.models);
      }
    }
  },
#

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 ($('#editor-field-type-geom').attr('checked')){
        this._geomFieldName = $('.editor-geom-field > select > option:selected').val();
        this._latFieldName = this._lonFieldName = false;
    } else {
        this._geomFieldName = false;
        this._latFieldName = $('.editor-lat-field > select > option:selected').val();
        this._lonFieldName = $('.editor-lon-field > select > option:selected').val();
    }
    this.geomReady = (this._geomFieldName || (this._latFieldName && this._lonFieldName));
    this.redraw();

    return false;
  },
#

Public: Shows the relevant select lists depending on the location field type selected.

  onFieldTypeChange: function(e){
    if (e.target.value == 'geom'){
        $('.editor-field-type-geom').show();
        $('.editor-field-type-latlon').hide();
    } else {
        $('.editor-field-type-geom').hide();
        $('.editor-field-type-latlon').show();
    }
  },
#

Private: Add one or n features to the map

For each document 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 document fields.

  _add: function(doc){

    var self = this;

    if (!(doc instanceof Array)) doc = [doc];

    doc.forEach(function(doc){
      var feature = self._getGeometryFromDocument(doc);
      if (feature instanceof Object){
#

Build popup contents TODO: mustache?

        html = ''
        for (key in doc.attributes){
          html += '<div><strong>' + key + '</strong>: '+ doc.attributes[key] + '</div>'
        }
        feature.properties = {popupContent: html};
#

Add a reference to the model id, which will allow us to link this Leaflet layer to a Recline doc

        feature.properties.cid = doc.cid;

        try {
            self.features.addGeoJSON(feature);
        } catch (except) {
            var msg = 'Wrong geometry value';
            if (except.message) msg += ' (' + except.message + ')';
            my.notify(msg,{category:'error'});
            _.breakLoop();
        }
      } else {
        my.notify('Wrong geometry value',{category:'error'});
        _.breakLoop();
      }
    });
  },
#

Private: Remove one or n features to the map

  _remove: function(doc){

    var self = this;

    if (!(doc instanceof Array)) doc = [doc];

    doc.forEach(function(doc){
      for (key in self.features._layers){
        if (self.features._layers[key].cid == doc.cid){
          self.features.removeLayer(self.features._layers[key]);
        }
      }
    });

  },
#

Private: Return a GeoJSON geomtry extracted from the document fields

  _getGeometryFromDocument: function(doc){
    if (this.geomReady){
      if (this._geomFieldName){
#

We assume that the contents of the field are a valid GeoJSON object

        return doc.attributes[this._geomFieldName];
      } else if (this._lonFieldName && this._latFieldName){
#

We'll create a GeoJSON like point object from the two lat/lon fields

        return {
          type: 'Point',
          coordinates: [
            doc.attributes[this._lonFieldName],
            doc.attributes[this._latFieldName]
            ]
        };
      }
      return null;
    }
  },
#

Private: Check if there is a field with GeoJSON geometries or alternatively, two fields with lat/lon values.

If not found, the user can define them via the UI form.

  _setupGeometryField: function(){
    var geomField, latField, lonField;

    this._geomFieldName = this._checkField(this.geometryFieldNames);
    this._latFieldName = this._checkField(this.latitudeFieldNames);
    this._lonFieldName = this._checkField(this.longitudeFieldNames);

    this.geomReady = (this._geomFieldName || (this._latFieldName && this._lonFieldName));
  },
#

Private: Check if a field in the current model exists in the provided list of names.

  _checkField: function(fieldNames){
    var field;
    var modelFieldNames = this.model.fields.pluck('id');
    for (var i = 0; i < fieldNames.length; i++){
      for (var j = 0; j < modelFieldNames.length; j++){
        if (modelFieldNames[j].toLowerCase() == fieldNames[i].toLowerCase())
          return modelFieldNames[j];
      }
    }
    return null;
  },
#

Private: Sets up the Leaflet map control and the features layer.

The map uses a base layer from MapQuest based on OpenStreetMap.

  _setupMap: function(){

    this.map = new L.Map(this.$map.get(0));

    var mapUrl = "http://otile{s}.mqcdn.com/tiles/1.0.0/osm/{z}/{x}/{y}.png";
    var osmAttribution = 'Map data &copy; 2011 OpenStreetMap contributors, Tiles Courtesy of <a href="http://www.mapquest.com/" target="_blank">MapQuest</a> <img src="http://developer.mapquest.com/content/osm/mq_logo.png">';
    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.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;
        }
      });
    }
  }

 });

})(jQuery, recline.View);