@kartotherian/module-loader
Version:
Kartotherian loader that instantiates tilelive and Kartotherian modules
207 lines (189 loc) • 7.36 kB
JavaScript
;
let util = require('util'),
pathLib = require('path'),
_ = require('underscore'),
Promise = require('bluebird'),
yaml = require('js-yaml'),
fs = require("fs"),
Err = require('@kartotherian/err'),
checkType = require('@kartotherian/input-validator'),
libxmljs = require('libxmljs');
Promise.promisifyAll(fs);
module.exports = XmlLoader;
/**
* Load source from opts config
* @param {object} opts
* @param {object|string} opts.xml
* @param {object} opts.xmlSetAttrs
* @param {object} opts.xmlSetParams
* @param {object} opts.xmlLayers
* @param {object} opts.xmlExceptLayers
* @param {object} opts.xmlSetDataSource
* @param {function} valueResolver
* @param {function} [logger]
* @constructor
*/
function XmlLoader(opts, valueResolver, logger) {
this._resolveValue = valueResolver;
this._opts = opts;
this._log = logger || (() => {});
}
/**
* @param {string} protocol
* @return {Promise.<string>}
*/
XmlLoader.prototype.load = function load(protocol) {
let self = this,
opts = this._opts,
xmlFile = self._resolveValue(opts.xml, 'xml', true);
if (typeof xmlFile === 'object') {
// this is a module loader, allow it to update loading options
xmlFile.module.apply(opts, xmlFile.params);
xmlFile = opts.xmlFile;
}
return fs.readFileAsync(xmlFile, 'utf8')
.then(xml => self.update(xml, xmlFile))
.then(xml => {
// override all query params except protocol
return {
protocol: protocol,
xml: xml,
base: pathLib.dirname(xmlFile)
};
});
};
/**
* Actually perform the XML modifications
* @param {string} xmlData string XML
* @param {string} xmlFile the name of the xml file to include in errors
* @return {string} modified xml string
*/
XmlLoader.prototype.update = function update(xmlData, xmlFile) {
let self = this,
opts = self._opts;
if (!opts.xmlSetAttrs && !opts.xmlSetParams && !opts.xmlLayers &&
!opts.xmlExceptLayers && !opts.xmlSetDataSource
) {
return xmlData;
}
let doc = libxmljs.parseXmlString(xmlData, { noblanks: true });
// 'xmlSetAttrs' overrides root attributes. Usage:
// xmlSetAttrs: { 'font-directory': 'string' }
if (checkType(opts, 'xmlSetAttrs', 'object')) {
self._setXmlAttributes(opts.xmlSetAttrs, doc.root());
}
// 'xmlSetParams' overrides root parameter values. Usage:
// xmlSetParams: { 'maxzoom': 20, 'source': {'ref':'v1gen'} }
if (checkType(opts, 'xmlSetParams', 'object')) {
let xmlParams = doc.root().get('Parameters');
if (!xmlParams) {
throw new Err('<Parameters> xml element was not found in %j', xmlFile);
}
self._setXmlParameters(doc, opts.xmlSetParams, xmlParams, xmlFile);
}
// 'xmlLayers' selects just the layers specified by a list (could be a single string)
// Remove layers that were not listed in the layer parameter. Keep all non-layer elements.
// Alternatively, use 'xmlExceptLayers' to exclude a list of layers.
// layers: ['waterway', 'building']
let layerFunc = getLayerFilter(opts);
if (layerFunc) {
doc.childNodes().forEach(xmlChild => {
if (xmlChild.name() === 'Layer' && !layerFunc(xmlChild)) {
xmlChild.remove();
}
});
}
// 'xmlSetDataSource' allows alterations to the datasource parameters in each layer.
// could be an object or an array of objects
// use 'if' to provide a set of values to match, and 'set' to change
// values, xmlLayers/xmlExceptLayers filters
if (checkType(opts, 'xmlSetDataSource', 'object')) {
let dataSources = opts.xmlSetDataSource;
if (typeof dataSources === 'object' && !Array.isArray(dataSources)) {
dataSources = [dataSources];
}
_.each(dataSources, ds => {
if (typeof ds !== 'object' || Array.isArray(ds)) {
throw new Err('XmlLoader: xmlSetDataSource must be an object');
}
let layerFunc = getLayerFilter(ds),
conditions = false;
if (checkType(ds, 'if', 'object')) {
conditions = _.mapObject(ds.if, (value, key) => self._resolveValue(value, key));
}
doc.eachChild(xmlLayer => {
if (xmlLayer.name() !== 'Layer' || (layerFunc && !layerFunc(xmlLayer))) {
return;
}
let xmlParams = xmlLayer.get('Datasource');
if (!xmlParams) {
self._log('warn', '<Datasource> xml element was not found in layer %j in %j',
xmlLayer.attr('name').value(), xmlFile);
return;
}
if (conditions) {
if (!_.all(conditions, (val, key) =>
self._xmlParamByName(doc, xmlParams, key, xmlFile).text(val))
) {
return;
}
}
self._log('trace', 'Updating layer ' + xmlLayer.attr('name').value());
checkType(ds, 'set', 'object', true);
self._setXmlParameters(doc, ds.set, xmlParams, xmlFile);
});
});
}
return doc.toString({ cdata: true });
};
/**
* @param {xmldoc.XmlDocument} xmlParams
* @param {string} name
* @param {string} xmlFile
* @param {boolean} [createIfMissing]
* @private
*/
XmlLoader.prototype._xmlParamByName = function(doc, xmlParams, name, xmlFile, createIfMissing) {
let param = xmlParams.get(`*[@name='${name}']`);
if (!param || param.name() !== 'Parameter') {
if (!createIfMissing) {
throw new Err('<Parameter name=%j> xml element was not found in %j', name, xmlFile);
}
param = new libxmljs.Element(doc, 'Parameter');
param.attr({name});
xmlParams.addChild(param);
}
return param;
};
XmlLoader.prototype._setXmlAttributes = function(newValues, xmlElement) {
let self = this;
_.each(newValues, (value, name) => {
const attr = {};
attr[name] = self._resolveValue(value, name);
xmlElement.attr(attr);
});
};
XmlLoader.prototype._setXmlParameters = function(doc, newValues, xmlParams, xmlFile) {
let self = this;
_.each(newValues, (value, name) => {
let param = self._xmlParamByName(doc, xmlParams, name, xmlFile, true);
param.text(self._resolveValue(value, name));
});
};
// returns a function that will test a layer for being in a list (or not in a list)
function getLayerFilter(opts) {
let include = checkType(opts, 'xmlLayers', 'string-array'),
exclude = checkType(opts, 'xmlExceptLayers', 'string-array');
if (!include && !exclude) {
return undefined;
}
if (include && exclude) {
throw new Err('XmlLoader: it may be either xmlLayers or xmlExceptLayers, not both');
}
let layers = include ? opts.xmlLayers : opts.xmlExceptLayers;
if (!Array.isArray(layers)) {
throw new Err(
'XmlLoader xmlLayers/xmlExceptLayers must be a string or an array of strings');
}
return xmlChild => _.contains(layers, xmlChild.attr('name').value()) === include;
}