lib/Map.js
var async = require("async");
var __ = require("lodash");
var renderer = require("./renderer");
var projector = require("./projector");
var cartoRenderer = require("./cartoRenderer");
var BUFFER_RATIO = 0.25;
/**
* var map = new Map();
* map.addData(function(minX, minY, maxX, maxY, projection) { ... });
* map.setStyle(...);
* map.render(0, 0, 180, 90, 500, 250);
*/
// default to EPSG:3857 (web mercator)
// http://spatialreference.org/ref/sr-org/7483/
var DEFAULT_PROJECTION = "EPSG:900913";//"+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs";
var Map = function(options) {
options = options || {};
this.datasources = [];
this.styles = [];
this.projection = DEFAULT_PROJECTION;
this.assetsPath = ".";
if (options.projection){
this.projection = projector.util.cleanProjString(options.projection);
console.log(this.projection);
}
this.boundsBuffer =
("boundsBuffer" in options) ? options.boundsBuffer : BUFFER_RATIO;
this._renderer = cartoRenderer;
};
Map.prototype = {
constructor: Map,
render: function(options) {
this._getData(options.bounds, options.boundsBuffer, function(error, shapes) {
if (error) {
options.callback(error);
}
else {
this._renderer.renderImage(__.extend({}, options, {
layers: shapes,
styles: this.processedStyles,
callback: function(error, canvas) {
options.callback && options.callback(error, canvas);
}
}));
}
}.bind(this));
},
// Should this really be here, or should it exist on a different object entirely?
renderGrid: function(options) {//minX, minY, maxX, maxY, width, height, asImage, callback) {
this._getData(options.bounds, options.boundsBuffer, function(error, shapes) {
if (error) {
options.callback(error);
console.error("ERROR! "+error);
}
else {
// this._renderer.renderGrid(minX, minY, maxX, maxY, width, height, shapes, this.processedStyles, asImage, callback);
this._renderer.renderGrid(__.extend({}, options, {
layers: shapes,
styles: this.processedStyles,
callback: function(error, canvas) {
options.callback && options.callback(error, canvas);
}
}));
}
}.bind(this));
},
_getData: function(bounds, buffer, callback) {
var dataBounds = this._bufferedBounds(bounds, buffer);
// this is a bit quick and dirty - we could possibly use style data
// to figure out more detailed queries than just geographic bounds
var projection = this.projection;
var self = this;
async.map(
this.datasources,
function(datasource, dataCallback) {
var sourceName = datasource.sourceName;
var preCallback = function(error, data) {
if (!error) {
data.source = sourceName;
}
dataCallback(error, data);
};
if (typeof datasource !== "function") {
datasource = datasource.getShapes.bind(datasource);
}
// allow simple sources to just return results immediately
var syncData = datasource(dataBounds.minX, dataBounds.minY, dataBounds.maxX, dataBounds.maxY, projection, preCallback);
if (syncData) {
preCallback(null, syncData);
}
},
callback
// HACK: this is temporary until all the style machinery is done
// should really be the above line
// function(error, data) {
// if (!error) {
// data.forEach(function(collection, index) {
// collection.styles = [self.styles[index].properties];
// });
// }
// callback(error, data);
// }
);
},
_bufferedBounds: function(bounds, buffer) {
if (buffer == null) {
buffer = this.boundsBuffer;
}
if (typeof buffer === "function") {
return buffer.call(this, bounds);
}
if (typeof buffer !== "number") {
buffer = BUFFER_RATIO;
}
amount = (bounds.maxX - bounds.minX) * buffer;
return {
minX: bounds.minX - amount,
minY: bounds.minY - amount,
maxX: bounds.maxX + amount,
maxY: bounds.maxY + amount
};
},
addData: function(datasource) {
// validate datasource
if (!(typeof datasource === "function" || typeof datasource.getShapes === "function")) {
console.warn("Datasource is not a function or an object with a 'getShapes()' function.");
return false;
}
var index = this.datasources.indexOf(datasource);
if (index === -1) {
this.datasources.push(datasource);
return true;
}
return false;
},
removeData: function(datasource) {
var index = this.datasources.indexOf(datasource);
if (index > -1) {
this.datasources.splice(index, 1);
return true;
}
return false;
},
setProjection: function(projection) {
// TODO: validate this somehow?
this.projection = projection;
},
addStyle: function(style) {
// may need to do better flattening, etc.
if (Object.prototype.toString.call(style) === "[object Array]") {
this.styles = this.styles.concat(style);
}
else {
this.styles.push(style);
}
this._processStyles();
},
setRenderer: function(renderer) {
if (renderer.renderImage && renderer.renderGrid && renderer.processStyles) {
this._renderer = renderer;
this._processStyles();
}
},
/**
* Triggers all the map's datasources to prepare themselves. Usually this
* connecting to a database, loading and processing a file, etc.
* Calling this method is completely optional, but allows you to speed up
* rendering of the first tile.
*/
prepare: function() {
var projection = this.projection;
this.datasources.forEach(function(datasource) {
datasource.load && datasource.load(function(error) {
datasource.project && datasource.project(projection);
});
});
},
_processStyles: function() {
this.processedStyles = this._renderer.processStyles(this.styles, this.assetsPath);
}
};
module.exports = Map;