mapshaper 0.5.75 → 0.5.79

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.74";
3
+ var VERSION = "0.5.78";
4
4
 
5
5
 
6
6
  var utils = /*#__PURE__*/Object.freeze({
@@ -52,6 +52,7 @@
52
52
  get initializeArray () { return initializeArray; },
53
53
  get replaceArray () { return replaceArray; },
54
54
  get repeatString () { return repeatString; },
55
+ get splitLines () { return splitLines; },
55
56
  get pluralSuffix () { return pluralSuffix; },
56
57
  get endsWith () { return endsWith; },
57
58
  get lpad () { return lpad; },
@@ -61,7 +62,9 @@
61
62
  get rtrim () { return rtrim; },
62
63
  get addThousandsSep () { return addThousandsSep; },
63
64
  get numToStr () { return numToStr; },
64
- get formatNumber () { return formatNumber$2; },
65
+ get formatNumber () { return formatNumber; },
66
+ get formatIntlNumber () { return formatIntlNumber; },
67
+ get formatNumberForDisplay () { return formatNumberForDisplay; },
65
68
  get shuffle () { return shuffle; },
66
69
  get sortOn () { return sortOn; },
67
70
  get genericSort () { return genericSort; },
@@ -532,6 +535,10 @@
532
535
  return str;
533
536
  }
534
537
 
538
+ function splitLines(str) {
539
+ return str.split(/\r?\n/);
540
+ }
541
+
535
542
  function pluralSuffix(count) {
536
543
  return count != 1 ? 's' : '';
537
544
  }
@@ -584,7 +591,16 @@
584
591
  return decimals >= 0 ? num.toFixed(decimals) : String(num);
585
592
  }
586
593
 
587
- function formatNumber$2(num, decimals, nullStr, showPos) {
594
+ function formatNumber(val) {
595
+ return val + '';
596
+ }
597
+
598
+ function formatIntlNumber(val) {
599
+ var str = formatNumber(val);
600
+ return '"' + str.replace('.', ',') + '"'; // need to quote if comma-delimited
601
+ }
602
+
603
+ function formatNumberForDisplay(num, decimals, nullStr, showPos) {
588
604
  var fmt;
589
605
  if (isNaN(num)) {
590
606
  fmt = nullStr || '-';
@@ -862,7 +878,8 @@
862
878
  str = str.toUpperCase();
863
879
  }
864
880
  else if (isNumber) {
865
- str = numToStr(val, isInt ? 0 : decimals);
881
+ // str = formatNumberForDisplay(val, isInt ? 0 : decimals);
882
+ str = numToStr(val, decimals);
866
883
  if (str[0] == '-') {
867
884
  isNeg = true;
868
885
  str = str.substr(1);
@@ -1313,9 +1330,10 @@
1313
1330
  }
1314
1331
 
1315
1332
  function logArgs(args) {
1316
- if (LOGGING && !getStateVar('QUIET') && utils.isArrayLike(args)) {
1317
- (!STDOUT && console.error || console.log).call(console, formatLogArgs(args));
1318
- }
1333
+ if (!LOGGING || getStateVar('QUIET') || !utils.isArrayLike(args)) return;
1334
+ var msg = formatLogArgs(args);
1335
+ if (STDOUT) console.log(msg);
1336
+ else console.error(msg);
1319
1337
  }
1320
1338
 
1321
1339
  var Logging = /*#__PURE__*/Object.freeze({
@@ -2967,12 +2985,33 @@
2967
2985
  orient2D(cx, cy, dx, dy, bx, by) <= 0;
2968
2986
  }
2969
2987
 
2988
+ // Useful for determining if a segment that intersects another segment is
2989
+ // entering or leaving an enclosed buffer area
2990
+ // returns -1 if angle of p1p2 -> p3p4 is counter-clockwise (left turn)
2991
+ // returns 1 if angle is clockwise
2992
+ // return 0 if segments are collinear
2993
+ function segmentTurn(p1, p2, p3, p4) {
2994
+ var ax = p1[0],
2995
+ ay = p1[1],
2996
+ bx = p2[0],
2997
+ by = p2[1],
2998
+ // shift p3p4 segment to start at p2
2999
+ dx = bx - p3[0],
3000
+ dy = by - p3[1],
3001
+ cx = p4[0] + dx,
3002
+ cy = p4[1] + dy,
3003
+ orientation = orient2D(ax, ay, bx, by, cx, cy);
3004
+ if (!orientation) return 0;
3005
+ return orientation < 0 ? 1 : -1;
3006
+ }
3007
+
2970
3008
  var SegmentGeom = /*#__PURE__*/Object.freeze({
2971
3009
  __proto__: null,
2972
3010
  segmentIntersection: segmentIntersection,
2973
3011
  findClosestPointOnSeg: findClosestPointOnSeg,
2974
3012
  orient2D: orient2D,
2975
- segmentHit: segmentHit
3013
+ segmentHit: segmentHit,
3014
+ segmentTurn: segmentTurn
2976
3015
  });
2977
3016
 
2978
3017
  var geom = Object.assign({}, Geom, PolygonGeom, PathGeom, SegmentGeom, PolygonCentroid);
@@ -8136,10 +8175,14 @@
8136
8175
  }
8137
8176
 
8138
8177
  function roundToDigits(n, d) {
8139
- return +n.toFixed(d);
8178
+ return +n.toFixed(d); // string conversion makes this slow
8140
8179
  }
8141
8180
 
8142
- // inc: Rounding incrememnt (e.g. 0.001 rounds to thousandths)
8181
+ function roundToTenths(n) {
8182
+ return (Math.round(n * 10)) / 10;
8183
+ }
8184
+
8185
+ // inc: Rounding increment (e.g. 0.001 rounds to thousandths)
8143
8186
  function getRoundingFunction(inc) {
8144
8187
  if (!utils.isNumber(inc) || inc === 0) {
8145
8188
  error("Rounding increment must be a non-zero number.");
@@ -8209,6 +8252,7 @@
8209
8252
  __proto__: null,
8210
8253
  roundToSignificantDigits: roundToSignificantDigits,
8211
8254
  roundToDigits: roundToDigits,
8255
+ roundToTenths: roundToTenths,
8212
8256
  getRoundingFunction: getRoundingFunction,
8213
8257
  getBoundsPrecisionForDisplay: getBoundsPrecisionForDisplay,
8214
8258
  getRoundedCoordString: getRoundedCoordString,
@@ -10270,6 +10314,12 @@
10270
10314
  };
10271
10315
  }
10272
10316
 
10317
+ /*
10318
+
10319
+
10320
+
10321
+ */
10322
+
10273
10323
  // TODO: use polygon pathfinder shared code
10274
10324
  function collectPolylineArcs(ids, nodes, testArc, useArc) {
10275
10325
  var parts = [];
@@ -11839,7 +11889,6 @@
11839
11889
  });
11840
11890
 
11841
11891
  // Map positive or negative integer ids to non-negative integer ids
11842
-
11843
11892
  function IdLookupIndex(n, clearable) {
11844
11893
  var fwdIndex = new Int32Array(n);
11845
11894
  var revIndex = new Int32Array(n);
@@ -13811,7 +13860,7 @@
13811
13860
  var symbolRenderers = {};
13812
13861
 
13813
13862
  function getTransform(xy, scale) {
13814
- var str = 'translate(' + xy[0] + ' ' + xy[1] + ')';
13863
+ var str = 'translate(' + roundToTenths(xy[0]) + ' ' + roundToTenths(xy[1]) + ')';
13815
13864
  if (scale && scale != 1) {
13816
13865
  str += ' scale(' + scale + ')';
13817
13866
  }
@@ -15539,6 +15588,139 @@ ${svg}
15539
15588
  }];
15540
15589
  }
15541
15590
 
15591
+ function exportRecordsAsFixedWidthString(fields, records, opts) {
15592
+ var rows = [], col;
15593
+ for (var i=0; i<fields.length; i++) {
15594
+ col = formatFixedWidthColumn(fields[i], records, opts);
15595
+ if (i === 0) {
15596
+ rows = col;
15597
+ } else for (var j=0; j<rows.length; j++) {
15598
+ rows[j] += ' ' + col[j];
15599
+ }
15600
+ }
15601
+ return rows.join('\n');
15602
+ }
15603
+
15604
+ function formatFixedWidthColumn(field, records, opts) {
15605
+ var arr = [],
15606
+ maxLen = field.length,
15607
+ n = records.length,
15608
+ i, val;
15609
+ arr.push(field);
15610
+ for (i=0; i<n; i++) {
15611
+ val = formatFixedWidthValue(records[i][field], opts);
15612
+ maxLen = Math.max(maxLen, val.length);
15613
+ arr.push(val);
15614
+ }
15615
+ for (i=0; i<arr.length; i++) {
15616
+ arr[i] = arr[i].padEnd(maxLen, ' ');
15617
+ }
15618
+ return arr;
15619
+ }
15620
+
15621
+ function formatFixedWidthValue(val, opts) {
15622
+ // TODO: remove duplication with mapshaper-delim-export.js
15623
+ var s;
15624
+ if (val == null) {
15625
+ s = '';
15626
+ } else if (utils.isString(val)) {
15627
+ s = val; // TODO: handle wide characters, newlines etc.
15628
+ } else if (utils.isNumber(val)) {
15629
+ s = opts.decimal_comma ? utils.formatIntlNumber(val) : utils.formatNumber(val);
15630
+ } else if (utils.isObject(val)) {
15631
+ s = JSON.stringify(val);
15632
+ } else {
15633
+ s = val + '';
15634
+ }
15635
+ return s;
15636
+ }
15637
+
15638
+
15639
+ function readFixedWidthRecords(reader, opts) {
15640
+ var str = reader.toString(opts.encoding || 'ascii');
15641
+ return readFixedWidthRecordsFromString(str, opts);
15642
+ }
15643
+
15644
+ function readFixedWidthRecordsFromString(str, ops) {
15645
+ var fields = parseFixedWidthInfo(str.substring(0, 2000));
15646
+ if (!fields) return [];
15647
+ var lines = utils.splitLines(str);
15648
+ if (lines[lines.length - 1] === '') lines.pop(); // handle newline at end of string
15649
+ var records = [];
15650
+ for (var i=1; i<lines.length; i++) {
15651
+ records.push(parseFixedWidthLine(lines[i], fields));
15652
+ }
15653
+ return records;
15654
+ }
15655
+
15656
+ function parseFixedWidthInfo(sample) {
15657
+ var lines = utils.splitLines(sample);
15658
+ if (lines.length > 2) lines.pop(); // remove possible partial line
15659
+ var n = getMaxLineLength(lines);
15660
+ var headerLine = lines[0];
15661
+ var colInfo = [];
15662
+ var colStart = 0;
15663
+ var inContent = false;
15664
+ var inHeader = false;
15665
+ var isContentChar, isHeaderChar, isColStart, colEnd;
15666
+ for (var i=0; i<=n; i++) {
15667
+ isHeaderChar = testContentChar(headerLine, i);
15668
+ isContentChar = !testEmptyCol(lines, i);
15669
+ isColStart = isHeaderChar && !inHeader;
15670
+ if (isColStart && inContent) {
15671
+ // all lines should have a space char in the position right before a header starts
15672
+ return null;
15673
+ }
15674
+ if (i == n || i > 0 && isColStart) {
15675
+ colEnd = i == n ? undefined : i-1;
15676
+ colInfo.push({
15677
+ name: readValue$1(headerLine, colStart, colEnd),
15678
+ end: colEnd,
15679
+ start: colStart
15680
+ });
15681
+ colStart = i;
15682
+ }
15683
+ inContent = isContentChar;
15684
+ inHeader = isHeaderChar;
15685
+ }
15686
+ return colInfo.length > 0 ? colInfo : null;
15687
+ }
15688
+
15689
+ function getMaxLineLength(lines) {
15690
+ var max = 0;
15691
+ for (var i=0; i<lines.length; i++) {
15692
+ max = Math.max(max, lines[i].length);
15693
+ }
15694
+ return max;
15695
+ }
15696
+
15697
+ function readValue$1(line, start, end) {
15698
+ return line.substring(start, end).trim();
15699
+ }
15700
+
15701
+ function parseFixedWidthLine(str, fields) {
15702
+ var obj = {}, field;
15703
+ for (var i=0; i<fields.length; i++) {
15704
+ field = fields[i];
15705
+ obj[field.name] = readValue$1(str, field.start, field.end);
15706
+ }
15707
+ return obj;
15708
+ }
15709
+
15710
+ function testContentChar(str, i) {
15711
+ return i < str.length && str[i] !== ' ';
15712
+ }
15713
+
15714
+ // return true iff all samples are blank at index i
15715
+ function testEmptyCol(samples, i) {
15716
+ var line;
15717
+ for (var j=0; j<samples.length; j++) {
15718
+ line = samples[j];
15719
+ if (testContentChar(line, i)) return false;
15720
+ }
15721
+ return true;
15722
+ }
15723
+
15542
15724
  // Generate output content from a dataset object
15543
15725
  function exportDelim(dataset, opts) {
15544
15726
  var delim = getExportDelimiter(dataset.info, opts),
@@ -15560,6 +15742,9 @@ ${svg}
15560
15742
  var encoding = opts.encoding || 'utf8';
15561
15743
  var records = lyr.data.getRecords();
15562
15744
  var fields = findFieldNames(records, opts.field_order);
15745
+ if (delim == ' ') {
15746
+ return exportRecordsAsFixedWidthString(fields, records, opts);
15747
+ }
15563
15748
  var formatRow = getDelimRowFormatter(fields, delim, opts);
15564
15749
  // exporting utf8 and ascii text as string by default (for now)
15565
15750
  var exportAsString = encodingIsUtf8(encoding) && !opts.to_buffer &&
@@ -15617,15 +15802,6 @@ ${svg}
15617
15802
  };
15618
15803
  }
15619
15804
 
15620
- function formatNumber$1(val) {
15621
- return val + '';
15622
- }
15623
-
15624
- function formatIntlNumber(val) {
15625
- var str = formatNumber$1(val);
15626
- return '"' + str.replace('.', ',') + '"'; // need to quote if comma-delimited
15627
- }
15628
-
15629
15805
  function getDelimValueFormatter(delim, opts) {
15630
15806
  var dquoteRxp = new RegExp('["\n\r' + delim + ']');
15631
15807
  var decimalComma = opts && opts.decimal_comma || false;
@@ -15642,7 +15818,7 @@ ${svg}
15642
15818
  } else if (utils.isString(val)) {
15643
15819
  s = formatString(val);
15644
15820
  } else if (utils.isNumber(val)) {
15645
- s = decimalComma ? formatIntlNumber(val) : formatNumber$1(val);
15821
+ s = decimalComma ? utils.formatIntlNumber(val) : utils.formatNumber(val);
15646
15822
  } else if (utils.isObject(val)) {
15647
15823
  s = formatString(JSON.stringify(val));
15648
15824
  } else {
@@ -15685,8 +15861,6 @@ ${svg}
15685
15861
  __proto__: null,
15686
15862
  exportDelim: exportDelim,
15687
15863
  exportLayerAsDSV: exportLayerAsDSV,
15688
- formatNumber: formatNumber$1,
15689
- formatIntlNumber: formatIntlNumber,
15690
15864
  getDelimValueFormatter: getDelimValueFormatter
15691
15865
  });
15692
15866
 
@@ -17050,8 +17224,9 @@ ${svg}
17050
17224
  //
17051
17225
  // TODO: confirm compatibility with all supported encodings
17052
17226
  function readDelimRecords(reader, delim, optsArg) {
17227
+ var opts = optsArg || {};
17228
+ if (delim == ' ') return readFixedWidthRecords(reader, opts);
17053
17229
  var reader2 = new Reader2(reader),
17054
- opts = optsArg || {},
17055
17230
  headerStr = readLinesAsString(reader2, getDelimHeaderLines(opts), opts.encoding),
17056
17231
  header = parseDelimHeaderSection(headerStr, delim, opts),
17057
17232
  convertRowArr = getRowConverter(header.import_fields),
@@ -17074,6 +17249,7 @@ ${svg}
17074
17249
  // for delimiter characters and newlines. Input size is limited by the maximum
17075
17250
  // string size.
17076
17251
  function readDelimRecordsFromString(str, delim, opts) {
17252
+ if (delim == ' ') return readFixedWidthRecordsFromString(str, opts);
17077
17253
  var header = parseDelimHeaderSection(str, delim, opts);
17078
17254
  if (header.import_fields.length === 0 || !header.remainder) return [];
17079
17255
  var convert = getRowConverter(header.import_fields);
@@ -17477,7 +17653,7 @@ ${svg}
17477
17653
  };
17478
17654
  }
17479
17655
 
17480
- var supportedDelimiters = ['|', '\t', ',', ';'];
17656
+ var supportedDelimiters = ['|', '\t', ',', ';', ' '];
17481
17657
 
17482
17658
  function isSupportedDelimiter(d) {
17483
17659
  return utils.contains(supportedDelimiters, d);
@@ -17838,9 +18014,21 @@ ${svg}
17838
18014
  }
17839
18015
 
17840
18016
  function cleanArgv(argv) {
17841
- argv = argv.map(function(s) {return s.trim();}); // trim whitespace
18017
+ // Note: original trim caused some quoted spaces to be removed
18018
+ // (e.g. bash shell seems to convert [delimiter=" "] to [delimiter= ],
18019
+ // which then got trimmed to [delimiter=] below)
18020
+ //// argv = argv.map(function(s) {return s.trim();}); // trim whitespace
18021
+
18022
+ // Updated: don't trim space from tokens like [delimeter= ]
18023
+ argv = argv.map(function(s) {
18024
+ if (!/= $/.test(s)) {
18025
+ s = s.trimEnd();
18026
+ }
18027
+ s = s.trimStart();
18028
+ return s;
18029
+ });
17842
18030
  argv = argv.filter(function(s) {return s !== '';}); // remove empty tokens
17843
- // removing trimQuotes() call... now, strings like 'name="Meg"' will no longer
18031
+ // Note: removing trimQuotes() call... now, strings like 'name="Meg"' will no longer
17844
18032
  // be parsed the same way as name=Meg and name="Meg"
17845
18033
  //// argv = argv.map(utils.trimQuotes); // remove one level of single or dbl quotes
17846
18034
  return argv;
@@ -18947,6 +19135,28 @@ ${svg}
18947
19135
  '$ mapshaper data.json -colorizer name=getColor nodata=#eee breaks=20,40 \\\n' +
18948
19136
  ' colors=#e0f3db,#a8ddb5,#43a2ca -each \'fill = getColor(RATING)\' -o output.json');
18949
19137
 
19138
+ parser.command('dashlines')
19139
+ .describe('split lines into sections, with or without a gap')
19140
+ .oldAlias('split-lines')
19141
+ .option('dash-length', {
19142
+ type: 'distance',
19143
+ describe: 'length of split-apart lines (e.g. 200km)'
19144
+ })
19145
+ .option('gap-length', {
19146
+ type: 'distance',
19147
+ describe: 'length of gaps between dashes (default is 0)'
19148
+ })
19149
+ .option('scaled', {
19150
+ type: 'flag',
19151
+ describe: 'scale dashes and gaps to prevent partial dashes'
19152
+ })
19153
+ .option('planar', {
19154
+ type: 'flag',
19155
+ describe: 'use planar geometry'
19156
+ })
19157
+ .option('where', whereOpt)
19158
+ .option('target', targetOpt);
19159
+
18950
19160
  parser.command('define')
18951
19161
  // .describe('define expression variables')
18952
19162
  .option('expression', {
@@ -19143,6 +19353,10 @@ ${svg}
19143
19353
  .option('keep-shapes', {
19144
19354
  type: 'flag'
19145
19355
  })
19356
+ .option('ids', {
19357
+ // describe: 'filter on a list of feature ids',
19358
+ type: 'numbers'
19359
+ })
19146
19360
  .option('cleanup', {type: 'flag'}) // TODO: document
19147
19361
  .option('name', nameOpt)
19148
19362
  .option('target', targetOpt)
@@ -19711,6 +19925,11 @@ ${svg}
19711
19925
  DEFAULT: true,
19712
19926
  describe: 'expression or field for grouping features and naming split layers'
19713
19927
  })
19928
+ .option('ids', {
19929
+ // used by gui history to split on selected features
19930
+ // describe: 'split on a list of feature ids',
19931
+ type: 'numbers'
19932
+ })
19714
19933
  .option('apart', {
19715
19934
  describe: 'save output layers to independent datasets',
19716
19935
  type: 'flag'
@@ -20122,6 +20341,16 @@ ${svg}
20122
20341
  })
20123
20342
  .option('name', nameOpt);
20124
20343
 
20344
+ // parser.command('shapes')
20345
+ // .describe('convert points to shapes')
20346
+ // .option('type', {
20347
+ // })
20348
+ // .option('size', {
20349
+ // })
20350
+ // .option('rotation', {
20351
+ // })
20352
+
20353
+
20125
20354
  parser.command('subdivide')
20126
20355
  .describe('recursively split a layer using a JS expression')
20127
20356
  .validate(validateExpressionOpt)
@@ -20990,7 +21219,7 @@ ${svg}
20990
21219
  var header = parseHeader(shpFile.readToBinArray(0, 100));
20991
21220
  var shpSize = shpFile.size();
20992
21221
  var RecordClass = new ShpRecordClass(header.type);
20993
- var shpOffset, recordCount, skippedBytes;
21222
+ var shpOffset, recordCount;
20994
21223
  var shxBin, shxFile;
20995
21224
 
20996
21225
  if (shxSrc) {
@@ -21017,61 +21246,36 @@ ${svg}
21017
21246
 
21018
21247
  // Iterator interface for reading shape records
21019
21248
  this.nextShape = function() {
21020
- var shape = readNextShape();
21021
- if (!shape) {
21022
- if (skippedBytes > 0) {
21023
- // Encountered in files from natural earth v2.0.0:
21024
- // ne_10m_admin_0_boundary_lines_land.shp
21025
- // ne_110m_admin_0_scale_rank.shp
21026
- verbose("Skipped over " + skippedBytes + " non-data bytes in the .shp file.");
21027
- }
21249
+ var shape = readNextShape(recordCount);
21250
+ if (shape) {
21251
+ recordCount++;
21252
+ } else {
21028
21253
  shpFile.close();
21029
21254
  reset();
21030
21255
  }
21031
21256
  return shape;
21032
21257
  };
21033
21258
 
21034
- function readNextShape() {
21035
- var expectedId = recordCount + 1; // Shapefile ids are 1-based
21259
+ // Returns a shape record or null if no more shapes can be read
21260
+ //
21261
+ function readNextShape(i) {
21262
+ var expectedId = i + 1; // Shapefile ids are 1-based
21036
21263
  var shape, offset;
21037
- if (done()) return null;
21038
21264
  if (shxBin) {
21039
- shxBin.position(100 + recordCount * 8);
21265
+ if (shxFile.size() <= 100 + i * 8) return null; // done
21266
+ shxBin.position(100 + i * 8);
21040
21267
  offset = shxBin.readUint32() * 2;
21041
- if (offset > shpOffset) {
21042
- skippedBytes += offset - shpOffset;
21043
- }
21268
+ shape = readIndexedShape(shpFile, offset, expectedId);
21044
21269
  } else {
21270
+ // Reading without a .shx file (returns null at end-of-file)
21045
21271
  offset = shpOffset;
21046
- }
21047
- shape = readShapeAtOffset(offset);
21048
- if (!shape) {
21049
- // Some in-the-wild .shp files contain junk bytes between records. This
21050
- // is a problem if the .shx index file is not present.
21051
- // Here, we try to scan past the junk to find the next record.
21052
- shape = huntForNextShape(offset, expectedId);
21053
- }
21054
- if (shape) {
21055
- if (shape.id < expectedId) {
21056
- message("Found a Shapefile record with the same id as a previous record (" + shape.id + ") -- skipping.");
21057
- return readNextShape();
21058
- } else if (shape.id > expectedId) {
21059
- stop("Shapefile contains an out-of-sequence record. Possible data corruption -- bailing.");
21060
- }
21061
- recordCount++;
21272
+ shape = readNonIndexedShape(shpFile, offset, expectedId);
21062
21273
  }
21063
21274
  return shape || null;
21064
21275
  }
21065
21276
 
21066
- function done() {
21067
- if (shxFile && shxFile.size() <= 100 + recordCount * 8) return true;
21068
- if (shpOffset + 12 > shpSize) return true;
21069
- return false;
21070
- }
21071
-
21072
21277
  function reset() {
21073
21278
  shpOffset = 100;
21074
- skippedBytes = 0;
21075
21279
  recordCount = 0;
21076
21280
  }
21077
21281
 
@@ -21101,7 +21305,8 @@ ${svg}
21101
21305
  return header;
21102
21306
  }
21103
21307
 
21104
- function readShapeAtOffset(offset) {
21308
+
21309
+ function readShapeAtOffset(shpFile, offset) {
21105
21310
  var shape = null,
21106
21311
  recordSize, recordType, recordId, goodSize, goodType, bin;
21107
21312
 
@@ -21116,32 +21321,62 @@ ${svg}
21116
21321
  if (goodSize && goodType) {
21117
21322
  bin = shpFile.readToBinArray(offset, recordSize);
21118
21323
  shape = new RecordClass(bin, recordSize);
21119
- shpOffset = offset + shape.byteLength; // advance read position
21120
21324
  }
21121
21325
  }
21122
21326
  return shape;
21123
21327
  }
21124
21328
 
21125
- // TODO: add tests
21126
- // Try to scan past unreadable content to find next record
21127
- function huntForNextShape(start, id) {
21128
- var offset = start + 4,
21329
+ function readIndexedShape(shpFile, offset, expectedId) {
21330
+ var shape = readShapeAtOffset(shpFile, offset);
21331
+ if (!shape) {
21332
+ stop('Index of Shapefile record', expectedId, 'in the .shx file is invalid.');
21333
+ }
21334
+ if (shape.id != expectedId) {
21335
+ // stop("Found a Shapefile record with an out-of-sequence id (" + shape.id + ") -- bailing.");
21336
+ message(`Warning: A feature has a different record number in .shx (${expectedId}) and .shp (${shape.id}).`);
21337
+ }
21338
+ // TODO: consider printing verbose message if a .shp file contains garbage bytes
21339
+ // example files:
21340
+ // ne_10m_admin_0_boundary_lines_land.shp
21341
+ // ne_110m_admin_0_scale_rank.shp
21342
+ return shape;
21343
+ }
21344
+
21345
+ // The Shapefile specification does not require records to be densely packed or
21346
+ // in consecutive sequence in the .shp file. This is a problem when the .shx
21347
+ // index file is not present.
21348
+ //
21349
+ // Here, we try to scan past invalid content to find the next record.
21350
+ // Records are required to be in sequential order.
21351
+ //
21352
+ function readNonIndexedShape(shpFile, start, expectedId) {
21353
+ var offset = start,
21354
+ fileSize = shpFile.size(),
21129
21355
  shape = null,
21130
- bin, recordId, recordType, count;
21131
- while (offset + 12 <= shpSize) {
21356
+ bin, recordId, recordType, isValidType;
21357
+ while (offset + 12 <= fileSize) {
21132
21358
  bin = shpFile.readToBinArray(offset, 12);
21133
21359
  recordId = bin.bigEndian().readUint32();
21134
21360
  recordType = bin.littleEndian().skipBytes(4).readUint32();
21135
- if (recordId == id && (recordType == header.type || recordType === 0)) {
21136
- // we have a likely position, but may still be unparsable
21137
- shape = readShapeAtOffset(offset);
21138
- break;
21361
+ isValidType = recordType == header.type || recordType === 0;
21362
+ if (!isValidType || recordId != expectedId && recordType === 0) {
21363
+ offset += 4; // keep scanning -- try next integer position
21364
+ continue;
21139
21365
  }
21140
- offset += 4; // try next integer position
21366
+ shape = readShapeAtOffset(shpFile, offset);
21367
+ if (!shape) break; // probably ran into end of file
21368
+ shpOffset = offset + shape.byteLength; // update
21369
+ if (recordId == expectedId) break; // found an apparently valid shape
21370
+ if (recordId < expectedId) {
21371
+ message("Found a Shapefile record with the same id as a previous record (" + shape.id + ") -- skipping.");
21372
+ offset += shape.byteLength;
21373
+ } else {
21374
+ stop("Shapefile contains an out-of-sequence record. Possible data corruption -- bailing.");
21375
+ }
21376
+ }
21377
+ if (shape && offset > start) {
21378
+ verbose("Skipped over " + (offset - start) + " non-data bytes in the .shp file.");
21141
21379
  }
21142
- count = shape ? offset - start : shpSize - start;
21143
- // debug('Skipped', count, 'bytes', shape ? 'before record ' + id : 'at the end of the file');
21144
- skippedBytes += count;
21145
21380
  return shape;
21146
21381
  }
21147
21382
  }
@@ -26263,97 +26498,6 @@ ${svg}
26263
26498
  geom.segmentIntersection(a[0], a[1], b[0], b[1], bb.xmax, bb.ymin, bb.xmin, bb.ymin));
26264
26499
  }
26265
26500
 
26266
- function getGeodesic(P) {
26267
- if (!isLatLngCRS(P)) error('Expected an unprojected CRS');
26268
- var f = P.es / (1 + Math.sqrt(P.one_es));
26269
- var GeographicLib = require('mproj').internal.GeographicLib;
26270
- return new GeographicLib.Geodesic.Geodesic(P.a, f);
26271
- }
26272
-
26273
- function getPlanarSegmentEndpoint(x, y, bearing, meterDist) {
26274
- var rad = bearing / 180 * Math.PI;
26275
- var dx = Math.sin(rad) * meterDist;
26276
- var dy = Math.cos(rad) * meterDist;
26277
- return [x + dx, y + dy];
26278
- }
26279
-
26280
- // source: https://github.com/mapbox/cheap-ruler/blob/master/index.js
26281
- function fastGeodeticSegmentFunction(lng, lat, bearing, meterDist) {
26282
- var D2R = Math.PI / 180;
26283
- var cos = Math.cos(lat * D2R);
26284
- var cos2 = 2 * cos * cos - 1;
26285
- var cos3 = 2 * cos * cos2 - cos;
26286
- var cos4 = 2 * cos * cos3 - cos2;
26287
- var cos5 = 2 * cos * cos4 - cos3;
26288
- var kx = (111.41513 * cos - 0.09455 * cos3 + 0.00012 * cos5) * 1000;
26289
- var ky = (111.13209 - 0.56605 * cos2 + 0.0012 * cos4) * 1000;
26290
- var bearingRad = bearing * D2R;
26291
- var lat2 = lat + Math.cos(bearingRad) * meterDist / ky;
26292
- var lng2 = lng + Math.sin(bearingRad) * meterDist / kx;
26293
- return [lng2, lat2];
26294
- }
26295
-
26296
- function getGeodeticSegmentFunction(P) {
26297
- if (!isLatLngCRS(P)) {
26298
- return getPlanarSegmentEndpoint;
26299
- }
26300
- var g = getGeodesic(P);
26301
- return function(lng, lat, bearing, meterDist) {
26302
- var o = g.Direct(lat, lng, bearing, meterDist);
26303
- var p = [o.lon2, o.lat2];
26304
- return p;
26305
- };
26306
- }
26307
-
26308
- function getFastGeodeticSegmentFunction(P) {
26309
- // CAREFUL: this function has higher error at very large distances and at the poles
26310
- // also, it wouldn't work for other planets than Earth
26311
- return isLatLngCRS(P) ? fastGeodeticSegmentFunction : getPlanarSegmentEndpoint;
26312
- }
26313
-
26314
- // Useful for determining if a segment that intersects another segment is
26315
- // entering or leaving an enclosed buffer area
26316
- // returns -1 if angle of p1p2 -> p3p4 is counter-clockwise (left turn)
26317
- // returns 1 if angle is clockwise
26318
- // return 0 if segments are collinear
26319
- function segmentTurn(p1, p2, p3, p4) {
26320
- var ax = p1[0],
26321
- ay = p1[1],
26322
- bx = p2[0],
26323
- by = p2[1],
26324
- // shift p3p4 segment to start at p2
26325
- dx = bx - p3[0],
26326
- dy = by - p3[1],
26327
- cx = p4[0] + dx,
26328
- cy = p4[1] + dy,
26329
- orientation = geom.orient2D(ax, ay, bx, by, cx, cy);
26330
- if (!orientation) return 0;
26331
- return orientation < 0 ? 1 : -1;
26332
- }
26333
-
26334
- function bearingDegrees(a, b, c, d) {
26335
- return geom.bearing(a, b, c, d) * 180 / Math.PI;
26336
- }
26337
-
26338
- function bearingDegrees2D(a, b, c, d) {
26339
- return geom.bearing2D(a, b, c, d) * 180 / Math.PI;
26340
- }
26341
-
26342
- // return function to calculate bearing of a segment in degrees
26343
- function getBearingFunction(dataset) {
26344
- var P = getDatasetCRS(dataset);
26345
- return isLatLngCRS(P) ? bearingDegrees : bearingDegrees2D;
26346
- }
26347
-
26348
- var Geodesic = /*#__PURE__*/Object.freeze({
26349
- __proto__: null,
26350
- getPlanarSegmentEndpoint: getPlanarSegmentEndpoint,
26351
- getGeodeticSegmentFunction: getGeodeticSegmentFunction,
26352
- getFastGeodeticSegmentFunction: getFastGeodeticSegmentFunction,
26353
- segmentTurn: segmentTurn,
26354
- getBearingFunction: getBearingFunction
26355
- });
26356
-
26357
26501
  function getPolylineBufferMaker2(arcs, geod, getBearing, opts) {
26358
26502
  var makeLeftBuffer = getPathBufferMaker2(arcs, geod, getBearing, opts);
26359
26503
  var geomType = opts.geometry_type;
@@ -26647,6 +26791,101 @@ ${svg}
26647
26791
  };
26648
26792
  }
26649
26793
 
26794
+ // GeographicLib docs: https://geographiclib.sourceforge.io/html/js/
26795
+ // https://geographiclib.sourceforge.io/html/js/module-GeographicLib_Geodesic.Geodesic.html
26796
+ // https://geographiclib.sourceforge.io/html/js/tutorial-2-interface.html
26797
+ function getGeodesic(P) {
26798
+ if (!isLatLngCRS(P)) error('Expected an unprojected CRS');
26799
+ var f = P.es / (1 + Math.sqrt(P.one_es));
26800
+ var GeographicLib = require('mproj').internal.GeographicLib;
26801
+ return new GeographicLib.Geodesic.Geodesic(P.a, f);
26802
+ }
26803
+
26804
+ function interpolatePoint2D(ax, ay, bx, by, k) {
26805
+ var j = 1 - k;
26806
+ return [ax * j + bx * k, ay * j + by * k];
26807
+ }
26808
+
26809
+ function getInterpolationFunction(P) {
26810
+ var spherical = P && isLatLngCRS(P);
26811
+ if (!spherical) return interpolatePoint2D;
26812
+ var geod = getGeodesic(P);
26813
+ return function(lng, lat, lng2, lat2, k) {
26814
+ var r = geod.Inverse(lat, lng, lat2, lng2);
26815
+ var dist = r.s12 * k;
26816
+ var r2 = geod.Direct(lat, lng, r.azi1, dist);
26817
+ return [r2.lon2, r2.lat2];
26818
+ };
26819
+ }
26820
+
26821
+ function getPlanarSegmentEndpoint(x, y, bearing, meterDist) {
26822
+ var rad = bearing / 180 * Math.PI;
26823
+ var dx = Math.sin(rad) * meterDist;
26824
+ var dy = Math.cos(rad) * meterDist;
26825
+ return [x + dx, y + dy];
26826
+ }
26827
+
26828
+ // source: https://github.com/mapbox/cheap-ruler/blob/master/index.js
26829
+ function fastGeodeticSegmentFunction(lng, lat, bearing, meterDist) {
26830
+ var D2R = Math.PI / 180;
26831
+ var cos = Math.cos(lat * D2R);
26832
+ var cos2 = 2 * cos * cos - 1;
26833
+ var cos3 = 2 * cos * cos2 - cos;
26834
+ var cos4 = 2 * cos * cos3 - cos2;
26835
+ var cos5 = 2 * cos * cos4 - cos3;
26836
+ var kx = (111.41513 * cos - 0.09455 * cos3 + 0.00012 * cos5) * 1000;
26837
+ var ky = (111.13209 - 0.56605 * cos2 + 0.0012 * cos4) * 1000;
26838
+ var bearingRad = bearing * D2R;
26839
+ var lat2 = lat + Math.cos(bearingRad) * meterDist / ky;
26840
+ var lng2 = lng + Math.sin(bearingRad) * meterDist / kx;
26841
+ return [lng2, lat2];
26842
+ }
26843
+
26844
+ function getGeodeticSegmentFunction(P) {
26845
+ if (!isLatLngCRS(P)) {
26846
+ return getPlanarSegmentEndpoint;
26847
+ }
26848
+ var g = getGeodesic(P);
26849
+ return function(lng, lat, bearing, meterDist) {
26850
+ var o = g.Direct(lat, lng, bearing, meterDist);
26851
+ var p = [o.lon2, o.lat2];
26852
+ return p;
26853
+ };
26854
+ }
26855
+
26856
+ function getFastGeodeticSegmentFunction(P) {
26857
+ // CAREFUL: this function has higher error at very large distances and at the poles
26858
+ // also, it wouldn't work for other planets than Earth
26859
+ return isLatLngCRS(P) ? fastGeodeticSegmentFunction : getPlanarSegmentEndpoint;
26860
+ }
26861
+
26862
+
26863
+ function bearingDegrees(a, b, c, d) {
26864
+ return geom.bearing(a, b, c, d) * 180 / Math.PI;
26865
+ }
26866
+
26867
+ function bearingDegrees2D(a, b, c, d) {
26868
+ return geom.bearing2D(a, b, c, d) * 180 / Math.PI;
26869
+ }
26870
+
26871
+ // return function to calculate bearing of a segment in degrees
26872
+ function getBearingFunction(dataset) {
26873
+ var P = getDatasetCRS(dataset);
26874
+ return isLatLngCRS(P) ? bearingDegrees : bearingDegrees2D;
26875
+ }
26876
+
26877
+ var Geodesic = /*#__PURE__*/Object.freeze({
26878
+ __proto__: null,
26879
+ interpolatePoint2D: interpolatePoint2D,
26880
+ getInterpolationFunction: getInterpolationFunction,
26881
+ getPlanarSegmentEndpoint: getPlanarSegmentEndpoint,
26882
+ getGeodeticSegmentFunction: getGeodeticSegmentFunction,
26883
+ getFastGeodeticSegmentFunction: getFastGeodeticSegmentFunction,
26884
+ bearingDegrees: bearingDegrees,
26885
+ bearingDegrees2D: bearingDegrees2D,
26886
+ getBearingFunction: getBearingFunction
26887
+ });
26888
+
26650
26889
  function makePolylineBuffer(lyr, dataset, opts) {
26651
26890
  var geojson = makeShapeBufferGeoJSON(lyr, dataset, opts);
26652
26891
  var dataset2 = importGeoJSON(geojson, {});
@@ -29682,6 +29921,178 @@ ${svg}
29682
29921
  getColorizerFunction: getColorizerFunction
29683
29922
  });
29684
29923
 
29924
+ function expressionUsesGeoJSON(exp) {
29925
+ return exp.includes('this.geojson');
29926
+ }
29927
+
29928
+ function getFeatureEditor(lyr, dataset) {
29929
+ var changed = false;
29930
+ var api = {};
29931
+ // need to copy attribute to avoid circular references if geojson is assigned
29932
+ // to a data property.
29933
+ var copy = copyLayer(lyr);
29934
+ var features = exportLayerAsGeoJSON(copy, dataset, {}, true);
29935
+
29936
+ api.get = function(i) {
29937
+ return features[i];
29938
+ };
29939
+
29940
+ api.set = function(feat, i) {
29941
+ changed = true;
29942
+ if (utils.isString(feat)) {
29943
+ feat = JSON.parse(feat);
29944
+ }
29945
+ features[i] = GeoJSON.toFeature(feat); // TODO: validate
29946
+ };
29947
+
29948
+ api.done = function() {
29949
+ if (!changed) return; // read-only expression
29950
+ // TODO: validate number of features, etc.
29951
+ var geojson = {
29952
+ type: 'FeatureCollection',
29953
+ features: features
29954
+ };
29955
+
29956
+ // console.log(JSON.stringify(geojson, null, 2))
29957
+ return importGeoJSON(geojson);
29958
+ };
29959
+ return api;
29960
+ }
29961
+
29962
+ cmd.dashlines = function(lyr, dataset, opts) {
29963
+ var crs = getDatasetCRS(dataset);
29964
+ var defs = getStateVar('defs');
29965
+ var exp = `this.geojson = splitFeature(this.geojson)`;
29966
+ requirePolylineLayer(lyr);
29967
+ defs.splitFeature = getSplitFeatureFunction(crs, opts);
29968
+ cmd.evaluateEachFeature(lyr, dataset, exp, opts);
29969
+ delete defs.splitFeature;
29970
+ };
29971
+
29972
+ function getSplitFeatureFunction(crs, opts) {
29973
+ var dashLen = opts.dash_length ? convertDistanceParam(opts.dash_length, crs) : 0;
29974
+ var gapLen = opts.gap_length ? convertDistanceParam(opts.gap_length, crs) : 0;
29975
+ if (dashLen > 0 === false) {
29976
+ stop('Missing required dash-length parameter');
29977
+ }
29978
+ if (gapLen >= 0 == false) {
29979
+ stop('Invalid gap-length option');
29980
+ }
29981
+ var splitLine = getSplitLineFunction(crs, dashLen, gapLen, opts);
29982
+ return function(feat) {
29983
+ var geom = feat.geometry;
29984
+ if (!geom) return feat;
29985
+ if (geom.type == 'LineString') {
29986
+ geom.type = 'MultiLineString';
29987
+ geom.coordinates = [geom.coordinates];
29988
+ }
29989
+ if (geom.type != 'MultiLineString') {
29990
+ error('Unexpected geometry:', geom.type);
29991
+ }
29992
+ geom.coordinates = geom.coordinates.reduce(function(memo, coords) {
29993
+ try {
29994
+ var parts = splitLine(coords);
29995
+ memo = memo.concat(parts);
29996
+ } catch(e) {
29997
+ console.error(e);
29998
+ throw e;
29999
+ }
30000
+ return memo;
30001
+ }, []);
30002
+
30003
+ return feat;
30004
+ };
30005
+ }
30006
+
30007
+ function getSplitLineFunction(crs, dashLen, gapLen, opts) {
30008
+ var planar = !!opts.planar;
30009
+ var interpolate = getInterpolationFunction(planar ? null : crs);
30010
+ var distance = isLatLngCRS(crs) ? greatCircleDistance : distance2D;
30011
+ var inDash, parts2, interval, scale;
30012
+ function addPart(coords) {
30013
+ if (inDash) parts2.push(coords);
30014
+ if (gapLen > 0) {
30015
+ inDash = !inDash;
30016
+ interval = scale * (inDash ? dashLen : gapLen);
30017
+ }
30018
+ }
30019
+
30020
+ return function splitLineString(coords) {
30021
+ var elapsedDist = 0;
30022
+ var p = coords[0];
30023
+ var coords2 = [p];
30024
+ var segLen, pct, prev;
30025
+ if (opts.scaled) {
30026
+ scale = scaleDashes(dashLen, gapLen, getLineLength(coords, distance));
30027
+ } else {
30028
+ scale = 1;
30029
+ }
30030
+ // init this LineString
30031
+ inDash = gapLen > 0 ? false : true;
30032
+ interval = scale * (inDash ? dashLen : gapLen);
30033
+ if (!inDash) {
30034
+ // start gapped lines with a half-gap
30035
+ // (a half-gap or a half-dash is probably better for rings and intersecting lines)
30036
+ interval *= 0.5;
30037
+ }
30038
+ parts2 = [];
30039
+ for (var i=1, n=coords.length; i<n; i++) {
30040
+ prev = p;
30041
+ p = coords[i];
30042
+ segLen = distance(prev[0], prev[1], p[0], p[1]);
30043
+ if (segLen <= 0) continue;
30044
+ while (elapsedDist + segLen >= interval) {
30045
+ // this segment contains a break either within it or at the far endpoint
30046
+ pct = (interval - elapsedDist) / segLen;
30047
+ if (pct > 0.999 && i == n - 1) {
30048
+ // snap to endpoint (so fp rounding errors don't result in a tiny
30049
+ // last segment)
30050
+ pct = 1;
30051
+ }
30052
+ if (pct < 1) {
30053
+ prev = interpolate(prev[0], prev[1], p[0], p[1], pct);
30054
+ } else {
30055
+ prev = p;
30056
+ }
30057
+ coords2.push(prev);
30058
+ addPart(coords2);
30059
+ // start a new part
30060
+ coords2 = pct < 1 ? [prev] : [];
30061
+ elapsedDist = 0;
30062
+ segLen = (1 - pct) * segLen;
30063
+ }
30064
+ coords2.push(p);
30065
+ elapsedDist += segLen;
30066
+ }
30067
+ if (elapsedDist > 0 && coords2.length > 1) {
30068
+ addPart(coords2);
30069
+ }
30070
+ return parts2;
30071
+ };
30072
+ }
30073
+
30074
+ function getLineLength(coords, distance) {
30075
+ var len = 0;
30076
+ for (var i=1, n=coords.length; i<n; i++) {
30077
+ len += distance(coords[i-1][0], coords[i-1][1], coords[i][0], coords[i][1]);
30078
+ }
30079
+ return len;
30080
+ }
30081
+
30082
+ function scaleDashes(dash, gap, len) {
30083
+ var dash2, gap2;
30084
+ var n = len / (dash + gap); // number of dashes
30085
+ var n1 = Math.floor(n);
30086
+ var n2 = Math.ceil(n);
30087
+ var k1 = len / (n1 * (dash + gap)); // scaled-up dashes, >1
30088
+ var k2 = len / (n2 * (dash + gap)); // scaled-down dashes <1
30089
+ var k = k2;
30090
+ if (k1 < 1/k2 && n1 > 0) {
30091
+ k = k1; // pick the smaller of the two scales
30092
+ }
30093
+ return k;
30094
+ }
30095
+
29685
30096
  // This function creates a continuous mosaic of data values in a
29686
30097
  // given field by assigning data from adjacent polygon features to polygons
29687
30098
  // that contain null values.
@@ -31206,43 +31617,6 @@ ${svg}
31206
31617
  }
31207
31618
  };
31208
31619
 
31209
- function expressionUsesGeoJSON(exp) {
31210
- return exp.includes('this.geojson');
31211
- }
31212
-
31213
- function getFeatureEditor(lyr, dataset) {
31214
- var changed = false;
31215
- var api = {};
31216
- // need to copy attribute to avoid circular references if geojson is assigned
31217
- // to a data property.
31218
- var copy = copyLayer(lyr);
31219
- var features = exportLayerAsGeoJSON(copy, dataset, {}, true);
31220
-
31221
- api.get = function(i) {
31222
- return features[i];
31223
- };
31224
-
31225
- api.set = function(feat, i) {
31226
- changed = true;
31227
- if (utils.isString(feat)) {
31228
- feat = JSON.parse(feat);
31229
- }
31230
- features[i] = GeoJSON.toFeature(feat); // TODO: validate
31231
- };
31232
-
31233
- api.done = function() {
31234
- if (!changed) return; // read-only expression
31235
- // TODO: validate number of features, etc.
31236
- var geojson = {
31237
- type: 'FeatureCollection',
31238
- features: features
31239
- };
31240
- // console.log(JSON.stringify(geojson, null, 2))
31241
- return importGeoJSON(geojson);
31242
- };
31243
- return api;
31244
- }
31245
-
31246
31620
  cmd.evaluateEachFeature = function(lyr, dataset, exp, opts) {
31247
31621
  var n = getFeatureCount(lyr),
31248
31622
  arcs = dataset.arcs,
@@ -31485,6 +31859,10 @@ ${svg}
31485
31859
  filter = compileValueExpression(opts.expression, lyr, arcs);
31486
31860
  }
31487
31861
 
31862
+ if (opts.ids) {
31863
+ filter = combineFilters(filter, getIdFilter(opts.ids));
31864
+ }
31865
+
31488
31866
  if (opts.remove_empty) {
31489
31867
  filter = combineFilters(filter, getNullGeometryFilter(lyr, arcs));
31490
31868
  }
@@ -31543,6 +31921,13 @@ ${svg}
31543
31921
  lyr.data = filteredRecords ? new DataTable(filteredRecords) : null;
31544
31922
  }
31545
31923
 
31924
+ function getIdFilter(ids) {
31925
+ var set = new Set(ids);
31926
+ return function(i) {
31927
+ return set.has(i);
31928
+ };
31929
+ }
31930
+
31546
31931
  function getNullGeometryFilter(lyr, arcs) {
31547
31932
  var shapes = lyr.shapes;
31548
31933
  if (lyr.geometry_type == 'polygon') {
@@ -31848,7 +32233,6 @@ ${svg}
31848
32233
  return joinTableToLayer(targetLyr, polygonLyr.data, joinFunction, opts);
31849
32234
  }
31850
32235
 
31851
-
31852
32236
  function prepJoinLayers(targetLyr, srcLyr) {
31853
32237
  if (!targetLyr.data) {
31854
32238
  // create an empty data table if target layer is missing attributes
@@ -33816,10 +34200,6 @@ ${svg}
33816
34200
 
33817
34201
 
33818
34202
 
33819
- function formatNumber(val) {
33820
- return val + '';
33821
- }
33822
-
33823
34203
  function maxChars(arr) {
33824
34204
  return arr.reduce(function(memo, str) {
33825
34205
  var w = stringDisplayWidth(str);
@@ -33843,14 +34223,14 @@ ${svg}
33843
34223
  }
33844
34224
 
33845
34225
  function countIntegralChars(val) {
33846
- return utils.isNumber(val) ? (formatNumber(val) + '.').indexOf('.') : 0;
34226
+ return utils.isNumber(val) ? (utils.formatNumber(val) + '.').indexOf('.') : 0;
33847
34227
  }
33848
34228
 
33849
34229
  function formatTableValue(val, integralChars) {
33850
34230
  var str;
33851
34231
  if (utils.isNumber(val)) {
33852
34232
  str = utils.lpad("", integralChars - countIntegralChars(val), ' ') +
33853
- formatNumber(val);
34233
+ utils.formatNumber(val);
33854
34234
  } else if (utils.isString(val)) {
33855
34235
  str = formatString(val);
33856
34236
  } else if (utils.isDate(val)) {
@@ -34403,11 +34783,6 @@ ${svg}
34403
34783
  });
34404
34784
  }
34405
34785
 
34406
- function interpolatePoint2D(ax, ay, bx, by, k) {
34407
- var j = 1 - k;
34408
- return [ax * j + bx * k, ay * j + by * k];
34409
- }
34410
-
34411
34786
  function interpolatePointsAlongArc(ids, arcs, interval) {
34412
34787
  var iter = arcs.getShapeIter(ids);
34413
34788
  var distance = arcs.isPlanar() ? geom.distance2D : geom.greatCircleDistance;
@@ -34659,6 +35034,38 @@ ${svg}
34659
35034
  }
34660
35035
  }
34661
35036
 
35037
+ function pointsFromPolylinesForJoin(lyr, dataset) {
35038
+ var shapes = lyr.shapes.map(function(shp) {
35039
+ return polylineToMidpoints(shp, dataset.arcs);
35040
+ });
35041
+ return {
35042
+ geometry_type: 'point',
35043
+ shapes: shapes,
35044
+ data: lyr.data // TODO copy if needed
35045
+ };
35046
+ }
35047
+
35048
+ function validateOpts(opts) {
35049
+ if (!opts.point_method) {
35050
+ stop('The "point-method" flag is required for polyline-polygon joins');
35051
+ }
35052
+ }
35053
+
35054
+ function joinPolylinesToPolygons(targetLyr, targetDataset, source, opts) {
35055
+ validateOpts(opts);
35056
+ var pointLyr = pointsFromPolylinesForJoin(source.layer, source.dataset);
35057
+ var retn = joinPointsToPolygons(targetLyr, targetDataset.arcs, pointLyr, opts);
35058
+ return retn;
35059
+ }
35060
+
35061
+ function joinPolygonsToPolylines(targetLyr, targetDataset, source, opts) {
35062
+ validateOpts(opts);
35063
+ var pointLyr = pointsFromPolylinesForJoin(targetLyr, targetDataset);
35064
+ var retn = joinPolygonsToPoints(pointLyr, source.layer, source.dataset.arcs, opts);
35065
+ targetLyr.data = pointLyr.data;
35066
+ return retn;
35067
+ }
35068
+
34662
35069
  class TinyQueue {
34663
35070
  constructor(data = [], compare = defaultCompare) {
34664
35071
  this.data = data;
@@ -34982,7 +35389,7 @@ ${svg}
34982
35389
 
34983
35390
  cmd.join = function(targetLyr, targetDataset, src, opts) {
34984
35391
  var srcType, targetType, retn;
34985
- if (!src || !src.layer.data || !src.dataset) {
35392
+ if (!src || !src.dataset) {
34986
35393
  stop("Missing a joinable data source");
34987
35394
  }
34988
35395
  if (opts.keys) {
@@ -34990,9 +35397,18 @@ ${svg}
34990
35397
  if (opts.keys.length != 2) {
34991
35398
  stop("Expected two key fields: a target field and a source field");
34992
35399
  }
35400
+ if (!src.layer.data) {
35401
+ stop("Source layer is missing attribute data");
35402
+ }
34993
35403
  retn = joinAttributesToFeatures(targetLyr, src.layer.data, opts);
34994
35404
  } else {
34995
35405
  // spatial join
35406
+ if (!src.layer.data) {
35407
+ // KLUDGE -- users might want to join a layer without attributes
35408
+ // to test for intersection... the simplest way to support this is
35409
+ // to add an empty data table to the source layer
35410
+ initDataTable(src.layer);
35411
+ }
34996
35412
  requireDatasetsHaveCompatibleCRS([targetDataset, src.dataset]);
34997
35413
  srcType = src.layer.geometry_type;
34998
35414
  targetType = targetLyr.geometry_type;
@@ -35004,6 +35420,10 @@ ${svg}
35004
35420
  retn = joinPointsToPoints(targetLyr, src.layer, getDatasetCRS(targetDataset), opts);
35005
35421
  } else if (srcType == 'polygon' && targetType == 'polygon') {
35006
35422
  retn = joinPolygonsToPolygons(targetLyr, targetDataset, src, opts);
35423
+ } else if (srcType == 'polyline' && targetType == 'polygon') {
35424
+ retn = joinPolylinesToPolygons(targetLyr, targetDataset, src, opts);
35425
+ } else if (srcType == 'polygon' && targetType == 'polyline') {
35426
+ retn = joinPolygonsToPolylines(targetLyr, targetDataset, src, opts);
35007
35427
  } else {
35008
35428
  stop(utils.format("Unable to join %s geometry to %s geometry",
35009
35429
  srcType || 'null', targetType || 'null'));
@@ -37182,13 +37602,20 @@ ${svg}
37182
37602
 
37183
37603
  // @expression: optional field name or expression
37184
37604
  //
37185
- cmd.splitLayer = function(src, expression, opts) {
37186
- var lyr0 = opts && opts.no_replace ? copyLayer(src) : src,
37605
+ cmd.splitLayer = function(src, expression, optsArg) {
37606
+ var opts = optsArg || {},
37607
+ lyr0 = opts.no_replace ? copyLayer(src) : src,
37187
37608
  properties = lyr0.data ? lyr0.data.getRecords() : null,
37188
37609
  shapes = lyr0.shapes,
37189
37610
  index = {},
37190
37611
  splitLayers = [],
37191
- namer = getSplitNameFunction(lyr0, expression);
37612
+ namer;
37613
+
37614
+ if (opts.ids) {
37615
+ namer = getIdSplitFunction(opts.ids);
37616
+ } else {
37617
+ namer = getSplitNameFunction(lyr0, expression);
37618
+ }
37192
37619
 
37193
37620
  // if (splitField) {
37194
37621
  // internal.requireDataField(lyr0, splitField);
@@ -37220,15 +37647,24 @@ ${svg}
37220
37647
  return splitLayers;
37221
37648
  };
37222
37649
 
37650
+ function getIdSplitFunction(ids) {
37651
+ var set = new Set(ids);
37652
+ return function(i) {
37653
+ return set.has(i) ? '1' : '2';
37654
+ };
37655
+ }
37656
+
37657
+ function getDefaultSplitFunction(lyr) {
37658
+ // if not splitting on an expression and layer is unnamed, name split-apart layers
37659
+ // like: split-1, split-2, ...
37660
+ return function(i) {
37661
+ return (lyr && lyr.name || 'split') + '-' + (i + 1);
37662
+ };
37663
+ }
37664
+
37223
37665
  function getSplitNameFunction(lyr, exp) {
37224
37666
  var compiled;
37225
- if (!exp) {
37226
- // if not splitting on an expression and layer is unnamed, name split-apart layers
37227
- // like: split-1, split-2, ...
37228
- return function(i) {
37229
- return (lyr && lyr.name || 'split') + '-' + (i + 1);
37230
- };
37231
- }
37667
+ if (!exp) return getDefaultSplitFunction(lyr);
37232
37668
  lyr = {name: lyr.name, data: lyr.data}; // remove shape info
37233
37669
  compiled = compileValueExpression(exp, lyr, null);
37234
37670
  return function(i) {
@@ -38028,6 +38464,9 @@ ${svg}
38028
38464
  } else if (name == 'colorizer') {
38029
38465
  outputLayers = cmd.colorizer(opts);
38030
38466
 
38467
+ } else if (name == 'dashlines') {
38468
+ applyCommandToEachLayer(cmd.dashlines, targetLayers, targetDataset, opts);
38469
+
38031
38470
  } else if (name == 'define') {
38032
38471
  cmd.define(opts);
38033
38472