mapshaper 0.5.70 → 0.5.71

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.
Files changed (3) hide show
  1. package/mapshaper.js +258 -160
  2. package/package.json +1 -1
  3. package/www/mapshaper.js +258 -160
package/mapshaper.js CHANGED
@@ -18678,7 +18678,8 @@ ${svg}
18678
18678
  .option('no-replace', noReplaceOpt);
18679
18679
 
18680
18680
  parser.command('classify')
18681
- .describe('apply sequential or categorical classification')
18681
+ // .describe('apply sequential or categorical classification')
18682
+ .describe('assign colors or values using one of several methods')
18682
18683
  .option('field', {
18683
18684
  describe: 'name of field to classify',
18684
18685
  DEFAULT: true
@@ -18698,6 +18699,10 @@ ${svg}
18698
18699
  // deprecated in favor of colors=
18699
18700
  // describe: 'name of a predefined color scheme (see -colors command)'
18700
18701
  })
18702
+ .option('non-adjacent', {
18703
+ describe: 'assign non-adjacent colors to a polygon layer',
18704
+ assign_to: 'method'
18705
+ })
18701
18706
  .option('stops', {
18702
18707
  describe: 'a pair of values (0-100) for limiting a color ramp',
18703
18708
  type: 'numbers'
@@ -18782,7 +18787,6 @@ ${svg}
18782
18787
  .option('key-last-suffix', {
18783
18788
  describe: 'string to append to last label'
18784
18789
  })
18785
-
18786
18790
  .option('target', targetOpt);
18787
18791
 
18788
18792
  parser.command('clean')
@@ -28063,25 +28067,241 @@ ${svg}
28063
28067
  return maxId + 1;
28064
28068
  }
28065
28069
 
28066
- 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
+ function getNonAdjacentClassifier(lyr, dataset, colors) {
28212
+ requirePolygonLayer(lyr);
28213
+ var getNeighbors = getNeighborLookupFunction(lyr, dataset.arcs);
28214
+ var errorCount = 0;
28215
+ var data = utils.range(getFeatureCount(lyr)).map(function(shpId) {
28216
+ var nabes = getNeighbors(shpId) || [];
28217
+ return {
28218
+ nabes: nabes,
28219
+ n: nabes.length,
28220
+ colorId: -1
28221
+ };
28222
+ });
28223
+ var getSortedColorIds = getUpdateFunction(colors.length);
28224
+ var colorIds = getSortedColorIds();
28225
+ // Sort adjacency data by number of neighbors in descending order
28226
+ var sorted = data.concat();
28227
+ utils.sortOn(sorted, 'n', false);
28228
+ // Assign colors, starting with polygons with the largest number of neighbors
28229
+ sorted.forEach(function(d) {
28230
+ var colorId = pickColor(d, data, colorIds);
28231
+ if (colorId == -1) {
28232
+ errorCount++;
28233
+ colorId = colorIds[0];
28234
+ }
28235
+ d.colorId = colorId;
28236
+ colorIds = getSortedColorIds(colorId);
28237
+ });
28238
+
28239
+ if (errorCount > 0) {
28240
+ message(`Unable to find non-adjacent colors for ${errorCount} ${errorCount == 1 ? 'polygon' : 'polygons'}`);
28241
+ }
28242
+ return function(shpId) {
28243
+ return colors[data[shpId].colorId];
28244
+ };
28245
+ }
28246
+
28247
+ // Pick the id of a color that is not shared with a neighboring polygon
28248
+ function pickColor(d, data, colorIds) {
28249
+ var candidateId;
28250
+ for (var i=0; i<colorIds.length; i++) {
28251
+ candidateId = colorIds[i];
28252
+ if (isAvailableColor(d, data, candidateId)) {
28253
+ return candidateId;
28254
+ }
28255
+ }
28256
+ return -1; // no colors are available
28257
+ }
28258
+
28259
+ function isAvailableColor(d, data, colorId) {
28260
+ var nabes = d.nabes;
28261
+ for (var i=0; i<nabes.length; i++) {
28262
+ if (data[nabes[i]].colorId === colorId) return false;
28263
+ }
28264
+ return true;
28265
+ }
28266
+
28267
+ // Update function returns an array of ids, sorted in descending order of preference
28268
+ // (less-used ids are preferred).
28269
+ // Function recieves an (optional) id that was just used.
28270
+ function getUpdateFunction(n) {
28271
+ var ids = utils.range(n);
28272
+ var counts = new Uint32Array(n);
28273
+ return function(i) {
28274
+ if (i >= 0 && i < n) {
28275
+ counts[i]++;
28276
+ utils.sortArrayIndex(ids, counts, true);
28277
+ } else if (i !== undefined) {
28278
+ error('Unexpected color index:', i);
28279
+ }
28280
+ return ids;
28281
+ };
28282
+ }
28283
+
28284
+ cmd.classify = function(lyr, dataset, optsArg) {
28285
+ if (!lyr.data) {
28286
+ initDataTable(lyr);
28287
+ }
28067
28288
  var opts = optsArg || {};
28068
28289
  var records = lyr.data && lyr.data.getRecords();
28069
28290
  var nullValue = opts.null_value || null;
28070
28291
  var looksLikeColors = !!opts.colors || !!opts.color_scheme;
28071
28292
  var colorScheme;
28072
- var classValues, classify;
28293
+ var classValues, classifyByValue, classifyById;
28073
28294
  var numBuckets, numValues;
28074
28295
  var dataField, outputField;
28075
28296
 
28076
28297
  // validate explicitly set classes
28077
28298
  if (opts.classes) {
28078
28299
  if (!utils.isInteger(opts.classes) || opts.classes > 1 === false) {
28079
- stop('Invalid classes= value:', opts.classes);
28300
+ stop('Invalid number of classes:', opts.classes, '(expected a value greater than 1)');
28080
28301
  }
28081
28302
  numBuckets = opts.classes;
28082
28303
  }
28083
28304
 
28084
-
28085
28305
  // TODO: better validation of breaks values
28086
28306
  if (opts.breaks) {
28087
28307
  numBuckets = opts.breaks.length + 1;
@@ -28099,9 +28319,6 @@ ${svg}
28099
28319
 
28100
28320
  } else if (opts.field) {
28101
28321
  dataField = opts.field;
28102
-
28103
- } else {
28104
- stop('Missing a data field to classify');
28105
28322
  }
28106
28323
 
28107
28324
  // expand categories if value is '*'
@@ -28109,7 +28326,18 @@ ${svg}
28109
28326
  opts.categories = getUniqFieldValues(records, dataField);
28110
28327
  }
28111
28328
 
28112
- requireDataField(lyr.data, dataField);
28329
+ if (opts.method == 'non-adjacent') {
28330
+ if (lyr.geometry_type != 'polygon') {
28331
+ stop('The non-adjacent option requires a polygon layer');
28332
+ }
28333
+ if (dataField) {
28334
+ stop('The non-adjacent option does not accept a data field argument');
28335
+ }
28336
+ } else if (!dataField) {
28337
+ stop('Missing a data field to classify');
28338
+ } else {
28339
+ requireDataField(lyr.data, dataField);
28340
+ }
28113
28341
 
28114
28342
  if (numBuckets) {
28115
28343
  numValues = opts.continuous ? numBuckets + 1 : numBuckets;
@@ -28177,15 +28405,25 @@ ${svg}
28177
28405
 
28178
28406
  // get a function to convert input data to class indexes
28179
28407
  //
28180
- if (opts.index_field) {
28408
+ if (opts.method == 'non-adjacent') {
28409
+ classifyById = getNonAdjacentClassifier(lyr, dataset, classValues);
28410
+ } else if (opts.index_field) {
28181
28411
  // data is pre-classified... just read the index from a field
28182
- classify = getIndexedClassifier(classValues, nullValue, opts);
28412
+ classifyByValue = getIndexedClassifier(classValues, nullValue, opts);
28183
28413
  } else if (opts.categories) {
28184
- classify = getCategoricalClassifier(classValues, nullValue, opts);
28414
+ classifyByValue = getCategoricalClassifier(classValues, nullValue, opts);
28185
28415
  } else {
28186
- classify = getSequentialClassifier(classValues, nullValue, getFieldValues(records, dataField), opts);
28416
+ classifyByValue = getSequentialClassifier(classValues, nullValue, getFieldValues(records, dataField), opts);
28417
+ }
28418
+
28419
+ if (classifyByValue) {
28420
+ classifyById = function(id) {
28421
+ var d = records[id] || {};
28422
+ return classifyByValue(d[dataField]);
28423
+ };
28187
28424
  }
28188
28425
 
28426
+
28189
28427
  // get the name of the output field
28190
28428
  //
28191
28429
  if (looksLikeColors) {
@@ -28201,8 +28439,7 @@ ${svg}
28201
28439
  }
28202
28440
 
28203
28441
  records.forEach(function(d, i) {
28204
- d = d || {};
28205
- d[outputField] = classify(d[dataField]);
28442
+ d[outputField] = classifyById(i);
28206
28443
  });
28207
28444
  };
28208
28445
 
@@ -29014,147 +29251,6 @@ ${svg}
29014
29251
  getClipMessage: getClipMessage
29015
29252
  });
29016
29253
 
29017
- // Returns a function for constructing a query function that accepts an arc id and
29018
- // returns information about the polygon or polygons that use the given arc.
29019
- // TODO: explain this better.
29020
- //
29021
- // options:
29022
- // filter: optional filter function; signature: function(idA, idB or -1) : boolean
29023
- // reusable: flag that lets an arc be queried multiple times.
29024
- function getArcClassifier(shapes, arcs) {
29025
- var opts = arguments[2] || {},
29026
- useOnce = !opts.reusable,
29027
- n = arcs.size(),
29028
- a = new Int32Array(n),
29029
- b = new Int32Array(n);
29030
-
29031
- utils.initializeArray(a, -1);
29032
- utils.initializeArray(b, -1);
29033
-
29034
- traversePaths(shapes, function(o) {
29035
- var i = absArcId(o.arcId);
29036
- var shpId = o.shapeId;
29037
- var aval = a[i];
29038
- if (aval == -1) {
29039
- a[i] = shpId;
29040
- } else if (shpId < aval) {
29041
- b[i] = aval;
29042
- a[i] = shpId;
29043
- } else {
29044
- b[i] = shpId;
29045
- }
29046
- });
29047
-
29048
- function classify(arcId, getKey) {
29049
- var i = absArcId(arcId);
29050
- var shpA = a[i];
29051
- var shpB = b[i];
29052
- var key;
29053
- if (shpA == -1) return null;
29054
- key = getKey(shpA, shpB);
29055
- if (key === null || key === false) return null;
29056
- if (useOnce) {
29057
- // arc can only be queried once
29058
- a[i] = -1;
29059
- b[i] = -1;
29060
- }
29061
- // use optional filter to exclude some arcs
29062
- if (opts.filter && !opts.filter(shpA, shpB)) return null;
29063
- return key;
29064
- }
29065
-
29066
- return function(getKey) {
29067
- return function(arcId) {
29068
- return classify(arcId, getKey);
29069
- };
29070
- };
29071
- }
29072
-
29073
- var ArcClassifier = /*#__PURE__*/Object.freeze({
29074
- __proto__: null,
29075
- getArcClassifier: getArcClassifier
29076
- });
29077
-
29078
- // Returns a function for querying the neighbors of a given shape. The function
29079
- // can be called in either of two ways:
29080
- //
29081
- // 1. function(shapeId, callback)
29082
- // Callback signature: function(adjacentShapeId, arcId)
29083
- // The callback function is called once for each arc that the given feature
29084
- // shares with another feature.
29085
- //
29086
- // 2. function(shapeId)
29087
- // The function returns an array of unique ids of neighboring shapes, or
29088
- // an empty array if a shape has no neighbors.
29089
- //
29090
- function getNeighborLookupFunction(lyr, arcs) {
29091
- var classifier = getArcClassifier(lyr.shapes, arcs, {reusable: true});
29092
- var classify = classifier(onShapes);
29093
- var currShapeId;
29094
- var neighbors;
29095
- var callback;
29096
-
29097
- function onShapes(a, b) {
29098
- if (b == -1) return -1; // outer edges are b == -1
29099
- return a == currShapeId ? b : a;
29100
- }
29101
-
29102
- function onArc(arcId) {
29103
- var nabeId = classify(arcId);
29104
- if (nabeId == -1) return;
29105
- if (callback) {
29106
- callback(nabeId, arcId);
29107
- } else if (neighbors.indexOf(nabeId) == -1) {
29108
- neighbors.push(nabeId);
29109
- }
29110
- }
29111
-
29112
- return function(shpId, cb) {
29113
- currShapeId = shpId;
29114
- if (cb) {
29115
- callback = cb;
29116
- forEachArcId(lyr.shapes[shpId], onArc);
29117
- callback = null;
29118
- } else {
29119
- neighbors = [];
29120
- forEachArcId(lyr.shapes[shpId], onArc);
29121
- return neighbors;
29122
- }
29123
- };
29124
- }
29125
-
29126
-
29127
- // Returns an array containing all pairs of adjacent shapes
29128
- // in a collection of polygon shapes. A pair of shapes is represented as
29129
- // an array of two shape indexes [a, b].
29130
- function findPairsOfNeighbors(shapes, arcs) {
29131
- var getKey = function(a, b) {
29132
- return b > -1 && a > -1 ? [a, b] : null;
29133
- };
29134
- var classify = getArcClassifier(shapes, arcs)(getKey);
29135
- var arr = [];
29136
- var index = {};
29137
- var onArc = function(arcId) {
29138
- var obj = classify(arcId);
29139
- var key;
29140
- if (obj) {
29141
- key = obj.join('~');
29142
- if (key in index === false) {
29143
- arr.push(obj);
29144
- index[key] = true;
29145
- }
29146
- }
29147
- };
29148
- forEachArcId(shapes, onArc);
29149
- return arr;
29150
- }
29151
-
29152
- var PolygonNeighbors = /*#__PURE__*/Object.freeze({
29153
- __proto__: null,
29154
- getNeighborLookupFunction: getNeighborLookupFunction,
29155
- findPairsOfNeighbors: findPairsOfNeighbors
29156
- });
29157
-
29158
29254
  // Assign a cluster id to each polygon in a dataset, which can be used with
29159
29255
  // one of the dissolve commands to dissolve the clusters
29160
29256
  // Works by iteratively grouping pairs of polygons with the smallest distance
@@ -29840,11 +29936,12 @@ ${svg}
29840
29936
  getJoinFilter: getJoinFilter
29841
29937
  });
29842
29938
 
29939
+ // Join data from @src table to records in @dest table
29843
29940
  function joinTables(dest, src, join, opts) {
29844
29941
  return joinTableToLayer({data: dest}, src, join, opts);
29845
29942
  }
29846
29943
 
29847
- // Join data from @src table to records in @dest table
29944
+ // Join data from @src table to records in @destLyr layer.
29848
29945
  // @join function
29849
29946
  // Receives index of record in the dest table
29850
29947
  // Returns array of matching records in src table, or null if no matches
@@ -29867,7 +29964,7 @@ ${svg}
29867
29964
  retn = {},
29868
29965
  srcRec, srcId, destRec, joins, count, filter, calc, i, j, n, m;
29869
29966
 
29870
- // support for duplication
29967
+ // support for duplication of destination records
29871
29968
  var duplicateRecords, destShapes;
29872
29969
  if (useDuplication) {
29873
29970
  if (opts.calc) stop('duplication and calc options cannot be used together');
@@ -29896,6 +29993,7 @@ ${svg}
29896
29993
  for (j=0, count=0, m=joins ? joins.length : 0; j<m; j++) {
29897
29994
  srcId = joins[j];
29898
29995
  srcRec = srcRecords[srcId];
29996
+ // duplication mode: many-to-one joins add new features to the target layer.
29899
29997
  if (count > 0 && useDuplication) {
29900
29998
  destRec = copyRecord(duplicateRecords[i]);
29901
29999
  destRecords.push(destRec);
@@ -37689,7 +37787,7 @@ ${svg}
37689
37787
  applyCommandToEachLayer(cmd.calc, targetLayers, arcs, opts);
37690
37788
 
37691
37789
  } else if (name == 'classify') {
37692
- applyCommandToEachLayer(cmd.classify, targetLayers, opts);
37790
+ applyCommandToEachLayer(cmd.classify, targetLayers, targetDataset, opts);
37693
37791
 
37694
37792
  } else if (name == 'clean') {
37695
37793
  cmd.cleanLayers(targetLayers, targetDataset, opts);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mapshaper",
3
- "version": "0.5.70",
3
+ "version": "0.5.71",
4
4
  "description": "A tool for editing vector datasets for mapping and GIS.",
5
5
  "keywords": [
6
6
  "shapefile",
package/www/mapshaper.js CHANGED
@@ -18678,7 +18678,8 @@ ${svg}
18678
18678
  .option('no-replace', noReplaceOpt);
18679
18679
 
18680
18680
  parser.command('classify')
18681
- .describe('apply sequential or categorical classification')
18681
+ // .describe('apply sequential or categorical classification')
18682
+ .describe('assign colors or values using one of several methods')
18682
18683
  .option('field', {
18683
18684
  describe: 'name of field to classify',
18684
18685
  DEFAULT: true
@@ -18698,6 +18699,10 @@ ${svg}
18698
18699
  // deprecated in favor of colors=
18699
18700
  // describe: 'name of a predefined color scheme (see -colors command)'
18700
18701
  })
18702
+ .option('non-adjacent', {
18703
+ describe: 'assign non-adjacent colors to a polygon layer',
18704
+ assign_to: 'method'
18705
+ })
18701
18706
  .option('stops', {
18702
18707
  describe: 'a pair of values (0-100) for limiting a color ramp',
18703
18708
  type: 'numbers'
@@ -18782,7 +18787,6 @@ ${svg}
18782
18787
  .option('key-last-suffix', {
18783
18788
  describe: 'string to append to last label'
18784
18789
  })
18785
-
18786
18790
  .option('target', targetOpt);
18787
18791
 
18788
18792
  parser.command('clean')
@@ -28063,25 +28067,241 @@ ${svg}
28063
28067
  return maxId + 1;
28064
28068
  }
28065
28069
 
28066
- 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
+ function getNonAdjacentClassifier(lyr, dataset, colors) {
28212
+ requirePolygonLayer(lyr);
28213
+ var getNeighbors = getNeighborLookupFunction(lyr, dataset.arcs);
28214
+ var errorCount = 0;
28215
+ var data = utils.range(getFeatureCount(lyr)).map(function(shpId) {
28216
+ var nabes = getNeighbors(shpId) || [];
28217
+ return {
28218
+ nabes: nabes,
28219
+ n: nabes.length,
28220
+ colorId: -1
28221
+ };
28222
+ });
28223
+ var getSortedColorIds = getUpdateFunction(colors.length);
28224
+ var colorIds = getSortedColorIds();
28225
+ // Sort adjacency data by number of neighbors in descending order
28226
+ var sorted = data.concat();
28227
+ utils.sortOn(sorted, 'n', false);
28228
+ // Assign colors, starting with polygons with the largest number of neighbors
28229
+ sorted.forEach(function(d) {
28230
+ var colorId = pickColor(d, data, colorIds);
28231
+ if (colorId == -1) {
28232
+ errorCount++;
28233
+ colorId = colorIds[0];
28234
+ }
28235
+ d.colorId = colorId;
28236
+ colorIds = getSortedColorIds(colorId);
28237
+ });
28238
+
28239
+ if (errorCount > 0) {
28240
+ message(`Unable to find non-adjacent colors for ${errorCount} ${errorCount == 1 ? 'polygon' : 'polygons'}`);
28241
+ }
28242
+ return function(shpId) {
28243
+ return colors[data[shpId].colorId];
28244
+ };
28245
+ }
28246
+
28247
+ // Pick the id of a color that is not shared with a neighboring polygon
28248
+ function pickColor(d, data, colorIds) {
28249
+ var candidateId;
28250
+ for (var i=0; i<colorIds.length; i++) {
28251
+ candidateId = colorIds[i];
28252
+ if (isAvailableColor(d, data, candidateId)) {
28253
+ return candidateId;
28254
+ }
28255
+ }
28256
+ return -1; // no colors are available
28257
+ }
28258
+
28259
+ function isAvailableColor(d, data, colorId) {
28260
+ var nabes = d.nabes;
28261
+ for (var i=0; i<nabes.length; i++) {
28262
+ if (data[nabes[i]].colorId === colorId) return false;
28263
+ }
28264
+ return true;
28265
+ }
28266
+
28267
+ // Update function returns an array of ids, sorted in descending order of preference
28268
+ // (less-used ids are preferred).
28269
+ // Function recieves an (optional) id that was just used.
28270
+ function getUpdateFunction(n) {
28271
+ var ids = utils.range(n);
28272
+ var counts = new Uint32Array(n);
28273
+ return function(i) {
28274
+ if (i >= 0 && i < n) {
28275
+ counts[i]++;
28276
+ utils.sortArrayIndex(ids, counts, true);
28277
+ } else if (i !== undefined) {
28278
+ error('Unexpected color index:', i);
28279
+ }
28280
+ return ids;
28281
+ };
28282
+ }
28283
+
28284
+ cmd.classify = function(lyr, dataset, optsArg) {
28285
+ if (!lyr.data) {
28286
+ initDataTable(lyr);
28287
+ }
28067
28288
  var opts = optsArg || {};
28068
28289
  var records = lyr.data && lyr.data.getRecords();
28069
28290
  var nullValue = opts.null_value || null;
28070
28291
  var looksLikeColors = !!opts.colors || !!opts.color_scheme;
28071
28292
  var colorScheme;
28072
- var classValues, classify;
28293
+ var classValues, classifyByValue, classifyById;
28073
28294
  var numBuckets, numValues;
28074
28295
  var dataField, outputField;
28075
28296
 
28076
28297
  // validate explicitly set classes
28077
28298
  if (opts.classes) {
28078
28299
  if (!utils.isInteger(opts.classes) || opts.classes > 1 === false) {
28079
- stop('Invalid classes= value:', opts.classes);
28300
+ stop('Invalid number of classes:', opts.classes, '(expected a value greater than 1)');
28080
28301
  }
28081
28302
  numBuckets = opts.classes;
28082
28303
  }
28083
28304
 
28084
-
28085
28305
  // TODO: better validation of breaks values
28086
28306
  if (opts.breaks) {
28087
28307
  numBuckets = opts.breaks.length + 1;
@@ -28099,9 +28319,6 @@ ${svg}
28099
28319
 
28100
28320
  } else if (opts.field) {
28101
28321
  dataField = opts.field;
28102
-
28103
- } else {
28104
- stop('Missing a data field to classify');
28105
28322
  }
28106
28323
 
28107
28324
  // expand categories if value is '*'
@@ -28109,7 +28326,18 @@ ${svg}
28109
28326
  opts.categories = getUniqFieldValues(records, dataField);
28110
28327
  }
28111
28328
 
28112
- requireDataField(lyr.data, dataField);
28329
+ if (opts.method == 'non-adjacent') {
28330
+ if (lyr.geometry_type != 'polygon') {
28331
+ stop('The non-adjacent option requires a polygon layer');
28332
+ }
28333
+ if (dataField) {
28334
+ stop('The non-adjacent option does not accept a data field argument');
28335
+ }
28336
+ } else if (!dataField) {
28337
+ stop('Missing a data field to classify');
28338
+ } else {
28339
+ requireDataField(lyr.data, dataField);
28340
+ }
28113
28341
 
28114
28342
  if (numBuckets) {
28115
28343
  numValues = opts.continuous ? numBuckets + 1 : numBuckets;
@@ -28177,15 +28405,25 @@ ${svg}
28177
28405
 
28178
28406
  // get a function to convert input data to class indexes
28179
28407
  //
28180
- if (opts.index_field) {
28408
+ if (opts.method == 'non-adjacent') {
28409
+ classifyById = getNonAdjacentClassifier(lyr, dataset, classValues);
28410
+ } else if (opts.index_field) {
28181
28411
  // data is pre-classified... just read the index from a field
28182
- classify = getIndexedClassifier(classValues, nullValue, opts);
28412
+ classifyByValue = getIndexedClassifier(classValues, nullValue, opts);
28183
28413
  } else if (opts.categories) {
28184
- classify = getCategoricalClassifier(classValues, nullValue, opts);
28414
+ classifyByValue = getCategoricalClassifier(classValues, nullValue, opts);
28185
28415
  } else {
28186
- classify = getSequentialClassifier(classValues, nullValue, getFieldValues(records, dataField), opts);
28416
+ classifyByValue = getSequentialClassifier(classValues, nullValue, getFieldValues(records, dataField), opts);
28417
+ }
28418
+
28419
+ if (classifyByValue) {
28420
+ classifyById = function(id) {
28421
+ var d = records[id] || {};
28422
+ return classifyByValue(d[dataField]);
28423
+ };
28187
28424
  }
28188
28425
 
28426
+
28189
28427
  // get the name of the output field
28190
28428
  //
28191
28429
  if (looksLikeColors) {
@@ -28201,8 +28439,7 @@ ${svg}
28201
28439
  }
28202
28440
 
28203
28441
  records.forEach(function(d, i) {
28204
- d = d || {};
28205
- d[outputField] = classify(d[dataField]);
28442
+ d[outputField] = classifyById(i);
28206
28443
  });
28207
28444
  };
28208
28445
 
@@ -29014,147 +29251,6 @@ ${svg}
29014
29251
  getClipMessage: getClipMessage
29015
29252
  });
29016
29253
 
29017
- // Returns a function for constructing a query function that accepts an arc id and
29018
- // returns information about the polygon or polygons that use the given arc.
29019
- // TODO: explain this better.
29020
- //
29021
- // options:
29022
- // filter: optional filter function; signature: function(idA, idB or -1) : boolean
29023
- // reusable: flag that lets an arc be queried multiple times.
29024
- function getArcClassifier(shapes, arcs) {
29025
- var opts = arguments[2] || {},
29026
- useOnce = !opts.reusable,
29027
- n = arcs.size(),
29028
- a = new Int32Array(n),
29029
- b = new Int32Array(n);
29030
-
29031
- utils.initializeArray(a, -1);
29032
- utils.initializeArray(b, -1);
29033
-
29034
- traversePaths(shapes, function(o) {
29035
- var i = absArcId(o.arcId);
29036
- var shpId = o.shapeId;
29037
- var aval = a[i];
29038
- if (aval == -1) {
29039
- a[i] = shpId;
29040
- } else if (shpId < aval) {
29041
- b[i] = aval;
29042
- a[i] = shpId;
29043
- } else {
29044
- b[i] = shpId;
29045
- }
29046
- });
29047
-
29048
- function classify(arcId, getKey) {
29049
- var i = absArcId(arcId);
29050
- var shpA = a[i];
29051
- var shpB = b[i];
29052
- var key;
29053
- if (shpA == -1) return null;
29054
- key = getKey(shpA, shpB);
29055
- if (key === null || key === false) return null;
29056
- if (useOnce) {
29057
- // arc can only be queried once
29058
- a[i] = -1;
29059
- b[i] = -1;
29060
- }
29061
- // use optional filter to exclude some arcs
29062
- if (opts.filter && !opts.filter(shpA, shpB)) return null;
29063
- return key;
29064
- }
29065
-
29066
- return function(getKey) {
29067
- return function(arcId) {
29068
- return classify(arcId, getKey);
29069
- };
29070
- };
29071
- }
29072
-
29073
- var ArcClassifier = /*#__PURE__*/Object.freeze({
29074
- __proto__: null,
29075
- getArcClassifier: getArcClassifier
29076
- });
29077
-
29078
- // Returns a function for querying the neighbors of a given shape. The function
29079
- // can be called in either of two ways:
29080
- //
29081
- // 1. function(shapeId, callback)
29082
- // Callback signature: function(adjacentShapeId, arcId)
29083
- // The callback function is called once for each arc that the given feature
29084
- // shares with another feature.
29085
- //
29086
- // 2. function(shapeId)
29087
- // The function returns an array of unique ids of neighboring shapes, or
29088
- // an empty array if a shape has no neighbors.
29089
- //
29090
- function getNeighborLookupFunction(lyr, arcs) {
29091
- var classifier = getArcClassifier(lyr.shapes, arcs, {reusable: true});
29092
- var classify = classifier(onShapes);
29093
- var currShapeId;
29094
- var neighbors;
29095
- var callback;
29096
-
29097
- function onShapes(a, b) {
29098
- if (b == -1) return -1; // outer edges are b == -1
29099
- return a == currShapeId ? b : a;
29100
- }
29101
-
29102
- function onArc(arcId) {
29103
- var nabeId = classify(arcId);
29104
- if (nabeId == -1) return;
29105
- if (callback) {
29106
- callback(nabeId, arcId);
29107
- } else if (neighbors.indexOf(nabeId) == -1) {
29108
- neighbors.push(nabeId);
29109
- }
29110
- }
29111
-
29112
- return function(shpId, cb) {
29113
- currShapeId = shpId;
29114
- if (cb) {
29115
- callback = cb;
29116
- forEachArcId(lyr.shapes[shpId], onArc);
29117
- callback = null;
29118
- } else {
29119
- neighbors = [];
29120
- forEachArcId(lyr.shapes[shpId], onArc);
29121
- return neighbors;
29122
- }
29123
- };
29124
- }
29125
-
29126
-
29127
- // Returns an array containing all pairs of adjacent shapes
29128
- // in a collection of polygon shapes. A pair of shapes is represented as
29129
- // an array of two shape indexes [a, b].
29130
- function findPairsOfNeighbors(shapes, arcs) {
29131
- var getKey = function(a, b) {
29132
- return b > -1 && a > -1 ? [a, b] : null;
29133
- };
29134
- var classify = getArcClassifier(shapes, arcs)(getKey);
29135
- var arr = [];
29136
- var index = {};
29137
- var onArc = function(arcId) {
29138
- var obj = classify(arcId);
29139
- var key;
29140
- if (obj) {
29141
- key = obj.join('~');
29142
- if (key in index === false) {
29143
- arr.push(obj);
29144
- index[key] = true;
29145
- }
29146
- }
29147
- };
29148
- forEachArcId(shapes, onArc);
29149
- return arr;
29150
- }
29151
-
29152
- var PolygonNeighbors = /*#__PURE__*/Object.freeze({
29153
- __proto__: null,
29154
- getNeighborLookupFunction: getNeighborLookupFunction,
29155
- findPairsOfNeighbors: findPairsOfNeighbors
29156
- });
29157
-
29158
29254
  // Assign a cluster id to each polygon in a dataset, which can be used with
29159
29255
  // one of the dissolve commands to dissolve the clusters
29160
29256
  // Works by iteratively grouping pairs of polygons with the smallest distance
@@ -29840,11 +29936,12 @@ ${svg}
29840
29936
  getJoinFilter: getJoinFilter
29841
29937
  });
29842
29938
 
29939
+ // Join data from @src table to records in @dest table
29843
29940
  function joinTables(dest, src, join, opts) {
29844
29941
  return joinTableToLayer({data: dest}, src, join, opts);
29845
29942
  }
29846
29943
 
29847
- // Join data from @src table to records in @dest table
29944
+ // Join data from @src table to records in @destLyr layer.
29848
29945
  // @join function
29849
29946
  // Receives index of record in the dest table
29850
29947
  // Returns array of matching records in src table, or null if no matches
@@ -29867,7 +29964,7 @@ ${svg}
29867
29964
  retn = {},
29868
29965
  srcRec, srcId, destRec, joins, count, filter, calc, i, j, n, m;
29869
29966
 
29870
- // support for duplication
29967
+ // support for duplication of destination records
29871
29968
  var duplicateRecords, destShapes;
29872
29969
  if (useDuplication) {
29873
29970
  if (opts.calc) stop('duplication and calc options cannot be used together');
@@ -29896,6 +29993,7 @@ ${svg}
29896
29993
  for (j=0, count=0, m=joins ? joins.length : 0; j<m; j++) {
29897
29994
  srcId = joins[j];
29898
29995
  srcRec = srcRecords[srcId];
29996
+ // duplication mode: many-to-one joins add new features to the target layer.
29899
29997
  if (count > 0 && useDuplication) {
29900
29998
  destRec = copyRecord(duplicateRecords[i]);
29901
29999
  destRecords.push(destRec);
@@ -37689,7 +37787,7 @@ ${svg}
37689
37787
  applyCommandToEachLayer(cmd.calc, targetLayers, arcs, opts);
37690
37788
 
37691
37789
  } else if (name == 'classify') {
37692
- applyCommandToEachLayer(cmd.classify, targetLayers, opts);
37790
+ applyCommandToEachLayer(cmd.classify, targetLayers, targetDataset, opts);
37693
37791
 
37694
37792
  } else if (name == 'clean') {
37695
37793
  cmd.cleanLayers(targetLayers, targetDataset, opts);