carto/lib/mess/renderer.js

466 lines
17 KiB
JavaScript
Raw Normal View History

2011-01-05 04:15:06 +08:00
var path = require('path'),
fs = require('fs'),
External = require('./external'),
2011-01-06 03:23:28 +08:00
Step = require('step'),
2011-01-05 04:15:06 +08:00
_ = require('underscore')._,
2011-01-18 02:19:14 +08:00
sys = require('sys'),
2011-01-22 00:13:48 +08:00
mess = require('mess'),
tree = require('mess/tree');
2011-01-05 04:15:06 +08:00
require.paths.unshift(path.join(__dirname, '..', 'lib'));
2011-01-18 02:19:14 +08:00
/**
* Rendering circuitry for JSON map manifests.
*
* This is node-only for the time being.
*/
2011-01-05 04:15:06 +08:00
2011-01-05 07:29:00 +08:00
/**
* Convert a two-element-per-item Array
* into an object, for the purpose of checking membership
* and replacing stuff.
* @param {Array} list a list.
*/
2011-01-05 06:44:09 +08:00
var to = function(list) {
return list && list[0] && _.reduce(list, function(m, r) {
if (r && r.length > 1) {
m[r[0]] = r[1];
}
2011-01-05 06:44:09 +08:00
return m;
}, {});
};
2011-01-05 04:15:06 +08:00
mess.Renderer = function Renderer(env) {
env = _.extend(env, {});
if (!env.data_dir) env.data_dir = '/tmp/';
if (!env.local_data_dir) env.local_data_dir = '';
if (!env.validation_data) env.validation_data = false;
2011-01-05 04:15:06 +08:00
return {
2011-01-18 02:19:14 +08:00
/**
* Keep a copy of passed-in environment variables
*/
2011-01-07 05:33:49 +08:00
env: env,
2011-01-05 07:37:40 +08:00
/**
* Wrapper for downloading externals: likely removable
*
2011-01-06 02:01:10 +08:00
* @param {String} uri the URI of the resource.
2011-01-05 07:37:40 +08:00
* @param {Function} callback
*/
2011-01-05 04:15:06 +08:00
grab: function(uri, callback) {
2011-01-22 05:17:52 +08:00
new External(this.env).process(uri, callback);
2011-01-05 04:15:06 +08:00
},
/**
* Ensure that map layers have a populated SRS value and attempt to
* autodetect SRS if missing. Requires that node-srs is available and
* that any remote datasources have been localized.
*
* @param {Object} m map object.
* @param {Function} callback
*/
ensureSRS: function(m, callback) {
Step(
function() {
var group = this.group();
var autodetect = _.filter(m.Layer, function(l) { return !l.srs; });
_.each(autodetect, function(l) {
var finish = group();
Step(
function() {
fs.readdir(path.dirname(l.Datasource.file), this);
},
function(err, files) {
var prj = _.detect(files, function(f) {
return path.extname(f).toLowerCase() == '.prj';
});
if (prj) {
prj = path.join(path.dirname(l.Datasource.file), prj);
fs.readFile(prj, 'utf-8', this);
} else {
this(new Error('No projection found'));
}
},
function(err, srs) {
if (!err) {
try {
l.srs = require('srs').parse(srs).proj4;
} catch (err) {
finish(err);
}
}
finish(err);
}
);
});
// If no layers missing SRS information, next.
autodetect.length === 0 && group()();
},
function(err) {
m.Layer = _.filter(m.Layer, function(l) { return l.srs; });
callback(err, m);
}
);
},
2011-01-05 04:15:06 +08:00
/**
2011-01-05 07:37:40 +08:00
* Download any file-based remote datsources.
*
* Usable as an entry point: does not expect any modification to
* the map object beyond JSON parsing.
*
2011-01-06 02:01:10 +08:00
* @param {Object} m map object.
2011-01-05 07:37:40 +08:00
* @param {Function} callback
2011-01-05 04:15:06 +08:00
*/
localizeExternals: function(m, callback) {
var that = this;
Step(
function() {
var group = this.group();
m.Layer.forEach(function(l) {
2011-01-05 04:15:06 +08:00
if (l.Datasource.file) {
that.grab(l.Datasource.file, group());
}
});
if (m.Layer.length == 0) group()();
2011-01-05 04:15:06 +08:00
},
function(err, results) {
2011-01-05 06:44:09 +08:00
var result_map = to(results);
m.Layer = _.map(_.filter(m.Layer,
function(l) {
return l.Datasource.file &&
result_map[l.Datasource.file];
}),
function(l) {
l.Datasource.file = result_map[l.Datasource.file];
return l;
}
);
2011-01-05 04:15:06 +08:00
callback(err, m);
}
);
},
2011-01-05 04:15:06 +08:00
/**
* Download any remote stylesheets
2011-01-05 07:37:40 +08:00
*
2011-01-06 02:01:10 +08:00
* @param {Object} m map object.
2011-01-05 07:37:40 +08:00
* @param {Function} callback
2011-01-05 04:15:06 +08:00
*/
localizeStyle: function(m, callback) {
var that = this;
Step(
function() {
var group = this.group();
m.Stylesheet.forEach(function(s) {
if (!s.id) {
that.grab(s, group());
}
2011-01-05 04:15:06 +08:00
});
group()();
2011-01-05 04:15:06 +08:00
},
function(err, results) {
2011-01-05 06:44:09 +08:00
var result_map = to(results);
for (s in m.Stylesheet) {
if (!m.Stylesheet[s].id) {
m.Stylesheet[s] = result_map[m.Stylesheet[s]];
}
2011-01-05 04:15:06 +08:00
}
callback(err, m);
}
);
},
2011-01-05 07:37:40 +08:00
/**
* Compile (already downloaded) styles with mess.js,
* calling callback with an array of [map object, [stylesheet objects]]
*
2011-01-06 02:01:10 +08:00
* Called with the results of localizeStyle or localizeExternals:
2011-01-05 07:37:40 +08:00
* expects not to handle downloading.
*
2011-01-06 02:01:10 +08:00
* @param {Object} m map object.
2011-01-05 07:37:40 +08:00
* @param {Function} callback
*/
2011-01-05 04:15:06 +08:00
style: function(m, callback) {
2011-01-15 07:51:36 +08:00
var that = this;
2011-01-05 04:15:06 +08:00
Step(
function() {
var group = this.group();
m.Stylesheet.forEach(function(s) {
if (s.id) {
group()(null, [s.id, s.data]);
} else {
fs.readFile(s, 'utf-8', function(err, data) {
group()(err, [s, data]);
});
}
2011-01-05 04:15:06 +08:00
});
},
function(e, results) {
var options = {},
group = this.group();
2011-01-05 06:44:09 +08:00
for (var i = 0, l = results.length; i < l; i++) {
2011-01-22 05:17:52 +08:00
new mess.Parser(_.extend(_.extend({
2011-01-05 04:15:06 +08:00
filename: s
2011-01-19 03:26:57 +08:00
}, that.env), this.env)).parse(results[i][1],
function(err, tree) {
2011-01-05 04:15:06 +08:00
if (err) {
2011-01-06 03:23:28 +08:00
mess.writeError(err, options);
throw err;
2011-01-05 04:15:06 +08:00
} else {
try {
2011-01-05 06:44:09 +08:00
group()(err, [
results[i][0],
tree]);
2011-01-08 05:14:37 +08:00
return;
2011-01-05 04:15:06 +08:00
} catch (e) {
throw e;
return;
2011-01-05 04:15:06 +08:00
}
}
});
}
},
2011-01-22 07:02:39 +08:00
function(err, res) {
callback(err, m, res);
2011-01-05 04:15:06 +08:00
}
);
},
/**
* Split definitions into sub-lists of definitions
* containing rules pertaining to only one
* symbolizer each
*/
2011-01-22 06:42:46 +08:00
splitSymbolizers: function(definitions) {
var bySymbolizer = {};
2011-01-21 03:20:33 +08:00
for (var i = 0; i < definitions.length; i++) {
definitions[i].symbolizers().forEach(function(sym) {
var index = sym + '/' + definitions[i].selector.attachment;
2011-01-22 06:42:46 +08:00
if(!bySymbolizer[index]) {
bySymbolizer[index] = [];
2011-01-05 04:15:06 +08:00
}
2011-01-22 06:42:46 +08:00
bySymbolizer[index].push(
definitions[i].filterSymbolizer(sym));
2011-01-21 03:20:33 +08:00
});
}
2011-01-22 06:42:46 +08:00
return bySymbolizer;
2011-01-21 03:20:33 +08:00
},
/**
* Pick the 'winners' - all elements that select
2011-01-21 04:16:30 +08:00
* properly. Apply inherited styles from their
* ancestors to them.
*/
2011-01-22 06:42:46 +08:00
processChain: function(definitions) {
2011-01-21 03:47:34 +08:00
// definitions are ordered in specificity,
// high to low
//
2011-01-22 00:13:48 +08:00
// basically if 'this level' has
2011-01-21 03:47:34 +08:00
// a filter, then keep going, otherwise
// this is the final selector.
var winners = [];
var ancestors = [];
2011-01-22 06:42:46 +08:00
var belowThreshold = false;
while (def = definitions.shift()) {
2011-01-22 06:42:46 +08:00
if (belowThreshold) {
ancestors.push(def);
} else if (def.selector.specificity()[2] > 0) {
winners.push(def);
} else {
winners.push(def);
// nothing below this level will win
2011-01-22 06:42:46 +08:00
belowThreshold = true;
2011-01-05 04:15:06 +08:00
}
}
// iterate in reverse - low to high specificity
for (var i = ancestors.length - 1; i >= 0; i--) {
for (var j = 0; j < winners.length; j++) {
2011-01-22 06:42:46 +08:00
winners[j].inheritFrom(ancestors[i]);
}
2011-01-21 03:47:34 +08:00
}
return this.resolveConditions(winners);
2011-01-21 03:47:34 +08:00
},
resolveConditions: function(definitions) {
var rules = [];
2011-01-22 00:13:48 +08:00
var negatedFilters = [];
var negatedZoom = tree.ZoomFilter.newFromRange([0, Infinity]);
2011-01-22 00:13:48 +08:00
2011-01-21 04:16:30 +08:00
for (var i = 0; i < definitions.length; i++) {
var definition = definitions[i];
var zooms = definition.selector.filters.filter(function(f) {
return f instanceof tree.ZoomFilter;
});
2011-01-22 00:13:48 +08:00
// Merge all zoom filters without overwriting existing zoom
// filters; they might be referenced in other selectors.
var zoom = tree.ZoomFilter.newFromRange([0, Infinity]);
zooms.forEach(function(f) { zoom.intersection(f); });
2011-01-22 00:13:48 +08:00
// Only add the negated current zoom when there actually are zoom filters.
2011-01-22 01:42:49 +08:00
var negation = zooms.length ? zoom.negate() : false;
zoom.intersection(negatedZoom);
var zoomRanges = zoom.getRanges();
if (!zoomRanges.length) {
continue;
} else if (negation) {
// This zoom range has ranged. Add it so that future rules
// will exclude the current zoom.
negatedZoom.intersection(negation);
}
// Resolve regular filters.
var filters = definition.selector.filters.filter(function(f) {
return f instanceof tree.Filter;
});
var negation = filters.map(function(f) { return f.negate(); });
// add in existing negations
// TODO: run uniq on this.
filters.push.apply(filters, negatedFilters);
// add this definition's filter's negations to the list
negatedFilters.push.apply(negatedFilters, negation);
definition.selector.filters = filters;
definition.selector.zoom = zoom;
// Add a separate rule for each zoom range.
for (var j = 0; j < zoomRanges.length; j++) {
var rule = definition.clone();
rule.selector.zoom = tree.ZoomFilter.newFromRange(zoomRanges[j]);
rules.push(rule);
}
2011-01-21 04:16:30 +08:00
}
return rules;
2011-01-21 04:16:30 +08:00
},
2011-01-21 03:47:34 +08:00
/**
* Find a rule like Map { background-color: #fff; },
* if any, and return a list of properties to be inserted
* into the <Map element of the resulting XML.
*
* @param {Array} rulesets the output of toList.
* @param {Object} env.
* @return {String} rendered properties.
*/
2011-01-22 06:42:46 +08:00
getMapProperties: function(rulesets, env) {
2011-01-21 23:59:18 +08:00
var properties = [];
rulesets.filter(function(r) {
2011-01-22 07:28:54 +08:00
return r.selector.layers() === 'Map';
2011-01-21 23:59:18 +08:00
}).forEach(function(r) {
for (var i = 0; i < r.rules.length; i++) {
if (r.rules[i].eval) r.rules[i] = r.rules[i].eval(env);
properties.push(r.rules[i].toXML(env));
2011-01-21 23:59:18 +08:00
}
});
return properties.join('');
2011-01-05 04:15:06 +08:00
},
2011-01-05 07:37:40 +08:00
/**
* Prepare full XML map output. Called with the results
* of this.style
*
2011-01-06 02:01:10 +08:00
* @param {Array} res array of [map object, stylesheets].
2011-01-05 07:37:40 +08:00
* @param {Function} callback
*/
2011-01-22 07:02:39 +08:00
template: function(err, m, stylesheets, callback) {
2011-01-08 04:19:23 +08:00
if (err) {
callback(err);
return;
}
// frames is a container for variables and other less.js
// constructs.
//
// effects is a container for side-effects, which currently
// are limited to FontSets.
2011-01-21 03:20:33 +08:00
var that = this,
env = {
frames: [],
effects: []
},
output = [];
var rulesets = _.flatten(stylesheets.map(function(rulesets) {
return rulesets[1].toList(env);
}));
m.Layer.forEach(function(l) {
l.styles = [];
var classes = (l['class'] || '').split(/\s+/g);
var matching = rulesets.filter(function(ruleset) {
return ruleset.selector.matches(l.id, classes);
});
2011-01-22 00:13:48 +08:00
// matching is an array of matching selectors,
// in order from high specificity to low.
var bySymbolizer = that.splitSymbolizers(matching);
for (sym in bySymbolizer) {
// Create styles out of chains of one-symbolizer rules,
// and assign those styles to layers
var new_style = new mess.tree.Style(
l.id,
sym,
that.processChain(bySymbolizer[sym]));
l.styles.push(new_style.name());
// env.effects can be modified by this call
output.push(new_style.toXML(env));
}
var nl = new mess.tree.Layer(l);
output.push(nl.toXML());
});
output.unshift(env.effects.map(function(e) {
return e.toXML(env);
}).join('\n'));
output.unshift(
'<?xml version="1.0" '
+ 'encoding="utf-8"?>\n'
+ '<!DOCTYPE Map[]>\n'
+ '<Map '
+ this.getMapProperties(rulesets, env)
+ ' srs="+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 +no_defs">\n');
output.push('</Map>');
callback(null, output.join('\n'));
2011-01-05 04:15:06 +08:00
},
/**
* Prepare a JML document (given as a string) into a
* fully-localized XML file ready for Mapnik2 consumption
*
2011-01-06 02:01:10 +08:00
* @param {String} str the JSON file as a string.
* @param {Function} callback to be called with err, XML representation.
*/
2011-01-05 04:15:06 +08:00
render: function(str, callback) {
var m = JSON.parse(str),
that = this;
this.localizeExternals(m, function(err, res) {
that.ensureSRS(res, function(err, res) {
that.localizeStyle(res, function(err, res) {
that.style(res, function(err, m, res) {
that.template(err, m, res, function(err, res) {
callback(err, res);
});
2011-01-05 04:15:06 +08:00
});
});
});
});
}
};
};
module.exports = mess;