\
@@ -2372,31 +2650,43 @@ my.Map = Backbone.View.extend({
this.el = $(this.el);
this.visible = true;
this.mapReady = false;
+ // this will be the Leaflet L.Map object (setup below)
+ this.map = null;
var stateData = _.extend({
geomField: null,
lonField: null,
latField: null,
- autoZoom: true
+ autoZoom: true,
+ cluster: false
},
options.state
);
this.state = new recline.Model.ObjectState(stateData);
+ this._clusterOptions = {
+ zoomToBoundsOnClick: true,
+ //disableClusteringAtZoom: 10,
+ maxClusterRadius: 80,
+ singleMarkerMode: false,
+ skipDuplicateAddTesting: true,
+ animateAddingMarkers: false
+ };
+
// Listen to changes in the fields
this.model.fields.bind('change', function() {
- self._setupGeometryField()
- self.render()
+ self._setupGeometryField();
+ self.render();
});
// Listen to changes in the records
- this.model.records.bind('add', function(doc){self.redraw('add',doc)});
+ this.model.records.bind('add', function(doc){self.redraw('add',doc);});
this.model.records.bind('change', function(doc){
self.redraw('remove',doc);
self.redraw('add',doc);
});
- this.model.records.bind('remove', function(doc){self.redraw('remove',doc)});
- this.model.records.bind('reset', function(){self.redraw('reset')});
+ this.model.records.bind('remove', function(doc){self.redraw('remove',doc);});
+ this.model.records.bind('reset', function(){self.redraw('reset');});
this.menu = new my.MapMenu({
model: this.model,
@@ -2406,9 +2696,40 @@ my.Map = Backbone.View.extend({
self.state.set(self.menu.state.toJSON());
self.redraw();
});
+ this.state.bind('change', function() {
+ self.redraw();
+ });
this.elSidebar = this.menu.el;
},
+ // ## Customization Functions
+ //
+ // The following methods are designed for overriding in order to customize
+ // behaviour
+
+ // ### infobox
+ //
+ // Function to create infoboxes used in popups. The default behaviour is very simple and just lists all attributes.
+ //
+ // Users should override this function to customize behaviour i.e.
+ //
+ // view = new View({...});
+ // view.infobox = function(record) {
+ // ...
+ // }
+ infobox: function(record) {
+ var html = '';
+ for (key in record.attributes){
+ if (!(this.state.get('geomField') && key == this.state.get('geomField'))){
+ html += '
' + key + ': '+ record.attributes[key] + '
';
+ }
+ }
+ return html;
+ },
+
+ // END: Customization section
+ // ----
+
// ### Public: Adds the necessary elements to the page.
//
// Also sets up the editor fields and the map if necessary.
@@ -2442,14 +2763,34 @@ my.Map = Backbone.View.extend({
}
if (this._geomReady() && this.mapReady){
- if (action == 'reset' || action == 'refresh'){
+ // removing ad re-adding the layer enables faster bulk loading
+ this.map.removeLayer(this.features);
+ this.map.removeLayer(this.markers);
+
+ var countBefore = 0;
+ this.features.eachLayer(function(){countBefore++;});
+
+ if (action == 'refresh' || action == 'reset') {
this.features.clearLayers();
+ // recreate cluster group because of issues with clearLayer
+ this.map.removeLayer(this.markers);
+ this.markers = new L.MarkerClusterGroup(this._clusterOptions);
this._add(this.model.records.models);
} else if (action == 'add' && doc){
this._add(doc);
} else if (action == 'remove' && doc){
this._remove(doc);
}
+
+ // enable clustering if there is a large number of markers
+ var countAfter = 0;
+ this.features.eachLayer(function(){countAfter++;});
+ var sizeIncreased = countAfter - countBefore > 0;
+ if (!this.state.get('cluster') && countAfter > 64 && sizeIncreased) {
+ this.state.set({cluster: true});
+ return;
+ }
+
if (this.state.get('autoZoom')){
if (this.visible){
this._zoomToFeatures();
@@ -2457,6 +2798,11 @@ my.Map = Backbone.View.extend({
this._zoomPending = true;
}
}
+ if (this.state.get('cluster')) {
+ this.map.addLayer(this.markers);
+ } else {
+ this.map.addLayer(this.features);
+ }
}
},
@@ -2496,29 +2842,22 @@ my.Map = Backbone.View.extend({
var count = 0;
var wrongSoFar = 0;
- _.every(docs,function(doc){
+ _.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 += '
' + key + ': '+ doc.attributes[key] + '
';
- }
- }
- 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;
+ feature.properties = {
+ popupContent: self.infobox(doc),
+ // Add a reference to the model id, which will allow us to
+ // link this Leaflet layer to a Recline doc
+ cid: doc.cid
+ };
try {
- self.features.addGeoJSON(feature);
+ self.features.addData(feature);
} catch (except) {
wrongSoFar += 1;
var msg = 'Wrong geometry value';
@@ -2528,7 +2867,7 @@ my.Map = Backbone.View.extend({
}
}
} else {
- wrongSoFar += 1
+ wrongSoFar += 1;
if (wrongSoFar <= 10) {
self.trigger('recline:flash', {message: 'Wrong geometry value', category:'error'});
}
@@ -2537,7 +2876,7 @@ my.Map = Backbone.View.extend({
});
},
- // Private: Remove one or n features to the map
+ // Private: Remove one or n features from the map
//
_remove: function(docs){
@@ -2547,7 +2886,7 @@ my.Map = Backbone.View.extend({
_.each(docs,function(doc){
for (key in self.features._layers){
- if (self.features._layers[key].cid == doc.cid){
+ if (self.features._layers[key].feature.properties.cid == doc.cid){
self.features.removeLayer(self.features._layers[key]);
}
}
@@ -2646,10 +2985,10 @@ my.Map = Backbone.View.extend({
//
_zoomToFeatures: function(){
var bounds = this.features.getBounds();
- if (bounds){
+ if (bounds && bounds.getNorthEast() && bounds.getSouthWest()){
this.map.fitBounds(bounds);
} else {
- this.map.setView(new L.LatLng(0, 0), 2);
+ this.map.setView([0, 0], 2);
}
},
@@ -2659,6 +2998,7 @@ my.Map = Backbone.View.extend({
// on [OpenStreetMap](http://openstreetmap.org).
//
_setupMap: function(){
+ var self = this;
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";
@@ -2666,37 +3006,18 @@ my.Map = Backbone.View.extend({
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.markers = new L.MarkerClusterGroup(this._clusterOptions);
+ this.features = new L.GeoJSON(null,{
+ pointToLayer: function (feature, latlng) {
+ var marker = new L.marker(latlng);
+ marker.bindPopup(feature.properties.popupContent);
+ self.markers.addLayer(marker);
+ return marker;
+ }
});
- // 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.map.setView([0, 0], 2);
this.mapReady = true;
},
@@ -2767,19 +3088,23 @@ my.MapMenu = Backbone.View.extend({
\