mapshaper 0.5.78 → 0.5.82

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({
@@ -1330,9 +1330,10 @@
1330
1330
  }
1331
1331
 
1332
1332
  function logArgs(args) {
1333
- if (LOGGING && !getStateVar('QUIET') && utils.isArrayLike(args)) {
1334
- (!STDOUT && console.error || console.log).call(console, formatLogArgs(args));
1335
- }
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);
1336
1337
  }
1337
1338
 
1338
1339
  var Logging = /*#__PURE__*/Object.freeze({
@@ -1744,9 +1745,17 @@
1744
1745
  return [xmin, ymin, xmax, ymax];
1745
1746
  }
1746
1747
 
1747
- // TODO: remove this constant, use actual data from dataset CRS
1748
- // also consider using ellipsoidal formulas when appropriate
1749
- 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;
1750
1759
  var D2R = Math.PI / 180;
1751
1760
  var R2D = 180 / Math.PI;
1752
1761
 
@@ -2341,15 +2350,21 @@
2341
2350
  }, 0);
2342
2351
  }
2343
2352
 
2344
- function getSphericalShapeArea(shp, arcs) {
2353
+ function getSphericalShapeArea(shp, arcs, R) {
2345
2354
  if (arcs.isPlanar()) {
2346
2355
  error("[getSphericalShapeArea()] Function requires decimal degree coordinates");
2347
2356
  }
2348
2357
  return (shp || []).reduce(function(area, ids) {
2349
- return area + getSphericalPathArea(ids, arcs);
2358
+ return area + getSphericalPathArea(ids, arcs, R);
2350
2359
  }, 0);
2351
2360
  }
2352
2361
 
2362
+ // export function getEllipsoidalShapeArea(shp, arcs, crs) {
2363
+ // return (shp || []).reduce(function(area, ids) {
2364
+ // return area + getEllipsoidalPathArea(ids, arcs, crs);
2365
+ // }, 0);
2366
+ // }
2367
+
2353
2368
  // Return true if point is inside or on boundary of a shape
2354
2369
  //
2355
2370
  function testPointInPolygon(x, y, shp, arcs) {
@@ -2463,16 +2478,17 @@
2463
2478
  return (arcs.isPlanar() ? getPlanarPathArea : getSphericalPathArea)(ids, arcs);
2464
2479
  }
2465
2480
 
2466
- function getSphericalPathArea(ids, arcs) {
2481
+ function getSphericalPathArea(ids, arcs, R) {
2467
2482
  var iter = arcs.getShapeIter(ids);
2468
- return getSphericalPathArea2(iter);
2483
+ return getSphericalPathArea2(iter, R);
2469
2484
  }
2470
2485
 
2471
- function getSphericalPathArea2(iter) {
2486
+ function getSphericalPathArea2(iter, R) {
2472
2487
  var sum = 0,
2473
2488
  started = false,
2474
2489
  deg2rad = Math.PI / 180,
2475
2490
  x, y, xp, yp;
2491
+ R = R || WGS84.SEMIMAJOR_AXIS;
2476
2492
  while (iter.hasNext()) {
2477
2493
  x = iter.x * deg2rad;
2478
2494
  y = Math.sin(iter.y * deg2rad);
@@ -2484,7 +2500,7 @@
2484
2500
  xp = x;
2485
2501
  yp = y;
2486
2502
  }
2487
- return sum / 2 * 6378137 * 6378137;
2503
+ return sum / 2 * R * R;
2488
2504
  }
2489
2505
 
2490
2506
  // Get path area from an array of [x, y] points
@@ -3948,7 +3964,7 @@
3948
3964
  function requireSinglePointLayer(lyr, msg) {
3949
3965
  requirePointLayer(lyr);
3950
3966
  if (countMultiPartFeatures(lyr) > 0) {
3951
- stop(msg || 'This command requires single points');
3967
+ stop(msg || 'This command requires single points; layer contains multi-point features.');
3952
3968
  }
3953
3969
  }
3954
3970
 
@@ -4003,6 +4019,7 @@
4003
4019
  return opts && opts.no_replace ? {geometry_type: src.geometry_type} : src;
4004
4020
  }
4005
4021
 
4022
+ //
4006
4023
  function setOutputLayerName(dest, src, defName, opts) {
4007
4024
  opts = opts || {};
4008
4025
  if (opts.name) {
@@ -4079,6 +4096,7 @@
4079
4096
  return counts;
4080
4097
  }
4081
4098
 
4099
+ // Returns a Bounds object
4082
4100
  function getLayerBounds(lyr, arcs) {
4083
4101
  var bounds = null;
4084
4102
  if (lyr.geometry_type == 'point') {
@@ -6150,9 +6168,11 @@
6150
6168
  arcCount += n;
6151
6169
  });
6152
6170
 
6153
- mergedArcs = mergeArcs(arcSources);
6154
- if (mergedArcs.size() != arcCount) {
6155
- error("[mergeDatasets()] Arc indexing error");
6171
+ if (arcSources.length > 0) {
6172
+ mergedArcs = mergeArcs(arcSources);
6173
+ if (mergedArcs.size() != arcCount) {
6174
+ error("[mergeDatasets()] Arc indexing error");
6175
+ }
6156
6176
  }
6157
6177
 
6158
6178
  return {
@@ -6171,6 +6191,8 @@
6171
6191
  }
6172
6192
 
6173
6193
  function mergeArcs(arr) {
6194
+ // Returning the original causes a test to fail
6195
+ // if (arr.length < 2) return arr[0];
6174
6196
  var dataArr = arr.map(function(arcs) {
6175
6197
  if (arcs.getRetainedInterval() > 0) {
6176
6198
  verbose("Baking-in simplification setting.");
@@ -6775,31 +6797,47 @@
6775
6797
  dataset.layers = currLayers;
6776
6798
  }
6777
6799
 
6778
- // Replace a layer in-place with a layer from a second dataset
6800
+ // Replace a layer with a layer from a second dataset
6801
+ // (in-place)
6779
6802
  // (Typically, the second dataset is imported from dynamically generated GeoJSON and contains one layer)
6780
6803
  function replaceLayerContents(lyr, dataset, dataset2) {
6804
+ var lyr2 = mergeOutputLayerIntoDataset(lyr, dataset, dataset2, {});
6805
+ if (layerHasPaths(lyr2)) {
6806
+ buildTopology(dataset);
6807
+ }
6808
+ }
6809
+
6810
+ function mergeOutputLayerIntoDataset(lyr, dataset, dataset2, opts) {
6781
6811
  if (!dataset2 || dataset2.layers.length != 1) {
6782
- error('Invalid replacement layer');
6812
+ error('Invalid source dataset');
6783
6813
  }
6784
6814
  if (dataset.layers.includes(lyr) === false) {
6785
6815
  error('Invalid target layer');
6786
6816
  }
6787
- var lyr2 = dataset2.layers[0];
6788
- if (dataset2.arcs) {
6789
- // this command returns merged layers instead of adding them to target dataset
6790
- mergeDatasetsIntoDataset(dataset, [dataset2]);
6791
- }
6817
+ // this command returns merged layers instead of adding them to target dataset
6818
+ var outputLayers = mergeDatasetsIntoDataset(dataset, [dataset2]);
6819
+ var lyr2 = outputLayers[0];
6792
6820
 
6793
- Object.assign(lyr, {data: null, shapes: null}, lyr2);
6821
+ // TODO: find a more reliable way of knowing when to copy data
6822
+ var copyData = !lyr2.data && lyr.data && getFeatureCount(lyr2) == lyr.data.size();
6794
6823
 
6795
- if (layerHasPaths(lyr2)) {
6796
- buildTopology(dataset);
6824
+ if (copyData) {
6825
+ lyr2.data = opts.no_replace ? lyr.data.clone() : lyr.data;
6797
6826
  }
6798
- if (layerHasPaths(lyr)) {
6799
- // Remove unused arcs from replaced layer
6800
- // TODO: consider using clean insead of this
6801
- dissolveArcs(dataset);
6827
+ if (opts.no_replace) {
6828
+ // dataset.layers.push(lyr2);
6829
+
6830
+ } else {
6831
+ lyr2 = Object.assign(lyr, {data: null, shapes: null}, lyr2);
6832
+ if (layerHasPaths(lyr)) {
6833
+ // Remove unused arcs from replaced layer
6834
+ // TODO: consider using clean insead of this
6835
+ dissolveArcs(dataset);
6836
+ }
6802
6837
  }
6838
+
6839
+ lyr2.name = opts.name || lyr2.name;
6840
+ return lyr2;
6803
6841
  }
6804
6842
 
6805
6843
  // Transform the points in a dataset in-place; don't clean up corrupted shapes
@@ -6828,6 +6866,7 @@
6828
6866
  pruneArcs: pruneArcs,
6829
6867
  replaceLayers: replaceLayers,
6830
6868
  replaceLayerContents: replaceLayerContents,
6869
+ mergeOutputLayerIntoDataset: mergeOutputLayerIntoDataset,
6831
6870
  transformPoints: transformPoints
6832
6871
  });
6833
6872
 
@@ -9411,6 +9450,12 @@
9411
9450
  area: function() {
9412
9451
  return _isPlanar ? ctx.planarArea : geom.getSphericalShapeArea(_ids, arcs);
9413
9452
  },
9453
+ // area2: function() {
9454
+ // return _isPlanar ? ctx.planarArea : geom.getSphericalShapeArea(_ids, arcs, WGS84.SEMIMINOR_RADIUS);
9455
+ // },
9456
+ // area3: function() {
9457
+ // return _isPlanar ? ctx.planarArea : geom.getSphericalShapeArea(_ids, arcs, WGS84.AUTHALIC_RADIUS);
9458
+ // },
9414
9459
  perimeter: function() {
9415
9460
  return geom.getShapePerimeter(_ids, arcs);
9416
9461
  },
@@ -13331,14 +13376,14 @@
13331
13376
  return mergeArcs([arcs, new ArcCollection(nn, xx, yy)]);
13332
13377
  }
13333
13378
 
13334
- var roundCoord$1 = getRoundingFunction(0.01);
13379
+ var roundCoord$2 = getRoundingFunction(0.01);
13335
13380
 
13336
13381
  function stringifyVertex(p) {
13337
- return ' ' + roundCoord$1(p[0]) + ' ' + roundCoord$1(p[1]);
13382
+ return ' ' + roundCoord$2(p[0]) + ' ' + roundCoord$2(p[1]);
13338
13383
  }
13339
13384
 
13340
13385
  function stringifyCP(p) {
13341
- return ' ' + roundCoord$1(p[2]) + ' ' + roundCoord$1(p[3]);
13386
+ return ' ' + roundCoord$2(p[2]) + ' ' + roundCoord$2(p[3]);
13342
13387
  }
13343
13388
 
13344
13389
  function isCubicCtrl(p) {
@@ -13373,40 +13418,9 @@
13373
13418
  stringifyCP(p2) + stringifyVertex(p2);
13374
13419
  }
13375
13420
 
13376
- function findArcCenter(p1, p2, degrees) {
13377
- var p3 = [(p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2], // midpoint betw. p1, p2
13378
- tan = 1 / Math.tan(degrees / 180 * Math.PI / 2),
13379
- cp = getAffineTransform(90, tan, [0, 0], p3)(p2[0], p2[1]);
13380
- return cp;
13381
- }
13382
-
13383
- // export function addBezierArcControlPoints(p1, p2, degrees) {
13384
- function addBezierArcControlPoints(points, degrees) {
13385
- // source: https://stackoverflow.com/questions/734076/how-to-best-approximate-a-geometrical-arc-with-a-bezier-curve
13386
- var p2 = points.pop(),
13387
- p1 = points.pop(),
13388
- cp = findArcCenter(p1, p2, degrees),
13389
- xc = cp[0],
13390
- yc = cp[1],
13391
- ax = p1[0] - xc,
13392
- ay = p1[1] - yc,
13393
- bx = p2[0] - xc,
13394
- by = p2[1] - yc,
13395
- q1 = ax * ax + ay * ay,
13396
- q2 = q1 + ax * bx + ay * by,
13397
- k2 = 4/3 * (Math.sqrt(2 * q1 * q2) - q2) / (ax * by - ay * bx);
13398
-
13399
- points.push(p1);
13400
- points.push([xc + ax - k2 * ay, yc + ay + k2 * ax, 'C']);
13401
- points.push([xc + bx + k2 * by, yc + by - k2 * bx, 'C']);
13402
- points.push(p2);
13403
- }
13404
-
13405
13421
  var SvgPathUtils = /*#__PURE__*/Object.freeze({
13406
13422
  __proto__: null,
13407
- stringifyLineStringCoords: stringifyLineStringCoords,
13408
- findArcCenter: findArcCenter,
13409
- addBezierArcControlPoints: addBezierArcControlPoints
13423
+ stringifyLineStringCoords: stringifyLineStringCoords
13410
13424
  });
13411
13425
 
13412
13426
  /* example patterns
@@ -13683,11 +13697,17 @@
13683
13697
  type: null,
13684
13698
  length: 'number', // e.g. arrow length
13685
13699
  rotation: 'number',
13700
+ radius: 'number',
13701
+ 'arrow-length': 'number',
13702
+ 'arrow-direction': 'number',
13686
13703
  'arrow-head-angle': 'number',
13687
13704
  'arrow-head-width': 'number',
13688
13705
  'arrow-stem-width': 'number',
13689
13706
  'arrow-stem-curve': 'number', // degrees of arc
13690
13707
  'arrow-stem-taper': 'number',
13708
+ 'arrow-stem-length': 'number',
13709
+ 'arrow-head-length': 'number',
13710
+ 'arrow-min-stem': 'number',
13691
13711
  'arrow-scaling': 'number',
13692
13712
  effect: null // e.g. "fade"
13693
13713
  }, stylePropertyTypes);
@@ -13729,8 +13749,8 @@
13729
13749
  if (!isSupportedSvgSymbolProperty(svgName)) {
13730
13750
  return;
13731
13751
  }
13732
- var strVal = opts[optName].trim();
13733
- functions[svgName] = getSymbolPropertyAccessor(strVal, svgName, lyr);
13752
+ var val = opts[optName];
13753
+ functions[svgName] = getSymbolPropertyAccessor(val, svgName, lyr);
13734
13754
  properties.push(svgName);
13735
13755
  });
13736
13756
 
@@ -13753,7 +13773,8 @@
13753
13773
  return /[(){}.+-/*?:&|=\[]/.test(str);
13754
13774
  }
13755
13775
 
13756
- function getSymbolPropertyAccessor(strVal, svgName, lyr) {
13776
+ function getSymbolPropertyAccessor(val, svgName, lyr) {
13777
+ var strVal = String(val).trim();
13757
13778
  var typeHint = symbolPropertyTypes[svgName];
13758
13779
  var fields = lyr.data ? lyr.data.getFields() : [];
13759
13780
  var literalVal = null;
@@ -13855,7 +13876,6 @@
13855
13876
  isSvgColor: isSvgColor
13856
13877
  });
13857
13878
 
13858
- var symbolBuilders = {};
13859
13879
  var symbolRenderers = {};
13860
13880
 
13861
13881
  function getTransform(xy, scale) {
@@ -13868,7 +13888,6 @@
13868
13888
 
13869
13889
  var SvgCommon = /*#__PURE__*/Object.freeze({
13870
13890
  __proto__: null,
13871
- symbolBuilders: symbolBuilders,
13872
13891
  symbolRenderers: symbolRenderers,
13873
13892
  getTransform: getTransform
13874
13893
  });
@@ -18021,9 +18040,9 @@ ${svg}
18021
18040
  // Updated: don't trim space from tokens like [delimeter= ]
18022
18041
  argv = argv.map(function(s) {
18023
18042
  if (!/= $/.test(s)) {
18024
- s = s.trimEnd();
18043
+ s = utils.rtrim(s);
18025
18044
  }
18026
- s = s.trimStart();
18045
+ s = utils.ltrim(s);
18027
18046
  return s;
18028
18047
  });
18029
18048
  argv = argv.filter(function(s) {return s !== '';}); // remove empty tokens
@@ -19478,6 +19497,13 @@ ${svg}
19478
19497
  })
19479
19498
  .option('target', targetOpt);
19480
19499
 
19500
+ parser.command('include')
19501
+ .describe('import JS data and functions for use in JS expressions')
19502
+ .option('file', {
19503
+ DEFAULT: true,
19504
+ describe: 'file containing a JS object with key:value pairs to import'
19505
+ });
19506
+
19481
19507
  parser.command('inlay')
19482
19508
  .describe('inscribe a polygon layer inside another polygon layer')
19483
19509
  .option('source', {
@@ -19790,6 +19816,7 @@ ${svg}
19790
19816
  })
19791
19817
  .option('target', targetOpt);
19792
19818
 
19819
+
19793
19820
  parser.command('simplify')
19794
19821
  .validate(validateSimplifyOpts)
19795
19822
  .example('Retain 10% of removable vertices\n$ mapshaper input.shp -simplify 10%')
@@ -20028,26 +20055,90 @@ ${svg}
20028
20055
  .option('target', targetOpt);
20029
20056
 
20030
20057
  parser.command('symbols')
20031
- // .describe('generate a variety of SVG symbols')
20058
+ // .describe('symbolize points as polygons, circles, stars or arrows')
20032
20059
  .option('type', {
20033
- describe: 'symbol type'
20060
+ describe: 'symbol type (e.g. star, polygon, circle, arrow)'
20061
+ })
20062
+ .option('scale', {
20063
+ describe: 'scale symbols by a factor',
20064
+ type: 'number'
20065
+ })
20066
+ .option('pixel-scale', {
20067
+ describe: 'symbol scale in meters-per-pixel (see polygons option)',
20068
+ type: 'number',
20069
+ })
20070
+ .option('polygons', {
20071
+ describe: 'generate symbols as polygons instead of SVG objects',
20072
+ type: 'flag'
20073
+ })
20074
+ .option('radius', {
20075
+ describe: 'distance from center to farthest point on the symbol',
20076
+ type: 'distance'
20077
+ })
20078
+ .option('sides', {
20079
+ describe: 'sides of a polygon or star symbol',
20080
+ type: 'number'
20081
+ })
20082
+ .option('rotation', {
20083
+ describe: 'rotation of symbol in degrees'
20084
+ })
20085
+ .option('orientation', {
20086
+ describe: 'use orientation=b for a rotated or flipped orientation'
20087
+ })
20088
+ .option('length', {
20089
+ // alias for arrow-length
20090
+ })
20091
+ .option('star-ratio', {
20092
+ describe: 'ratio of major to minor radius of star',
20093
+ type: 'number'
20094
+ })
20095
+ .option('arrow-length', {
20096
+ describe: 'length of arrows in pixels (use with type=arrow)'
20097
+ })
20098
+ .option('arrow-direction', {
20099
+ describe: 'angle off of vertical (-90 = left-pointing arrow)'
20100
+ })
20101
+ .option('arrow-head-angle', {
20102
+ describe: 'angle of tip of arrow (default is 40 degrees)'
20103
+ })
20104
+ .option('arrow-head-width', {
20105
+ describe: 'size of arrow head from side to side'
20106
+ })
20107
+ .option('arrow-head-length', {
20108
+ describe: 'length of arrow head (alternative to arrow-head-angle)'
20109
+ })
20110
+ .option('arrow-head-shape', {
20111
+ // describe: 'options: a b c'
20112
+ })
20113
+ .option('arrow-stem-width', {
20114
+ describe: 'width of stem at its widest point'
20115
+ })
20116
+ .option('arrow-stem-length', {
20117
+ describe: 'alternative to arrow-length'
20118
+ })
20119
+ .option('arrow-stem-taper', {
20120
+ describe: 'factor for tapering the width of the stem'
20121
+ })
20122
+ .option('arrow-stem-curve', {
20123
+ describe: 'curvature in degrees (arrows are straight by default)'
20124
+ })
20125
+ .option('arrow-min-stem', {
20126
+ describe: 'min ratio of stem to total length (for small arrows)',
20127
+ type: 'number'
20034
20128
  })
20035
20129
  .option('stroke', {})
20036
20130
  .option('stroke-width', {})
20037
- .option('fill', {})
20038
- .option('length', {})
20039
- .option('rotation', {})
20131
+ .option('fill', {
20132
+ describe: 'symbol fill color'
20133
+ })
20040
20134
  .option('effect', {})
20041
- .option('arrow-head-angle', {})
20042
- .option('arrow-stem-width', {})
20043
- .option('arrow-head-width', {})
20044
- .option('arrow-stem-curve', {})
20045
- .option('arrow-stem-taper', {})
20046
- .option('arrow-scaling', {})
20047
- .option('where', whereOpt)
20048
- .option('target', targetOpt);
20135
+ // .option('where', whereOpt)
20136
+ .option('name', nameOpt)
20137
+ .option('target', targetOpt)
20138
+ .option('no-replace', noReplaceOpt);
20049
20139
  // .option('name', nameOpt);
20050
20140
 
20141
+
20051
20142
  parser.command('target')
20052
20143
  .describe('set active layer (or layers)')
20053
20144
  .option('target', {
@@ -20198,13 +20289,6 @@ ${svg}
20198
20289
  })
20199
20290
  .option('name', nameOpt);
20200
20291
 
20201
- parser.command('include')
20202
- .describe('import JS data and functions for use in JS expressions')
20203
- .option('file', {
20204
- DEFAULT: true,
20205
- describe: 'file containing a JS object with key:value pairs to import'
20206
- });
20207
-
20208
20292
  parser.command('fuzzy-join')
20209
20293
  .describe('join points to polygons, with data fill and fuzzy match')
20210
20294
  .option('source', {
@@ -21203,12 +21287,13 @@ ${svg}
21203
21287
  if (this instanceof ShpReader === false) {
21204
21288
  return new ShpReader(shpSrc, shxSrc);
21205
21289
  }
21206
-
21207
21290
  var shpFile = utils.isString(shpSrc) ? new FileReader(shpSrc) : new BufferReader(shpSrc);
21208
21291
  var header = parseHeader(shpFile.readToBinArray(0, 100));
21209
- var shpSize = shpFile.size();
21210
- var RecordClass = new ShpRecordClass(header.type);
21211
- var shpOffset, recordCount, skippedBytes;
21292
+ var shpType = header.type;
21293
+ var shpOffset = 100; // used when reading .shp without .shx
21294
+ var recordCount = 0;
21295
+ var badRecordNumberCount = 0;
21296
+ var RecordClass = new ShpRecordClass(shpType);
21212
21297
  var shxBin, shxFile;
21213
21298
 
21214
21299
  if (shxSrc) {
@@ -21216,8 +21301,6 @@ ${svg}
21216
21301
  shxBin = shxFile.readToBinArray(0, shxFile.size()).bigEndian();
21217
21302
  }
21218
21303
 
21219
- reset();
21220
-
21221
21304
  this.header = function() {
21222
21305
  return header;
21223
21306
  };
@@ -21235,62 +21318,35 @@ ${svg}
21235
21318
 
21236
21319
  // Iterator interface for reading shape records
21237
21320
  this.nextShape = function() {
21238
- var shape = readNextShape();
21321
+ var shape;
21322
+ if (!shpFile) {
21323
+ error('Tried to read from a used ShpReader');
21324
+ // return null; // this reader was already used
21325
+ }
21326
+ shape = readNextShape(recordCount);
21239
21327
  if (!shape) {
21240
- if (skippedBytes > 0) {
21241
- // Encountered in files from natural earth v2.0.0:
21242
- // ne_10m_admin_0_boundary_lines_land.shp
21243
- // ne_110m_admin_0_scale_rank.shp
21244
- verbose("Skipped over " + skippedBytes + " non-data bytes in the .shp file.");
21245
- }
21246
- shpFile.close();
21247
- reset();
21328
+ done();
21329
+ return null;
21248
21330
  }
21331
+ recordCount++;
21249
21332
  return shape;
21250
21333
  };
21251
21334
 
21252
- function readNextShape() {
21253
- var expectedId = recordCount + 1; // Shapefile ids are 1-based
21254
- var shape, offset;
21255
- if (done()) return null;
21256
- if (shxBin) {
21257
- shxBin.position(100 + recordCount * 8);
21258
- offset = shxBin.readUint32() * 2;
21259
- if (offset > shpOffset) {
21260
- skippedBytes += offset - shpOffset;
21261
- }
21262
- } else {
21263
- offset = shpOffset;
21264
- }
21265
- shape = readShapeAtOffset(offset);
21266
- if (!shape) {
21267
- // Some in-the-wild .shp files contain junk bytes between records. This
21268
- // is a problem if the .shx index file is not present.
21269
- // Here, we try to scan past the junk to find the next record.
21270
- shape = huntForNextShape(offset, expectedId);
21271
- }
21272
- if (shape) {
21273
- if (shape.id < expectedId) {
21274
- message("Found a Shapefile record with the same id as a previous record (" + shape.id + ") -- skipping.");
21275
- return readNextShape();
21276
- } else if (shape.id > expectedId) {
21277
- stop("Shapefile contains an out-of-sequence record. Possible data corruption -- bailing.");
21278
- }
21279
- recordCount++;
21280
- }
21281
- return shape || null;
21335
+ // Returns a shape record or null if no more shapes can be read
21336
+ // i: Expected 0-based index of the next record
21337
+ //
21338
+ function readNextShape(i) {
21339
+ return shxBin ?
21340
+ readIndexedShape(shpFile, shxBin, i) :
21341
+ readNonIndexedShape(shpFile, shpOffset, i);
21282
21342
  }
21283
21343
 
21284
21344
  function done() {
21285
- if (shxFile && shxFile.size() <= 100 + recordCount * 8) return true;
21286
- if (shpOffset + 12 > shpSize) return true;
21287
- return false;
21288
- }
21289
-
21290
- function reset() {
21291
- shpOffset = 100;
21292
- skippedBytes = 0;
21293
- recordCount = 0;
21345
+ shpFile.close();
21346
+ shpFile = shxFile = shxBin = null;
21347
+ if (badRecordNumberCount > 0) {
21348
+ message(`Warning: ${badRecordNumberCount}/${recordCount} features have non-standard record numbers in the .shp file.`);
21349
+ }
21294
21350
  }
21295
21351
 
21296
21352
  function parseHeader(bin) {
@@ -21319,47 +21375,81 @@ ${svg}
21319
21375
  return header;
21320
21376
  }
21321
21377
 
21322
- function readShapeAtOffset(offset) {
21323
- var shape = null,
21324
- recordSize, recordType, recordId, goodSize, goodType, bin;
21325
21378
 
21326
- if (offset + 12 <= shpSize) {
21327
- bin = shpFile.readToBinArray(offset, 12);
21328
- recordId = bin.bigEndian().readUint32();
21329
- // record size is bytes in content section + 8 header bytes
21330
- recordSize = bin.readUint32() * 2 + 8;
21331
- recordType = bin.littleEndian().readUint32();
21332
- goodSize = offset + recordSize <= shpSize && recordSize >= 12;
21333
- goodType = recordType === 0 || recordType == header.type;
21334
- if (goodSize && goodType) {
21335
- bin = shpFile.readToBinArray(offset, recordSize);
21336
- shape = new RecordClass(bin, recordSize);
21337
- shpOffset = offset + shape.byteLength; // advance read position
21338
- }
21379
+ function readShapeAtOffset(shpFile, offset) {
21380
+ var fileSize = shpFile.size();
21381
+ if (offset + 12 > fileSize) return null; // reached end-of-file
21382
+ var bin = shpFile.readToBinArray(offset, 12);
21383
+ var recordId = bin.bigEndian().readUint32();
21384
+ // record size is bytes in content section + 8 header bytes
21385
+ var recordSize = bin.readUint32() * 2 + 8;
21386
+ var recordType = bin.littleEndian().readUint32();
21387
+ var goodSize = offset + recordSize <= fileSize && recordSize >= 12;
21388
+ var goodType = recordType === 0 || recordType == shpType;
21389
+ if (!goodSize || !goodType) {
21390
+ return null;
21391
+ }
21392
+ bin = shpFile.readToBinArray(offset, recordSize);
21393
+ return new RecordClass(bin, recordSize);
21394
+ }
21395
+
21396
+ function readIndexedShape(shpFile, shxBin, i) {
21397
+ if (shxBin.size() <= 100 + i * 8) return null; // done
21398
+ shxBin.position(100 + i * 8);
21399
+ var expectedId = i + 1;
21400
+ var offset = shxBin.readUint32() * 2;
21401
+ var recLen = shxBin.readUint32() * 2; // TODO: match this to recLen in .shp
21402
+ var shape = readShapeAtOffset(shpFile, offset);
21403
+ if (!shape) {
21404
+ stop('Index of Shapefile record', expectedId, 'in the .shx file is invalid.');
21405
+ }
21406
+ if (shape.id != expectedId) {
21407
+ badRecordNumberCount++;
21408
+ verbose(`Warning: A feature has a different record number in .shx (${expectedId}) and .shp (${shape.id}).`);
21339
21409
  }
21410
+ // TODO: consider printing verbose message if a .shp file contains garbage bytes
21411
+ // example files:
21412
+ // ne_10m_admin_0_boundary_lines_land.shp
21413
+ // ne_110m_admin_0_scale_rank.shp
21340
21414
  return shape;
21341
21415
  }
21342
21416
 
21343
- // TODO: add tests
21344
- // Try to scan past unreadable content to find next record
21345
- function huntForNextShape(start, id) {
21346
- var offset = start + 4,
21417
+ // The Shapefile specification does not require records to be densely packed or
21418
+ // in consecutive sequence in the .shp file. This is a problem when the .shx
21419
+ // index file is not present.
21420
+ //
21421
+ // Here, we try to scan past any invalid content to find the next record.
21422
+ // Records are required to be in sequential order.
21423
+ //
21424
+ function readNonIndexedShape(shpFile, start, i) {
21425
+ var expectedId = i + 1, // Shapefile ids are 1-based
21426
+ offset = start,
21427
+ fileSize = shpFile.size(),
21347
21428
  shape = null,
21348
- bin, recordId, recordType, count;
21349
- while (offset + 12 <= shpSize) {
21429
+ bin, recordId, recordType, isValidType;
21430
+ while (offset + 12 <= fileSize) {
21350
21431
  bin = shpFile.readToBinArray(offset, 12);
21351
21432
  recordId = bin.bigEndian().readUint32();
21352
21433
  recordType = bin.littleEndian().skipBytes(4).readUint32();
21353
- if (recordId == id && (recordType == header.type || recordType === 0)) {
21354
- // we have a likely position, but may still be unparsable
21355
- shape = readShapeAtOffset(offset);
21356
- break;
21434
+ isValidType = recordType == shpType || recordType === 0;
21435
+ if (!isValidType || recordId != expectedId && recordType === 0) {
21436
+ offset += 4; // keep scanning -- try next integer position
21437
+ continue;
21438
+ }
21439
+ shape = readShapeAtOffset(shpFile, offset);
21440
+ if (!shape) break; // probably ran into end of file
21441
+ shpOffset = offset + shape.byteLength; // update
21442
+ if (recordId == expectedId) break; // found an apparently valid shape
21443
+ if (recordId < expectedId) {
21444
+ message("Found a Shapefile record with the same id as a previous record (" + shape.id + ") -- skipping.");
21445
+ offset += shape.byteLength;
21446
+ } else {
21447
+ stop("Shapefile contains an out-of-sequence record. Possible data corruption -- bailing.");
21357
21448
  }
21358
- offset += 4; // try next integer position
21359
21449
  }
21360
- count = shape ? offset - start : shpSize - start;
21361
- // debug('Skipped', count, 'bytes', shape ? 'before record ' + id : 'at the end of the file');
21362
- skippedBytes += count;
21450
+ if (shape && offset > start) {
21451
+ verbose("Skipped over " + (offset - start) + " non-data bytes in the .shp file.");
21452
+ }
21363
21453
  return shape;
21364
21454
  }
21365
21455
  }
@@ -21368,22 +21458,6 @@ ${svg}
21368
21458
  return this.header().type;
21369
21459
  };
21370
21460
 
21371
- ShpReader.prototype.getCounts = function() {
21372
- var counts = {
21373
- nullCount: 0,
21374
- partCount: 0,
21375
- shapeCount: 0,
21376
- pointCount: 0
21377
- };
21378
- this.forEachShape(function(shp) {
21379
- if (shp.isNull) counts.nullCount++;
21380
- counts.pointCount += shp.pointCount;
21381
- counts.partCount += shp.partCount;
21382
- counts.shapeCount++;
21383
- });
21384
- return counts;
21385
- };
21386
-
21387
21461
  // Apply snapping, remove duplicate coords and clean up defective paths in a dataset
21388
21462
  // Assumes that any CRS info has been added to the dataset
21389
21463
  // @opts: import options
@@ -26192,7 +26266,6 @@ ${svg}
26192
26266
  if (constTol) return constTol;
26193
26267
  return constTol ? constTol : meterDist * pctOfRadius;
26194
26268
  };
26195
-
26196
26269
  }
26197
26270
 
26198
26271
  function getBufferDistanceFunction(lyr, dataset, opts) {
@@ -27365,7 +27438,7 @@ ${svg}
27365
27438
  cmd.buffer = makeBufferLayer;
27366
27439
 
27367
27440
  function makeBufferLayer(lyr, dataset, opts) {
27368
- var dataset2, lyr2;
27441
+ var dataset2;
27369
27442
  if (lyr.geometry_type == 'point') {
27370
27443
  dataset2 = makePointBuffer(lyr, dataset, opts);
27371
27444
  } else if (lyr.geometry_type == 'polyline') {
@@ -27375,13 +27448,9 @@ ${svg}
27375
27448
  } else {
27376
27449
  stop("Unsupported geometry type");
27377
27450
  }
27378
- var outputLayers = mergeDatasetsIntoDataset(dataset, [dataset2]);
27379
- lyr2 = outputLayers[0];
27380
- setOutputLayerName(lyr2, lyr, null, opts);
27381
- if (lyr.data && !lyr2.data) {
27382
- lyr2.data = opts.no_replace ? lyr.data.clone() : lyr.data;
27383
- }
27384
- return outputLayers;
27451
+
27452
+ var lyr2 = mergeOutputLayerIntoDataset(lyr, dataset, dataset2, opts);
27453
+ return [lyr2];
27385
27454
  }
27386
27455
 
27387
27456
  // TODO: support three or more stops
@@ -33966,7 +34035,7 @@ ${svg}
33966
34035
 
33967
34036
  function createMeridianPart(x, ymin, ymax) {
33968
34037
  var coords = densifyPathByInterval([[x, ymin], [x, ymax]], precision);
33969
- meridians.push(graticuleFeature(coords, {type: 'meridian', value: roundCoord(x)}));
34038
+ meridians.push(graticuleFeature(coords, {type: 'meridian', value: roundCoord$1(x)}));
33970
34039
  }
33971
34040
 
33972
34041
  function createParallel(y) {
@@ -33976,7 +34045,7 @@ ${svg}
33976
34045
  }
33977
34046
 
33978
34047
  // remove tiny offsets
33979
- function roundCoord(x) {
34048
+ function roundCoord$1(x) {
33980
34049
  return +x.toFixed(3) || 0;
33981
34050
  }
33982
34051
 
@@ -36627,6 +36696,99 @@ ${svg}
36627
36696
  parseScalebarLabelToKm: parseScalebarLabelToKm
36628
36697
  });
36629
36698
 
36699
+ cmd.shape = function(targetDataset, opts) {
36700
+ var geojson, dataset;
36701
+ if (opts.coordinates) {
36702
+ geojson = makeShapeFromCoords(opts);
36703
+ } else if (opts.type == 'circle') {
36704
+ geojson = makeCircle(opts);
36705
+ } else if (opts.type == 'rectangle' && opts.bbox) {
36706
+ geojson = getRectangleGeoJSON(opts);
36707
+ } else {
36708
+ stop('Missing coordinates parameter');
36709
+ }
36710
+ // TODO: project shape if targetDataset is projected
36711
+ dataset = importGeoJSON(geojson, {});
36712
+ if (opts.rotation) {
36713
+ rotateDatasetCoords(dataset, opts.rotation);
36714
+ }
36715
+ dataset.layers[0].name = opts.name || opts.type || 'shape';
36716
+ return dataset;
36717
+ };
36718
+
36719
+ function getRectangleGeoJSON(opts) {
36720
+ var bbox = opts.bbox,
36721
+ xmin = bbox[0],
36722
+ ymin = bbox[1],
36723
+ xmax = bbox[2],
36724
+ ymax = bbox[3],
36725
+ interval = 0.5,
36726
+ coords = [],
36727
+ type = opts.geometry == 'polyline' ? 'LineString' : 'Polygon';
36728
+ addSide(xmin, ymin, xmin, ymax);
36729
+ addSide(xmin, ymax, xmax, ymax);
36730
+ addSide(xmax, ymax, xmax, ymin);
36731
+ addSide(xmax, ymin, xmin, ymin);
36732
+ coords.push([xmin, ymin]);
36733
+ return {
36734
+ type: type,
36735
+ coordinates: type == 'Polygon' ? [coords] : coords
36736
+ };
36737
+
36738
+ function addSide(x1, y1, x2, y2) {
36739
+ var dx = x2 - x1,
36740
+ dy = y2 - y1,
36741
+ n = Math.ceil(Math.max(Math.abs(dx) / interval, Math.abs(dy) / interval)),
36742
+ xint = dx / n,
36743
+ yint = dy / n;
36744
+ for (var i=0; i<n; i++) {
36745
+ coords.push([x1 + i * xint, y1 + i * yint]);
36746
+ }
36747
+ }
36748
+ }
36749
+
36750
+ function makeCircle(opts) {
36751
+ if (opts.radius > 0 === false && opts.radius_angle > 0 === false) {
36752
+ stop('Missing required radius parameter.');
36753
+ }
36754
+ var cp = opts.center || [0, 0];
36755
+ var radius = opts.radius || getCircleRadiusFromAngle(getCRS('wgs84'), opts.radius_angle);
36756
+ return getCircleGeoJSON(cp, radius, null, {geometry_type : opts.geometry || 'polygon'});
36757
+ }
36758
+
36759
+ function makeShapeFromCoords(opts) {
36760
+ var coordinates = [];
36761
+ var offsets = opts.offsets || [];
36762
+ var coords = opts.coordinates;
36763
+ var type, i, x, y;
36764
+ if (coords.length >= 2 === false) {
36765
+ stop('Invalid coordinates parameter.');
36766
+ }
36767
+ for (i=0; i<coords.length; i+= 2) {
36768
+ x = coords[i];
36769
+ y = coords[i + 1];
36770
+ coordinates.push([x, y]);
36771
+ }
36772
+ for (i=0; i<offsets.length; i+=2) {
36773
+ x += offsets[i];
36774
+ y += offsets[i + 1];
36775
+ coordinates.push([x, y]);
36776
+ }
36777
+ if (GeoJSON.pathIsRing(coordinates)) {
36778
+ type = 'Polygon';
36779
+ } else if (opts.closed && coordinates.length >= 3) {
36780
+ type = 'Polygon';
36781
+ coordinates.push(coordinates[0]);
36782
+ } else {
36783
+ type = 'LineString';
36784
+ }
36785
+ return {
36786
+ type: type,
36787
+ coordinates: type == 'Polygon' ? [coordinates] : coordinates
36788
+ };
36789
+
36790
+ }
36791
+
36630
36792
  function calcSimplifyStats(arcs, use3D) {
36631
36793
  var distSq = use3D ? pointSegGeoDistSq : geom.pointSegDistSq,
36632
36794
  calcAngle = use3D ? geom.signedAngleSph : geom.signedAngle,
@@ -37714,144 +37876,378 @@ ${svg}
37714
37876
  });
37715
37877
  };
37716
37878
 
37717
- symbolBuilders.arrow = function(d) {
37718
- var len = 'length' in d ? d.length : 10;
37719
- var filled = 'fill' in d;
37720
- return filled ? getFilledArrow(d, len) : getStickArrow(d, len);
37721
- };
37879
+ var roundCoord = getRoundingFunction(0.01);
37722
37880
 
37723
- function getStickArrow(d, len) {
37724
- return {
37725
- type: 'polyline',
37726
- coordinates: getStickArrowCoords(d, len),
37727
- stroke: d.stroke || 'magenta',
37728
- 'stroke-width': 'stroke-width' in d ? d['stroke-width'] : 1
37729
- };
37881
+ function forEachSymbolCoord(coords, cb) {
37882
+ var isPoint = coords && utils.isNumber(coords[0]);
37883
+ var isNested = !isPoint && coords && Array.isArray(coords[0]);
37884
+ if (isPoint) return cb(coords);
37885
+ for (var i=0; i<coords.length; i++) {
37886
+ if (isNested) forEachSymbolCoord(coords[i], cb);
37887
+ }
37730
37888
  }
37731
37889
 
37732
- function getFilledArrow(d, totalLen) {
37733
- return {
37734
- type: 'polygon',
37735
- coordinates: getFilledArrowCoords(d, totalLen),
37736
- fill: d.fill || 'magenta'
37737
- };
37890
+ function flipY(coords) {
37891
+ forEachSymbolCoord(coords, function(p) {
37892
+ p[1] = -p[1];
37893
+ });
37738
37894
  }
37739
37895
 
37740
- function getScale(totalLen, headLen) {
37741
- var maxHeadPct = 0.60;
37742
- var headPct = headLen / totalLen;
37743
- if (headPct > maxHeadPct) {
37744
- return maxHeadPct / headPct;
37745
- }
37746
- return 1;
37896
+ function scaleAndShiftCoords(coords, scale, shift) {
37897
+ forEachSymbolCoord(coords, function(xy) {
37898
+ xy[0] = xy[0] * scale + shift[0];
37899
+ xy[1] = xy[1] * scale + shift[1];
37900
+ });
37747
37901
  }
37748
37902
 
37749
- function getStickArrowTip(totalLen, curve) {
37750
- // curve/2 intersects the arrowhead at 90deg (trigonometry)
37751
- var theta = Math.abs(curve/2) / 180 * Math.PI;
37752
- var dx = totalLen * Math.sin(theta) * (curve > 0 ? -1 : 1);
37753
- var dy = totalLen * Math.cos(theta);
37754
- return [dx, dy];
37903
+ function roundCoordsForSVG(coords) {
37904
+ forEachSymbolCoord(coords, function(p) {
37905
+ p[0] = roundCoord(p[0]);
37906
+ p[1] = roundCoord(p[1]);
37907
+ });
37755
37908
  }
37756
37909
 
37757
- function addPoints(a, b) {
37758
- return [a[0] + b[0], a[1] + b[1]];
37910
+ function rotateCoords(coords, rotation) {
37911
+ if (!rotation) return;
37912
+ var f = getAffineTransform(rotation, 1, [0, 0], [0, 0]);
37913
+ forEachSymbolCoord(coords, function(p) {
37914
+ var p2 = f(p[0], p[1]);
37915
+ p[0] = p2[0];
37916
+ p[1] = p2[1];
37917
+ });
37759
37918
  }
37760
37919
 
37761
- function getStickArrowCoords(d, totalLen) {
37762
- var headAngle = d['arrow-head-angle'] || 90;
37763
- var curve = d['arrow-stem-curve'] || 0;
37764
- var unscaledHeadWidth = d['arrow-head-width'] || 9;
37765
- var unscaledHeadLen = getHeadLength(unscaledHeadWidth, headAngle);
37766
- var scale = getScale(totalLen, unscaledHeadLen); // scale down small arrows
37767
- var headWidth = unscaledHeadWidth * scale;
37768
- var headLen = unscaledHeadLen * scale;
37769
- var tip = getStickArrowTip(totalLen, curve);
37770
- var stem = [[0, 0], tip.concat()];
37771
- if (curve) {
37772
- addBezierArcControlPoints(stem, curve);
37773
- }
37774
- if (!headLen) return [stem];
37775
- var head = [addPoints([-headWidth / 2, -headLen], tip), tip.concat(), addPoints([headWidth / 2, -headLen], tip)];
37920
+ function findArcCenter(p1, p2, degrees) {
37921
+ var p3 = [(p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2], // midpoint betw. p1, p2
37922
+ tan = 1 / Math.tan(degrees / 180 * Math.PI / 2),
37923
+ cp = getAffineTransform(90, tan, [0, 0], p3)(p2[0], p2[1]);
37924
+ return cp;
37925
+ }
37776
37926
 
37777
- rotateSymbolCoords(stem, d.rotation);
37778
- rotateSymbolCoords(head, d.rotation);
37779
- return [stem, head];
37927
+ // export function addBezierArcControlPoints(p1, p2, degrees) {
37928
+ function addBezierArcControlPoints(points, degrees) {
37929
+ // source: https://stackoverflow.com/questions/734076/how-to-best-approximate-a-geometrical-arc-with-a-bezier-curve
37930
+ var p2 = points.pop(),
37931
+ p1 = points.pop(),
37932
+ cp = findArcCenter(p1, p2, degrees),
37933
+ xc = cp[0],
37934
+ yc = cp[1],
37935
+ ax = p1[0] - xc,
37936
+ ay = p1[1] - yc,
37937
+ bx = p2[0] - xc,
37938
+ by = p2[1] - yc,
37939
+ q1 = ax * ax + ay * ay,
37940
+ q2 = q1 + ax * bx + ay * by,
37941
+ k2 = 4/3 * (Math.sqrt(2 * q1 * q2) - q2) / (ax * by - ay * bx);
37942
+
37943
+ points.push(p1);
37944
+ points.push([xc + ax - k2 * ay, yc + ay + k2 * ax, 'C']);
37945
+ points.push([xc + bx + k2 * by, yc + by - k2 * bx, 'C']);
37946
+ points.push(p2);
37780
37947
  }
37781
37948
 
37782
- function getHeadLength(headWidth, headAngle) {
37783
- var headRatio = 1 / Math.tan(Math.PI * headAngle / 180 / 2) / 2; // length-to-width head ratio
37784
- return headWidth * headRatio;
37949
+ // export function getStickArrowCoords(d, totalLen) {
37950
+ // var minStemRatio = getMinStemRatio(d);
37951
+ // var headAngle = d['arrow-head-angle'] || 90;
37952
+ // var curve = d['arrow-stem-curve'] || 0;
37953
+ // var unscaledHeadWidth = d['arrow-head-width'] || 9;
37954
+ // var unscaledHeadLen = getHeadLength(unscaledHeadWidth, headAngle);
37955
+ // var scale = getScale(totalLen, unscaledHeadLen, minStemRatio);
37956
+ // var headWidth = unscaledHeadWidth * scale;
37957
+ // var headLen = unscaledHeadLen * scale;
37958
+ // var tip = getStickArrowTip(totalLen, curve);
37959
+ // var stem = [[0, 0], tip.concat()];
37960
+ // if (curve) {
37961
+ // addBezierArcControlPoints(stem, curve);
37962
+ // }
37963
+ // if (!headLen) return [stem];
37964
+ // var head = [addPoints([-headWidth / 2, -headLen], tip), tip.concat(), addPoints([headWidth / 2, -headLen], tip)];
37965
+
37966
+ // rotateCoords(stem, d.rotation);
37967
+ // rotateCoords(head, d.rotation);
37968
+ // return [stem, head];
37969
+ // }
37970
+
37971
+ function getMinStemRatio(d) {
37972
+ return d['arrow-min-stem'] >= 0 ? d['arrow-min-stem'] : 0.4;
37785
37973
  }
37786
37974
 
37787
- function getFilledArrowCoords(d, totalLen) {
37788
- var headAngle = d['arrow-head-angle'] || 40,
37975
+ function getFilledArrowCoords(d) {
37976
+ var totalLen = d['arrow-length'] || d.radius || d.length || d.r || 0,
37977
+ direction = d.rotation || d['arrow-direction'] || 0,
37789
37978
  unscaledStemWidth = d['arrow-stem-width'] || 2,
37790
37979
  unscaledHeadWidth = d['arrow-head-width'] || unscaledStemWidth * 3,
37791
- unscaledHeadLen = getHeadLength(unscaledHeadWidth, headAngle),
37792
- scale = getScale(totalLen, unscaledHeadLen), // scale down small arrows
37793
- headWidth = unscaledHeadWidth * scale,
37794
- headLen = unscaledHeadLen * scale,
37795
- stemWidth = unscaledStemWidth * scale,
37980
+ unscaledHeadLen = getHeadLength(unscaledHeadWidth, d),
37796
37981
  stemTaper = d['arrow-stem-taper'] || 0,
37797
- stemLen = totalLen - headLen;
37982
+ stemCurve = d['arrow-stem-curve'] || 0;
37983
+
37984
+ var headLen, headWidth, stemLen, stemWidth;
37985
+
37986
+ var scale = 1;
37987
+
37988
+ if (totalLen > 0) {
37989
+ scale = getScale(totalLen, unscaledHeadLen, getMinStemRatio(d));
37990
+ headWidth = unscaledHeadWidth * scale;
37991
+ headLen = unscaledHeadLen * scale;
37992
+ stemWidth = unscaledStemWidth * scale;
37993
+ stemLen = totalLen - headLen;
37994
+
37995
+ } else {
37996
+ headWidth = unscaledHeadWidth;
37997
+ headLen = unscaledHeadLen;
37998
+ stemWidth = unscaledStemWidth;
37999
+ stemLen = d['arrow-stem-length'] || 0;
38000
+ totalLen = headLen + stemLen;
38001
+ }
38002
+
38003
+ if (totalLen > 0 === false) return null;
38004
+
38005
+ var coords;
37798
38006
 
37799
38007
  var headDx = headWidth / 2,
37800
38008
  stemDx = stemWidth / 2,
37801
38009
  baseDx = stemDx * (1 - stemTaper);
37802
38010
 
37803
- var coords = [[baseDx, 0], [stemDx, stemLen], [headDx, stemLen], [0, stemLen + headLen],
38011
+ if (unscaledHeadWidth < unscaledStemWidth) {
38012
+ stop('Arrow head must be at least as wide as the stem.');
38013
+ }
38014
+
38015
+ if (!stemCurve || Math.abs(stemCurve) > 90) {
38016
+ coords = [[baseDx, 0], [stemDx, stemLen], [headDx, stemLen], [0, stemLen + headLen],
37804
38017
  [-headDx, stemLen], [-stemDx, stemLen], [-baseDx, 0], [baseDx, 0]];
38018
+ } else {
38019
+ if (direction > 0) stemCurve = -stemCurve;
38020
+ coords = getCurvedArrowCoords(stemLen, headLen, stemCurve, stemDx, headDx, baseDx);
38021
+ }
37805
38022
 
37806
- rotateSymbolCoords(coords, d.rotation);
38023
+ rotateCoords(coords, direction);
37807
38024
  return [coords];
37808
38025
  }
37809
38026
 
37810
- function rotateSymbolCoords(coords, rotation) {
37811
- // TODO: consider avoiding re-instantiating function on every call
37812
- var f = getAffineTransform(rotation || 0, 1, [0, 0], [0, 0]);
37813
- coords.forEach(function(p) {
37814
- var p2 = f ? f(p[0], p[1]) : p;
37815
- p[0] = p2[0];
37816
- p[1] = -p2[1]; // flip y-axis (to produce display coords)
37817
- });
38027
+
38028
+ function getScale(totalLen, headLen, minStemRatio) {
38029
+ var maxHeadPct = 1 - minStemRatio;
38030
+ var headPct = headLen / totalLen;
38031
+ if (headPct > maxHeadPct) {
38032
+ return maxHeadPct / headPct;
38033
+ }
38034
+ return 1;
37818
38035
  }
37819
38036
 
37820
- // TODO: refactor to remove duplication in mapshaper-svg-style.js
37821
- cmd.symbols = function(lyr, opts) {
37822
- var f, filter;
37823
- // console.log("-symbols opts", opts)
37824
- requirePointLayer(lyr);
37825
- f = getSymbolDataAccessor(lyr, opts);
37826
- if (opts.where) {
37827
- filter = compileValueExpression(opts.where, lyr, null);
38037
+ // function getStickArrowTip(totalLen, curve) {
38038
+ // // curve/2 intersects the arrowhead at 90deg (trigonometry)
38039
+ // var theta = Math.abs(curve/2) / 180 * Math.PI;
38040
+ // var dx = totalLen * Math.sin(theta) * (curve > 0 ? -1 : 1);
38041
+ // var dy = totalLen * Math.cos(theta);
38042
+ // return [dx, dy];
38043
+ // }
38044
+
38045
+ function addPoints(a, b) {
38046
+ return [a[0] + b[0], a[1] + b[1]];
38047
+ }
38048
+
38049
+
38050
+ function getHeadLength(headWidth, d) {
38051
+ var headLength = d['arrow-head-length'];
38052
+ if (headLength > 0) return headLength;
38053
+ var headAngle = d['arrow-head-angle'] || 40;
38054
+ var headRatio = 1 / Math.tan(Math.PI * headAngle / 180 / 2) / 2; // length-to-width head ratio
38055
+ return headWidth * headRatio;
38056
+ }
38057
+
38058
+ function getCurvedArrowCoords(stemLen, headLen, curvature, stemDx, headDx, baseDx) {
38059
+ // coordinates go counter clockwise, starting from the leftmost head coordinate
38060
+ var theta = Math.abs(curvature) / 180 * Math.PI;
38061
+ var sign = curvature > 0 ? 1 : -1;
38062
+ var dx = stemLen * Math.sin(theta / 2) * sign;
38063
+ var dy = stemLen * Math.cos(theta / 2);
38064
+ var head = [[stemDx + dx, dy], [headDx + dx, dy],
38065
+ [dx, headLen + dy], [-headDx + dx, dy], [-stemDx + dx, dy]];
38066
+ var ax = baseDx * Math.cos(theta); // rotate arrow base
38067
+ var ay = baseDx * Math.sin(theta) * -sign;
38068
+ var leftStem = getCurvedStemCoords(-ax, -ay, -stemDx + dx, dy, theta);
38069
+ var rightStem = getCurvedStemCoords(ax, ay, stemDx + dx, dy, theta);
38070
+ var stem = leftStem.concat(rightStem.reverse());
38071
+ // stem.pop();
38072
+ return stem.concat(head);
38073
+ }
38074
+
38075
+ // ax, ay: point on the base
38076
+ // bx, by: point on the stem
38077
+ function getCurvedStemCoords(ax, ay, bx, by, theta0) {
38078
+ // case: curved side intrudes into head (because stem is too short)
38079
+ if (ay > by) {
38080
+ return [[ax * by / ay, by]];
37828
38081
  }
37829
- getLayerDataTable(lyr).getRecords().forEach(function(rec, i) {
37830
- if (filter && filter(i)) {
37831
- if ('svg-symbol' in rec === false) {
37832
- rec['svg-symbol'] = undefined;
37833
- }
38082
+ var dx = bx - ax,
38083
+ dy = by - ay,
38084
+ dy1 = (dy * dy - dx * dx) / (2 * dy),
38085
+ dy2 = dy - dy1,
38086
+ dx2 = Math.sqrt(dx * dx + dy * dy) / 2,
38087
+ theta = Math.PI - Math.asin(dx2 / dy2) * 2,
38088
+ degrees = theta * 180 / Math.PI,
38089
+ radius = dy2 / Math.tan(theta / 2),
38090
+ leftBend = bx > ax,
38091
+ sign = leftBend ? 1 : -1,
38092
+ points = Math.round(degrees / 5) + 2,
38093
+ increment = theta / (points + 1),
38094
+ coords = [[bx, by]];
38095
+
38096
+ for (var i=1; i<= points; i++) {
38097
+ var phi = i * increment / 2;
38098
+ var sinPhi = Math.sin(phi);
38099
+ var cosPhi = Math.cos(phi);
38100
+ var c = sinPhi * radius * 2;
38101
+ var a = sinPhi * c;
38102
+ var b = cosPhi * c;
38103
+ coords.push([bx - a * sign, by - b]);
38104
+ }
38105
+ coords.push([ax, ay]);
38106
+ return coords;
38107
+ }
38108
+
38109
+ // sides: e.g. 5-pointed star has 10 sides
38110
+ // radius: distance from center to point
38111
+ //
38112
+ function getPolygonCoords(opts) {
38113
+ var radius = opts.radius || opts.length || opts.r;
38114
+ if (radius > 0 === false) return null;
38115
+ var type = opts.type;
38116
+ var sides = +opts.sides || getDefaultSides(type);
38117
+ var isStar = type == 'star';
38118
+ if (isStar && (sides < 6 || sides % 2 !== 0)) {
38119
+ stop(`Invalid number of sides for a star (${sides})`);
38120
+ } else if (sides >= 3 === false) {
38121
+ stop(`Invalid number of sides (${sides})`);
38122
+ }
38123
+ var coords = [],
38124
+ angle = 360 / sides,
38125
+ b = isStar ? 1 : 0.5,
38126
+ theta, even, len;
38127
+ if (opts.orientation == 'b') {
38128
+ b = 0;
38129
+ }
38130
+ for (var i=0; i<sides; i++) {
38131
+ even = i % 2 == 0;
38132
+ len = radius;
38133
+ if (isStar && even) {
38134
+ len *= (opts.star_ratio || 0.5);
38135
+ }
38136
+ theta = (i + b) * angle % 360;
38137
+ coords.push(getPlanarSegmentEndpoint(0, 0, theta, len));
38138
+ }
38139
+ coords.push(coords[0].concat());
38140
+ return [coords];
38141
+ }
38142
+
38143
+ function getDefaultSides(type) {
38144
+ return {
38145
+ star: 10,
38146
+ circle: 72,
38147
+ triangle: 3,
38148
+ square: 4,
38149
+ pentagon: 5,
38150
+ hexagon: 6,
38151
+ heptagon: 7,
38152
+ octagon: 8,
38153
+ nonagon: 9,
38154
+ decagon: 10
38155
+ }[type] || 4;
38156
+ }
38157
+
38158
+ // TODO: refactor to remove duplication in mapshaper-svg-style.js
38159
+ cmd.symbols = function(inputLyr, dataset, opts) {
38160
+ requireSinglePointLayer(inputLyr);
38161
+ var lyr = opts.no_replace ? copyLayer(inputLyr) : inputLyr;
38162
+ var polygonMode = !!opts.polygons;
38163
+ var metersPerPx;
38164
+ if (polygonMode) {
38165
+ requireProjectedDataset(dataset);
38166
+ metersPerPx = opts.pixel_scale || getMetersPerPixel(lyr, dataset);
38167
+ }
38168
+ var records = getLayerDataTable(lyr).getRecords();
38169
+ var getSymbolData = getSymbolDataAccessor(lyr, opts);
38170
+ var geometries = lyr.shapes.map(function(shp, i) {
38171
+ if (!shp) return null;
38172
+ var d = getSymbolData(i);
38173
+ var rec = records[i] || {};
38174
+ var coords;
38175
+ if (d.type == 'arrow') {
38176
+ coords = getFilledArrowCoords(d);
38177
+ } else {
38178
+ coords = getPolygonCoords(d);
38179
+ }
38180
+ if (!coords) return null;
38181
+ rotateCoords(coords, +d.rotation || 0);
38182
+ if (!polygonMode) {
38183
+ flipY(coords);
38184
+ }
38185
+ if (+opts.scale) {
38186
+ scaleAndShiftCoords(coords, +opts.scale, [0, 0]);
38187
+ }
38188
+ if (polygonMode) {
38189
+ scaleAndShiftCoords(coords, metersPerPx, shp[0]);
38190
+ if (d.tfill) rec.fill = d.fill;
38191
+ return createGeometry(coords);
37834
38192
  } else {
37835
- rec['svg-symbol'] = buildSymbol(f(i));
38193
+ rec['svg-symbol'] = makeSvgSymbol(coords, d);
37836
38194
  }
37837
38195
  });
38196
+
38197
+ var outputLyr, dataset2;
38198
+ if (polygonMode) {
38199
+ dataset2 = importGeometries(geometries, records);
38200
+ outputLyr = mergeOutputLayerIntoDataset(inputLyr, dataset, dataset2, opts);
38201
+ outputLyr.data = lyr.data;
38202
+ } else {
38203
+ outputLyr = lyr;
38204
+ }
38205
+ return [outputLyr];
37838
38206
  };
37839
38207
 
38208
+ function importGeometries(geometries, records) {
38209
+ var features = geometries.map(function(geom, i) {
38210
+ var d = records[i];
38211
+ return {
38212
+ type: 'Feature',
38213
+ properties: records[i] || null,
38214
+ geometry: geom
38215
+ };
38216
+ });
38217
+ var geojson = {
38218
+ type: 'FeatureCollection',
38219
+ features: features
38220
+ };
38221
+ return importGeoJSON(geojson);
38222
+ }
38223
+
38224
+ function createGeometry(coords) {
38225
+ return {
38226
+ type: 'Polygon',
38227
+ coordinates: coords
38228
+ };
38229
+ }
38230
+
38231
+ function getMetersPerPixel(lyr, dataset) {
38232
+
38233
+ // TODO: handle single point, no extent
38234
+ var bounds = getLayerBounds(lyr);
38235
+ return bounds.width() / 800;
38236
+ }
38237
+
37840
38238
  // Returns an svg-symbol data object for one symbol
37841
- function buildSymbol(properties) {
37842
- var type = properties.type;
37843
- var f = symbolBuilders[type];
37844
- if (!type) {
37845
- stop('Missing required "type" parameter');
37846
- } else if (!f) {
37847
- stop('Unknown symbol type:', type);
37848
- }
37849
- return f(properties);
38239
+ function makeSvgSymbol(coords, properties) {
38240
+ roundCoordsForSVG(coords);
38241
+ return {
38242
+ type: 'polygon',
38243
+ coordinates: coords,
38244
+ fill: properties.fill || 'magenta'
38245
+ };
37850
38246
  }
37851
38247
 
37852
38248
  var Symbols = /*#__PURE__*/Object.freeze({
37853
38249
  __proto__: null,
37854
- buildSymbol: buildSymbol
38250
+ makeSvgSymbol: makeSvgSymbol
37855
38251
  });
37856
38252
 
37857
38253
  cmd.target = function(catalog, opts) {
@@ -37997,99 +38393,6 @@ ${svg}
37997
38393
  }
37998
38394
  };
37999
38395
 
38000
- cmd.shape = function(targetDataset, opts) {
38001
- var geojson, dataset;
38002
- if (opts.coordinates) {
38003
- geojson = makeShapeFromCoords(opts);
38004
- } else if (opts.type == 'circle') {
38005
- geojson = makeCircle(opts);
38006
- } else if (opts.type == 'rectangle' && opts.bbox) {
38007
- geojson = getRectangleGeoJSON(opts);
38008
- } else {
38009
- stop('Missing coordinates parameter');
38010
- }
38011
- // TODO: project shape if targetDataset is projected
38012
- dataset = importGeoJSON(geojson, {});
38013
- if (opts.rotation) {
38014
- rotateDatasetCoords(dataset, opts.rotation);
38015
- }
38016
- dataset.layers[0].name = opts.name || opts.type || 'shape';
38017
- return dataset;
38018
- };
38019
-
38020
- function getRectangleGeoJSON(opts) {
38021
- var bbox = opts.bbox,
38022
- xmin = bbox[0],
38023
- ymin = bbox[1],
38024
- xmax = bbox[2],
38025
- ymax = bbox[3],
38026
- interval = 0.5,
38027
- coords = [],
38028
- type = opts.geometry == 'polyline' ? 'LineString' : 'Polygon';
38029
- addSide(xmin, ymin, xmin, ymax);
38030
- addSide(xmin, ymax, xmax, ymax);
38031
- addSide(xmax, ymax, xmax, ymin);
38032
- addSide(xmax, ymin, xmin, ymin);
38033
- coords.push([xmin, ymin]);
38034
- return {
38035
- type: type,
38036
- coordinates: type == 'Polygon' ? [coords] : coords
38037
- };
38038
-
38039
- function addSide(x1, y1, x2, y2) {
38040
- var dx = x2 - x1,
38041
- dy = y2 - y1,
38042
- n = Math.ceil(Math.max(Math.abs(dx) / interval, Math.abs(dy) / interval)),
38043
- xint = dx / n,
38044
- yint = dy / n;
38045
- for (var i=0; i<n; i++) {
38046
- coords.push([x1 + i * xint, y1 + i * yint]);
38047
- }
38048
- }
38049
- }
38050
-
38051
- function makeCircle(opts) {
38052
- if (opts.radius > 0 === false && opts.radius_angle > 0 === false) {
38053
- stop('Missing required radius parameter.');
38054
- }
38055
- var cp = opts.center || [0, 0];
38056
- var radius = opts.radius || getCircleRadiusFromAngle(getCRS('wgs84'), opts.radius_angle);
38057
- return getCircleGeoJSON(cp, radius, null, {geometry_type : opts.geometry || 'polygon'});
38058
- }
38059
-
38060
- function makeShapeFromCoords(opts) {
38061
- var coordinates = [];
38062
- var offsets = opts.offsets || [];
38063
- var coords = opts.coordinates;
38064
- var type, i, x, y;
38065
- if (coords.length >= 2 === false) {
38066
- stop('Invalid coordinates parameter.');
38067
- }
38068
- for (i=0; i<coords.length; i+= 2) {
38069
- x = coords[i];
38070
- y = coords[i + 1];
38071
- coordinates.push([x, y]);
38072
- }
38073
- for (i=0; i<offsets.length; i+=2) {
38074
- x += offsets[i];
38075
- y += offsets[i + 1];
38076
- coordinates.push([x, y]);
38077
- }
38078
- if (GeoJSON.pathIsRing(coordinates)) {
38079
- type = 'Polygon';
38080
- } else if (opts.closed && coordinates.length >= 3) {
38081
- type = 'Polygon';
38082
- coordinates.push(coordinates[0]);
38083
- } else {
38084
- type = 'LineString';
38085
- }
38086
- return {
38087
- type: type,
38088
- coordinates: type == 'Polygon' ? [coordinates] : coordinates
38089
- };
38090
-
38091
- }
38092
-
38093
38396
  cmd.variableSimplify = function(layers, dataset, opts) {
38094
38397
  var lyr = layers[0];
38095
38398
  var arcs = dataset.arcs;
@@ -38631,6 +38934,9 @@ ${svg}
38631
38934
  } else if (name == 'shape') {
38632
38935
  catalog.addDataset(cmd.shape(targetDataset, opts));
38633
38936
 
38937
+ } else if (name == 'shapes') {
38938
+ outputLayers = applyCommandToEachLayer(cmd.shapes, targetLayers, targetDataset, opts);
38939
+
38634
38940
  } else if (name == 'simplify') {
38635
38941
  if (opts.variable) {
38636
38942
  cmd.variableSimplify(targetLayers, targetDataset, opts);
@@ -38660,7 +38966,7 @@ ${svg}
38660
38966
  applyCommandToEachLayer(cmd.svgStyle, targetLayers, targetDataset, opts);
38661
38967
 
38662
38968
  } else if (name == 'symbols') {
38663
- applyCommandToEachLayer(cmd.symbols, targetLayers, opts);
38969
+ outputLayers = applyCommandToEachLayer(cmd.symbols, targetLayers, targetDataset, opts);
38664
38970
 
38665
38971
  } else if (name == 'subdivide') {
38666
38972
  outputLayers = applyCommandToEachLayer(cmd.subdivideLayer, targetLayers, arcs, opts.expression);