mapshaper 0.5.79 → 0.5.80

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/www/mapshaper.js CHANGED
@@ -1,6 +1,6 @@
1
1
  (function () {
2
2
 
3
- var VERSION = "0.5.78";
3
+ var VERSION = "0.5.79";
4
4
 
5
5
 
6
6
  var utils = /*#__PURE__*/Object.freeze({
@@ -1745,9 +1745,17 @@
1745
1745
  return [xmin, ymin, xmax, ymax];
1746
1746
  }
1747
1747
 
1748
- // TODO: remove this constant, use actual data from dataset CRS
1749
- // also consider using ellipsoidal formulas when appropriate
1750
- var R$1 = 6378137;
1748
+ var WGS84 = {
1749
+ // https://en.wikipedia.org/wiki/Earth_radius
1750
+ SEMIMAJOR_AXIS: 6378137,
1751
+ SEMIMINOR_AXIS: 6356752.3142,
1752
+ AUTHALIC_RADIUS: 6371007.2,
1753
+ VOLUMETRIC_RADIUS: 6371000.8
1754
+ };
1755
+
1756
+ // TODO: remove this constant, use actual data from dataset CRS,
1757
+ // also consider using ellipsoidal formulas where greater accuracy might be important.
1758
+ var R$1 = WGS84.SEMIMAJOR_AXIS;
1751
1759
  var D2R = Math.PI / 180;
1752
1760
  var R2D = 180 / Math.PI;
1753
1761
 
@@ -2342,12 +2350,12 @@
2342
2350
  }, 0);
2343
2351
  }
2344
2352
 
2345
- function getSphericalShapeArea(shp, arcs) {
2353
+ function getSphericalShapeArea(shp, arcs, R) {
2346
2354
  if (arcs.isPlanar()) {
2347
2355
  error("[getSphericalShapeArea()] Function requires decimal degree coordinates");
2348
2356
  }
2349
2357
  return (shp || []).reduce(function(area, ids) {
2350
- return area + getSphericalPathArea(ids, arcs);
2358
+ return area + getSphericalPathArea(ids, arcs, R);
2351
2359
  }, 0);
2352
2360
  }
2353
2361
 
@@ -2464,16 +2472,17 @@
2464
2472
  return (arcs.isPlanar() ? getPlanarPathArea : getSphericalPathArea)(ids, arcs);
2465
2473
  }
2466
2474
 
2467
- function getSphericalPathArea(ids, arcs) {
2475
+ function getSphericalPathArea(ids, arcs, R) {
2468
2476
  var iter = arcs.getShapeIter(ids);
2469
- return getSphericalPathArea2(iter);
2477
+ return getSphericalPathArea2(iter, R);
2470
2478
  }
2471
2479
 
2472
- function getSphericalPathArea2(iter) {
2480
+ function getSphericalPathArea2(iter, R) {
2473
2481
  var sum = 0,
2474
2482
  started = false,
2475
2483
  deg2rad = Math.PI / 180,
2476
2484
  x, y, xp, yp;
2485
+ R = R || WGS84.SEMIMAJOR_AXIS;
2477
2486
  while (iter.hasNext()) {
2478
2487
  x = iter.x * deg2rad;
2479
2488
  y = Math.sin(iter.y * deg2rad);
@@ -2485,7 +2494,7 @@
2485
2494
  xp = x;
2486
2495
  yp = y;
2487
2496
  }
2488
- return sum / 2 * 6378137 * 6378137;
2497
+ return sum / 2 * R * R;
2489
2498
  }
2490
2499
 
2491
2500
  // Get path area from an array of [x, y] points
@@ -3949,7 +3958,7 @@
3949
3958
  function requireSinglePointLayer(lyr, msg) {
3950
3959
  requirePointLayer(lyr);
3951
3960
  if (countMultiPartFeatures(lyr) > 0) {
3952
- stop(msg || 'This command requires single points');
3961
+ stop(msg || 'This command requires single points; layer contains multi-point features.');
3953
3962
  }
3954
3963
  }
3955
3964
 
@@ -4004,6 +4013,7 @@
4004
4013
  return opts && opts.no_replace ? {geometry_type: src.geometry_type} : src;
4005
4014
  }
4006
4015
 
4016
+ //
4007
4017
  function setOutputLayerName(dest, src, defName, opts) {
4008
4018
  opts = opts || {};
4009
4019
  if (opts.name) {
@@ -4080,6 +4090,7 @@
4080
4090
  return counts;
4081
4091
  }
4082
4092
 
4093
+ // Returns a Bounds object
4083
4094
  function getLayerBounds(lyr, arcs) {
4084
4095
  var bounds = null;
4085
4096
  if (lyr.geometry_type == 'point') {
@@ -6151,9 +6162,11 @@
6151
6162
  arcCount += n;
6152
6163
  });
6153
6164
 
6154
- mergedArcs = mergeArcs(arcSources);
6155
- if (mergedArcs.size() != arcCount) {
6156
- error("[mergeDatasets()] Arc indexing error");
6165
+ if (arcSources.length > 0) {
6166
+ mergedArcs = mergeArcs(arcSources);
6167
+ if (mergedArcs.size() != arcCount) {
6168
+ error("[mergeDatasets()] Arc indexing error");
6169
+ }
6157
6170
  }
6158
6171
 
6159
6172
  return {
@@ -6172,6 +6185,8 @@
6172
6185
  }
6173
6186
 
6174
6187
  function mergeArcs(arr) {
6188
+ // Returning the original causes a test to fail
6189
+ // if (arr.length < 2) return arr[0];
6175
6190
  var dataArr = arr.map(function(arcs) {
6176
6191
  if (arcs.getRetainedInterval() > 0) {
6177
6192
  verbose("Baking-in simplification setting.");
@@ -6776,31 +6791,47 @@
6776
6791
  dataset.layers = currLayers;
6777
6792
  }
6778
6793
 
6779
- // Replace a layer in-place with a layer from a second dataset
6794
+ // Replace a layer with a layer from a second dataset
6795
+ // (in-place)
6780
6796
  // (Typically, the second dataset is imported from dynamically generated GeoJSON and contains one layer)
6781
6797
  function replaceLayerContents(lyr, dataset, dataset2) {
6798
+ var lyr2 = mergeOutputLayerIntoDataset(lyr, dataset, dataset2, {});
6799
+ if (layerHasPaths(lyr2)) {
6800
+ buildTopology(dataset);
6801
+ }
6802
+ }
6803
+
6804
+ function mergeOutputLayerIntoDataset(lyr, dataset, dataset2, opts) {
6782
6805
  if (!dataset2 || dataset2.layers.length != 1) {
6783
- error('Invalid replacement layer');
6806
+ error('Invalid source dataset');
6784
6807
  }
6785
6808
  if (dataset.layers.includes(lyr) === false) {
6786
6809
  error('Invalid target layer');
6787
6810
  }
6788
- var lyr2 = dataset2.layers[0];
6789
- if (dataset2.arcs) {
6790
- // this command returns merged layers instead of adding them to target dataset
6791
- mergeDatasetsIntoDataset(dataset, [dataset2]);
6792
- }
6811
+ // this command returns merged layers instead of adding them to target dataset
6812
+ var outputLayers = mergeDatasetsIntoDataset(dataset, [dataset2]);
6813
+ var lyr2 = outputLayers[0];
6793
6814
 
6794
- Object.assign(lyr, {data: null, shapes: null}, lyr2);
6815
+ // TODO: find a more reliable way of knowing when to copy data
6816
+ var copyData = !lyr2.data && lyr.data && getFeatureCount(lyr2) == lyr.data.size();
6795
6817
 
6796
- if (layerHasPaths(lyr2)) {
6797
- buildTopology(dataset);
6818
+ if (copyData) {
6819
+ lyr2.data = opts.no_replace ? lyr.data.clone() : lyr.data;
6798
6820
  }
6799
- if (layerHasPaths(lyr)) {
6800
- // Remove unused arcs from replaced layer
6801
- // TODO: consider using clean insead of this
6802
- dissolveArcs(dataset);
6821
+ if (opts.no_replace) {
6822
+ // dataset.layers.push(lyr2);
6823
+
6824
+ } else {
6825
+ lyr2 = Object.assign(lyr, {data: null, shapes: null}, lyr2);
6826
+ if (layerHasPaths(lyr)) {
6827
+ // Remove unused arcs from replaced layer
6828
+ // TODO: consider using clean insead of this
6829
+ dissolveArcs(dataset);
6830
+ }
6803
6831
  }
6832
+
6833
+ lyr2.name = opts.name || lyr2.name;
6834
+ return lyr2;
6804
6835
  }
6805
6836
 
6806
6837
  // Transform the points in a dataset in-place; don't clean up corrupted shapes
@@ -6829,6 +6860,7 @@
6829
6860
  pruneArcs: pruneArcs,
6830
6861
  replaceLayers: replaceLayers,
6831
6862
  replaceLayerContents: replaceLayerContents,
6863
+ mergeOutputLayerIntoDataset: mergeOutputLayerIntoDataset,
6832
6864
  transformPoints: transformPoints
6833
6865
  });
6834
6866
 
@@ -9412,6 +9444,12 @@
9412
9444
  area: function() {
9413
9445
  return _isPlanar ? ctx.planarArea : geom.getSphericalShapeArea(_ids, arcs);
9414
9446
  },
9447
+ // area2: function() {
9448
+ // return _isPlanar ? ctx.planarArea : geom.getSphericalShapeArea(_ids, arcs, WGS84.SEMIMINOR_RADIUS);
9449
+ // },
9450
+ // area3: function() {
9451
+ // return _isPlanar ? ctx.planarArea : geom.getSphericalShapeArea(_ids, arcs, WGS84.AUTHALIC_RADIUS);
9452
+ // },
9415
9453
  perimeter: function() {
9416
9454
  return geom.getShapePerimeter(_ids, arcs);
9417
9455
  },
@@ -13332,14 +13370,14 @@
13332
13370
  return mergeArcs([arcs, new ArcCollection(nn, xx, yy)]);
13333
13371
  }
13334
13372
 
13335
- var roundCoord$1 = getRoundingFunction(0.01);
13373
+ var roundCoord$2 = getRoundingFunction(0.01);
13336
13374
 
13337
13375
  function stringifyVertex(p) {
13338
- return ' ' + roundCoord$1(p[0]) + ' ' + roundCoord$1(p[1]);
13376
+ return ' ' + roundCoord$2(p[0]) + ' ' + roundCoord$2(p[1]);
13339
13377
  }
13340
13378
 
13341
13379
  function stringifyCP(p) {
13342
- return ' ' + roundCoord$1(p[2]) + ' ' + roundCoord$1(p[3]);
13380
+ return ' ' + roundCoord$2(p[2]) + ' ' + roundCoord$2(p[3]);
13343
13381
  }
13344
13382
 
13345
13383
  function isCubicCtrl(p) {
@@ -13374,40 +13412,9 @@
13374
13412
  stringifyCP(p2) + stringifyVertex(p2);
13375
13413
  }
13376
13414
 
13377
- function findArcCenter(p1, p2, degrees) {
13378
- var p3 = [(p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2], // midpoint betw. p1, p2
13379
- tan = 1 / Math.tan(degrees / 180 * Math.PI / 2),
13380
- cp = getAffineTransform(90, tan, [0, 0], p3)(p2[0], p2[1]);
13381
- return cp;
13382
- }
13383
-
13384
- // export function addBezierArcControlPoints(p1, p2, degrees) {
13385
- function addBezierArcControlPoints(points, degrees) {
13386
- // source: https://stackoverflow.com/questions/734076/how-to-best-approximate-a-geometrical-arc-with-a-bezier-curve
13387
- var p2 = points.pop(),
13388
- p1 = points.pop(),
13389
- cp = findArcCenter(p1, p2, degrees),
13390
- xc = cp[0],
13391
- yc = cp[1],
13392
- ax = p1[0] - xc,
13393
- ay = p1[1] - yc,
13394
- bx = p2[0] - xc,
13395
- by = p2[1] - yc,
13396
- q1 = ax * ax + ay * ay,
13397
- q2 = q1 + ax * bx + ay * by,
13398
- k2 = 4/3 * (Math.sqrt(2 * q1 * q2) - q2) / (ax * by - ay * bx);
13399
-
13400
- points.push(p1);
13401
- points.push([xc + ax - k2 * ay, yc + ay + k2 * ax, 'C']);
13402
- points.push([xc + bx + k2 * by, yc + by - k2 * bx, 'C']);
13403
- points.push(p2);
13404
- }
13405
-
13406
13415
  var SvgPathUtils = /*#__PURE__*/Object.freeze({
13407
13416
  __proto__: null,
13408
- stringifyLineStringCoords: stringifyLineStringCoords,
13409
- findArcCenter: findArcCenter,
13410
- addBezierArcControlPoints: addBezierArcControlPoints
13417
+ stringifyLineStringCoords: stringifyLineStringCoords
13411
13418
  });
13412
13419
 
13413
13420
  /* example patterns
@@ -13684,11 +13691,15 @@
13684
13691
  type: null,
13685
13692
  length: 'number', // e.g. arrow length
13686
13693
  rotation: 'number',
13694
+ radius: 'number',
13695
+ 'arrow-length': 'number',
13696
+ 'arrow-direction': 'number',
13687
13697
  'arrow-head-angle': 'number',
13688
13698
  'arrow-head-width': 'number',
13689
13699
  'arrow-stem-width': 'number',
13690
13700
  'arrow-stem-curve': 'number', // degrees of arc
13691
13701
  'arrow-stem-taper': 'number',
13702
+ 'arrow-min-stem': 'number',
13692
13703
  'arrow-scaling': 'number',
13693
13704
  effect: null // e.g. "fade"
13694
13705
  }, stylePropertyTypes);
@@ -13730,8 +13741,8 @@
13730
13741
  if (!isSupportedSvgSymbolProperty(svgName)) {
13731
13742
  return;
13732
13743
  }
13733
- var strVal = opts[optName].trim();
13734
- functions[svgName] = getSymbolPropertyAccessor(strVal, svgName, lyr);
13744
+ var val = opts[optName];
13745
+ functions[svgName] = getSymbolPropertyAccessor(val, svgName, lyr);
13735
13746
  properties.push(svgName);
13736
13747
  });
13737
13748
 
@@ -13754,7 +13765,8 @@
13754
13765
  return /[(){}.+-/*?:&|=\[]/.test(str);
13755
13766
  }
13756
13767
 
13757
- function getSymbolPropertyAccessor(strVal, svgName, lyr) {
13768
+ function getSymbolPropertyAccessor(val, svgName, lyr) {
13769
+ var strVal = String(val).trim();
13758
13770
  var typeHint = symbolPropertyTypes[svgName];
13759
13771
  var fields = lyr.data ? lyr.data.getFields() : [];
13760
13772
  var literalVal = null;
@@ -13856,7 +13868,6 @@
13856
13868
  isSvgColor: isSvgColor
13857
13869
  });
13858
13870
 
13859
- var symbolBuilders = {};
13860
13871
  var symbolRenderers = {};
13861
13872
 
13862
13873
  function getTransform(xy, scale) {
@@ -13869,7 +13880,6 @@
13869
13880
 
13870
13881
  var SvgCommon = /*#__PURE__*/Object.freeze({
13871
13882
  __proto__: null,
13872
- symbolBuilders: symbolBuilders,
13873
13883
  symbolRenderers: symbolRenderers,
13874
13884
  getTransform: getTransform
13875
13885
  });
@@ -19479,6 +19489,13 @@ ${svg}
19479
19489
  })
19480
19490
  .option('target', targetOpt);
19481
19491
 
19492
+ parser.command('include')
19493
+ .describe('import JS data and functions for use in JS expressions')
19494
+ .option('file', {
19495
+ DEFAULT: true,
19496
+ describe: 'file containing a JS object with key:value pairs to import'
19497
+ });
19498
+
19482
19499
  parser.command('inlay')
19483
19500
  .describe('inscribe a polygon layer inside another polygon layer')
19484
19501
  .option('source', {
@@ -19791,6 +19808,7 @@ ${svg}
19791
19808
  })
19792
19809
  .option('target', targetOpt);
19793
19810
 
19811
+
19794
19812
  parser.command('simplify')
19795
19813
  .validate(validateSimplifyOpts)
19796
19814
  .example('Retain 10% of removable vertices\n$ mapshaper input.shp -simplify 10%')
@@ -20029,26 +20047,81 @@ ${svg}
20029
20047
  .option('target', targetOpt);
20030
20048
 
20031
20049
  parser.command('symbols')
20032
- // .describe('generate a variety of SVG symbols')
20050
+ // .describe('symbolize points as polygons, circles, stars or arrows')
20033
20051
  .option('type', {
20034
- describe: 'symbol type'
20052
+ describe: 'symbol type (e.g. star, polygon, circle, arrow)'
20053
+ })
20054
+ .option('scale', {
20055
+ describe: 'scale symbols by a factor',
20056
+ type: 'number'
20057
+ })
20058
+ .option('pixel-scale', {
20059
+ describe: 'symbol scale in meters-per-pixel (see polygons option)',
20060
+ type: 'number',
20061
+ })
20062
+ .option('polygons', {
20063
+ describe: 'generate symbols as polygons instead of SVG objects',
20064
+ type: 'flag'
20065
+ })
20066
+ .option('radius', {
20067
+ describe: 'distance from center to farthest point on the symbol',
20068
+ type: 'distance'
20069
+ })
20070
+ .option('sides', {
20071
+ describe: 'sides of a polygon or star symbol',
20072
+ type: 'number'
20073
+ })
20074
+ .option('rotation', {
20075
+ describe: 'rotation of symbol in degrees'
20076
+ })
20077
+ .option('orientation', {
20078
+ describe: 'use orientation=b for a rotated or flipped orientation'
20079
+ })
20080
+ .option('length', {
20081
+ // alias for arrow-length
20082
+ })
20083
+ .option('star-ratio', {
20084
+ describe: 'ratio of major to minor radius of star',
20085
+ type: 'number'
20086
+ })
20087
+ .option('arrow-length', {
20088
+ describe: 'length of arrows in pixels (use with type=arrow)'
20089
+ })
20090
+ .option('arrow-direction', {
20091
+ describe: 'angle off of vertical (-90 = left-pointing arrow)'
20092
+ })
20093
+ .option('arrow-head-angle', {
20094
+ describe: 'angle of tip of arrow (default is 40 degrees)'
20095
+ })
20096
+ .option('arrow-head-width', {
20097
+ describe: 'size of arrow head from side to side'
20098
+ })
20099
+ .option('arrow-stem-width', {
20100
+ describe: 'width of stem at its widest point'
20101
+ })
20102
+ .option('arrow-stem-taper', {
20103
+ describe: 'factor for tapering the width of the stem'
20104
+ })
20105
+ .option('arrow-stem-curve', {
20106
+ describe: 'curvature in degrees (arrows are straight by default)'
20107
+ })
20108
+ .option('arrow-min-stem', {
20109
+ describe: 'min ratio of stem to total length (for small arrows)',
20110
+ type: 'number'
20035
20111
  })
20036
20112
  .option('stroke', {})
20037
20113
  .option('stroke-width', {})
20038
- .option('fill', {})
20039
- .option('length', {})
20040
- .option('rotation', {})
20114
+ .option('fill', {
20115
+ describe: 'symbol fill color'
20116
+ })
20041
20117
  .option('effect', {})
20042
- .option('arrow-head-angle', {})
20043
- .option('arrow-stem-width', {})
20044
- .option('arrow-head-width', {})
20045
- .option('arrow-stem-curve', {})
20046
- .option('arrow-stem-taper', {})
20047
- .option('arrow-scaling', {})
20048
- .option('where', whereOpt)
20049
- .option('target', targetOpt);
20118
+ // .option('where', whereOpt)
20119
+ .option('name', nameOpt)
20120
+ .option('target', targetOpt)
20121
+ .option('no-replace', noReplaceOpt);
20050
20122
  // .option('name', nameOpt);
20051
20123
 
20124
+
20052
20125
  parser.command('target')
20053
20126
  .describe('set active layer (or layers)')
20054
20127
  .option('target', {
@@ -20199,13 +20272,6 @@ ${svg}
20199
20272
  })
20200
20273
  .option('name', nameOpt);
20201
20274
 
20202
- parser.command('include')
20203
- .describe('import JS data and functions for use in JS expressions')
20204
- .option('file', {
20205
- DEFAULT: true,
20206
- describe: 'file containing a JS object with key:value pairs to import'
20207
- });
20208
-
20209
20275
  parser.command('fuzzy-join')
20210
20276
  .describe('join points to polygons, with data fill and fuzzy match')
20211
20277
  .option('source', {
@@ -20341,16 +20407,6 @@ ${svg}
20341
20407
  })
20342
20408
  .option('name', nameOpt);
20343
20409
 
20344
- // parser.command('shapes')
20345
- // .describe('convert points to shapes')
20346
- // .option('type', {
20347
- // })
20348
- // .option('size', {
20349
- // })
20350
- // .option('rotation', {
20351
- // })
20352
-
20353
-
20354
20410
  parser.command('subdivide')
20355
20411
  .describe('recursively split a layer using a JS expression')
20356
20412
  .validate(validateExpressionOpt)
@@ -21214,12 +21270,13 @@ ${svg}
21214
21270
  if (this instanceof ShpReader === false) {
21215
21271
  return new ShpReader(shpSrc, shxSrc);
21216
21272
  }
21217
-
21218
21273
  var shpFile = utils.isString(shpSrc) ? new FileReader(shpSrc) : new BufferReader(shpSrc);
21219
21274
  var header = parseHeader(shpFile.readToBinArray(0, 100));
21220
- var shpSize = shpFile.size();
21221
- var RecordClass = new ShpRecordClass(header.type);
21222
- var shpOffset, recordCount;
21275
+ var shpType = header.type;
21276
+ var shpOffset = 100; // used when reading .shp without .shx
21277
+ var recordCount = 0;
21278
+ var badRecordNumberCount = 0;
21279
+ var RecordClass = new ShpRecordClass(shpType);
21223
21280
  var shxBin, shxFile;
21224
21281
 
21225
21282
  if (shxSrc) {
@@ -21227,8 +21284,6 @@ ${svg}
21227
21284
  shxBin = shxFile.readToBinArray(0, shxFile.size()).bigEndian();
21228
21285
  }
21229
21286
 
21230
- reset();
21231
-
21232
21287
  this.header = function() {
21233
21288
  return header;
21234
21289
  };
@@ -21246,37 +21301,35 @@ ${svg}
21246
21301
 
21247
21302
  // Iterator interface for reading shape records
21248
21303
  this.nextShape = function() {
21249
- var shape = readNextShape(recordCount);
21250
- if (shape) {
21251
- recordCount++;
21252
- } else {
21253
- shpFile.close();
21254
- reset();
21304
+ var shape;
21305
+ if (!shpFile) {
21306
+ error('Tried to read from a used ShpReader');
21307
+ // return null; // this reader was already used
21255
21308
  }
21309
+ shape = readNextShape(recordCount);
21310
+ if (!shape) {
21311
+ done();
21312
+ return null;
21313
+ }
21314
+ recordCount++;
21256
21315
  return shape;
21257
21316
  };
21258
21317
 
21259
21318
  // Returns a shape record or null if no more shapes can be read
21319
+ // i: Expected 0-based index of the next record
21260
21320
  //
21261
21321
  function readNextShape(i) {
21262
- var expectedId = i + 1; // Shapefile ids are 1-based
21263
- var shape, offset;
21264
- if (shxBin) {
21265
- if (shxFile.size() <= 100 + i * 8) return null; // done
21266
- shxBin.position(100 + i * 8);
21267
- offset = shxBin.readUint32() * 2;
21268
- shape = readIndexedShape(shpFile, offset, expectedId);
21269
- } else {
21270
- // Reading without a .shx file (returns null at end-of-file)
21271
- offset = shpOffset;
21272
- shape = readNonIndexedShape(shpFile, offset, expectedId);
21273
- }
21274
- return shape || null;
21322
+ return shxBin ?
21323
+ readIndexedShape(shpFile, shxBin, i) :
21324
+ readNonIndexedShape(shpFile, shpOffset, i);
21275
21325
  }
21276
21326
 
21277
- function reset() {
21278
- shpOffset = 100;
21279
- recordCount = 0;
21327
+ function done() {
21328
+ shpFile.close();
21329
+ shpFile = shxFile = shxBin = null;
21330
+ if (badRecordNumberCount > 0) {
21331
+ message(`Warning: ${badRecordNumberCount}/${recordCount} features have non-standard record numbers in the .shp file.`);
21332
+ }
21280
21333
  }
21281
21334
 
21282
21335
  function parseHeader(bin) {
@@ -21307,33 +21360,35 @@ ${svg}
21307
21360
 
21308
21361
 
21309
21362
  function readShapeAtOffset(shpFile, offset) {
21310
- var shape = null,
21311
- recordSize, recordType, recordId, goodSize, goodType, bin;
21312
-
21313
- if (offset + 12 <= shpSize) {
21314
- bin = shpFile.readToBinArray(offset, 12);
21315
- recordId = bin.bigEndian().readUint32();
21316
- // record size is bytes in content section + 8 header bytes
21317
- recordSize = bin.readUint32() * 2 + 8;
21318
- recordType = bin.littleEndian().readUint32();
21319
- goodSize = offset + recordSize <= shpSize && recordSize >= 12;
21320
- goodType = recordType === 0 || recordType == header.type;
21321
- if (goodSize && goodType) {
21322
- bin = shpFile.readToBinArray(offset, recordSize);
21323
- shape = new RecordClass(bin, recordSize);
21324
- }
21363
+ var fileSize = shpFile.size();
21364
+ if (offset + 12 > fileSize) return null; // reached end-of-file
21365
+ var bin = shpFile.readToBinArray(offset, 12);
21366
+ var recordId = bin.bigEndian().readUint32();
21367
+ // record size is bytes in content section + 8 header bytes
21368
+ var recordSize = bin.readUint32() * 2 + 8;
21369
+ var recordType = bin.littleEndian().readUint32();
21370
+ var goodSize = offset + recordSize <= fileSize && recordSize >= 12;
21371
+ var goodType = recordType === 0 || recordType == shpType;
21372
+ if (!goodSize || !goodType) {
21373
+ return null;
21325
21374
  }
21326
- return shape;
21375
+ bin = shpFile.readToBinArray(offset, recordSize);
21376
+ return new RecordClass(bin, recordSize);
21327
21377
  }
21328
21378
 
21329
- function readIndexedShape(shpFile, offset, expectedId) {
21379
+ function readIndexedShape(shpFile, shxBin, i) {
21380
+ if (shxBin.size() <= 100 + i * 8) return null; // done
21381
+ shxBin.position(100 + i * 8);
21382
+ var expectedId = i + 1;
21383
+ var offset = shxBin.readUint32() * 2;
21384
+ var recLen = shxBin.readUint32() * 2; // TODO: match this to recLen in .shp
21330
21385
  var shape = readShapeAtOffset(shpFile, offset);
21331
21386
  if (!shape) {
21332
21387
  stop('Index of Shapefile record', expectedId, 'in the .shx file is invalid.');
21333
21388
  }
21334
21389
  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}).`);
21390
+ badRecordNumberCount++;
21391
+ verbose(`Warning: A feature has a different record number in .shx (${expectedId}) and .shp (${shape.id}).`);
21337
21392
  }
21338
21393
  // TODO: consider printing verbose message if a .shp file contains garbage bytes
21339
21394
  // example files:
@@ -21346,11 +21401,12 @@ ${svg}
21346
21401
  // in consecutive sequence in the .shp file. This is a problem when the .shx
21347
21402
  // index file is not present.
21348
21403
  //
21349
- // Here, we try to scan past invalid content to find the next record.
21404
+ // Here, we try to scan past any invalid content to find the next record.
21350
21405
  // Records are required to be in sequential order.
21351
21406
  //
21352
- function readNonIndexedShape(shpFile, start, expectedId) {
21353
- var offset = start,
21407
+ function readNonIndexedShape(shpFile, start, i) {
21408
+ var expectedId = i + 1, // Shapefile ids are 1-based
21409
+ offset = start,
21354
21410
  fileSize = shpFile.size(),
21355
21411
  shape = null,
21356
21412
  bin, recordId, recordType, isValidType;
@@ -21358,7 +21414,7 @@ ${svg}
21358
21414
  bin = shpFile.readToBinArray(offset, 12);
21359
21415
  recordId = bin.bigEndian().readUint32();
21360
21416
  recordType = bin.littleEndian().skipBytes(4).readUint32();
21361
- isValidType = recordType == header.type || recordType === 0;
21417
+ isValidType = recordType == shpType || recordType === 0;
21362
21418
  if (!isValidType || recordId != expectedId && recordType === 0) {
21363
21419
  offset += 4; // keep scanning -- try next integer position
21364
21420
  continue;
@@ -21385,22 +21441,6 @@ ${svg}
21385
21441
  return this.header().type;
21386
21442
  };
21387
21443
 
21388
- ShpReader.prototype.getCounts = function() {
21389
- var counts = {
21390
- nullCount: 0,
21391
- partCount: 0,
21392
- shapeCount: 0,
21393
- pointCount: 0
21394
- };
21395
- this.forEachShape(function(shp) {
21396
- if (shp.isNull) counts.nullCount++;
21397
- counts.pointCount += shp.pointCount;
21398
- counts.partCount += shp.partCount;
21399
- counts.shapeCount++;
21400
- });
21401
- return counts;
21402
- };
21403
-
21404
21444
  // Apply snapping, remove duplicate coords and clean up defective paths in a dataset
21405
21445
  // Assumes that any CRS info has been added to the dataset
21406
21446
  // @opts: import options
@@ -26209,7 +26249,6 @@ ${svg}
26209
26249
  if (constTol) return constTol;
26210
26250
  return constTol ? constTol : meterDist * pctOfRadius;
26211
26251
  };
26212
-
26213
26252
  }
26214
26253
 
26215
26254
  function getBufferDistanceFunction(lyr, dataset, opts) {
@@ -27382,7 +27421,7 @@ ${svg}
27382
27421
  cmd.buffer = makeBufferLayer;
27383
27422
 
27384
27423
  function makeBufferLayer(lyr, dataset, opts) {
27385
- var dataset2, lyr2;
27424
+ var dataset2;
27386
27425
  if (lyr.geometry_type == 'point') {
27387
27426
  dataset2 = makePointBuffer(lyr, dataset, opts);
27388
27427
  } else if (lyr.geometry_type == 'polyline') {
@@ -27392,13 +27431,9 @@ ${svg}
27392
27431
  } else {
27393
27432
  stop("Unsupported geometry type");
27394
27433
  }
27395
- var outputLayers = mergeDatasetsIntoDataset(dataset, [dataset2]);
27396
- lyr2 = outputLayers[0];
27397
- setOutputLayerName(lyr2, lyr, null, opts);
27398
- if (lyr.data && !lyr2.data) {
27399
- lyr2.data = opts.no_replace ? lyr.data.clone() : lyr.data;
27400
- }
27401
- return outputLayers;
27434
+
27435
+ var lyr2 = mergeOutputLayerIntoDataset(lyr, dataset, dataset2, opts);
27436
+ return [lyr2];
27402
27437
  }
27403
27438
 
27404
27439
  // TODO: support three or more stops
@@ -33983,7 +34018,7 @@ ${svg}
33983
34018
 
33984
34019
  function createMeridianPart(x, ymin, ymax) {
33985
34020
  var coords = densifyPathByInterval([[x, ymin], [x, ymax]], precision);
33986
- meridians.push(graticuleFeature(coords, {type: 'meridian', value: roundCoord(x)}));
34021
+ meridians.push(graticuleFeature(coords, {type: 'meridian', value: roundCoord$1(x)}));
33987
34022
  }
33988
34023
 
33989
34024
  function createParallel(y) {
@@ -33993,7 +34028,7 @@ ${svg}
33993
34028
  }
33994
34029
 
33995
34030
  // remove tiny offsets
33996
- function roundCoord(x) {
34031
+ function roundCoord$1(x) {
33997
34032
  return +x.toFixed(3) || 0;
33998
34033
  }
33999
34034
 
@@ -36644,6 +36679,99 @@ ${svg}
36644
36679
  parseScalebarLabelToKm: parseScalebarLabelToKm
36645
36680
  });
36646
36681
 
36682
+ cmd.shape = function(targetDataset, opts) {
36683
+ var geojson, dataset;
36684
+ if (opts.coordinates) {
36685
+ geojson = makeShapeFromCoords(opts);
36686
+ } else if (opts.type == 'circle') {
36687
+ geojson = makeCircle(opts);
36688
+ } else if (opts.type == 'rectangle' && opts.bbox) {
36689
+ geojson = getRectangleGeoJSON(opts);
36690
+ } else {
36691
+ stop('Missing coordinates parameter');
36692
+ }
36693
+ // TODO: project shape if targetDataset is projected
36694
+ dataset = importGeoJSON(geojson, {});
36695
+ if (opts.rotation) {
36696
+ rotateDatasetCoords(dataset, opts.rotation);
36697
+ }
36698
+ dataset.layers[0].name = opts.name || opts.type || 'shape';
36699
+ return dataset;
36700
+ };
36701
+
36702
+ function getRectangleGeoJSON(opts) {
36703
+ var bbox = opts.bbox,
36704
+ xmin = bbox[0],
36705
+ ymin = bbox[1],
36706
+ xmax = bbox[2],
36707
+ ymax = bbox[3],
36708
+ interval = 0.5,
36709
+ coords = [],
36710
+ type = opts.geometry == 'polyline' ? 'LineString' : 'Polygon';
36711
+ addSide(xmin, ymin, xmin, ymax);
36712
+ addSide(xmin, ymax, xmax, ymax);
36713
+ addSide(xmax, ymax, xmax, ymin);
36714
+ addSide(xmax, ymin, xmin, ymin);
36715
+ coords.push([xmin, ymin]);
36716
+ return {
36717
+ type: type,
36718
+ coordinates: type == 'Polygon' ? [coords] : coords
36719
+ };
36720
+
36721
+ function addSide(x1, y1, x2, y2) {
36722
+ var dx = x2 - x1,
36723
+ dy = y2 - y1,
36724
+ n = Math.ceil(Math.max(Math.abs(dx) / interval, Math.abs(dy) / interval)),
36725
+ xint = dx / n,
36726
+ yint = dy / n;
36727
+ for (var i=0; i<n; i++) {
36728
+ coords.push([x1 + i * xint, y1 + i * yint]);
36729
+ }
36730
+ }
36731
+ }
36732
+
36733
+ function makeCircle(opts) {
36734
+ if (opts.radius > 0 === false && opts.radius_angle > 0 === false) {
36735
+ stop('Missing required radius parameter.');
36736
+ }
36737
+ var cp = opts.center || [0, 0];
36738
+ var radius = opts.radius || getCircleRadiusFromAngle(getCRS('wgs84'), opts.radius_angle);
36739
+ return getCircleGeoJSON(cp, radius, null, {geometry_type : opts.geometry || 'polygon'});
36740
+ }
36741
+
36742
+ function makeShapeFromCoords(opts) {
36743
+ var coordinates = [];
36744
+ var offsets = opts.offsets || [];
36745
+ var coords = opts.coordinates;
36746
+ var type, i, x, y;
36747
+ if (coords.length >= 2 === false) {
36748
+ stop('Invalid coordinates parameter.');
36749
+ }
36750
+ for (i=0; i<coords.length; i+= 2) {
36751
+ x = coords[i];
36752
+ y = coords[i + 1];
36753
+ coordinates.push([x, y]);
36754
+ }
36755
+ for (i=0; i<offsets.length; i+=2) {
36756
+ x += offsets[i];
36757
+ y += offsets[i + 1];
36758
+ coordinates.push([x, y]);
36759
+ }
36760
+ if (GeoJSON.pathIsRing(coordinates)) {
36761
+ type = 'Polygon';
36762
+ } else if (opts.closed && coordinates.length >= 3) {
36763
+ type = 'Polygon';
36764
+ coordinates.push(coordinates[0]);
36765
+ } else {
36766
+ type = 'LineString';
36767
+ }
36768
+ return {
36769
+ type: type,
36770
+ coordinates: type == 'Polygon' ? [coordinates] : coordinates
36771
+ };
36772
+
36773
+ }
36774
+
36647
36775
  function calcSimplifyStats(arcs, use3D) {
36648
36776
  var distSq = use3D ? pointSegGeoDistSq : geom.pointSegDistSq,
36649
36777
  calcAngle = use3D ? geom.signedAngleSph : geom.signedAngle,
@@ -37731,56 +37859,83 @@ ${svg}
37731
37859
  });
37732
37860
  };
37733
37861
 
37734
- symbolBuilders.arrow = function(d) {
37735
- var len = 'length' in d ? d.length : 10;
37736
- var filled = 'fill' in d;
37737
- return filled ? getFilledArrow(d, len) : getStickArrow(d, len);
37738
- };
37862
+ var roundCoord = getRoundingFunction(0.01);
37739
37863
 
37740
- function getStickArrow(d, len) {
37741
- return {
37742
- type: 'polyline',
37743
- coordinates: getStickArrowCoords(d, len),
37744
- stroke: d.stroke || 'magenta',
37745
- 'stroke-width': 'stroke-width' in d ? d['stroke-width'] : 1
37746
- };
37864
+ function forEachSymbolCoord(coords, cb) {
37865
+ var isPoint = coords && utils.isNumber(coords[0]);
37866
+ var isNested = !isPoint && coords && Array.isArray(coords[0]);
37867
+ if (isPoint) return cb(coords);
37868
+ for (var i=0; i<coords.length; i++) {
37869
+ if (isNested) forEachSymbolCoord(coords[i], cb);
37870
+ }
37747
37871
  }
37748
37872
 
37749
- function getFilledArrow(d, totalLen) {
37750
- return {
37751
- type: 'polygon',
37752
- coordinates: getFilledArrowCoords(d, totalLen),
37753
- fill: d.fill || 'magenta'
37754
- };
37873
+ function flipY(coords) {
37874
+ forEachSymbolCoord(coords, function(p) {
37875
+ p[1] = -p[1];
37876
+ });
37755
37877
  }
37756
37878
 
37757
- function getScale(totalLen, headLen) {
37758
- var maxHeadPct = 0.60;
37759
- var headPct = headLen / totalLen;
37760
- if (headPct > maxHeadPct) {
37761
- return maxHeadPct / headPct;
37762
- }
37763
- return 1;
37879
+ function scaleAndShiftCoords(coords, scale, shift) {
37880
+ forEachSymbolCoord(coords, function(xy) {
37881
+ xy[0] = xy[0] * scale + shift[0];
37882
+ xy[1] = xy[1] * scale + shift[1];
37883
+ });
37764
37884
  }
37765
37885
 
37766
- function getStickArrowTip(totalLen, curve) {
37767
- // curve/2 intersects the arrowhead at 90deg (trigonometry)
37768
- var theta = Math.abs(curve/2) / 180 * Math.PI;
37769
- var dx = totalLen * Math.sin(theta) * (curve > 0 ? -1 : 1);
37770
- var dy = totalLen * Math.cos(theta);
37771
- return [dx, dy];
37886
+ function roundCoordsForSVG(coords) {
37887
+ forEachSymbolCoord(coords, function(p) {
37888
+ p[0] = roundCoord(p[0]);
37889
+ p[1] = roundCoord(p[1]);
37890
+ });
37772
37891
  }
37773
37892
 
37774
- function addPoints(a, b) {
37775
- return [a[0] + b[0], a[1] + b[1]];
37893
+ function rotateCoords(coords, rotation) {
37894
+ if (!rotation) return;
37895
+ var f = getAffineTransform(rotation, 1, [0, 0], [0, 0]);
37896
+ forEachSymbolCoord(coords, function(p) {
37897
+ var p2 = f(p[0], p[1]);
37898
+ p[0] = p2[0];
37899
+ p[1] = p2[1];
37900
+ });
37901
+ }
37902
+
37903
+ function findArcCenter(p1, p2, degrees) {
37904
+ var p3 = [(p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2], // midpoint betw. p1, p2
37905
+ tan = 1 / Math.tan(degrees / 180 * Math.PI / 2),
37906
+ cp = getAffineTransform(90, tan, [0, 0], p3)(p2[0], p2[1]);
37907
+ return cp;
37908
+ }
37909
+
37910
+ // export function addBezierArcControlPoints(p1, p2, degrees) {
37911
+ function addBezierArcControlPoints(points, degrees) {
37912
+ // source: https://stackoverflow.com/questions/734076/how-to-best-approximate-a-geometrical-arc-with-a-bezier-curve
37913
+ var p2 = points.pop(),
37914
+ p1 = points.pop(),
37915
+ cp = findArcCenter(p1, p2, degrees),
37916
+ xc = cp[0],
37917
+ yc = cp[1],
37918
+ ax = p1[0] - xc,
37919
+ ay = p1[1] - yc,
37920
+ bx = p2[0] - xc,
37921
+ by = p2[1] - yc,
37922
+ q1 = ax * ax + ay * ay,
37923
+ q2 = q1 + ax * bx + ay * by,
37924
+ k2 = 4/3 * (Math.sqrt(2 * q1 * q2) - q2) / (ax * by - ay * bx);
37925
+
37926
+ points.push(p1);
37927
+ points.push([xc + ax - k2 * ay, yc + ay + k2 * ax, 'C']);
37928
+ points.push([xc + bx + k2 * by, yc + by - k2 * bx, 'C']);
37929
+ points.push(p2);
37776
37930
  }
37777
37931
 
37778
37932
  function getStickArrowCoords(d, totalLen) {
37933
+ var minStemRatio = getMinStemRatio(d);
37779
37934
  var headAngle = d['arrow-head-angle'] || 90;
37780
37935
  var curve = d['arrow-stem-curve'] || 0;
37781
37936
  var unscaledHeadWidth = d['arrow-head-width'] || 9;
37782
37937
  var unscaledHeadLen = getHeadLength(unscaledHeadWidth, headAngle);
37783
- var scale = getScale(totalLen, unscaledHeadLen); // scale down small arrows
37938
+ var scale = getScale(totalLen, unscaledHeadLen, minStemRatio);
37784
37939
  var headWidth = unscaledHeadWidth * scale;
37785
37940
  var headLen = unscaledHeadLen * scale;
37786
37941
  var tip = getStickArrowTip(totalLen, curve);
@@ -37791,84 +37946,268 @@ ${svg}
37791
37946
  if (!headLen) return [stem];
37792
37947
  var head = [addPoints([-headWidth / 2, -headLen], tip), tip.concat(), addPoints([headWidth / 2, -headLen], tip)];
37793
37948
 
37794
- rotateSymbolCoords(stem, d.rotation);
37795
- rotateSymbolCoords(head, d.rotation);
37949
+ rotateCoords(stem, d.rotation);
37950
+ rotateCoords(head, d.rotation);
37796
37951
  return [stem, head];
37797
37952
  }
37798
37953
 
37799
- function getHeadLength(headWidth, headAngle) {
37800
- var headRatio = 1 / Math.tan(Math.PI * headAngle / 180 / 2) / 2; // length-to-width head ratio
37801
- return headWidth * headRatio;
37954
+ function getMinStemRatio(d) {
37955
+ return d['arrow-min-stem'] >= 0 ? d['arrow-min-stem'] : 0.4;
37802
37956
  }
37803
37957
 
37804
- function getFilledArrowCoords(d, totalLen) {
37805
- var headAngle = d['arrow-head-angle'] || 40,
37958
+ function getFilledArrowCoords(totalLen, d) {
37959
+ var minStemRatio = getMinStemRatio(d),
37960
+ headAngle = d['arrow-head-angle'] || 40,
37961
+ direction = d.rotation || d['arrow-direction'] || 0,
37806
37962
  unscaledStemWidth = d['arrow-stem-width'] || 2,
37807
37963
  unscaledHeadWidth = d['arrow-head-width'] || unscaledStemWidth * 3,
37808
37964
  unscaledHeadLen = getHeadLength(unscaledHeadWidth, headAngle),
37809
- scale = getScale(totalLen, unscaledHeadLen), // scale down small arrows
37965
+ scale = getScale(totalLen, unscaledHeadLen, minStemRatio),
37810
37966
  headWidth = unscaledHeadWidth * scale,
37811
37967
  headLen = unscaledHeadLen * scale,
37812
37968
  stemWidth = unscaledStemWidth * scale,
37813
37969
  stemTaper = d['arrow-stem-taper'] || 0,
37970
+ stemCurve = d['arrow-stem-curve'] || 0,
37814
37971
  stemLen = totalLen - headLen;
37815
37972
 
37816
37973
  var headDx = headWidth / 2,
37817
37974
  stemDx = stemWidth / 2,
37818
37975
  baseDx = stemDx * (1 - stemTaper);
37819
37976
 
37820
- var coords = [[baseDx, 0], [stemDx, stemLen], [headDx, stemLen], [0, stemLen + headLen],
37977
+ var coords;
37978
+
37979
+ if (!stemCurve || Math.abs(stemCurve) > 90) {
37980
+ coords = [[baseDx, 0], [stemDx, stemLen], [headDx, stemLen], [0, stemLen + headLen],
37821
37981
  [-headDx, stemLen], [-stemDx, stemLen], [-baseDx, 0], [baseDx, 0]];
37982
+ } else {
37983
+ if (direction > 0) stemCurve = -stemCurve;
37984
+ coords = getCurvedArrowCoords(stemLen, headLen, stemCurve, stemDx, headDx, baseDx);
37985
+ }
37822
37986
 
37823
- rotateSymbolCoords(coords, d.rotation);
37987
+ rotateCoords(coords, direction);
37824
37988
  return [coords];
37825
37989
  }
37826
37990
 
37827
- function rotateSymbolCoords(coords, rotation) {
37828
- // TODO: consider avoiding re-instantiating function on every call
37829
- var f = getAffineTransform(rotation || 0, 1, [0, 0], [0, 0]);
37830
- coords.forEach(function(p) {
37831
- var p2 = f ? f(p[0], p[1]) : p;
37832
- p[0] = p2[0];
37833
- p[1] = -p2[1]; // flip y-axis (to produce display coords)
37834
- });
37991
+
37992
+ function getScale(totalLen, headLen, minStemRatio) {
37993
+ var maxHeadPct = 1 - minStemRatio;
37994
+ var headPct = headLen / totalLen;
37995
+ if (headPct > maxHeadPct) {
37996
+ return maxHeadPct / headPct;
37997
+ }
37998
+ return 1;
37835
37999
  }
37836
38000
 
37837
- // TODO: refactor to remove duplication in mapshaper-svg-style.js
37838
- cmd.symbols = function(lyr, opts) {
37839
- var f, filter;
37840
- // console.log("-symbols opts", opts)
37841
- requirePointLayer(lyr);
37842
- f = getSymbolDataAccessor(lyr, opts);
37843
- if (opts.where) {
37844
- filter = compileValueExpression(opts.where, lyr, null);
38001
+ function getStickArrowTip(totalLen, curve) {
38002
+ // curve/2 intersects the arrowhead at 90deg (trigonometry)
38003
+ var theta = Math.abs(curve/2) / 180 * Math.PI;
38004
+ var dx = totalLen * Math.sin(theta) * (curve > 0 ? -1 : 1);
38005
+ var dy = totalLen * Math.cos(theta);
38006
+ return [dx, dy];
38007
+ }
38008
+
38009
+ function addPoints(a, b) {
38010
+ return [a[0] + b[0], a[1] + b[1]];
38011
+ }
38012
+
38013
+
38014
+ function getHeadLength(headWidth, headAngle) {
38015
+ var headRatio = 1 / Math.tan(Math.PI * headAngle / 180 / 2) / 2; // length-to-width head ratio
38016
+ return headWidth * headRatio;
38017
+ }
38018
+
38019
+ function getCurvedArrowCoords(stemLen, headLen, curvature, stemDx, headDx, baseDx) {
38020
+ // coordinates go counter clockwise, starting from the leftmost head coordinate
38021
+ var theta = Math.abs(curvature) / 180 * Math.PI;
38022
+ var sign = curvature > 0 ? 1 : -1;
38023
+ var dx = stemLen * Math.sin(theta / 2) * sign;
38024
+ var dy = stemLen * Math.cos(theta / 2);
38025
+ var head = [[stemDx + dx, dy], [headDx + dx, dy],
38026
+ [dx, headLen + dy], [-headDx + dx, dy], [-stemDx + dx, dy]];
38027
+ var ax = baseDx * Math.cos(theta); // rotate arrow base
38028
+ var ay = baseDx * Math.sin(theta) * -sign;
38029
+ var leftStem = getCurvedStemCoords(-ax, -ay, -stemDx + dx, dy, theta);
38030
+ var rightStem = getCurvedStemCoords(ax, ay, stemDx + dx, dy, theta);
38031
+ // if (stemTaper == 1) leftStem.pop();
38032
+ var stem = leftStem.concat(rightStem.reverse());
38033
+ stem.pop();
38034
+ return stem.concat(head);
38035
+ }
38036
+
38037
+ // ax, ay: point on the base
38038
+ // bx, by: point on the stem
38039
+ function getCurvedStemCoords(ax, ay, bx, by, theta0) {
38040
+ var dx = bx - ax,
38041
+ dy = by - ay,
38042
+ dy1 = (dy * dy - dx * dx) / (2 * dy),
38043
+ dy2 = dy - dy1,
38044
+ dx2 = Math.sqrt(dx * dx + dy * dy) / 2,
38045
+ theta = Math.PI - Math.asin(dx2 / dy2) * 2,
38046
+ degrees = theta * 180 / Math.PI,
38047
+ radius = dy2 / Math.tan(theta / 2),
38048
+ leftBend = bx > ax,
38049
+ sign = leftBend ? 1 : -1,
38050
+ points = Math.round(degrees / 5) + 2,
38051
+ // points = theta > 2 && 7 || theta > 1 && 6 || 5,
38052
+ increment = theta / (points + 1);
38053
+
38054
+ var coords = [[bx, by]];
38055
+ for (var i=1; i<= points; i++) {
38056
+ var phi = i * increment / 2;
38057
+ var sinPhi = Math.sin(phi);
38058
+ var cosPhi = Math.cos(phi);
38059
+ var c = sinPhi * radius * 2;
38060
+ var a = sinPhi * c;
38061
+ var b = cosPhi * c;
38062
+ coords.push([bx - a * sign, by - b]);
38063
+ }
38064
+ coords.push([ax, ay]);
38065
+ return coords;
38066
+ }
38067
+
38068
+ // sides: e.g. 5-pointed star has 10 sides
38069
+ // radius: distance from center to point
38070
+ //
38071
+ function getPolygonCoords(radius, opts) {
38072
+ var type = opts.type;
38073
+ var sides = +opts.sides || getDefaultSides(type);
38074
+ var isStar = type == 'star';
38075
+ if (isStar && (sides < 6 || sides % 2 !== 0)) {
38076
+ stop(`Invalid number of sides for a star (${sides})`);
38077
+ } else if (sides >= 3 === false) {
38078
+ stop(`Invalid number of sides (${sides})`);
37845
38079
  }
37846
- getLayerDataTable(lyr).getRecords().forEach(function(rec, i) {
37847
- if (filter && filter(i)) {
37848
- if ('svg-symbol' in rec === false) {
37849
- rec['svg-symbol'] = undefined;
37850
- }
38080
+ var coords = [],
38081
+ angle = 360 / sides,
38082
+ b = isStar ? 1 : 0.5,
38083
+ theta, even, len;
38084
+ if (opts.orientation == 'b') {
38085
+ b = 0;
38086
+ }
38087
+ for (var i=0; i<sides; i++) {
38088
+ even = i % 2 == 0;
38089
+ len = radius;
38090
+ if (isStar && even) {
38091
+ len *= (opts.star_ratio || 0.5);
38092
+ }
38093
+ theta = (i + b) * angle % 360;
38094
+ coords.push(getPlanarSegmentEndpoint(0, 0, theta, len));
38095
+ }
38096
+ coords.push(coords[0].concat());
38097
+ return [coords];
38098
+ }
38099
+
38100
+ function getDefaultSides(type) {
38101
+ return {
38102
+ star: 10,
38103
+ circle: 72,
38104
+ triangle: 3,
38105
+ square: 4,
38106
+ pentagon: 5,
38107
+ hexagon: 6,
38108
+ heptagon: 7,
38109
+ octagon: 8,
38110
+ nonagon: 9,
38111
+ decagon: 10
38112
+ }[type] || 4;
38113
+ }
38114
+
38115
+ // TODO: refactor to remove duplication in mapshaper-svg-style.js
38116
+ cmd.symbols = function(inputLyr, dataset, opts) {
38117
+ requireSinglePointLayer(inputLyr);
38118
+ var lyr = opts.no_replace ? copyLayer(inputLyr) : inputLyr;
38119
+ var polygonMode = !!opts.polygons;
38120
+ var metersPerPx;
38121
+ if (polygonMode) {
38122
+ requireProjectedDataset(dataset);
38123
+ metersPerPx = opts.pixel_scale || getMetersPerPixel(lyr, dataset);
38124
+ }
38125
+ var records = getLayerDataTable(lyr).getRecords();
38126
+ var getSymbolData = getSymbolDataAccessor(lyr, opts);
38127
+ var geometries = lyr.shapes.map(function(shp, i) {
38128
+ if (!shp) return null;
38129
+ var d = getSymbolData(i);
38130
+ var rec = records[i] || {};
38131
+ var coords, size, constructor;
38132
+ if (d.type == 'arrow') {
38133
+ size = d.radius || d.length || d['arrow-length'] || d.r;
38134
+ constructor = getFilledArrowCoords;
37851
38135
  } else {
37852
- rec['svg-symbol'] = buildSymbol(f(i));
38136
+ size = d.radius || d.length || d.r;
38137
+ constructor = getPolygonCoords;
38138
+ }
38139
+ if (size > 0 === false) return null;
38140
+ coords = constructor(size, d, opts);
38141
+ rotateCoords(coords, +d.rotation || 0);
38142
+ if (!polygonMode) {
38143
+ flipY(coords);
38144
+ }
38145
+ if (+opts.scale) {
38146
+ scaleAndShiftCoords(coords, +opts.scale, [0, 0]);
38147
+ }
38148
+ if (polygonMode) {
38149
+ scaleAndShiftCoords(coords, metersPerPx, shp[0]);
38150
+ if (d.tfill) rec.fill = d.fill;
38151
+ return createGeometry(coords);
38152
+ } else {
38153
+ rec['svg-symbol'] = makeSvgSymbol(coords, d);
37853
38154
  }
37854
38155
  });
38156
+
38157
+ var outputLyr, dataset2;
38158
+ if (polygonMode) {
38159
+ dataset2 = importGeometries(geometries, records);
38160
+ outputLyr = mergeOutputLayerIntoDataset(inputLyr, dataset, dataset2, opts);
38161
+ outputLyr.data = lyr.data;
38162
+ } else {
38163
+ outputLyr = lyr;
38164
+ }
38165
+ return [outputLyr];
37855
38166
  };
37856
38167
 
38168
+ function importGeometries(geometries, records) {
38169
+ var features = geometries.map(function(geom, i) {
38170
+ var d = records[i];
38171
+ return {
38172
+ type: 'Feature',
38173
+ properties: records[i] || null,
38174
+ geometry: geom
38175
+ };
38176
+ });
38177
+ var geojson = {
38178
+ type: 'FeatureCollection',
38179
+ features: features
38180
+ };
38181
+ return importGeoJSON(geojson);
38182
+ }
38183
+
38184
+ function createGeometry(coords) {
38185
+ return {
38186
+ type: 'Polygon',
38187
+ coordinates: coords
38188
+ };
38189
+ }
38190
+
38191
+ function getMetersPerPixel(lyr, dataset) {
38192
+
38193
+ // TODO: handle single point, no extent
38194
+ var bounds = getLayerBounds(lyr);
38195
+ return bounds.width() / 800;
38196
+ }
38197
+
37857
38198
  // Returns an svg-symbol data object for one symbol
37858
- function buildSymbol(properties) {
37859
- var type = properties.type;
37860
- var f = symbolBuilders[type];
37861
- if (!type) {
37862
- stop('Missing required "type" parameter');
37863
- } else if (!f) {
37864
- stop('Unknown symbol type:', type);
37865
- }
37866
- return f(properties);
38199
+ function makeSvgSymbol(coords, properties) {
38200
+ roundCoordsForSVG(coords);
38201
+ return {
38202
+ type: 'polygon',
38203
+ coordinates: coords,
38204
+ fill: properties.fill || 'magenta'
38205
+ };
37867
38206
  }
37868
38207
 
37869
38208
  var Symbols = /*#__PURE__*/Object.freeze({
37870
38209
  __proto__: null,
37871
- buildSymbol: buildSymbol
38210
+ makeSvgSymbol: makeSvgSymbol
37872
38211
  });
37873
38212
 
37874
38213
  cmd.target = function(catalog, opts) {
@@ -38014,99 +38353,6 @@ ${svg}
38014
38353
  }
38015
38354
  };
38016
38355
 
38017
- cmd.shape = function(targetDataset, opts) {
38018
- var geojson, dataset;
38019
- if (opts.coordinates) {
38020
- geojson = makeShapeFromCoords(opts);
38021
- } else if (opts.type == 'circle') {
38022
- geojson = makeCircle(opts);
38023
- } else if (opts.type == 'rectangle' && opts.bbox) {
38024
- geojson = getRectangleGeoJSON(opts);
38025
- } else {
38026
- stop('Missing coordinates parameter');
38027
- }
38028
- // TODO: project shape if targetDataset is projected
38029
- dataset = importGeoJSON(geojson, {});
38030
- if (opts.rotation) {
38031
- rotateDatasetCoords(dataset, opts.rotation);
38032
- }
38033
- dataset.layers[0].name = opts.name || opts.type || 'shape';
38034
- return dataset;
38035
- };
38036
-
38037
- function getRectangleGeoJSON(opts) {
38038
- var bbox = opts.bbox,
38039
- xmin = bbox[0],
38040
- ymin = bbox[1],
38041
- xmax = bbox[2],
38042
- ymax = bbox[3],
38043
- interval = 0.5,
38044
- coords = [],
38045
- type = opts.geometry == 'polyline' ? 'LineString' : 'Polygon';
38046
- addSide(xmin, ymin, xmin, ymax);
38047
- addSide(xmin, ymax, xmax, ymax);
38048
- addSide(xmax, ymax, xmax, ymin);
38049
- addSide(xmax, ymin, xmin, ymin);
38050
- coords.push([xmin, ymin]);
38051
- return {
38052
- type: type,
38053
- coordinates: type == 'Polygon' ? [coords] : coords
38054
- };
38055
-
38056
- function addSide(x1, y1, x2, y2) {
38057
- var dx = x2 - x1,
38058
- dy = y2 - y1,
38059
- n = Math.ceil(Math.max(Math.abs(dx) / interval, Math.abs(dy) / interval)),
38060
- xint = dx / n,
38061
- yint = dy / n;
38062
- for (var i=0; i<n; i++) {
38063
- coords.push([x1 + i * xint, y1 + i * yint]);
38064
- }
38065
- }
38066
- }
38067
-
38068
- function makeCircle(opts) {
38069
- if (opts.radius > 0 === false && opts.radius_angle > 0 === false) {
38070
- stop('Missing required radius parameter.');
38071
- }
38072
- var cp = opts.center || [0, 0];
38073
- var radius = opts.radius || getCircleRadiusFromAngle(getCRS('wgs84'), opts.radius_angle);
38074
- return getCircleGeoJSON(cp, radius, null, {geometry_type : opts.geometry || 'polygon'});
38075
- }
38076
-
38077
- function makeShapeFromCoords(opts) {
38078
- var coordinates = [];
38079
- var offsets = opts.offsets || [];
38080
- var coords = opts.coordinates;
38081
- var type, i, x, y;
38082
- if (coords.length >= 2 === false) {
38083
- stop('Invalid coordinates parameter.');
38084
- }
38085
- for (i=0; i<coords.length; i+= 2) {
38086
- x = coords[i];
38087
- y = coords[i + 1];
38088
- coordinates.push([x, y]);
38089
- }
38090
- for (i=0; i<offsets.length; i+=2) {
38091
- x += offsets[i];
38092
- y += offsets[i + 1];
38093
- coordinates.push([x, y]);
38094
- }
38095
- if (GeoJSON.pathIsRing(coordinates)) {
38096
- type = 'Polygon';
38097
- } else if (opts.closed && coordinates.length >= 3) {
38098
- type = 'Polygon';
38099
- coordinates.push(coordinates[0]);
38100
- } else {
38101
- type = 'LineString';
38102
- }
38103
- return {
38104
- type: type,
38105
- coordinates: type == 'Polygon' ? [coordinates] : coordinates
38106
- };
38107
-
38108
- }
38109
-
38110
38356
  cmd.variableSimplify = function(layers, dataset, opts) {
38111
38357
  var lyr = layers[0];
38112
38358
  var arcs = dataset.arcs;
@@ -38648,6 +38894,9 @@ ${svg}
38648
38894
  } else if (name == 'shape') {
38649
38895
  catalog.addDataset(cmd.shape(targetDataset, opts));
38650
38896
 
38897
+ } else if (name == 'shapes') {
38898
+ outputLayers = applyCommandToEachLayer(cmd.shapes, targetLayers, targetDataset, opts);
38899
+
38651
38900
  } else if (name == 'simplify') {
38652
38901
  if (opts.variable) {
38653
38902
  cmd.variableSimplify(targetLayers, targetDataset, opts);
@@ -38677,7 +38926,7 @@ ${svg}
38677
38926
  applyCommandToEachLayer(cmd.svgStyle, targetLayers, targetDataset, opts);
38678
38927
 
38679
38928
  } else if (name == 'symbols') {
38680
- applyCommandToEachLayer(cmd.symbols, targetLayers, opts);
38929
+ outputLayers = applyCommandToEachLayer(cmd.symbols, targetLayers, targetDataset, opts);
38681
38930
 
38682
38931
  } else if (name == 'subdivide') {
38683
38932
  outputLayers = applyCommandToEachLayer(cmd.subdivideLayer, targetLayers, arcs, opts.expression);