mapshaper 0.5.68 → 0.5.72

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/mapshaper.js CHANGED
@@ -1,6 +1,6 @@
1
1
  (function () {
2
2
 
3
- var VERSION = "0.5.68";
3
+ var VERSION = "0.5.72";
4
4
 
5
5
 
6
6
  var utils = /*#__PURE__*/Object.freeze({
@@ -1167,6 +1167,10 @@
1167
1167
  throw new UserError(formatLogArgs(arguments));
1168
1168
  };
1169
1169
 
1170
+ var _interrupt = function() {
1171
+ throw new NonFatalError(formatLogArgs(arguments));
1172
+ };
1173
+
1170
1174
  var _message = function() {
1171
1175
  logArgs(arguments);
1172
1176
  };
@@ -1185,10 +1189,14 @@
1185
1189
  }
1186
1190
 
1187
1191
  // Handle an error caused by invalid input or misuse of API
1188
- function stop () {
1192
+ function stop() {
1189
1193
  _stop.apply(null, utils.toArray(arguments));
1190
1194
  }
1191
1195
 
1196
+ function interrupt() {
1197
+ _interrupt.apply(null, utils.toArray(arguments));
1198
+ }
1199
+
1192
1200
  // Print a status message
1193
1201
  function message() {
1194
1202
  _message.apply(null, messageArgs(arguments));
@@ -1228,7 +1236,9 @@
1228
1236
  if (utils.isString(err)) {
1229
1237
  err = new UserError(err);
1230
1238
  }
1231
- if (err.name == 'UserError') {
1239
+ if (err.name == 'NonFatalError') {
1240
+ console.error(messageArgs([err.message]).join(' '));
1241
+ } else if (err.name == 'UserError') {
1232
1242
  msg = err.message;
1233
1243
  if (!/Error/.test(msg)) {
1234
1244
  msg = "Error: " + msg;
@@ -1248,6 +1258,12 @@
1248
1258
  return err;
1249
1259
  }
1250
1260
 
1261
+ function NonFatalError(msg) {
1262
+ var err = new Error(msg);
1263
+ err.name = 'NonFatalError';
1264
+ return err;
1265
+ }
1266
+
1251
1267
  function formatColumns(arr, alignments) {
1252
1268
  var widths = arr.reduce(function(memo, line) {
1253
1269
  return line.map(function(str, i) {
@@ -1308,6 +1324,7 @@
1308
1324
  loggingEnabled: loggingEnabled,
1309
1325
  error: error,
1310
1326
  stop: stop,
1327
+ interrupt: interrupt,
1311
1328
  message: message,
1312
1329
  setLoggingFunctions: setLoggingFunctions,
1313
1330
  print: print,
@@ -1315,6 +1332,7 @@
1315
1332
  debug: debug,
1316
1333
  printError: printError,
1317
1334
  UserError: UserError,
1335
+ NonFatalError: NonFatalError,
1318
1336
  formatColumns: formatColumns,
1319
1337
  formatStringsAsGrid: formatStringsAsGrid,
1320
1338
  formatLogArgs: formatLogArgs,
@@ -14306,7 +14324,7 @@
14306
14324
  joinStr = obj.tag == 'text' || obj.tag == 'tspan' ? '' : '\n';
14307
14325
  svg += '>' + joinStr;
14308
14326
  if (obj.value) {
14309
- svg += obj.value;
14327
+ svg += stringEscape(obj.value);
14310
14328
  }
14311
14329
  if (obj.children) {
14312
14330
  svg += obj.children.map(stringify).join(joinStr);
@@ -14329,8 +14347,13 @@
14329
14347
  "'": '''
14330
14348
  };
14331
14349
  function stringEscape(s) {
14332
- return String(s).replace(rxp, function(s) {
14333
- return map[s];
14350
+ return String(s).replace(rxp, function(match, i) {
14351
+ var entity = map[match];
14352
+ // don't replace & with &
14353
+ if (match == '&' && s.substr(i, entity.length) == entity) {
14354
+ return '&';
14355
+ }
14356
+ return entity;
14334
14357
  });
14335
14358
  }
14336
14359
 
@@ -18655,7 +18678,8 @@ ${svg}
18655
18678
  .option('no-replace', noReplaceOpt);
18656
18679
 
18657
18680
  parser.command('classify')
18658
- .describe('apply sequential or categorical classification')
18681
+ // .describe('apply sequential or categorical classification')
18682
+ .describe('assign colors or values using one of several methods')
18659
18683
  .option('field', {
18660
18684
  describe: 'name of field to classify',
18661
18685
  DEFAULT: true
@@ -18675,6 +18699,10 @@ ${svg}
18675
18699
  // deprecated in favor of colors=
18676
18700
  // describe: 'name of a predefined color scheme (see -colors command)'
18677
18701
  })
18702
+ .option('non-adjacent', {
18703
+ describe: 'assign non-adjacent colors to a polygon layer',
18704
+ assign_to: 'method'
18705
+ })
18678
18706
  .option('stops', {
18679
18707
  describe: 'a pair of values (0-100) for limiting a color ramp',
18680
18708
  type: 'numbers'
@@ -18759,7 +18787,6 @@ ${svg}
18759
18787
  .option('key-last-suffix', {
18760
18788
  describe: 'string to append to last label'
18761
18789
  })
18762
-
18763
18790
  .option('target', targetOpt);
18764
18791
 
18765
18792
  parser.command('clean')
@@ -19168,6 +19195,14 @@ ${svg}
19168
19195
  .option('target', targetOpt)
19169
19196
  .option('no-replace', noReplaceOpt);
19170
19197
 
19198
+ parser.command('ignore')
19199
+ // .describe('stop processing if a condition is met')
19200
+ .option('empty', {
19201
+ describe: 'ignore empty files',
19202
+ type: 'flag'
19203
+ })
19204
+ .option('target', targetOpt);
19205
+
19171
19206
  parser.command('inlay')
19172
19207
  .describe('inscribe a polygon layer inside another polygon layer')
19173
19208
  .option('source', {
@@ -19239,6 +19274,10 @@ ${svg}
19239
19274
  // describe: 'use planar geometry when interpolating by area' // useful for testing
19240
19275
  type: 'flag'
19241
19276
  })
19277
+ .option('duplication', {
19278
+ describe: 'duplicate target features on many-to-one joins',
19279
+ type: 'flag'
19280
+ })
19242
19281
  .option('string-fields', stringFieldsOpt)
19243
19282
  .option('field-types', fieldTypesOpt)
19244
19283
  .option('sum-fields', {
@@ -28028,25 +28067,301 @@ ${svg}
28028
28067
  return maxId + 1;
28029
28068
  }
28030
28069
 
28031
- cmd.classify = function(lyr, optsArg) {
28070
+ // Returns a function for constructing a query function that accepts an arc id and
28071
+ // returns information about the polygon or polygons that use the given arc.
28072
+ // TODO: explain this better.
28073
+ //
28074
+ // options:
28075
+ // filter: optional filter function; signature: function(idA, idB or -1) : boolean
28076
+ // reusable: flag that lets an arc be queried multiple times.
28077
+ function getArcClassifier(shapes, arcs) {
28078
+ var opts = arguments[2] || {},
28079
+ useOnce = !opts.reusable,
28080
+ n = arcs.size(),
28081
+ a = new Int32Array(n),
28082
+ b = new Int32Array(n);
28083
+
28084
+ utils.initializeArray(a, -1);
28085
+ utils.initializeArray(b, -1);
28086
+
28087
+ traversePaths(shapes, function(o) {
28088
+ var i = absArcId(o.arcId);
28089
+ var shpId = o.shapeId;
28090
+ var aval = a[i];
28091
+ if (aval == -1) {
28092
+ a[i] = shpId;
28093
+ } else if (shpId < aval) {
28094
+ b[i] = aval;
28095
+ a[i] = shpId;
28096
+ } else {
28097
+ b[i] = shpId;
28098
+ }
28099
+ });
28100
+
28101
+ function classify(arcId, getKey) {
28102
+ var i = absArcId(arcId);
28103
+ var shpA = a[i];
28104
+ var shpB = b[i];
28105
+ var key;
28106
+ if (shpA == -1) return null;
28107
+ key = getKey(shpA, shpB);
28108
+ if (key === null || key === false) return null;
28109
+ if (useOnce) {
28110
+ // arc can only be queried once
28111
+ a[i] = -1;
28112
+ b[i] = -1;
28113
+ }
28114
+ // use optional filter to exclude some arcs
28115
+ if (opts.filter && !opts.filter(shpA, shpB)) return null;
28116
+ return key;
28117
+ }
28118
+
28119
+ return function(getKey) {
28120
+ return function(arcId) {
28121
+ return classify(arcId, getKey);
28122
+ };
28123
+ };
28124
+ }
28125
+
28126
+ var ArcClassifier = /*#__PURE__*/Object.freeze({
28127
+ __proto__: null,
28128
+ getArcClassifier: getArcClassifier
28129
+ });
28130
+
28131
+ // Returns a function for querying the neighbors of a given shape. The function
28132
+ // can be called in either of two ways:
28133
+ //
28134
+ // 1. function(shapeId, callback)
28135
+ // Callback signature: function(adjacentShapeId, arcId)
28136
+ // The callback function is called once for each arc that the given feature
28137
+ // shares with another feature.
28138
+ //
28139
+ // 2. function(shapeId)
28140
+ // The function returns an array of unique ids of neighboring shapes, or
28141
+ // an empty array if a shape has no neighbors.
28142
+ //
28143
+ function getNeighborLookupFunction(lyr, arcs) {
28144
+ var classifier = getArcClassifier(lyr.shapes, arcs, {reusable: true});
28145
+ var classify = classifier(onShapes);
28146
+ var currShapeId;
28147
+ var neighbors;
28148
+ var callback;
28149
+
28150
+ function onShapes(a, b) {
28151
+ if (b == -1) return -1; // outer edges are b == -1
28152
+ return a == currShapeId ? b : a;
28153
+ }
28154
+
28155
+ function onArc(arcId) {
28156
+ var nabeId = classify(arcId);
28157
+ if (nabeId == -1) return;
28158
+ if (callback) {
28159
+ callback(nabeId, arcId);
28160
+ } else if (neighbors.indexOf(nabeId) == -1) {
28161
+ neighbors.push(nabeId);
28162
+ }
28163
+ }
28164
+
28165
+ return function(shpId, cb) {
28166
+ currShapeId = shpId;
28167
+ if (cb) {
28168
+ callback = cb;
28169
+ forEachArcId(lyr.shapes[shpId], onArc);
28170
+ callback = null;
28171
+ } else {
28172
+ neighbors = [];
28173
+ forEachArcId(lyr.shapes[shpId], onArc);
28174
+ return neighbors;
28175
+ }
28176
+ };
28177
+ }
28178
+
28179
+
28180
+ // Returns an array containing all pairs of adjacent shapes
28181
+ // in a collection of polygon shapes. A pair of shapes is represented as
28182
+ // an array of two shape indexes [a, b].
28183
+ function findPairsOfNeighbors(shapes, arcs) {
28184
+ var getKey = function(a, b) {
28185
+ return b > -1 && a > -1 ? [a, b] : null;
28186
+ };
28187
+ var classify = getArcClassifier(shapes, arcs)(getKey);
28188
+ var arr = [];
28189
+ var index = {};
28190
+ var onArc = function(arcId) {
28191
+ var obj = classify(arcId);
28192
+ var key;
28193
+ if (obj) {
28194
+ key = obj.join('~');
28195
+ if (key in index === false) {
28196
+ arr.push(obj);
28197
+ index[key] = true;
28198
+ }
28199
+ }
28200
+ };
28201
+ forEachArcId(shapes, onArc);
28202
+ return arr;
28203
+ }
28204
+
28205
+ var PolygonNeighbors = /*#__PURE__*/Object.freeze({
28206
+ __proto__: null,
28207
+ getNeighborLookupFunction: getNeighborLookupFunction,
28208
+ findPairsOfNeighbors: findPairsOfNeighbors
28209
+ });
28210
+
28211
+ var BALANCE_COLORS = true;
28212
+
28213
+ function getNonAdjacentClassifier(lyr, dataset, colors) {
28214
+ requirePolygonLayer(lyr);
28215
+ var getNeighbors = getNeighborLookupFunction(lyr, dataset.arcs);
28216
+ var errorCount = 0;
28217
+ var data = utils.range(getFeatureCount(lyr)).map(function(shpId) {
28218
+ var nabes = getNeighbors(shpId) || [];
28219
+ return {
28220
+ // id: shpId,
28221
+ nabes: nabes,
28222
+ colorId: -1,
28223
+ nabeColors: [],
28224
+ uncolored: nabes.length,
28225
+ saturation: 0,
28226
+ saturation2: 0
28227
+ };
28228
+ });
28229
+ var getSortedColorIds = getUpdateFunction(colors.length);
28230
+ var colorIds = getSortedColorIds();
28231
+ // Sort adjacency data by number of neighbors in descending order
28232
+ var iter = getNodeIterator(data);
28233
+ // Assign colors, starting with polygons with the largest number of neighbors
28234
+ iter.forEach(function(d) {
28235
+ var colorId = pickColor(d, data, colorIds);
28236
+ if (colorId == -1) {
28237
+ errorCount++;
28238
+ colorId = colorIds[0];
28239
+ }
28240
+ d.colorId = colorId;
28241
+ if (BALANCE_COLORS) {
28242
+ colorIds = getSortedColorIds(colorId);
28243
+ }
28244
+ });
28245
+
28246
+ if (errorCount > 0) {
28247
+ message(`Unable to find non-adjacent colors for ${errorCount} ${errorCount == 1 ? 'polygon' : 'polygons'}`);
28248
+ }
28249
+ return function(shpId) {
28250
+ return colors[data[shpId].colorId];
28251
+ };
28252
+ }
28253
+
28254
+ function getNodeIterator(data) {
28255
+ var sorted = data.concat();
28256
+ utils.sortOn(sorted, 'uncolored', true);
28257
+ function forEach(cb) {
28258
+ var item;
28259
+ while(sorted.length > 0) {
28260
+ item = sorted.pop();
28261
+ cb(item);
28262
+ updateNeighbors(item, sorted, data);
28263
+ }
28264
+ }
28265
+
28266
+ return {
28267
+ forEach: forEach
28268
+ };
28269
+ }
28270
+
28271
+ function updateNeighbors(item, sorted, data) {
28272
+ var nabe;
28273
+ var ids = item.nabes;
28274
+ for (var i=0; i<ids.length; i++) {
28275
+ nabe = data[ids[i]];
28276
+ if (nabe.colorId > -1) continue;
28277
+ updateNeighbor(nabe, item.colorId, sorted);
28278
+ }
28279
+ }
28280
+
28281
+ function updateNeighbor(a, colorId, sorted) {
28282
+ var i = sorted.indexOf(a); // could optimize with a binary search
28283
+ var n = sorted.length;
28284
+ var b;
28285
+ if (i == -1) {
28286
+ error('Indexing error');
28287
+ }
28288
+ a.uncolored--;
28289
+ a.saturation2++;
28290
+ if (!a.nabeColors.includes(colorId)) {
28291
+ a.saturation++;
28292
+ a.nabeColors.push(colorId);
28293
+ }
28294
+ // insertion sort with a stopping condition
28295
+ while (++i < n) {
28296
+ b = sorted[i];
28297
+ // standard dsatur
28298
+ if (a.saturation < b.saturation || a.saturation == b.saturation && a.uncolored > b.uncolored) break;
28299
+ // based on 4-color tests with counties and zipcodes, this condition adds a bit of strength
28300
+ if (a.saturation == b.saturation && a.uncolored == b.uncolored && a.saturation2 < b.saturation2) break; // 332
28301
+ sorted[i-1] = b;
28302
+ sorted[i] = a;
28303
+ }
28304
+ }
28305
+
28306
+
28307
+ // Pick the id of a color that is not shared with a neighboring polygon
28308
+ function pickColor(d, data, colorIds) {
28309
+ var candidateId;
28310
+ for (var i=0; i<colorIds.length; i++) {
28311
+ candidateId = colorIds[i];
28312
+ if (isAvailableColor(d, data, candidateId)) {
28313
+ return candidateId;
28314
+ }
28315
+ }
28316
+ return -1; // no colors are available
28317
+ }
28318
+
28319
+ function isAvailableColor(d, data, colorId) {
28320
+ var nabes = d.nabes;
28321
+ for (var i=0; i<nabes.length; i++) {
28322
+ if (data[nabes[i]].colorId === colorId) return false;
28323
+ }
28324
+ return true;
28325
+ }
28326
+
28327
+ // Update function returns an array of ids, sorted in descending order of preference
28328
+ // (less-used ids are preferred).
28329
+ // Function recieves an (optional) id that was just used.
28330
+ function getUpdateFunction(n) {
28331
+ var ids = utils.range(n);
28332
+ var counts = new Uint32Array(n);
28333
+ return function(i) {
28334
+ if (i >= 0 && i < n) {
28335
+ counts[i]++;
28336
+ utils.sortArrayIndex(ids, counts, true);
28337
+ } else if (i !== undefined) {
28338
+ error('Unexpected color index:', i);
28339
+ }
28340
+ return ids;
28341
+ };
28342
+ }
28343
+
28344
+ cmd.classify = function(lyr, dataset, optsArg) {
28345
+ if (!lyr.data) {
28346
+ initDataTable(lyr);
28347
+ }
28032
28348
  var opts = optsArg || {};
28033
28349
  var records = lyr.data && lyr.data.getRecords();
28034
28350
  var nullValue = opts.null_value || null;
28035
28351
  var looksLikeColors = !!opts.colors || !!opts.color_scheme;
28036
28352
  var colorScheme;
28037
- var classValues, classify;
28353
+ var classValues, classifyByValue, classifyById;
28038
28354
  var numBuckets, numValues;
28039
28355
  var dataField, outputField;
28040
28356
 
28041
28357
  // validate explicitly set classes
28042
28358
  if (opts.classes) {
28043
28359
  if (!utils.isInteger(opts.classes) || opts.classes > 1 === false) {
28044
- stop('Invalid classes= value:', opts.classes);
28360
+ stop('Invalid number of classes:', opts.classes, '(expected a value greater than 1)');
28045
28361
  }
28046
28362
  numBuckets = opts.classes;
28047
28363
  }
28048
28364
 
28049
-
28050
28365
  // TODO: better validation of breaks values
28051
28366
  if (opts.breaks) {
28052
28367
  numBuckets = opts.breaks.length + 1;
@@ -28064,9 +28379,6 @@ ${svg}
28064
28379
 
28065
28380
  } else if (opts.field) {
28066
28381
  dataField = opts.field;
28067
-
28068
- } else {
28069
- stop('Missing a data field to classify');
28070
28382
  }
28071
28383
 
28072
28384
  // expand categories if value is '*'
@@ -28074,7 +28386,18 @@ ${svg}
28074
28386
  opts.categories = getUniqFieldValues(records, dataField);
28075
28387
  }
28076
28388
 
28077
- requireDataField(lyr.data, dataField);
28389
+ if (opts.method == 'non-adjacent') {
28390
+ if (lyr.geometry_type != 'polygon') {
28391
+ stop('The non-adjacent option requires a polygon layer');
28392
+ }
28393
+ if (dataField) {
28394
+ stop('The non-adjacent option does not accept a data field argument');
28395
+ }
28396
+ } else if (!dataField) {
28397
+ stop('Missing a data field to classify');
28398
+ } else {
28399
+ requireDataField(lyr.data, dataField);
28400
+ }
28078
28401
 
28079
28402
  if (numBuckets) {
28080
28403
  numValues = opts.continuous ? numBuckets + 1 : numBuckets;
@@ -28142,15 +28465,25 @@ ${svg}
28142
28465
 
28143
28466
  // get a function to convert input data to class indexes
28144
28467
  //
28145
- if (opts.index_field) {
28468
+ if (opts.method == 'non-adjacent') {
28469
+ classifyById = getNonAdjacentClassifier(lyr, dataset, classValues);
28470
+ } else if (opts.index_field) {
28146
28471
  // data is pre-classified... just read the index from a field
28147
- classify = getIndexedClassifier(classValues, nullValue, opts);
28472
+ classifyByValue = getIndexedClassifier(classValues, nullValue, opts);
28148
28473
  } else if (opts.categories) {
28149
- classify = getCategoricalClassifier(classValues, nullValue, opts);
28474
+ classifyByValue = getCategoricalClassifier(classValues, nullValue, opts);
28150
28475
  } else {
28151
- classify = getSequentialClassifier(classValues, nullValue, getFieldValues(records, dataField), opts);
28476
+ classifyByValue = getSequentialClassifier(classValues, nullValue, getFieldValues(records, dataField), opts);
28152
28477
  }
28153
28478
 
28479
+ if (classifyByValue) {
28480
+ classifyById = function(id) {
28481
+ var d = records[id] || {};
28482
+ return classifyByValue(d[dataField]);
28483
+ };
28484
+ }
28485
+
28486
+
28154
28487
  // get the name of the output field
28155
28488
  //
28156
28489
  if (looksLikeColors) {
@@ -28166,8 +28499,7 @@ ${svg}
28166
28499
  }
28167
28500
 
28168
28501
  records.forEach(function(d, i) {
28169
- d = d || {};
28170
- d[outputField] = classify(d[dataField]);
28502
+ d[outputField] = classifyById(i);
28171
28503
  });
28172
28504
  };
28173
28505
 
@@ -28979,147 +29311,6 @@ ${svg}
28979
29311
  getClipMessage: getClipMessage
28980
29312
  });
28981
29313
 
28982
- // Returns a function for constructing a query function that accepts an arc id and
28983
- // returns information about the polygon or polygons that use the given arc.
28984
- // TODO: explain this better.
28985
- //
28986
- // options:
28987
- // filter: optional filter function; signature: function(idA, idB or -1) : boolean
28988
- // reusable: flag that lets an arc be queried multiple times.
28989
- function getArcClassifier(shapes, arcs) {
28990
- var opts = arguments[2] || {},
28991
- useOnce = !opts.reusable,
28992
- n = arcs.size(),
28993
- a = new Int32Array(n),
28994
- b = new Int32Array(n);
28995
-
28996
- utils.initializeArray(a, -1);
28997
- utils.initializeArray(b, -1);
28998
-
28999
- traversePaths(shapes, function(o) {
29000
- var i = absArcId(o.arcId);
29001
- var shpId = o.shapeId;
29002
- var aval = a[i];
29003
- if (aval == -1) {
29004
- a[i] = shpId;
29005
- } else if (shpId < aval) {
29006
- b[i] = aval;
29007
- a[i] = shpId;
29008
- } else {
29009
- b[i] = shpId;
29010
- }
29011
- });
29012
-
29013
- function classify(arcId, getKey) {
29014
- var i = absArcId(arcId);
29015
- var shpA = a[i];
29016
- var shpB = b[i];
29017
- var key;
29018
- if (shpA == -1) return null;
29019
- key = getKey(shpA, shpB);
29020
- if (key === null || key === false) return null;
29021
- if (useOnce) {
29022
- // arc can only be queried once
29023
- a[i] = -1;
29024
- b[i] = -1;
29025
- }
29026
- // use optional filter to exclude some arcs
29027
- if (opts.filter && !opts.filter(shpA, shpB)) return null;
29028
- return key;
29029
- }
29030
-
29031
- return function(getKey) {
29032
- return function(arcId) {
29033
- return classify(arcId, getKey);
29034
- };
29035
- };
29036
- }
29037
-
29038
- var ArcClassifier = /*#__PURE__*/Object.freeze({
29039
- __proto__: null,
29040
- getArcClassifier: getArcClassifier
29041
- });
29042
-
29043
- // Returns a function for querying the neighbors of a given shape. The function
29044
- // can be called in either of two ways:
29045
- //
29046
- // 1. function(shapeId, callback)
29047
- // Callback signature: function(adjacentShapeId, arcId)
29048
- // The callback function is called once for each arc that the given feature
29049
- // shares with another feature.
29050
- //
29051
- // 2. function(shapeId)
29052
- // The function returns an array of unique ids of neighboring shapes, or
29053
- // an empty array if a shape has no neighbors.
29054
- //
29055
- function getNeighborLookupFunction(lyr, arcs) {
29056
- var classifier = getArcClassifier(lyr.shapes, arcs, {reusable: true});
29057
- var classify = classifier(onShapes);
29058
- var currShapeId;
29059
- var neighbors;
29060
- var callback;
29061
-
29062
- function onShapes(a, b) {
29063
- if (b == -1) return -1; // outer edges are b == -1
29064
- return a == currShapeId ? b : a;
29065
- }
29066
-
29067
- function onArc(arcId) {
29068
- var nabeId = classify(arcId);
29069
- if (nabeId == -1) return;
29070
- if (callback) {
29071
- callback(nabeId, arcId);
29072
- } else if (neighbors.indexOf(nabeId) == -1) {
29073
- neighbors.push(nabeId);
29074
- }
29075
- }
29076
-
29077
- return function(shpId, cb) {
29078
- currShapeId = shpId;
29079
- if (cb) {
29080
- callback = cb;
29081
- forEachArcId(lyr.shapes[shpId], onArc);
29082
- callback = null;
29083
- } else {
29084
- neighbors = [];
29085
- forEachArcId(lyr.shapes[shpId], onArc);
29086
- return neighbors;
29087
- }
29088
- };
29089
- }
29090
-
29091
-
29092
- // Returns an array containing all pairs of adjacent shapes
29093
- // in a collection of polygon shapes. A pair of shapes is represented as
29094
- // an array of two shape indexes [a, b].
29095
- function findPairsOfNeighbors(shapes, arcs) {
29096
- var getKey = function(a, b) {
29097
- return b > -1 && a > -1 ? [a, b] : null;
29098
- };
29099
- var classify = getArcClassifier(shapes, arcs)(getKey);
29100
- var arr = [];
29101
- var index = {};
29102
- var onArc = function(arcId) {
29103
- var obj = classify(arcId);
29104
- var key;
29105
- if (obj) {
29106
- key = obj.join('~');
29107
- if (key in index === false) {
29108
- arr.push(obj);
29109
- index[key] = true;
29110
- }
29111
- }
29112
- };
29113
- forEachArcId(shapes, onArc);
29114
- return arr;
29115
- }
29116
-
29117
- var PolygonNeighbors = /*#__PURE__*/Object.freeze({
29118
- __proto__: null,
29119
- getNeighborLookupFunction: getNeighborLookupFunction,
29120
- findPairsOfNeighbors: findPairsOfNeighbors
29121
- });
29122
-
29123
29314
  // Assign a cluster id to each polygon in a dataset, which can be used with
29124
29315
  // one of the dissolve commands to dissolve the clusters
29125
29316
  // Works by iteratively grouping pairs of polygons with the smallest distance
@@ -29806,12 +29997,19 @@ ${svg}
29806
29997
  });
29807
29998
 
29808
29999
  // Join data from @src table to records in @dest table
30000
+ function joinTables(dest, src, join, opts) {
30001
+ return joinTableToLayer({data: dest}, src, join, opts);
30002
+ }
30003
+
30004
+ // Join data from @src table to records in @destLyr layer.
29809
30005
  // @join function
29810
30006
  // Receives index of record in the dest table
29811
30007
  // Returns array of matching records in src table, or null if no matches
29812
30008
  //
29813
- function joinTables(dest, src, join, opts) {
29814
- var srcRecords = src.getRecords(),
30009
+ function joinTableToLayer(destLyr, src, join, opts) {
30010
+ var dest = destLyr.data,
30011
+ useDuplication = !!opts.duplication,
30012
+ srcRecords = src.getRecords(),
29815
30013
  destRecords = dest.getRecords(),
29816
30014
  prefix = opts.prefix || '',
29817
30015
  unmatchedRecords = [],
@@ -29826,6 +30024,14 @@ ${svg}
29826
30024
  retn = {},
29827
30025
  srcRec, srcId, destRec, joins, count, filter, calc, i, j, n, m;
29828
30026
 
30027
+ // support for duplication of destination records
30028
+ var duplicateRecords, destShapes;
30029
+ if (useDuplication) {
30030
+ if (opts.calc) stop('duplication and calc options cannot be used together');
30031
+ duplicateRecords = dest.clone().getRecords();
30032
+ destShapes = destLyr.shapes || [];
30033
+ }
30034
+
29829
30035
  if (opts.where) {
29830
30036
  filter = getJoinFilter(src, opts.where);
29831
30037
  }
@@ -29835,7 +30041,8 @@ ${svg}
29835
30041
  }
29836
30042
 
29837
30043
  // join source records to target records
29838
- for (i=0, n=destRecords.length; i<n; i++) {
30044
+ n = destRecords.length;
30045
+ for (i=0; i<n; i++) {
29839
30046
  destRec = destRecords[i];
29840
30047
  joins = join(i);
29841
30048
  if (joins && filter) {
@@ -29846,7 +30053,13 @@ ${svg}
29846
30053
  for (j=0, count=0, m=joins ? joins.length : 0; j<m; j++) {
29847
30054
  srcId = joins[j];
29848
30055
  srcRec = srcRecords[srcId];
29849
- if (count === 0) {
30056
+ // duplication mode: many-to-one joins add new features to the target layer.
30057
+ if (count > 0 && useDuplication) {
30058
+ destRec = copyRecord(duplicateRecords[i]);
30059
+ destRecords.push(destRec);
30060
+ destShapes.push(cloneShape(destShapes[i]));
30061
+ }
30062
+ if (count === 0 || useDuplication) {
29850
30063
  if (copyFields.length > 0) {
29851
30064
  // only copying the first match
29852
30065
  joinByCopy(destRec, srcRec, copyFields, prefix);
@@ -29878,7 +30091,7 @@ ${svg}
29878
30091
  }
29879
30092
  }
29880
30093
 
29881
- printJoinMessage(matchCount, destRecords.length,
30094
+ printJoinMessage(matchCount, n,
29882
30095
  countJoins(joinCounts), srcRecords.length, skipCount, collisionCount, collisionFields);
29883
30096
 
29884
30097
  if (opts.unjoined) {
@@ -29906,6 +30119,7 @@ ${svg}
29906
30119
  });
29907
30120
  }
29908
30121
 
30122
+
29909
30123
  function countJoins(counts) {
29910
30124
  var joinCount = 0;
29911
30125
  for (var i=0, n=counts.length; i<n; i++) {
@@ -29968,14 +30182,16 @@ ${svg}
29968
30182
 
29969
30183
  function printJoinMessage(matches, n, joins, m, skipped, collisions, collisionFields) {
29970
30184
  // TODO: add tip for troubleshooting join problems, if join is less than perfect.
30185
+ var unmatched = n - matches;
29971
30186
  if (matches > 0 === false) {
29972
30187
  message("No records could be joined");
29973
30188
  return;
29974
30189
  }
29975
30190
  message(utils.format("Joined data from %'d source record%s to %'d target record%s",
29976
30191
  joins, utils.pluralSuffix(joins), matches, utils.pluralSuffix(matches)));
29977
- if (matches < n) {
29978
- message(utils.format('%d/%d target records received no data', n-matches, n));
30192
+ if (unmatched > 0) {
30193
+ message(utils.format('%d target record%s received no data', unmatched, utils.pluralSuffix(unmatched)));
30194
+ // message(utils.format('%d target records received no data', n-matches));
29979
30195
  }
29980
30196
  if (joins < m) {
29981
30197
  message(utils.format("%d/%d source records could not be joined", m-joins, m));
@@ -30024,6 +30240,7 @@ ${svg}
30024
30240
  var JoinTables = /*#__PURE__*/Object.freeze({
30025
30241
  __proto__: null,
30026
30242
  joinTables: joinTables,
30243
+ joinTableToLayer: joinTableToLayer,
30027
30244
  validateFieldNames: validateFieldNames,
30028
30245
  updateUnmatchedRecord: updateUnmatchedRecord,
30029
30246
  findCollisionFields: findCollisionFields,
@@ -31490,13 +31707,13 @@ ${svg}
31490
31707
  // TODO: option to copy points that can't be joined to a new layer
31491
31708
  var joinFunction = getPolygonToPointsFunction(targetLyr, arcs, pointLyr, opts);
31492
31709
  prepJoinLayers(targetLyr, pointLyr);
31493
- return joinTables(targetLyr.data, pointLyr.data, joinFunction, opts);
31710
+ return joinTableToLayer(targetLyr, pointLyr.data, joinFunction, opts);
31494
31711
  }
31495
31712
 
31496
31713
  function joinPolygonsToPoints(targetLyr, polygonLyr, arcs, opts) {
31497
31714
  var joinFunction = getPointToPolygonsFunction(targetLyr, polygonLyr, arcs, opts);
31498
31715
  prepJoinLayers(targetLyr, polygonLyr);
31499
- return joinTables(targetLyr.data, polygonLyr.data, joinFunction, opts);
31716
+ return joinTableToLayer(targetLyr, polygonLyr.data, joinFunction, opts);
31500
31717
  }
31501
31718
 
31502
31719
 
@@ -33282,6 +33499,12 @@ ${svg}
33282
33499
  };
33283
33500
  }
33284
33501
 
33502
+ cmd.ignore = function(targetLayer, dataset, opts) {
33503
+ if (opts.empty && layerIsEmpty(targetLayer)) {
33504
+ interrupt('Layer is empty, stopping processing');
33505
+ }
33506
+ };
33507
+
33285
33508
  cmd.include = function(opts) {
33286
33509
  var content, obj, context;
33287
33510
  // TODO: handle web context
@@ -33657,9 +33880,10 @@ ${svg}
33657
33880
 
33658
33881
  var joinOpts = utils.extend({}, opts);
33659
33882
  var joinFunction = getPolygonToPolygonFunction(targetLyr, sourceLyr, mosaicIndex, opts);
33660
- var retn = joinTables(targetLyr.data, sourceLyr.data, joinFunction, joinOpts);
33883
+ var retn = joinTableToLayer(targetLyr, sourceLyr.data, joinFunction, joinOpts);
33661
33884
 
33662
33885
  if (opts.interpolate) {
33886
+ if (opts.duplication) stop('duplication and interpolate options cannot be used together');
33663
33887
  interpolateFieldsByArea(targetLyr, sourceLyr, mosaicIndex, opts);
33664
33888
  }
33665
33889
  return retn;
@@ -34574,7 +34798,7 @@ ${svg}
34574
34798
  function joinPointsToPoints(targetLyr, srcLyr, crs, opts) {
34575
34799
  var joinFunction = getPointToPointFunction(targetLyr, srcLyr, crs, opts);
34576
34800
  prepJoinLayers(targetLyr, srcLyr);
34577
- return joinTables(targetLyr.data, srcLyr.data, joinFunction, opts);
34801
+ return joinTableToLayer(targetLyr, srcLyr.data, joinFunction, opts);
34578
34802
  }
34579
34803
 
34580
34804
  function getPointToPointFunction(targetLyr, srcLyr, crs, opts) {
@@ -34624,14 +34848,14 @@ ${svg}
34624
34848
  }
34625
34849
  };
34626
34850
 
34627
- function joinAttributesToFeatures(lyr, srcTable, opts) {
34851
+ function joinAttributesToFeatures(destLyr, srcTable, opts) {
34628
34852
  var keys = opts.keys,
34629
34853
  destKey = keys[0],
34630
34854
  srcKey = keys[1],
34631
- destTable = lyr.data,
34855
+ destTable = destLyr.data,
34632
34856
  joinFunction = getJoinByKey(destTable, destKey, srcTable, srcKey);
34633
34857
  validateFieldNames(keys);
34634
- return joinTables(destTable, srcTable, joinFunction, opts);
34858
+ return joinTableToLayer(destLyr, srcTable, joinFunction, opts);
34635
34859
  }
34636
34860
 
34637
34861
  // Return a function for translating a target id to an array of source ids based on values
@@ -37623,7 +37847,7 @@ ${svg}
37623
37847
  applyCommandToEachLayer(cmd.calc, targetLayers, arcs, opts);
37624
37848
 
37625
37849
  } else if (name == 'classify') {
37626
- applyCommandToEachLayer(cmd.classify, targetLayers, opts);
37850
+ applyCommandToEachLayer(cmd.classify, targetLayers, targetDataset, opts);
37627
37851
 
37628
37852
  } else if (name == 'clean') {
37629
37853
  cmd.cleanLayers(targetLayers, targetDataset, opts);
@@ -37707,6 +37931,9 @@ ${svg}
37707
37931
  outputLayers = targetDataset.layers; // kludge to allow layer naming below
37708
37932
  }
37709
37933
 
37934
+ } else if (name == 'ignore') {
37935
+ applyCommandToEachLayer(cmd.ignore, targetLayers, targetDataset, opts);
37936
+
37710
37937
  } else if (name == 'include') {
37711
37938
  cmd.include(opts);
37712
37939
 
@@ -38077,6 +38304,7 @@ ${svg}
38077
38304
  cmd.options.output = outputArr;
38078
38305
  }
38079
38306
  });
38307
+
38080
38308
  runParsedCommands(commands, null, callback);
38081
38309
  }
38082
38310
 
@@ -38137,10 +38365,6 @@ ${svg}
38137
38365
  error("Changed in v0.4: runParsedCommands() takes a Catalog object");
38138
38366
  }
38139
38367
 
38140
- if (!utils.isFunction(done)) {
38141
- error("Missing a callback function");
38142
- }
38143
-
38144
38368
  if (!utils.isArray(commands)) {
38145
38369
  error("Expected an array of parsed commands");
38146
38370
  }
@@ -38161,18 +38385,25 @@ ${svg}
38161
38385
  // that it can modify the output dataset in-place instead of making a copy.
38162
38386
  commands[commands.length-1].options.final = true;
38163
38387
  }
38164
- commands = divideImportCommand(commands);
38165
- utils.reduceAsync(commands, catalog, nextCommand, done);
38166
38388
 
38167
- function nextCommand(catalog, cmd, next) {
38168
- setStateVar('current_command', cmd.name); // for log msgs
38169
- setStateVar('verbose', !!cmd.options.verbose);
38170
- setStateVar('debug', !!cmd.options.debug);
38171
- runCommand(cmd, catalog, next);
38389
+ var groups = divideImportCommand(commands);
38390
+ if (groups.length == 1) {
38391
+ // run a simple sequence of commands (input files are not batched)
38392
+ return runParsedCommands2(commands, catalog, done);
38393
+ }
38394
+
38395
+ // run duplicated commands (i.e. batch mode)
38396
+ utils.reduceAsync(groups, catalog, nextGroup, done);
38397
+
38398
+ function nextGroup(catalog, commands, next) {
38399
+ runParsedCommands2(commands, catalog, function(err, catalog) {
38400
+ err = filterError(err);
38401
+ next(err, catalog);
38402
+ });
38172
38403
  }
38173
38404
 
38174
38405
  function done(err, catalog) {
38175
- if (err) printError(err);
38406
+ err = filterError(err);
38176
38407
  cb(err, catalog);
38177
38408
  setStateVar('current_command', null);
38178
38409
  setStateVar('verbose', false);
@@ -38180,11 +38411,31 @@ ${svg}
38180
38411
  }
38181
38412
  }
38182
38413
 
38414
+ function filterError(err) {
38415
+ if (err) printError(err);
38416
+ if (err && err.name == 'NonFatalError') {
38417
+ return null;
38418
+ }
38419
+ return err;
38420
+ }
38421
+
38422
+ function runParsedCommands2(commands, catalog, cb) {
38423
+ utils.reduceAsync(commands, catalog, nextCommand, cb);
38424
+
38425
+ function nextCommand(catalog, cmd, next) {
38426
+ setStateVar('current_command', cmd.name); // for log msgs
38427
+ setStateVar('verbose', !!cmd.options.verbose);
38428
+ setStateVar('debug', !!cmd.options.debug);
38429
+ runCommand(cmd, catalog, next);
38430
+ }
38431
+ }
38432
+
38433
+
38183
38434
  // If an initial import command indicates that several input files should be
38184
38435
  // processed separately, then duplicate the sequence of commands to run
38185
38436
  // once for each input file
38186
38437
  // @commands Array of parsed commands
38187
- // Returns: either original command array or array of duplicated commands.
38438
+ // Returns: Array of one or more sequences of parsed commands
38188
38439
  //
38189
38440
  function divideImportCommand(commands) {
38190
38441
  var firstCmd = commands[0],
@@ -38192,23 +38443,23 @@ ${svg}
38192
38443
 
38193
38444
  if (firstCmd.name != 'i' || opts.stdin || opts.merge_files ||
38194
38445
  opts.combine_files || !opts.files || opts.files.length < 2) {
38195
- return commands;
38446
+ return [commands];
38196
38447
  }
38197
38448
 
38198
- return (opts.files).reduce(function(memo, file) {
38199
- var importCmd = {
38449
+ return opts.files.map(function(file) {
38450
+ var group = [{
38200
38451
  name: 'i',
38201
38452
  options: utils.defaults({
38202
38453
  files:[file],
38203
38454
  replace: true // kludge to replace data catalog
38204
38455
  }, opts)
38205
- };
38206
- memo.push(importCmd);
38207
- memo.push.apply(memo, commands.slice(1));
38208
- return memo;
38209
- }, []);
38456
+ }];
38457
+ group.push.apply(group, commands.slice(1));
38458
+ return group;
38459
+ });
38210
38460
  }
38211
38461
 
38462
+
38212
38463
  function printStartupMessages() {
38213
38464
  // print heap memory message if running with a custom amount
38214
38465
  var rxp = /^--max-old-space-size=([0-9]+)$/;