diff --git a/src/cli/mapshaper-command-utils.mjs b/src/cli/mapshaper-command-utils.mjs index 2bc97fd0..2cde709d 100644 --- a/src/cli/mapshaper-command-utils.mjs +++ b/src/cli/mapshaper-command-utils.mjs @@ -6,7 +6,7 @@ import { dissolveArcs } from '../paths/mapshaper-arc-dissolve'; // Apply a command to an array of target layers export function applyCommandToEachLayer(func, targetLayers) { var args = utils.toArray(arguments).slice(2); - return targetLayers.reduce(function(memo, lyr) { + var output = targetLayers.reduce(function(memo, lyr) { var result = func.apply(null, [lyr].concat(args)); if (utils.isArray(result)) { // some commands return an array of layers memo = memo.concat(result); @@ -15,6 +15,7 @@ export function applyCommandToEachLayer(func, targetLayers) { } return memo; }, []); + return output.length > 0 ? output : null; } export function applyCommandToEachTarget(func, targets) { diff --git a/src/cli/mapshaper-options.mjs b/src/cli/mapshaper-options.mjs index 2cc1d9dd..c06508b5 100644 --- a/src/cli/mapshaper-options.mjs +++ b/src/cli/mapshaper-options.mjs @@ -14,9 +14,17 @@ export function getOptionParser() { alias: '+', type: 'flag', label: '+, no-replace', // show alias as primary option - // describe: 'retain the original layer(s) instead of replacing' describe: 'retain both input and output layer(s)' }, + nameOpt2 = { // for -calc and -info + describe: 'name the output layer' + }, + noReplaceOpt2 = { // for -calc and -info + alias: '+', + type: 'flag', + label: '+', + describe: 'save output to a new layer' + }, noSnapOpt = { // describe: 'don't snap points before applying command' type: 'flag' @@ -1911,8 +1919,8 @@ export function getOptionParser() { type: 'flag' }) .option('calc', calcOpt) - .option('name', nameOpt) .option('target', targetOpt) + .option('name', nameOpt) .option('no-replace', noReplaceOpt); parser.command('require') @@ -2072,7 +2080,9 @@ export function getOptionParser() { describe: 'functions: sum() average() median() max() min() count()' }) .option('where', whereOpt) - .option('target', targetOpt); + .option('target', targetOpt) + .option('to-layer', noReplaceOpt2) + .option('name', nameOpt2); parser.command('colors') .describe('print list of color scheme names'); @@ -2102,7 +2112,9 @@ export function getOptionParser() { .option('save-to', { describe: 'name of file to save info in JSON format' }) - .option('target', targetOpt); + .option('target', targetOpt) + .option('to-layer', noReplaceOpt2) + .option('name', nameOpt2); parser.command('inspect') .describe('print information about a feature') diff --git a/src/cli/mapshaper-run-command.mjs b/src/cli/mapshaper-run-command.mjs index 3c177aef..9d5af7ba 100644 --- a/src/cli/mapshaper-run-command.mjs +++ b/src/cli/mapshaper-run-command.mjs @@ -203,7 +203,7 @@ export async function runCommand(command, job) { applyCommandToEachLayer(cmd.cluster, targetLayers, arcs, opts); } else if (name == 'calc') { - applyCommandToEachLayer(cmd.calc, targetLayers, arcs, opts); + outputDataset = cmd.calc(targetLayers, arcs, opts); } else if (name == 'classify') { applyCommandToEachLayer(cmd.classify, targetLayers, targetDataset, opts); @@ -317,7 +317,7 @@ export async function runCommand(command, job) { cmd.include(opts); } else if (name == 'info') { - cmd.info(targets, opts); + outputDataset = cmd.info(targets, opts); } else if (name == 'inlay') { outputLayers = cmd.inlay(targetLayers, source, targetDataset, opts); diff --git a/src/commands/mapshaper-calc.mjs b/src/commands/mapshaper-calc.mjs index 26cae86c..a9a6d4a0 100644 --- a/src/commands/mapshaper-calc.mjs +++ b/src/commands/mapshaper-calc.mjs @@ -6,6 +6,20 @@ import cmd from '../mapshaper-cmd'; import utils from '../utils/mapshaper-utils'; import { getStashedVar } from '../mapshaper-stash'; import { message, error, stop } from '../utils/mapshaper-logging'; +import { DataTable } from '../datatable/mapshaper-data-table'; + + +cmd.calc = function(layers, arcs, opts) { + var arr = layers.map(lyr => applyCalcExpression(lyr, arcs, opts)); + if (!opts.to_layer) return null; + return { + info: {}, + layers: [{ + name: opts.name || 'info', + data: new DataTable(arr) + }] + }; +}; // Calculate an expression across a group of features, print and return the result // Supported functions include sum(), average(), max(), min(), median(), count() @@ -14,9 +28,9 @@ import { message, error, stop } from '../utils/mapshaper-logging'; // opts.expression Expression to evaluate // opts.where Optional filter expression (see -filter command) // -cmd.calc = function(lyr, arcs, opts) { +export function applyCalcExpression(lyr, arcs, opts) { var msg = opts.expression, - result, compiled, defs; + result, compiled, defs, d; if (opts.where) { // TODO: implement no_replace option for filter() instead of this lyr = getLayerSelection(lyr, arcs, opts); @@ -27,9 +41,17 @@ cmd.calc = function(lyr, arcs, opts) { defs = getStashedVar('defs'); compiled = compileCalcExpression(lyr, arcs, opts.expression); result = compiled(null, defs); - message(msg + ": " + result); - return result; -}; + if (!opts.to_layer) { + message(msg + ": " + result); + } + d = { + expression: opts.expression, + value: result + }; + if (opts.where) d.where = opts.where; + if (lyr.name) d.layer_name = lyr.name; + return d; +} export function evalCalcExpression(lyr, arcs, exp) { return compileCalcExpression(lyr, arcs, exp)(); diff --git a/src/commands/mapshaper-info.mjs b/src/commands/mapshaper-info.mjs index 6b5ab30f..072e6699 100644 --- a/src/commands/mapshaper-info.mjs +++ b/src/commands/mapshaper-info.mjs @@ -9,6 +9,7 @@ import geom from '../geom/mapshaper-geom'; import { message } from '../utils/mapshaper-logging'; import { NodeCollection } from '../topology/mapshaper-nodes'; import cmd from '../mapshaper-cmd'; +import { DataTable } from '../datatable/mapshaper-data-table'; var MAX_RULE_LEN = 50; @@ -17,7 +18,7 @@ cmd.info = function(targets, opts) { var arr = layers.map(function(o) { return getLayerInfo(o.layer, o.dataset); }); - message(formatInfo(arr)); + if (opts.save_to) { var output = [{ filename: opts.save_to + (opts.save_to.endsWith('.json') ? '' : '.json'), @@ -25,6 +26,16 @@ cmd.info = function(targets, opts) { }]; writeFiles(output, opts); } + if (opts.to_layer) { + return { + info: {}, + layers: [{ + name: opts.name || 'info', + data: new DataTable(arr) + }] + }; + } + message(formatInfo(arr)); }; cmd.printInfo = cmd.info; // old name diff --git a/test/calc-test.mjs b/test/calc-test.mjs index 4cb85eb9..a824f80a 100644 --- a/test/calc-test.mjs +++ b/test/calc-test.mjs @@ -16,8 +16,14 @@ describe('mapshaper-calc.js', function () { done(); }); }) - }); + it('+ option creates a new layer', async function() { + var data = [{a: 1}, {a: 3}]; + var cmd = 'data.json -calc + "sum(a)" -o out.csv'; + var out = await api.applyCommands(cmd, {'data.json': data}); + assert.equal(out['out.csv'], 'expression,value,layer_name\nsum(a),4,data') + }) + }); describe('evalCalcExpression()', function () { var data1 = [{foo: -1}, {foo: 3}, {foo: 4}], @@ -178,9 +184,9 @@ describe('mapshaper-calc.js', function () { data: new api.internal.DataTable(data2) }; - var result = api.cmd.calc(lyr2, null, - {no_replace: true, expression: 'average(foo)', where: '!!bar'}); - assert.deepEqual(result.data.getRecords(), [{ + var result = api.cmd.calc([lyr2], null, + {to_layer: true, expression: 'average(foo)', where: '!!bar'}); + assert.deepEqual(result.layers[0].data.getRecords(), [{ value: 1, where: '!!bar', expression: 'average(foo)' diff --git a/test/info-test.mjs b/test/info-test.mjs index ef30d455..a0d627fa 100644 --- a/test/info-test.mjs +++ b/test/info-test.mjs @@ -3,6 +3,17 @@ import api from '../mapshaper.js'; describe('mapshaper-info.js', function () { + describe('+ option', function() { + it('simple table', async function() { + var data = [{foo: 'bar'}, {foo: 'baz'}]; + var cmd = 'data.json -info + -o format=json'; + var out = await api.applyCommands(cmd, {'data.json': data}); + var d = JSON.parse(out['info.json'])[0]; + assert.equal(d.layer_name, 'data'); + assert.equal(d.feature_count, 2); + }); + + }) describe('save-to option', function() {