#!/usr/bin/env node var fs = require("fs"), vm = require("vm"), commander = require("commander"), topojson = require("../"); commander .version(require("../package.json").version) .usage("[options] [file]") .description("Merges the source TopoJSON geometry collection, assigning to the target.") .option("-o, --out ", "output topology file name; defaults to “-” for stdout", "-") .option("-k, --key ", "group geometries by key") .option("-f, --filter ", "filter merged geometries or meshed lines") .option("--mesh", "mesh lines instead of merging polygons") .parse(process.argv); if (commander.args.length < 1) { console.error(); console.error(" error: missing source and target names"); console.error(); process.exit(1); } else if (commander.args.length > 2) { console.error(); console.error(" error: multiple input files"); console.error(); process.exit(1); } else if (commander.args.length === 1) { commander.args.push("-"); } var keyFunction = function() {}, postfilterFunction = function() { return true; }, prefilterFunction = function() { return true; }; if (commander.key != null) { var keySandbox = {d: undefined, i: -1}, keyContext = new vm.createContext(keySandbox), keyScript = new vm.Script("(" + commander.key + ")"); keyFunction = function(d, i) { keySandbox.d = d; keySandbox.i = i; return keyScript.runInContext(keyContext); }; } if (commander.filter != null) { if (commander.mesh) { var filterSandbox = {a: undefined, b: undefined}, filterContext = new vm.createContext(filterSandbox), filterScript = new vm.Script("(" + commander.filter + ")"); postfilterFunction = function(a, b) { filterSandbox.a = a; filterSandbox.b = b; return filterScript.runInContext(filterContext); }; } else { var filterSandbox = {d: undefined, i: -1}, filterContext = new vm.createContext(filterSandbox), filterScript = new vm.Script("(" + commander.filter + ")"); prefilterFunction = function(d, i) { filterSandbox.d = d; filterSandbox.i = i; return filterScript.runInContext(filterContext); }; } } read(commander.args[1]).then(merge).then(write(commander.out)).catch(abort); function read(file) { return new Promise(function(resolve, reject) { var data = [], stream = file === "-" ? process.stdin : fs.createReadStream(file); stream .on("data", function(d) { data.push(d); }) .on("end", function() { resolve(JSON.parse(Buffer.concat(data))); }) .on("error", reject); }); } function merge(topology) { var name = commander.args[0], i = name.indexOf("="), sourceName = i >= 0 ? name.slice(i + 1) : name, targetName = i >= 0 ? name.slice(0, i) : name, source = topology.objects[sourceName], target = topology.objects[targetName] = {type: "GeometryCollection", geometries: []}, geometries = target.geometries, geometriesByKey = {}, k; if (!source) { console.error(); console.error(" error: source object “" + name + "” not found"); console.error(); process.exit(1); } if (source.type !== "GeometryCollection") { console.error(); console.error(" error: expected GeometryCollection, not " + source.type); console.error(); process.exit(1); } source.geometries.forEach(function(geometry, i) { if (!prefilterFunction(geometry, i)) return; var k = stringify(keyFunction(geometry, i)), v; if (v = geometriesByKey[k]) v.push(geometry); else geometriesByKey[k] = v = [geometry]; }); if (commander.mesh) { for (k in geometriesByKey) { var v = geometriesByKey[k], o = topojson.meshArcs(topology, {type: "GeometryCollection", geometries: v}, postfilterFunction); o.id = k.length > 1 ? k.slice(1) : undefined; o.properties = properties(v); geometries.push(o); } } else { for (k in geometriesByKey) { var v = geometriesByKey[k], o = topojson.mergeArcs(topology, v); o.id = k.length > 1 ? k.slice(1) : undefined; o.properties = properties(v); geometries.push(o); } } return topology; } function stringify(key) { return key == null ? "$" : "$" + key; } function properties(objects) { var properties = undefined, hasProperties; objects.forEach(function(object) { var newProperties = object.properties, key; // If no properties have yet been merged, // then we need to initialize the merged properties object. if (properties === undefined) { // If the first set of properties is null, undefined or empty, // then the result of the merge will be the empty set. // Otherwise, the new properties can copied into the merged object. if (newProperties != null) for (key in newProperties) { properties = {}; for (key in newProperties) properties[key] = newProperties[key]; return; } properties = null; return; } // If any of the new properties are null or undefined, // then the result of the merge will be the empty set. if (newProperties == null) properties = null; if (properties === null) return; // Now mark as inconsistent any of the properties // that differ from previously-merged values. for (key in newProperties) { if ((key in properties) && !is(properties[key], newProperties[key])) { properties[key] = undefined; } } // And mark as inconsistent any of the properties // that are missing from this new set of merged values. for (key in properties) { if (!(key in newProperties)) { properties[key] = undefined; } } return object; }); // Return undefined if there are no properties. for (var key in properties) { if (properties[key] !== undefined) { return properties; } } }; function write(file) { var stream = (file === "-" ? process.stdout : fs.createWriteStream(file)).on("error", handleEpipe); return function(topology) { return new Promise(function(resolve, reject) { stream.on("error", reject)[stream === process.stdout ? "write" : "end"](JSON.stringify(topology) + "\n", function(error) { if (error) reject(error); else resolve(); }); }); }; } function handleEpipe(error) { if (error.code === "EPIPE" || error.errno === "EPIPE") { process.exit(0); } } function abort(error) { console.error(error.stack); } function is(x, y) { return x === y ? x !== 0 || 1 / x === 1 / y : x !== x && y !== y; }