topomerge 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. #!/usr/bin/env node
  2. var fs = require("fs"),
  3. vm = require("vm"),
  4. commander = require("commander"),
  5. topojson = require("../");
  6. commander
  7. .version(require("../package.json").version)
  8. .usage("[options] <target=source> [file]")
  9. .description("Merges the source TopoJSON geometry collection, assigning to the target.")
  10. .option("-o, --out <file>", "output topology file name; defaults to “-” for stdout", "-")
  11. .option("-k, --key <expression>", "group geometries by key")
  12. .option("-f, --filter <expression>", "filter merged geometries or meshed lines")
  13. .option("--mesh", "mesh lines instead of merging polygons")
  14. .parse(process.argv);
  15. if (commander.args.length < 1) {
  16. console.error();
  17. console.error(" error: missing source and target names");
  18. console.error();
  19. process.exit(1);
  20. } else if (commander.args.length > 2) {
  21. console.error();
  22. console.error(" error: multiple input files");
  23. console.error();
  24. process.exit(1);
  25. } else if (commander.args.length === 1) {
  26. commander.args.push("-");
  27. }
  28. var keyFunction = function() {},
  29. postfilterFunction = function() { return true; },
  30. prefilterFunction = function() { return true; };
  31. if (commander.key != null) {
  32. var keySandbox = {d: undefined, i: -1},
  33. keyContext = new vm.createContext(keySandbox),
  34. keyScript = new vm.Script("(" + commander.key + ")");
  35. keyFunction = function(d, i) {
  36. keySandbox.d = d;
  37. keySandbox.i = i;
  38. return keyScript.runInContext(keyContext);
  39. };
  40. }
  41. if (commander.filter != null) {
  42. if (commander.mesh) {
  43. var filterSandbox = {a: undefined, b: undefined},
  44. filterContext = new vm.createContext(filterSandbox),
  45. filterScript = new vm.Script("(" + commander.filter + ")");
  46. postfilterFunction = function(a, b) {
  47. filterSandbox.a = a;
  48. filterSandbox.b = b;
  49. return filterScript.runInContext(filterContext);
  50. };
  51. } else {
  52. var filterSandbox = {d: undefined, i: -1},
  53. filterContext = new vm.createContext(filterSandbox),
  54. filterScript = new vm.Script("(" + commander.filter + ")");
  55. prefilterFunction = function(d, i) {
  56. filterSandbox.d = d;
  57. filterSandbox.i = i;
  58. return filterScript.runInContext(filterContext);
  59. };
  60. }
  61. }
  62. read(commander.args[1]).then(merge).then(write(commander.out)).catch(abort);
  63. function read(file) {
  64. return new Promise(function(resolve, reject) {
  65. var data = [], stream = file === "-" ? process.stdin : fs.createReadStream(file);
  66. stream
  67. .on("data", function(d) { data.push(d); })
  68. .on("end", function() { resolve(JSON.parse(Buffer.concat(data))); })
  69. .on("error", reject);
  70. });
  71. }
  72. function merge(topology) {
  73. var name = commander.args[0], i = name.indexOf("="),
  74. sourceName = i >= 0 ? name.slice(i + 1) : name,
  75. targetName = i >= 0 ? name.slice(0, i) : name,
  76. source = topology.objects[sourceName],
  77. target = topology.objects[targetName] = {type: "GeometryCollection", geometries: []},
  78. geometries = target.geometries,
  79. geometriesByKey = {},
  80. k;
  81. if (!source) {
  82. console.error();
  83. console.error(" error: source object “" + name + "” not found");
  84. console.error();
  85. process.exit(1);
  86. }
  87. if (source.type !== "GeometryCollection") {
  88. console.error();
  89. console.error(" error: expected GeometryCollection, not " + source.type);
  90. console.error();
  91. process.exit(1);
  92. }
  93. source.geometries.forEach(function(geometry, i) {
  94. if (!prefilterFunction(geometry, i)) return;
  95. var k = stringify(keyFunction(geometry, i)), v;
  96. if (v = geometriesByKey[k]) v.push(geometry);
  97. else geometriesByKey[k] = v = [geometry];
  98. });
  99. if (commander.mesh) {
  100. for (k in geometriesByKey) {
  101. var v = geometriesByKey[k],
  102. o = topojson.meshArcs(topology, {type: "GeometryCollection", geometries: v}, postfilterFunction);
  103. o.id = k.length > 1 ? k.slice(1) : undefined;
  104. o.properties = properties(v);
  105. geometries.push(o);
  106. }
  107. } else {
  108. for (k in geometriesByKey) {
  109. var v = geometriesByKey[k],
  110. o = topojson.mergeArcs(topology, v);
  111. o.id = k.length > 1 ? k.slice(1) : undefined;
  112. o.properties = properties(v);
  113. geometries.push(o);
  114. }
  115. }
  116. return topology;
  117. }
  118. function stringify(key) {
  119. return key == null ? "$" : "$" + key;
  120. }
  121. function properties(objects) {
  122. var properties = undefined, hasProperties;
  123. objects.forEach(function(object) {
  124. var newProperties = object.properties, key;
  125. // If no properties have yet been merged,
  126. // then we need to initialize the merged properties object.
  127. if (properties === undefined) {
  128. // If the first set of properties is null, undefined or empty,
  129. // then the result of the merge will be the empty set.
  130. // Otherwise, the new properties can copied into the merged object.
  131. if (newProperties != null) for (key in newProperties) {
  132. properties = {};
  133. for (key in newProperties) properties[key] = newProperties[key];
  134. return;
  135. }
  136. properties = null;
  137. return;
  138. }
  139. // If any of the new properties are null or undefined,
  140. // then the result of the merge will be the empty set.
  141. if (newProperties == null) properties = null;
  142. if (properties === null) return;
  143. // Now mark as inconsistent any of the properties
  144. // that differ from previously-merged values.
  145. for (key in newProperties) {
  146. if ((key in properties) && !is(properties[key], newProperties[key])) {
  147. properties[key] = undefined;
  148. }
  149. }
  150. // And mark as inconsistent any of the properties
  151. // that are missing from this new set of merged values.
  152. for (key in properties) {
  153. if (!(key in newProperties)) {
  154. properties[key] = undefined;
  155. }
  156. }
  157. return object;
  158. });
  159. // Return undefined if there are no properties.
  160. for (var key in properties) {
  161. if (properties[key] !== undefined) {
  162. return properties;
  163. }
  164. }
  165. };
  166. function write(file) {
  167. var stream = (file === "-" ? process.stdout : fs.createWriteStream(file)).on("error", handleEpipe);
  168. return function(topology) {
  169. return new Promise(function(resolve, reject) {
  170. stream.on("error", reject)[stream === process.stdout ? "write" : "end"](JSON.stringify(topology) + "\n", function(error) {
  171. if (error) reject(error);
  172. else resolve();
  173. });
  174. });
  175. };
  176. }
  177. function handleEpipe(error) {
  178. if (error.code === "EPIPE" || error.errno === "EPIPE") {
  179. process.exit(0);
  180. }
  181. }
  182. function abort(error) {
  183. console.error(error.stack);
  184. }
  185. function is(x, y) {
  186. return x === y ? x !== 0 || 1 / x === 1 / y : x !== x && y !== y;
  187. }