mapshaper 0.7.62 → 0.7.64

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/mapshaper.js CHANGED
@@ -25445,10 +25445,21 @@
25445
25445
 
25446
25446
 
25447
25447
  function getFrameData(dataset, exportOpts) {
25448
- var frameLyr = findFrameLayerInDataset(dataset);
25448
+ var frameTarget = resolveExportFrame({targetDataset: dataset, mode: 'cli'});
25449
+ var frameLyr = frameTarget && frameTarget.layer;
25449
25450
  var data;
25450
- if (frameLyr) {
25451
- data = getFrameLayerData(frameLyr, dataset.arcs);
25451
+ if (exportOpts.gui_frame?.data) {
25452
+ data = Object.assign({}, exportOpts.gui_frame.data, {
25453
+ bbox: exportOpts.gui_frame.data.bbox.slice()
25454
+ });
25455
+ if (exportOpts.width > 0 || exportOpts.height > 0) {
25456
+ data = resizeFrameForExport(data, exportOpts);
25457
+ }
25458
+ } else if (frameLyr) {
25459
+ data = getFrameLayerData(frameLyr, dataset.arcs, getDatasetCRS(dataset));
25460
+ if (exportOpts.width > 0 || exportOpts.height > 0) {
25461
+ data = resizeFrameForExport(data, exportOpts);
25462
+ }
25452
25463
  } else {
25453
25464
  data = calcFrameData(dataset, exportOpts);
25454
25465
  }
@@ -25457,6 +25468,32 @@
25457
25468
  return data;
25458
25469
  }
25459
25470
 
25471
+ function resizeFrameForExport(frame, opts) {
25472
+ var bounds = new Bounds(frame.bbox);
25473
+ // A stored frame is an exact geographic crop. The SVG exporter's default
25474
+ // 1px content margin applies only to frameless output.
25475
+ var outputOpts = Object.assign({}, opts, {margin: 0});
25476
+ var outputBounds = calcOutputBounds(bounds, outputOpts);
25477
+ var data = {
25478
+ type: 'frame',
25479
+ bbox: bounds.toArray(),
25480
+ bbox2: outputBounds.toArray(),
25481
+ width: Math.round(outputBounds.width()),
25482
+ height: Math.round(outputBounds.height()) || 1,
25483
+ // The authored unit still describes this frame, but an explicit output size
25484
+ // supersedes any fixed page shape, so the aspect is no longer pinned.
25485
+ aspect_ratio: null,
25486
+ units: frame.units || 'px'
25487
+ };
25488
+ if (data.width != frame.width || data.height != frame.height) {
25489
+ warn(
25490
+ `Output size ${data.width}×${data.height}px overrides the map frame's ` +
25491
+ `nominal size ${frame.width}×${frame.height}px; symbol and label sizes are not rescaled.`
25492
+ );
25493
+ }
25494
+ return data;
25495
+ }
25496
+
25460
25497
  function fitDatasetToFrame(dataset, frame) {
25461
25498
  var bounds = new Bounds(frame.bbox);
25462
25499
  var bounds2 = frame.bbox2 ? new Bounds(frame.bbox2) : new Bounds(0, 0, frame.width, frame.height);
@@ -25467,18 +25504,29 @@
25467
25504
  });
25468
25505
  }
25469
25506
 
25470
- function getFrameLayerData(lyr, arcs) {
25507
+ function getFrameLayerData(lyr, arcs, crs) {
25508
+ if (!isFrameLayer(lyr, arcs)) {
25509
+ error('Invalid map frame layer');
25510
+ }
25471
25511
  var bounds = getLayerBounds(lyr, arcs);
25472
- var d = lyr.data.getReadOnlyRecordAt(0);
25473
- var w = d.width || 800;
25512
+ var d = getSingleFrameRecord(lyr);
25513
+ var w = d.width;
25514
+ var hasFixedAspect = utils.isFiniteNumber(d.frame_aspect_ratio) &&
25515
+ d.frame_aspect_ratio > 0;
25516
+ var aspectRatio = hasFixedAspect ? d.frame_aspect_ratio :
25517
+ bounds.width() / bounds.height();
25474
25518
  // prevent rounding errors (like 1000.0000000002)
25475
- var h = Math.round(w * bounds.height() / bounds.width());
25476
- return {
25519
+ var h = Math.round(w / aspectRatio);
25520
+ var data = {
25477
25521
  type: 'frame',
25478
25522
  width: w,
25479
25523
  height: h,
25480
- bbox: bounds.toArray()
25524
+ bbox: bounds.toArray(),
25525
+ aspect_ratio: hasFixedAspect ? d.frame_aspect_ratio : null,
25526
+ units: d.frame_units || 'px'
25481
25527
  };
25528
+ if (crs) data.crs = crs;
25529
+ return data;
25482
25530
  }
25483
25531
 
25484
25532
 
@@ -25515,9 +25563,63 @@
25515
25563
  }
25516
25564
 
25517
25565
 
25566
+ var frameReservedFields =
25567
+ ['type', 'width', 'height', 'frame_aspect_ratio', 'frame_units'];
25568
+
25569
+ function parseFrameSize(arg) {
25570
+ var str = String(arg).toLowerCase();
25571
+ var units = /px|pix/.test(str) && 'px' ||
25572
+ /pt|point/.test(str) && 'pt' ||
25573
+ /in/.test(str) && 'in' ||
25574
+ /cm/.test(str) && 'cm' ||
25575
+ 'px';
25576
+ return {valuePx: parseSizeParam(arg), units: units};
25577
+ }
25578
+
25579
+ function formatFrameSizeForDisplay(frame) {
25580
+ var units = frame.units || 'px';
25581
+ var k = units == 'in' ? 72 :
25582
+ units == 'cm' ? 28.3465 : 1;
25583
+ return formatFrameDimension(frame.width / k) + ' × ' +
25584
+ formatFrameDimension(frame.height / k) + ' ' + units;
25585
+ }
25586
+
25587
+ function formatFrameDimension(value) {
25588
+ var rounded = Math.round(value * 100) / 100;
25589
+ return String(rounded);
25590
+ }
25591
+
25592
+ function isFrameReservedField(name) {
25593
+ return frameReservedFields.includes(name);
25594
+ }
25595
+
25596
+ function demoteFrameLayer(lyr, operation) {
25597
+ var rec = lyr.data && lyr.data.getRecords()[0];
25598
+ if (!rec) return;
25599
+ operation = operation || 'frame';
25600
+ lyr.data.captureTableBefore({operation: operation});
25601
+ frameReservedFields.forEach(function(field) {
25602
+ delete rec[field];
25603
+ });
25604
+ lyr.data.markChanged({operation: operation});
25605
+ }
25606
+
25607
+ function getSingleFrameRecord(lyr) {
25608
+ if (!lyr || !lyr.data || !lyr.shapes ||
25609
+ lyr.shapes.length != 1 || lyr.data.size() != 1) {
25610
+ return null;
25611
+ }
25612
+ return lyr.data.getReadOnlyRecordAt(0) || null;
25613
+ }
25614
+
25518
25615
  // @lyr dataset layer
25519
25616
  function isFrameLayer(lyr, arcs) {
25520
- return getFurnitureLayerType(lyr) == 'frame' &&
25617
+ var rec = getSingleFrameRecord(lyr);
25618
+ return !!rec &&
25619
+ lyr.geometry_type == 'polygon' &&
25620
+ rec.type == 'frame' &&
25621
+ utils.isFiniteNumber(rec.width) &&
25622
+ rec.width > 0 &&
25521
25623
  layerIsRectangle(lyr, arcs);
25522
25624
  }
25523
25625
 
@@ -25527,21 +25629,97 @@
25527
25629
  });
25528
25630
  }
25529
25631
 
25530
- // TODO: handle multiple frames in catalog
25632
+ function findFrames(catalog) {
25633
+ return catalog.getLayers().filter(function(o) {
25634
+ return isFrameLayer(o.layer, o.dataset.arcs);
25635
+ });
25636
+ }
25637
+
25638
+ // Remembers the frame conflict that was last reported, so that resolving the
25639
+ // frame on every map render does not repeat the same warning. Unlike
25640
+ // warnOnce(), this resets when the conflict clears, so a long-lived GUI session
25641
+ // warns again if the user undoes the change and then repeats it.
25642
+ var reportedFrameConflict = null;
25643
+
25644
+ // Generic field commands can promote an ordinary rectangle into a second frame
25645
+ // (e.g. -each 'type="frame", width=400'), and the GUI resolves the frame on
25646
+ // every render, so this has to degrade to a deterministic choice rather than an
25647
+ // error. Rejecting the change outright is assertSingleFrameUpdate()'s job,
25648
+ // where it can still be refused before it is committed.
25649
+ function getActiveFrame(catalog) {
25650
+ var frames = findFrames(catalog);
25651
+ var key;
25652
+ if (frames.length > 1) {
25653
+ key = frames.map(getFrameName).join(', ');
25654
+ if (key !== reportedFrameConflict) {
25655
+ reportedFrameConflict = key;
25656
+ warn('Multiple map frames are not supported; using',
25657
+ getFrameName(frames[0]) + '. Ignoring:',
25658
+ frames.slice(1).map(getFrameName).join(', '));
25659
+ }
25660
+ } else {
25661
+ reportedFrameConflict = null;
25662
+ }
25663
+ return frames[0] || null;
25664
+ }
25665
+
25531
25666
  function findFrameDataset(catalog) {
25532
- var target = findFrame(catalog);
25667
+ var target = getActiveFrame(catalog);
25533
25668
  return target && target.dataset || null;
25534
25669
  }
25535
25670
 
25536
25671
  function findFrameLayer(catalog) {
25537
- var target = findFrame(catalog);
25672
+ var target = getActiveFrame(catalog);
25538
25673
  return target && target.layer || null;
25539
25674
  }
25540
25675
 
25541
25676
  function findFrame(catalog) {
25542
- return utils.find(catalog.getLayers(), function(o) {
25543
- return isFrameLayer(o.layer, o.dataset.arcs);
25677
+ return getActiveFrame(catalog);
25678
+ }
25679
+
25680
+ function resolveExportFrame(opts) {
25681
+ if (opts.explicitFrame) return opts.explicitFrame;
25682
+ if (opts.targetDataset) {
25683
+ var lyr = findFrameLayerInDataset(opts.targetDataset);
25684
+ return lyr ? {layer: lyr, dataset: opts.targetDataset} : null;
25685
+ }
25686
+ return opts.mode == 'gui' && opts.catalog ? getActiveFrame(opts.catalog) : null;
25687
+ }
25688
+
25689
+ function assertSingleFrameUpdate(catalog, additions, removals) {
25690
+ var removed = removals || [];
25691
+ var frames = catalog.getLayers().filter(function(o) {
25692
+ return !removed.includes(o.layer) && isFrameLayer(o.layer, o.dataset.arcs);
25693
+ });
25694
+ (additions || []).forEach(function(o) {
25695
+ if (!removed.includes(o.layer) && isFrameLayer(o.layer, o.dataset.arcs)) {
25696
+ frames.push(o);
25697
+ }
25698
+ });
25699
+ frames = frames.filter(function(o, i) {
25700
+ return frames.findIndex(function(o2) {
25701
+ return o2.layer == o.layer;
25702
+ }) == i;
25703
+ });
25704
+ if (frames.length > 1) {
25705
+ stop$1('Multiple map frames are not supported:', frames.map(getFrameName).join(', '));
25706
+ }
25707
+ }
25708
+
25709
+ function assertCatalogCanAddDatasets(catalog, datasets, removals) {
25710
+ var existingDatasets = catalog.getDatasets();
25711
+ var additions = [];
25712
+ datasets.forEach(function(dataset) {
25713
+ if (existingDatasets.includes(dataset)) return;
25714
+ dataset.layers.forEach(function(lyr) {
25715
+ additions.push({layer: lyr, dataset: dataset});
25716
+ });
25544
25717
  });
25718
+ assertSingleFrameUpdate(catalog, additions, removals);
25719
+ }
25720
+
25721
+ function getFrameName(o) {
25722
+ return o.layer.name || '[unnamed frame]';
25545
25723
  }
25546
25724
 
25547
25725
  function getFrameLayerBounds(lyr) {
@@ -25670,20 +25848,31 @@
25670
25848
 
25671
25849
  var FrameUtils = /*#__PURE__*/Object.freeze({
25672
25850
  __proto__: null,
25851
+ assertCatalogCanAddDatasets: assertCatalogCanAddDatasets,
25852
+ assertSingleFrameUpdate: assertSingleFrameUpdate,
25673
25853
  calcFrameData: calcFrameData,
25674
25854
  calcOutputBounds: calcOutputBounds,
25855
+ demoteFrameLayer: demoteFrameLayer,
25675
25856
  findFrame: findFrame,
25676
25857
  findFrameDataset: findFrameDataset,
25677
25858
  findFrameLayer: findFrameLayer,
25678
25859
  findFrameLayerInDataset: findFrameLayerInDataset,
25860
+ findFrames: findFrames,
25679
25861
  fitDatasetToFrame: fitDatasetToFrame,
25862
+ formatFrameSizeForDisplay: formatFrameSizeForDisplay,
25863
+ frameReservedFields: frameReservedFields,
25864
+ getActiveFrame: getActiveFrame,
25680
25865
  getFrameData: getFrameData,
25681
25866
  getFrameLayerBounds: getFrameLayerBounds,
25682
25867
  getFrameLayerData: getFrameLayerData,
25683
25868
  getFrameSize: getFrameSize,
25684
25869
  getMapFrameMetersPerPixel: getMapFrameMetersPerPixel,
25870
+ getSingleFrameRecord: getSingleFrameRecord,
25685
25871
  isFrameLayer: isFrameLayer,
25686
- parseMarginOption: parseMarginOption
25872
+ isFrameReservedField: isFrameReservedField,
25873
+ parseFrameSize: parseFrameSize,
25874
+ parseMarginOption: parseMarginOption,
25875
+ resolveExportFrame: resolveExportFrame
25687
25876
  });
25688
25877
 
25689
25878
  function addFurnitureLayer(lyr, catalog) {
@@ -27792,6 +27981,12 @@
27792
27981
  // invariant under translation, rotation and scaling (Hobby 1986), which is
27793
27982
  // what lets the tool fit in display coordinates while storing knots in map
27794
27983
  // coordinates.
27984
+ //
27985
+ // Left alone, the method bulges: a hard turn followed by a much shorter
27986
+ // segment throws the long segment into a wide arc, several times the length of
27987
+ // the short one. Two limits below hold that in, MAX_HANDLE and
27988
+ // MAX_CHORD_RATIO. Both are inert on a path whose chords are of comparable
27989
+ // length, so an ordinary label path is fitted exactly as Hobby describes.
27795
27990
 
27796
27991
  // Curl controls how the curve behaves at the two ends of a run. At 0 it
27797
27992
  // approaches a straight line there; at 1 (Metafont's default) it approaches a
@@ -27879,6 +28074,24 @@
27879
28074
  VEL_B = 1 / 16,
27880
28075
  VEL_C = (3 - Math.sqrt(5)) / 2;
27881
28076
 
28077
+ // Longest a control point may sit from its knot, as a fraction of the chord
28078
+ // length that handleLimit() works out for it. See "Bulge" in the design doc: a
28079
+ // hard turn followed by a short segment otherwise throws the long segment into
28080
+ // a wide arc, which during editing is most of the time, because a short
28081
+ // segment is what a half-dragged knot leaves behind.
28082
+ //
28083
+ // The value is a floor, not a preference. Four points on a circle want 0.3905,
28084
+ // which is the roundest any sensible path asks for, so a limit just above it
28085
+ // never touches a curve that was not already misbehaving.
28086
+ var MAX_HANDLE = 0.4;
28087
+
28088
+ // How much longer one chord may be than its neighbour *for the purposes of the
28089
+ // solve*. Hobby equalizes mock curvature across each knot, and a short chord
28090
+ // carries a lot of curvature, so without this a short segment dictates a wide
28091
+ // arc to a long one. Capping the ratio limits how far that reaches without
28092
+ // moving any knot or touching the curve where the chords are even.
28093
+ var MAX_CHORD_RATIO = 1.5;
28094
+
27882
28095
  // Fits a run of knots, returning a cubic per interval.
27883
28096
  function fitRun(pts) {
27884
28097
  var n = pts.length - 1, // segment count
@@ -27895,16 +28108,54 @@
27895
28108
  psi.push(0);
27896
28109
  for (i = 1; i < n; i++) psi.push(wrapAngle(om[i] - om[i - 1]));
27897
28110
 
27898
- theta = solveDepartureAngles(dd, psi, n);
28111
+ theta = solveDepartureAngles(evenOutChords(dd, n), psi, n);
27899
28112
  phi = getArrivalAngles(theta, psi, n);
27900
28113
 
27901
28114
  for (i = 0; i < n; i++) {
27902
28115
  segments.push(buildSegment(pts[i], pts[i + 1], dd[i], om[i],
27903
- theta[i], phi[i + 1]));
28116
+ theta[i], phi[i + 1],
28117
+ handleLimit(dd[i], i > 0 ? dd[i - 1] : Infinity),
28118
+ handleLimit(dd[i], i < n - 1 ? dd[i + 1] : Infinity)));
27904
28119
  }
27905
28120
  return segments;
27906
28121
  }
27907
28122
 
28123
+ // Longest a handle may be at one end of a segment whose own chord is @own and
28124
+ // whose neighbour across that knot is @neighbor. (At the first and last knot
28125
+ // of a run there is no neighbour, and the segment's own chord is the measure.)
28126
+ //
28127
+ // The scale is the geometric mean of the two, rather than the shorter of them.
28128
+ // Measuring against the shorter alone holds the bulge down just as well, but
28129
+ // it makes a long segment meeting a short one spend nearly all of its shape at
28130
+ // its far end: on a 490-unit arm meeting a 50-unit leg the two handles came
28131
+ // out 9.8:1, so the arm ran almost straight and then hooked hard into the
28132
+ // knot, and the curvature either side of that knot differed by a factor of 14.
28133
+ // The geometric mean gives the long segment a handle in proportion to how
28134
+ // lopsided the pair actually is, which brings the same case to 3.1:1 and the
28135
+ // curvature to within a factor of 1.4 -- about what Hobby's mock curvature
28136
+ // leaves on an ordinary path anyway -- for two points of extra bulge.
28137
+ function handleLimit(own, neighbor) {
28138
+ return MAX_HANDLE * Math.sqrt(own * Math.min(own, neighbor));
28139
+ }
28140
+
28141
+ // The chord lengths the solve sees, with the ratio between neighbours held to
28142
+ // MAX_CHORD_RATIO. Each length becomes the smallest that any chord permits it
28143
+ // to be, allowing for a factor of MAX_CHORD_RATIO per step -- a forward pass
28144
+ // and a backward pass compute that exactly, so this needs no iteration.
28145
+ //
28146
+ // Only the tangent directions are affected. The curve is still built on the
28147
+ // true chord lengths.
28148
+ function evenOutChords(dd, n) {
28149
+ var out = dd.concat(), i;
28150
+ for (i = 1; i < n; i++) {
28151
+ if (out[i] > out[i - 1] * MAX_CHORD_RATIO) out[i] = out[i - 1] * MAX_CHORD_RATIO;
28152
+ }
28153
+ for (i = n - 2; i >= 0; i--) {
28154
+ if (out[i] > out[i + 1] * MAX_CHORD_RATIO) out[i] = out[i + 1] * MAX_CHORD_RATIO;
28155
+ }
28156
+ return out;
28157
+ }
28158
+
27908
28159
  // The tridiagonal system of Hobby's mock-curvature conditions, in the
27909
28160
  // departure angle at each knot measured from the chord leaving it. Written for
27910
28161
  // unit tension throughout; the general form carries a tension per knot, which
@@ -27948,21 +28199,30 @@
27948
28199
 
27949
28200
  // Turns a pair of angles into a cubic, by way of Hobby's velocity functions --
27950
28201
  // how far along each tangent the control point sits, as a multiple of a third
27951
- // of the chord.
27952
- function buildSegment(p0, p1, d, om, theta, phi) {
28202
+ // of the chord -- with each distance held to the caller's limit.
28203
+ //
28204
+ // Shortening a handle leaves its direction alone, so the tangent the two
28205
+ // segments share at a knot is untouched and the curve stays smooth through it.
28206
+ // What it gives up is Hobby's mock-curvature match across that knot, and only
28207
+ // at a knot lopsided enough for the limit to bite.
28208
+ function buildSegment(p0, p1, d, om, theta, phi, maxOut, maxIn) {
27953
28209
  var alpha = VEL_A * (Math.sin(theta) - VEL_B * Math.sin(phi)) *
27954
28210
  (Math.sin(phi) - VEL_B * Math.sin(theta)) *
27955
28211
  (Math.cos(theta) - Math.cos(phi)),
27956
28212
  rho = (2 + alpha) /
27957
28213
  (1 + (1 - VEL_C) * Math.cos(theta) + VEL_C * Math.cos(phi)),
27958
28214
  sigma = (2 - alpha) /
27959
- (1 + (1 - VEL_C) * Math.cos(phi) + VEL_C * Math.cos(theta));
28215
+ (1 + (1 - VEL_C) * Math.cos(phi) + VEL_C * Math.cos(theta)),
28216
+ out = d * rho / 3,
28217
+ arr = d * sigma / 3;
28218
+ if (out > maxOut) out = maxOut;
28219
+ if (arr > maxIn) arr = maxIn;
27960
28220
  return {
27961
28221
  p0: p0,
27962
- c1: [p0[0] + d * rho / 3 * Math.cos(theta + om),
27963
- p0[1] + d * rho / 3 * Math.sin(theta + om)],
27964
- c2: [p1[0] - d * sigma / 3 * Math.cos(om - phi),
27965
- p1[1] - d * sigma / 3 * Math.sin(om - phi)],
28222
+ c1: [p0[0] + out * Math.cos(theta + om),
28223
+ p0[1] + out * Math.sin(theta + om)],
28224
+ c2: [p1[0] - arr * Math.cos(om - phi),
28225
+ p1[1] - arr * Math.sin(om - phi)],
27966
28226
  p3: p1
27967
28227
  };
27968
28228
  }
@@ -28649,7 +28909,8 @@
28649
28909
  function exportSVG(dataset, opts) {
28650
28910
  var namespace = 'xmlns="http://www.w3.org/2000/svg"';
28651
28911
  var defs = [];
28652
- var frame, svg, layers, metadataJSON, files, svgFile;
28912
+ var frame, frameLyr, frameBackground, frameNeatline;
28913
+ var svg, layers, metadataJSON, files, svgFile;
28653
28914
  var style = '';
28654
28915
 
28655
28916
  // kludge for map keys
@@ -28686,6 +28947,21 @@
28686
28947
  if (opts.scalebar) {
28687
28948
  layers.push(getScalebarLayer({})); // default options
28688
28949
  }
28950
+ frameLyr = layers.find(function(lyr) {
28951
+ return isFrameLayer(lyr, dataset.arcs);
28952
+ });
28953
+ if (frameLyr) {
28954
+ frameBackground = exportFrameStylePhase(frameLyr, dataset, opts, 'background');
28955
+ frameNeatline = exportFrameStylePhase(frameLyr, dataset, opts, 'neatline');
28956
+ layers = layers.filter(function(lyr) { return lyr != frameLyr; });
28957
+ } else if (opts.gui_frame?.style) {
28958
+ frameBackground = exportExplicitFrameStyle(
28959
+ frame, opts.gui_frame, 'background'
28960
+ );
28961
+ frameNeatline = exportExplicitFrameStyle(
28962
+ frame, opts.gui_frame, 'neatline'
28963
+ );
28964
+ }
28689
28965
  svg = layers.map(function(lyr) {
28690
28966
  var obj;
28691
28967
  if (layerHasFurniture(lyr)) {
@@ -28698,6 +28974,8 @@
28698
28974
  convertPropertiesToDefinitions(obj, defs);
28699
28975
  return stringify(obj);
28700
28976
  }).join('\n');
28977
+ if (frameBackground) svg = frameBackground + '\n' + svg;
28978
+ if (frameNeatline) svg += '\n' + frameNeatline;
28701
28979
 
28702
28980
  if (metadataJSON) {
28703
28981
  svg = getMetadataBlock(metadataJSON, [0, 0, frame.width, frame.height]) + svg;
@@ -28732,6 +29010,65 @@ ${svg}
28732
29010
  return files;
28733
29011
  }
28734
29012
 
29013
+ function exportFrameStylePhase(lyr, dataset, opts, phase) {
29014
+ var copy = copyLayer(lyr);
29015
+ var rec = copy.data && copy.data.getRecords()[0];
29016
+ var obj;
29017
+ if (!rec) return '';
29018
+ if (phase == 'background') {
29019
+ if (!rec.fill || rec.fill == 'none') return '';
29020
+ removeStylePrefix(rec, 'stroke');
29021
+ copy.name = (lyr.name || 'frame') + '-background';
29022
+ } else {
29023
+ if (!rec.stroke || rec.stroke == 'none' ||
29024
+ Number(rec['stroke-width']) <= 0) return '';
29025
+ removeStylePrefix(rec, 'fill');
29026
+ rec.fill = 'none';
29027
+ copy.name = (lyr.name || 'frame') + '-neatline';
29028
+ }
29029
+ obj = exportLayerForSVG(copy, dataset, opts);
29030
+ return stringify(obj);
29031
+ }
29032
+
29033
+ function removeStylePrefix(rec, prefix) {
29034
+ Object.keys(rec).forEach(function(key) {
29035
+ if (key == prefix || key.indexOf(prefix + '-') === 0) {
29036
+ delete rec[key];
29037
+ }
29038
+ });
29039
+ }
29040
+
29041
+ function exportExplicitFrameStyle(frame, context, phase) {
29042
+ var style = context.style || {};
29043
+ var properties = {
29044
+ id: (context.name || 'frame') + '-' + phase,
29045
+ x: 0,
29046
+ y: 0,
29047
+ width: frame.width,
29048
+ height: frame.height
29049
+ };
29050
+ if (phase == 'background') {
29051
+ if (!style.fill || style.fill == 'none') return '';
29052
+ copyProperties(properties, style,
29053
+ ['fill', 'fill-opacity', 'fill-rule', 'opacity']);
29054
+ } else {
29055
+ if (!style.stroke || style.stroke == 'none' ||
29056
+ Number(style['stroke-width']) <= 0) return '';
29057
+ properties.fill = 'none';
29058
+ copyProperties(properties, style, [
29059
+ 'stroke', 'stroke-width', 'stroke-opacity', 'stroke-linecap',
29060
+ 'stroke-linejoin', 'stroke-dasharray', 'opacity'
29061
+ ]);
29062
+ }
29063
+ return stringify({tag: 'rect', properties: properties});
29064
+ }
29065
+
29066
+ function copyProperties(dest, src, names) {
29067
+ names.forEach(function(name) {
29068
+ if (src[name] !== undefined) dest[name] = src[name];
29069
+ });
29070
+ }
29071
+
28735
29072
  function getSvgFileBase(dataset, opts) {
28736
29073
  var file = opts.file || getOutputFileBase(dataset) + '.svg';
28737
29074
  return file.replace(/\.svg$/i, '');
@@ -28807,6 +29144,10 @@ ${svg}
28807
29144
 
28808
29145
  function exportLayerForSVG(lyr, dataset, opts) {
28809
29146
  var layerObj = getEmptyLayerForSVG(lyr, opts);
29147
+ if (isFrameLayer(lyr, dataset.arcs) && !frameHasExportStyle(lyr)) {
29148
+ layerObj.children = [];
29149
+ return layerObj;
29150
+ }
28810
29151
  if (layerIsRectangle(lyr, dataset.arcs)) {
28811
29152
  lyr = copyLayer(lyr);
28812
29153
  adjustRectangleStyle(lyr);
@@ -28815,6 +29156,14 @@ ${svg}
28815
29156
  return layerObj;
28816
29157
  }
28817
29158
 
29159
+ function frameHasExportStyle(lyr) {
29160
+ var rec = lyr.data && lyr.data.getReadOnlyRecordAt(0);
29161
+ var fields = findStylePropertiesBySymbolGeom(Object.keys(rec || {}), 'polygon');
29162
+ return fields.some(function(name) {
29163
+ return rec[name] !== null && rec[name] !== undefined && rec[name] !== '';
29164
+ });
29165
+ }
29166
+
28818
29167
  function exportRasterLayerForSVG(lyr, frame, opts) {
28819
29168
  var raster = lyr.raster;
28820
29169
  var rendered = renderRasterForSVG(raster, frame, opts);
@@ -42582,6 +42931,45 @@ ${svg}
42582
42931
  // projected coordinate is not read as lat-long
42583
42932
  .option('target', targetOpt);
42584
42933
 
42934
+ // used by GUI
42935
+ parser.command('update-frame')
42936
+ // .describe('update an existing map frame layer')
42937
+ .option('bbox', {
42938
+ describe: 'frame coordinates (xmin,ymin,xmax,ymax)',
42939
+ type: 'bbox'
42940
+ })
42941
+ .option('width', {
42942
+ describe: 'nominal frame width (e.g. 5in, 10cm, 600px)'
42943
+ })
42944
+ .option('height', {
42945
+ describe: 'nominal frame height'
42946
+ })
42947
+ .option('aspect-ratio', {
42948
+ describe: 'fixed page aspect ratio',
42949
+ type: 'number'
42950
+ })
42951
+ .option('auto-aspect', {
42952
+ describe: 'derive page height from the frame extent',
42953
+ type: 'flag'
42954
+ })
42955
+ .option('fix-scale', {
42956
+ describe: 'hold the map scale; derive the size from the new extent',
42957
+ type: 'flag'
42958
+ })
42959
+ .option('offset', {
42960
+ describe: 'padding in display units or pct of width/height, e.g. 5cm 20px 5%',
42961
+ type: 'strings'
42962
+ })
42963
+ .option('offsets', {
42964
+ describe: 'separate offsets for each side, in l,b,r,t order',
42965
+ type: 'strings'
42966
+ })
42967
+ .option('remove', {
42968
+ describe: 'demote the frame to an ordinary rectangle layer',
42969
+ type: 'flag'
42970
+ })
42971
+ .option('target', targetOpt);
42972
+
42585
42973
  // used by GUI
42586
42974
  parser.command('update-label')
42587
42975
  // .describe('move the anchor or curve knots of an existing label')
@@ -42694,7 +43082,7 @@ ${svg}
42694
43082
  type: 'bbox'
42695
43083
  })
42696
43084
  .option('offset', {
42697
- describe: 'padding in display units or pct of width, e.g. 5cm 20px 5%',
43085
+ describe: 'padding in display units or pct of width/height, e.g. 5cm 20px 5%',
42698
43086
  type: 'strings'
42699
43087
  })
42700
43088
  .option('offsets', {
@@ -42702,6 +43090,10 @@ ${svg}
42702
43090
  type: 'strings'
42703
43091
  })
42704
43092
  .option('name', nameOpt)
43093
+ .option('replace', {
43094
+ describe: 'replace the existing map frame',
43095
+ type: 'flag'
43096
+ })
42705
43097
  .option('target', targetOpt);
42706
43098
 
42707
43099
  // .option('height', {
@@ -49327,12 +49719,13 @@ ${svg}
49327
49719
  return layers;
49328
49720
  };
49329
49721
 
49330
- this.addDataset = function(dataset) {
49331
- this.setDefaultTarget(dataset.layers, dataset);
49722
+ this.addDataset = function(dataset, opts) {
49723
+ this.setDefaultTarget(dataset.layers, dataset, opts);
49332
49724
  return this;
49333
49725
  };
49334
49726
 
49335
49727
  this.addDatasets = function(datasets) {
49728
+ assertCatalogCanAddDatasets(this, datasets);
49336
49729
  datasets.forEach(function(dataset) {
49337
49730
  this.addDataset(dataset);
49338
49731
  }, this);
@@ -49361,7 +49754,7 @@ ${svg}
49361
49754
  return defaultTargets;
49362
49755
  };
49363
49756
 
49364
- this.setDefaultTarget = function(layers, dataset) {
49757
+ this.setDefaultTarget = function(layers, dataset, opts) {
49365
49758
  this.setDefaultTargets([{
49366
49759
  // Copy layers array, in case layers is a reference to dataset.layers.
49367
49760
  // This prevents layers that are added to the dataset inside a command from
@@ -49369,12 +49762,19 @@ ${svg}
49369
49762
  // by '-join unmatched unjoined'.
49370
49763
  layers: layers.concat(),
49371
49764
  dataset: dataset
49372
- }]);
49765
+ }], opts);
49373
49766
  };
49374
49767
 
49375
49768
  // arr: array of target objects {layers:[], dataset:{}}
49376
- this.setDefaultTargets = function(arr) {
49769
+ this.setDefaultTargets = function(arr, opts) {
49377
49770
  if (targetsAreSame(defaultTargets, arr)) return;
49771
+ var newDatasets = arr.reduce(function(memo, target) {
49772
+ if (!datasets.includes(target.dataset) && !memo.includes(target.dataset)) {
49773
+ memo.push(target.dataset);
49774
+ }
49775
+ return memo;
49776
+ }, []);
49777
+ assertCatalogCanAddDatasets(this, newDatasets, opts && opts.removeLayers);
49378
49778
  this.captureCatalogBefore({operation: 'setDefaultTargets'});
49379
49779
  arr.forEach(function(target) {
49380
49780
  if (datasets.indexOf(target.dataset) == -1) {
@@ -49824,165 +50224,1124 @@ ${svg}
49824
50224
  return dataset;
49825
50225
  }
49826
50226
 
49827
- var OPERATION = 'update-label';
50227
+ // Returns number of arcs that were removed
50228
+ function editArcs(arcs, onPoint) {
50229
+ var nn2 = [],
50230
+ xx2 = [],
50231
+ yy2 = [],
50232
+ errors = 0,
50233
+ n;
49828
50234
 
49829
- cmd.updateLabel = updateLabel;
50235
+ arcs.forEach(function(arc, i) {
50236
+ editArc(arc, onPoint);
50237
+ });
50238
+ arcs.updateVertexData(nn2, xx2, yy2);
50239
+ return errors;
49830
50240
 
49831
- // Rewrites the knots of a label that already exists.
49832
- //
49833
- // -add-label creates and -style sets properties, so neither can move a label:
49834
- // a label's knots are its geometry, not an attribute. This is the command the
49835
- // GUI emits when a knot or an anchor is dragged, so that an interactive move is
49836
- // an ordinary command with undo and session history rather than a direct
49837
- // mutation with hand-rolled undo.
49838
- //
49839
- // See docs/development/label-tool-design.md.
49840
- function updateLabel(targetLayers, dataset, opts) {
49841
- var lyr, id, coords, rec;
49842
- if (targetLayers.length > 1) {
49843
- // One coordinate list describes one label, so there is no sensible reading
49844
- // of this across layers.
49845
- stop$1('Command expects a single target layer');
50241
+ function append(p) {
50242
+ if (p) {
50243
+ xx2.push(p[0]);
50244
+ yy2.push(p[1]);
50245
+ n++;
50246
+ }
49846
50247
  }
49847
- lyr = targetLayers[0];
49848
- id = getTargetId(lyr, opts);
49849
- coords = parseLabelCoords(opts.coordinates);
49850
- rec = getLayerDataTable(lyr).getRecords()[id];
49851
- if (!featureIsLabel(rec)) {
49852
- // Moving an arbitrary point through a command named -update-label would be
49853
- // a surprising way to succeed, and in the GUI it would mean the hit test
49854
- // handed over the wrong feature.
49855
- stop$1('Feature', id, 'in layer "' + layerName(lyr) + '" is not a label ' +
49856
- '(it has no label-text property)');
50248
+
50249
+ function editArc(arc, cb) {
50250
+ var x, y, xp, yp, retn;
50251
+ var valid = true;
50252
+ var i = 0;
50253
+ n = 0;
50254
+ while (arc.hasNext()) {
50255
+ x = arc.x;
50256
+ y = arc.y;
50257
+ retn = cb(append, x, y, xp, yp, i++);
50258
+ if (retn === false) {
50259
+ valid = false;
50260
+ // assumes that it's ok for the arc iterator to be interrupted.
50261
+ break;
50262
+ }
50263
+ xp = x;
50264
+ yp = y;
50265
+ }
50266
+ if (valid && n == 1) {
50267
+ // only one valid point was added to this arc (invalid)
50268
+ // e.g. this could happen during reprojection.
50269
+ // making this arc empty
50270
+ // error("An invalid arc was created");
50271
+ message("An invalid arc was created");
50272
+ valid = false;
50273
+ }
50274
+ if (valid) {
50275
+ nn2.push(n);
50276
+ } else {
50277
+ // remove any points that were added for an invalid arc
50278
+ while (n-- > 0) {
50279
+ xx2.pop();
50280
+ yy2.pop();
50281
+ }
50282
+ nn2.push(0); // add empty arc (to preserve mapping from paths to arcs)
50283
+ errors++;
50284
+ }
49857
50285
  }
49858
- warnIfCurveIsUnprojected(dataset, coords.length);
49859
- // Undo works from what a command declares it is about to change, so an edit
49860
- // that does not say so is an edit that cannot be undone -- the shapes array
49861
- // keeps its identity here, leaving nothing for the transaction to notice
49862
- // afterwards.
49863
- noteLayerWillChange(lyr, {operation: OPERATION, unit: 'shapes'});
49864
- // Point shapes hold their coordinates directly, so this is the whole edit.
49865
- // The pairs are copied rather than aliased: the GUI drags a live copy of the
49866
- // knots and would otherwise keep a handle on the layer's own arrays.
49867
- lyr.shapes[id] = coords.map(function(p) { return [p[0], p[1]]; });
49868
- markLayerChanged(lyr, {operation: OPERATION, unit: 'shapes'});
49869
- // Nothing about the label's text is touched, and nothing needs to be: a text
49870
- // measurement describes the *text*, and moving a knot changes the length of
49871
- // the *path*. The fit check compares the two at render time, against the
49872
- // curve as it then stands, so a move can turn a fitting label into an
49873
- // overflowing one with no measurement going out of date.
49874
50286
  }
49875
50287
 
49876
- // Which feature to move. One coordinate list can only describe one label, so
49877
- // this takes a single id rather than the ids= list that -style accepts.
49878
- function getTargetId(lyr, opts) {
49879
- var ids = opts.ids;
49880
- var id;
49881
- requirePointLayer(lyr);
49882
- if (!ids || ids.length === 0) {
49883
- stop$1('Missing required ids parameter (the feature id of the label to update)');
49884
- }
49885
- if (ids.length > 1) {
49886
- stop$1('Command expects a single feature id; received', ids.join(','));
49887
- }
49888
- id = ids[0];
49889
- if (id >= 0 === false || id >= getFeatureCount(lyr)) {
49890
- stop$1('Layer "' + layerName(lyr) + '" has no feature with id', id);
50288
+ function DatasetEditor(dataset) {
50289
+ var layers = [];
50290
+ var arcs = [];
50291
+
50292
+ this.done = function() {
50293
+ dataset.layers = layers;
50294
+ if (arcs.length) {
50295
+ noteDatasetWillChange(dataset, {operation: 'DatasetEditor.done', unit: 'arcs'});
50296
+ dataset.arcs = new ArcCollection(arcs);
50297
+ markDatasetChanged(dataset, {operation: 'DatasetEditor.done', unit: 'arcs'});
50298
+ withActiveUndoTransaction(null, function() {
50299
+ buildTopology(dataset);
50300
+ });
50301
+ }
50302
+ };
50303
+
50304
+ this.editLayer = function(lyr, cb) {
50305
+ var type = lyr.geometry_type;
50306
+ if (dataset.layers.indexOf(lyr) != layers.length) {
50307
+ error('Layer was edited out-of-order');
50308
+ }
50309
+ if (!type) {
50310
+ layers.push(lyr);
50311
+ return;
50312
+ }
50313
+ var shapes = lyr.shapes.map(function(shape, shpId) {
50314
+ var shape2 = [], retn, input;
50315
+ for (var i=0, n=shape ? shape.length : 0; i<n; i++) {
50316
+ input = type == 'point' ? shape[i] : idsToCoords(shape[i]);
50317
+ retn = cb(input, i, shape);
50318
+ if (!Array.isArray(retn)) continue;
50319
+ if (type == 'point') {
50320
+ shape2.push(retn);
50321
+ } else if (type == 'polygon' || type == 'polyline') {
50322
+ extendPathShape(shape2, retn || []);
50323
+ }
50324
+ }
50325
+ return shape2.length > 0 ? shape2 : null;
50326
+ });
50327
+ noteLayerWillChange(lyr, {operation: 'DatasetEditor.editLayer', unit: 'shapes'});
50328
+ layers.push(Object.assign(lyr, {shapes: shapes}));
50329
+ markLayerChanged(lyr, {operation: 'DatasetEditor.editLayer', unit: 'shapes'});
50330
+ };
50331
+
50332
+ function extendPathShape(shape, parts) {
50333
+ for (var i=0; i<parts.length; i++) {
50334
+ shape.push([arcs.length]);
50335
+ arcs.push(parts[i]);
50336
+ }
49891
50337
  }
49892
- if (!lyr.shapes || !lyr.shapes[id]) {
49893
- stop$1('Feature', id, 'in layer "' + layerName(lyr) + '" has no geometry');
50338
+
50339
+ function idsToCoords(ids) {
50340
+ var coords = [];
50341
+ var iter = dataset.arcs.getShapeIter(ids);
50342
+ while (iter.hasNext()) {
50343
+ coords.push([iter.x, iter.y]);
50344
+ }
50345
+ return coords;
49894
50346
  }
49895
- return id;
49896
50347
  }
49897
50348
 
49898
- function requirePointLayer(lyr) {
49899
- if (!lyr || getFeatureCount(lyr) === 0) {
49900
- stop$1('Missing a target layer containing labels');
50349
+ // Planar densification by an interval
50350
+ function densifyPathByInterval(coords, interval, interpolate) {
50351
+ if (findMaxPathInterval(coords) < interval) return coords;
50352
+ if (!interpolate) {
50353
+ interpolate = getIntervalInterpolator(interval);
49901
50354
  }
49902
- if (lyr.geometry_type != 'point') {
49903
- stop$1('Labels can only be updated in a point layer; target "' +
49904
- layerName(lyr) + '" contains ' + (lyr.geometry_type || 'no geometry'));
50355
+ var coords2 = [coords[0]], a, b;
50356
+ for (var i=1, n=coords.length; i<n; i++) {
50357
+ a = coords[i-1];
50358
+ b = coords[i];
50359
+ if (geom.distance2D(a[0], a[1], b[0], b[1]) > interval + 1e-4) {
50360
+ appendArr(coords2, interpolate(a, b));
50361
+ }
50362
+ coords2.push(b);
49905
50363
  }
50364
+ return coords2;
49906
50365
  }
49907
50366
 
49908
- function layerName(lyr) {
49909
- return lyr && lyr.name || '[unnamed]';
50367
+ function getIntervalInterpolator(interval) {
50368
+ return function(a, b) {
50369
+ var points = [];
50370
+ // var rev = a[0] == b[0] ? a[1] > b[1] : a[0] > b[0];
50371
+ var dist = geom.distance2D(a[0], a[1], b[0], b[1]);
50372
+ var n = Math.round(dist / interval) - 1;
50373
+ var dx = (b[0] - a[0]) / (n + 1),
50374
+ dy = (b[1] - a[1]) / (n + 1);
50375
+ for (var i=1; i<=n; i++) {
50376
+ points.push([a[0] + dx * i, a[1] + dy * i]);
50377
+ }
50378
+ return points;
50379
+ };
49910
50380
  }
49911
50381
 
49912
- cmd.addShape = addShape;
49913
50382
 
49914
- function addShape(targetLayers, targetDataset, opts) {
49915
- if (targetLayers.length > 1) {
49916
- stop$1('Command expects a single target layer');
50383
+ // Interpolate the same points regardless of segment direction
50384
+ function densifyAntimeridianSegment(a, b, interval) {
50385
+ var y1, y2;
50386
+ var coords = [];
50387
+ var ascending = a[1] < b[1];
50388
+ if (a[0] != b[0]) error('Expected an edge segment');
50389
+ if (ascending) {
50390
+ y1 = a[1];
50391
+ y2 = b[1];
50392
+ } else {
50393
+ y1 = b[1];
50394
+ y2 = a[1];
49917
50395
  }
49918
- var targetLyr = targetLayers[0]; // may be undefined
49919
- var targetType = !opts.no_replace && targetLyr && targetLyr.geometry_type || null;
49920
- var dataset = importGeoJSON(toFeature(opts, targetType));
49921
- var outputLyr = mergeDatasetsIntoDataset(targetDataset, [dataset])[0];
49922
- if (opts.no_replace || !targetLyr) {
49923
- // create new layer
49924
- setOutputLayerName(outputLyr, targetLyr && targetLyr.name, null, opts);
49925
- return [outputLyr];
50396
+ var y = Math.floor(y1 / interval) * interval + interval;
50397
+ while (y < y2) {
50398
+ coords.push([a[0], y]);
50399
+ y += interval;
49926
50400
  }
49927
- // merge into target layer
49928
- return cmd.mergeLayers([targetLyr, outputLyr], {force: true});
50401
+ if (!ascending) coords.reverse();
50402
+ return coords;
49929
50403
  }
49930
50404
 
49931
- function toFeature(opts, geomType) {
49932
- if (opts.geojson) {
49933
- return parseArg(opts.geojson);
49934
- }
49935
-
49936
- var geom = opts.coordinates && parseCoordsAsGeometry(opts.coordinates) || null;
50405
+ function appendArr(dest, src) {
50406
+ for (var i=0; i<src.length; i++) dest.push(src[i]);
50407
+ }
49937
50408
 
49938
- if (!geom) {
49939
- stop$1('Missing required shape coordinates');
50409
+ function findMaxPathInterval(coords) {
50410
+ var maxSq = 0, intSq, a, b;
50411
+ for (var i=1, n=coords.length; i<n; i++) {
50412
+ a = coords[i-1];
50413
+ b = coords[i];
50414
+ intSq = geom.distanceSq(a[0], a[1], b[0], b[1]);
50415
+ if (intSq > maxSq) maxSq = intSq;
49940
50416
  }
50417
+ return Math.sqrt(maxSq);
50418
+ }
49941
50419
 
49942
- if (geomType == 'point' && geom.type != 'Point') {
49943
- stop$1('Expected point coordinates, received', geom.type);
49944
- }
50420
+ function projectAndDensifyArcs(arcs, proj) {
50421
+ var interval = getDefaultDensifyInterval(arcs, proj);
50422
+ var minIntervalSq = interval * interval * 25;
50423
+ var p;
50424
+ return editArcs(arcs, onPoint);
49945
50425
 
49946
- if (geomType == 'polygon' && geom.type != 'Polygon') {
49947
- stop$1('Expected polygon coordinates, received', geom.type);
49948
- }
50426
+ function onPoint(append, lng, lat, prevLng, prevLat, i) {
50427
+ var pp = p;
50428
+ p = proj(lng, lat);
50429
+ if (!p) return false; // signal that current arc contains an error
49949
50430
 
49950
- if (geomType == 'polyline') {
49951
- if (geom.type == 'Polygon') {
49952
- geom.coordinates = geom.coordinates[0];
49953
- geom.type = 'LineString';
49954
- } else {
49955
- stop$1('Expected polyline coordinates, received', geom.type);
50431
+ // Don't try to densify shorter segments (optimization)
50432
+ if (i > 0 && geom.distanceSq(p[0], p[1], pp[0], pp[1]) > minIntervalSq) {
50433
+ densifySegment(prevLng, prevLat, pp[0], pp[1], lng, lat, p[0], p[1], proj, interval)
50434
+ .forEach(append);
49956
50435
  }
50436
+ append(p);
49957
50437
  }
49958
-
49959
- return {
49960
- type: 'Feature',
49961
- properties: parseProperties(opts.properties),
49962
- geometry: geom
49963
- };
49964
50438
  }
49965
50439
 
49966
- function parseArg(obj) {
49967
- return typeof obj == 'string' ? JSON.parse(obj) : obj;
50440
+ // Use the median of intervals computed by projecting segments.
50441
+ // We're probing a number of points, because @proj might only be valid in
50442
+ // a sub-region of the dataset bbox (e.g. +proj=tpers)
50443
+ function findDensifyInterval(bounds, xy, proj) {
50444
+ var steps = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9];
50445
+ var points = [];
50446
+ for (var i=0; i<steps.length; i++) {
50447
+ for (var j=0; j<steps.length; j++) {
50448
+ points.push([steps[i], steps[j]]);
50449
+ }
50450
+ }
50451
+ var intervals = points.map(function(pos) {
50452
+ var x = bounds.xmin + bounds.width() * pos[0];
50453
+ var y = bounds.ymin + bounds.height() * pos[1];
50454
+ var a = proj(x, y);
50455
+ var b = proj(x + xy[0], y + xy[1]);
50456
+ return a && b ? geom.distance2D(a[0], a[1], b[0], b[1]) : Infinity;
50457
+ }).filter(function(int) {return int < Infinity;});
50458
+ return intervals.length > 0 ? utils.findMedian(intervals) : Infinity;
49968
50459
  }
49969
50460
 
49970
- function parseProperties(arg) {
49971
- if (!arg) return null;
49972
- return parseArg(arg);
50461
+ // Kludgy way to get a useful interval for densifying a bounding box.
50462
+ // Uses a fraction of average bbox side length)
50463
+ // TODO: improve
50464
+ function findDensifyInterval2(bb, proj) {
50465
+ var a = proj(bb.centerX(), bb.centerY()),
50466
+ c = proj(bb.centerX(), bb.ymin), // right center
50467
+ d = proj(bb.xmax, bb.centerY()); // bottom center
50468
+ var interval = a && c && d ? (geom.distance2D(a[0], a[1], c[0], c[1]) +
50469
+ geom.distance2D(a[0], a[1], d[0], d[1])) / 5000 : Infinity;
50470
+ return interval;
49973
50471
  }
49974
50472
 
49975
- function isArrayOfNumbers(arr) {
49976
- return arr.length >= 2 && arr.every(utils.isNumber);
50473
+ // Returns an interval in projected units
50474
+ function getDefaultDensifyInterval(arcs, proj) {
50475
+ var xy = getAvgSegment2(arcs),
50476
+ bb = arcs.getBounds(),
50477
+ intervalA = findDensifyInterval(bb, xy, proj),
50478
+ intervalB = findDensifyInterval2(bb, proj),
50479
+ interval = Math.min(intervalA, intervalB);
50480
+ if (interval == Infinity) {
50481
+ error('Densification error');
50482
+ }
50483
+ return interval;
49977
50484
  }
49978
50485
 
49979
- function isClosedPath$1(arr) {
49980
- return isArrayOfPoints(arr) && arr.length > 3 && samePoint$2(arr[0], arr[arr.length - 1]);
50486
+ // Interpolate points into a projected line segment if needed to prevent large
50487
+ // deviations from path of original unprojected segment.
50488
+ // @points (optional) array of accumulated points
50489
+ function densifySegment(lng0, lat0, x0, y0, lng2, lat2, x2, y2, proj, interval, points) {
50490
+ // Find midpoint between two endpoints and project it (assumes longitude does
50491
+ // not wrap). TODO Consider bisecting along great circle path -- although this
50492
+ // would not be good for boundaries that follow line of constant latitude.
50493
+ var lng1 = (lng0 + lng2) / 2,
50494
+ lat1 = (lat0 + lat2) / 2,
50495
+ p = proj(lng1, lat1),
50496
+ distSq;
50497
+ if (!p) return; // TODO: consider if this is adequate for handling proj. errors
50498
+ distSq = geom.pointSegDistSq2(p[0], p[1], x0, y0, x2, y2); // sq displacement
50499
+ points = points || [];
50500
+ // Bisect current segment if the projected midpoint deviates from original
50501
+ // segment by more than the @interval parameter.
50502
+ // ... but don't bisect very small segments to prevent infinite recursion
50503
+ // (e.g. if projection function is discontinuous)
50504
+ if (distSq > interval * interval * 0.25 && geom.distance2D(lng0, lat0, lng2, lat2) > 0.01) {
50505
+ densifySegment(lng0, lat0, x0, y0, lng1, lat1, p[0], p[1], proj, interval, points);
50506
+ points.push(p);
50507
+ densifySegment(lng1, lat1, p[0], p[1], lng2, lat2, x2, y2, proj, interval, points);
50508
+ }
50509
+ return points;
49981
50510
  }
49982
50511
 
49983
- function samePoint$2(a, b) {
49984
- return a[0] == b[0] && a[1] == b[1];
49985
- }
50512
+ // Create rectangles around each feature in a layer
50513
+ cmd.rectangles = function(targetLyr, targetDataset, opts) {
50514
+ var crsInfo = getDatasetCrsInfo(targetDataset);
50515
+ var records = targetLyr.data ? targetLyr.data.getRecords() : null;
50516
+ var geometries;
50517
+
50518
+ if (opts.bbox) {
50519
+ geometries = bboxExpressionToGeometries(opts.bbox, targetLyr, targetDataset);
50520
+
50521
+ } else {
50522
+ if (!layerHasGeometry$1(targetLyr)) {
50523
+ stop$1("Layer is missing geometric shapes");
50524
+ }
50525
+ geometries = shapesToBoxGeometries(targetLyr, targetDataset, opts);
50526
+ }
50527
+
50528
+ var geojson = {
50529
+ type: 'FeatureCollection',
50530
+ features: geometries.map(function(geom, i) {
50531
+ var rec = records && records[i] || null;
50532
+ if (rec && opts.no_replace) {
50533
+ rec = utils.extend({}, rec); // make a copy
50534
+ }
50535
+ return {
50536
+ type: 'Feature',
50537
+ properties: rec,
50538
+ geometry: geom
50539
+ };
50540
+ })
50541
+ };
50542
+ var dataset = importGeoJSON(geojson, {});
50543
+ setDatasetCrsInfo(dataset, crsInfo);
50544
+ var outputLayers = mergeDatasetsIntoDataset(targetDataset, [dataset]);
50545
+ setOutputLayerName(outputLayers[0], targetLyr, null, opts);
50546
+ return outputLayers;
50547
+ };
50548
+
50549
+
50550
+
50551
+
50552
+ function shapesToBoxGeometries(lyr, dataset, opts) {
50553
+ var crsInfo = getDatasetCrsInfo(dataset);
50554
+ return lyr.shapes.map(function(shp) {
50555
+ var bounds = lyr.geometry_type == 'point' ?
50556
+ getPointFeatureBounds(shp) : dataset.arcs.getMultiShapeBounds(shp);
50557
+ bounds = applyRectangleOptions(bounds, crsInfo.crs, opts);
50558
+ if (!bounds) return null;
50559
+ return bboxToPolygon(bounds.toArray(), opts);
50560
+ });
50561
+ }
50562
+
50563
+ function bboxExpressionToGeometries(exp, lyr, dataset, opts) {
50564
+ var compiled = compileFeatureExpression(exp, lyr, dataset.arcs, {});
50565
+ var n = getFeatureCount(lyr);
50566
+ var result;
50567
+ var geometries = [];
50568
+ for (var i=0; i<n; i++) {
50569
+ result = compiled(i);
50570
+ if (!looksLikeBbox(result)) {
50571
+ stop$1('Invalid bbox value (expected a GeoJSON-type bbox):', result);
50572
+ }
50573
+ geometries.push(bboxToPolygon(result));
50574
+ }
50575
+ return geometries;
50576
+ }
50577
+
50578
+ function looksLikeBbox(o) {
50579
+ if (!o || o.length != 4) return false;
50580
+ if (o.some(isNaN)) return false;
50581
+ if (o[0] <= o[2] == false || o[1] <= o[3] == false) return false;
50582
+ return true;
50583
+ }
50584
+
50585
+ // Create rectangles around one or more target layers
50586
+ //
50587
+ cmd.rectangle2 = function(target, opts) {
50588
+ // if target layer is a rectangle and we're applying frame properties,
50589
+ // turn the target into a frame instead of creating a new rectangle
50590
+ if (target.layers.length == 1 && opts.width &&
50591
+ layerIsRectangle(target.layers[0], target.dataset.arcs)) {
50592
+ applyFrameProperties(target.layers[0], opts);
50593
+ return;
50594
+ }
50595
+ var datasets = target.layers.map(function(lyr) {
50596
+ var dataset = cmd.rectangle({layer: lyr, dataset: target.dataset}, opts);
50597
+ setOutputLayerName(dataset.layers[0], lyr, null, opts);
50598
+ if (!opts.no_replace) {
50599
+ dataset.layers[0].name = lyr.name || dataset.layers[0].name;
50600
+ }
50601
+ return dataset;
50602
+ });
50603
+ return mergeDatasetsIntoDataset(target.dataset, datasets);
50604
+ };
50605
+
50606
+ cmd.rectangle = function(target, opts) {
50607
+ var bounds, crsInfo;
50608
+ if (opts.bbox) {
50609
+ bounds = new Bounds(opts.bbox);
50610
+ crsInfo = target && getDatasetCrsInfo(target.dataset) ||
50611
+ probablyDecimalDegreeBounds(bounds) && getCrsInfo('wgs84') || {};
50612
+ } else if (target) {
50613
+ bounds = getLayerBounds(target.layer, target.dataset.arcs);
50614
+ crsInfo = getDatasetCrsInfo(target.dataset);
50615
+ }
50616
+ bounds = bounds && applyRectangleOptions(bounds, crsInfo.crs, opts);
50617
+ if (!bounds || !bounds.hasBounds()) {
50618
+ stop$1('Missing rectangle extent');
50619
+ }
50620
+ var feature = {
50621
+ type: 'Feature',
50622
+ properties: {},
50623
+ geometry: bboxToPolygon(bounds.toArray(), opts)
50624
+ };
50625
+ var dataset = importGeoJSON(feature, {});
50626
+ applyFrameProperties(dataset.layers[0], opts);
50627
+ dataset.layers[0].name = opts.name || 'rectangle';
50628
+ setDatasetCrsInfo(dataset, crsInfo);
50629
+ return dataset;
50630
+ };
50631
+
50632
+ function applyFrameProperties(lyr, opts) {
50633
+ if (!opts.width) return;
50634
+ if (!lyr.data) initDataTable(lyr);
50635
+ var d = lyr.data.getRecords()[0] || {};
50636
+ var size = parseFrameSize(opts.width);
50637
+ d.width = size.valuePx;
50638
+ d.type = 'frame';
50639
+ d.frame_units = size.units;
50640
+ }
50641
+
50642
+ function applyRectangleOptions(bounds, crs, opts) {
50643
+ var isGeoBox = probablyDecimalDegreeBounds(bounds);
50644
+ if (opts.offset) {
50645
+ bounds = applyBoundsOffset(opts.offset, bounds, crs);
50646
+ }
50647
+ if (bounds.area() > 0 === false) return null;
50648
+ if (opts.aspect_ratio) {
50649
+ bounds = applyAspectRatio(opts.aspect_ratio, bounds);
50650
+ }
50651
+ if (isGeoBox) {
50652
+ bounds = clampToWorldBounds(bounds);
50653
+ }
50654
+ return bounds;
50655
+ }
50656
+
50657
+ // opt: aspect ratio as a single number or a range (e.g. "1,2");
50658
+ function applyAspectRatio(opt, bounds) {
50659
+ var range = String(opt).split(',').map(parseFloat),
50660
+ aspectRatio = bounds.width() / bounds.height(),
50661
+ min, max; // min is height limit, max is width limit
50662
+ if (range.length == 1) {
50663
+ range.push(range[0]);
50664
+ } else if (range[0] > range[1]) {
50665
+ range.reverse();
50666
+ }
50667
+ min = range[0];
50668
+ max = range[1];
50669
+ if (!min && !max) return bounds;
50670
+ if (!min) min = -Infinity;
50671
+ if (!max) max = Infinity;
50672
+ if (aspectRatio < min) {
50673
+ bounds.fillOut(min);
50674
+ } else if (aspectRatio > max) {
50675
+ bounds.fillOut(max);
50676
+ }
50677
+ return bounds;
50678
+ }
50679
+
50680
+ function applyBoundsOffset(offsetOpt, bounds, crs) {
50681
+ var offsets = convertFourSides(offsetOpt, crs, bounds);
50682
+ bounds.padBounds(offsets[0], offsets[1], offsets[2], offsets[3]);
50683
+ return bounds;
50684
+ }
50685
+
50686
+ function bboxToPolygon(bbox, optsArg) {
50687
+ var opts = optsArg || {};
50688
+ var coords = bboxToCoords(bbox);
50689
+ if (opts.interval > 0) {
50690
+ coords = densifyPathByInterval(coords, opts.interval);
50691
+ }
50692
+ return {
50693
+ type: 'Polygon',
50694
+ coordinates: [coords]
50695
+ };
50696
+ }
50697
+
50698
+ var Rectangle = /*#__PURE__*/Object.freeze({
50699
+ __proto__: null,
50700
+ applyAspectRatio: applyAspectRatio,
50701
+ bboxToPolygon: bboxToPolygon
50702
+ });
50703
+
50704
+ cmd.frame = function(catalog, targets, opts) {
50705
+ var widthPx, heightPx, aspectRatio, bbox;
50706
+ var existingFrame = getActiveFrame(catalog);
50707
+ if (opts.width) {
50708
+ widthPx = parseFrameSize(opts.width).valuePx;
50709
+ if (widthPx > 0 === false) {
50710
+ stop$1('Invalid width parameter:', opts.width);
50711
+ }
50712
+ }
50713
+ if (opts.height) {
50714
+ heightPx = parseFrameSize(opts.height).valuePx;
50715
+ if (heightPx > 0 === false) {
50716
+ stop$1('Invalid height parameter:', opts.height);
50717
+ }
50718
+ }
50719
+ if (!widthPx && !heightPx) {
50720
+ widthPx = 800;
50721
+ message('Using default 800px frame width');
50722
+ }
50723
+
50724
+ if (opts.aspect_ratio) {
50725
+ if (opts.aspect_ratio > 0 === false) {
50726
+ stop$1('Invalid aspect-ratio parameter:', opts.aspect_ratio);
50727
+ }
50728
+ if (!heightPx) {
50729
+ heightPx = roundToDigits(widthPx / opts.aspect_ratio, 1);
50730
+ } else if (!widthPx) {
50731
+ widthPx = roundToDigits(heightPx * opts.aspect_ratio, 1);
50732
+ }
50733
+ }
50734
+
50735
+ if (opts.bbox) {
50736
+ bbox = opts.bbox;
50737
+ // TODO: validate
50738
+ } else {
50739
+ var datasets = utils.pluck(targets, 'dataset');
50740
+ requireDatasetsHaveCompatibleCRS(datasets, 'Targets include both projected and unprojected coordinates');
50741
+ bbox = getTargetBbox(targets);
50742
+ if (!bbox) {
50743
+ stop$1('Command target is missing geographical bounds');
50744
+ }
50745
+ }
50746
+
50747
+ applyPercentageOffsets(bbox, opts.offset || opts.offsets);
50748
+ applyPixelOffsets(bbox, widthPx, heightPx, opts.offset || opts.offsets);
50749
+
50750
+ if (bbox[3] - bbox[1] > 0 === false || bbox[2] - bbox[0] > 0 === false) {
50751
+ stop$1('Frame has a collapsed bbox');
50752
+ }
50753
+
50754
+ aspectRatio = (bbox[2] - bbox[0]) / (bbox[3] - bbox[1]);
50755
+ if (!widthPx) {
50756
+ widthPx = roundToDigits(heightPx * aspectRatio, 1);
50757
+ } else if (!heightPx) {
50758
+ heightPx = roundToDigits(widthPx / aspectRatio, 1);
50759
+ }
50760
+
50761
+ var feature = {
50762
+ type: 'Feature',
50763
+ properties: getFrameProperties(widthPx, heightPx, opts),
50764
+ geometry: bboxToPolygon(bbox)
50765
+ };
50766
+ var frameDataset = importGeoJSON(feature);
50767
+ // set CRS from target dataset
50768
+ // TODO: handle case: targets have different projections
50769
+ // TODO: handle case: first target is missing CRS
50770
+ if (targets.length > 0) {
50771
+ var crsInfo = getDatasetCrsInfo(targets[0].dataset);
50772
+ setDatasetCrsInfo(frameDataset, crsInfo);
50773
+ }
50774
+ frameDataset.layers[0].name = opts.name || 'frame';
50775
+ if (existingFrame) {
50776
+ if (!opts.replace) {
50777
+ stop$1('A map frame already exists:', existingFrame.layer.name || '[unnamed frame]');
50778
+ }
50779
+ demoteFrameLayer(existingFrame.layer);
50780
+ }
50781
+ catalog.addDataset(frameDataset);
50782
+ };
50783
+
50784
+ function getFrameProperties(width, height, opts) {
50785
+ var properties = {
50786
+ type: 'frame',
50787
+ width: width,
50788
+ height: height,
50789
+ frame_units: opts.width || opts.height ?
50790
+ parseFrameSize(opts.width || opts.height).units : 'px'
50791
+ };
50792
+ if (opts.aspect_ratio > 0 || opts.width && opts.height) {
50793
+ properties.frame_aspect_ratio = width / height;
50794
+ }
50795
+ return properties;
50796
+ }
50797
+
50798
+ function fillOutBbox(bbox, widthPx, heightPx) {
50799
+ var hpad = 0, vpad = 0;
50800
+ var w = bbox[2] - bbox[0];
50801
+ var h = bbox[3] - bbox[1];
50802
+ if (widthPx / heightPx > w / h) { // need to add horizontal padding
50803
+ hpad = h * widthPx / heightPx - w;
50804
+ } else {
50805
+ vpad = w * heightPx / widthPx - h;
50806
+ }
50807
+ bbox[0] -= hpad / 2;
50808
+ bbox[1] -= vpad / 2;
50809
+ bbox[2] += hpad / 2;
50810
+ bbox[3] += vpad / 2;
50811
+ }
50812
+
50813
+ function applyPercentageOffsets(bbox, arg) {
50814
+ var sides = getPctOffsets(arg);
50815
+ var l = sides[0],
50816
+ b = sides[1],
50817
+ r = sides[2],
50818
+ t = sides[3],
50819
+ w2 = (bbox[2] - bbox[0]) / (1 - l - r),
50820
+ h2 = (bbox[3] - bbox[1]) / (1 - t - b);
50821
+ bbox[0] -= l * w2;
50822
+ bbox[1] -= b * h2;
50823
+ bbox[2] += r * w2;
50824
+ bbox[3] += t * h2;
50825
+ }
50826
+
50827
+ function applyPixelOffsets(bbox, widthPx, heightPx, arg) {
50828
+ var sides = getPixelOffsets(arg);
50829
+ var l = sides[0],
50830
+ b = sides[1],
50831
+ r = sides[2],
50832
+ t = sides[3],
50833
+ scale, w;
50834
+
50835
+ if (widthPx && heightPx) {
50836
+ // add padding to bbox to match pixel dimensions, if needed
50837
+ fillOutBbox(bbox, widthPx, heightPx);
50838
+ }
50839
+
50840
+ w = bbox[2] - bbox[0];
50841
+ bbox[3] - bbox[1];
50842
+
50843
+ if (widthPx) {
50844
+ scale = w / (widthPx - l - r);
50845
+ } else {
50846
+ scale = w / (heightPx - t - b);
50847
+ }
50848
+
50849
+ bbox[0] -= scale * l;
50850
+ bbox[1] -= scale * b;
50851
+ bbox[2] += scale * r;
50852
+ bbox[3] += scale * t;
50853
+ return scale;
50854
+ }
50855
+
50856
+ function getPctOffsets(arg) {
50857
+ return adjustOffsetsArg(arg).map(str => {
50858
+ return str.includes('%') ? parsePercent(str) : 0;
50859
+ });
50860
+ }
50861
+
50862
+ function getPixelOffsets(arg) {
50863
+ return adjustOffsetsArg(arg).map(str => {
50864
+ return str.includes('%') ? 0 : parseSizeParam(str);
50865
+ });
50866
+ }
50867
+
50868
+ function adjustOffsetsArg(arg) {
50869
+ if (!arg) arg = ['0'];
50870
+ if (arg.length == 1) {
50871
+ return [arg[0], arg[0], arg[0], arg[0]];
50872
+ }
50873
+ if (arg.length != 4) {
50874
+ stop$1('List of offsets should have 4 values');
50875
+ }
50876
+ return arg;
50877
+ }
50878
+
50879
+ function getTargetBbox(targets) {
50880
+ var expanded = expandCommandTargets(targets);
50881
+ var bounds = expanded.reduce(function(memo, o) {
50882
+ return memo.mergeBounds(getLayerBounds(o.layer, o.dataset.arcs));
50883
+ }, new Bounds());
50884
+ return bounds.hasBounds() ? bounds.toArray() : null;
50885
+ }
50886
+
50887
+ // Convert width and height args to aspect ratio arg for the rectangle() function
50888
+ function getAspectRatioArg(widthArg, heightArg) {
50889
+ // heightArg is a string containing either a number or a
50890
+ // comma-sep. pair of numbers (range);
50891
+ return heightArg.split(',').map(function(opt) {
50892
+ var height = Number(opt),
50893
+ width = Number(widthArg);
50894
+ if (!opt) return '';
50895
+ return width / height;
50896
+ }).reverse().join(',');
50897
+ }
50898
+
50899
+ var Frame = /*#__PURE__*/Object.freeze({
50900
+ __proto__: null,
50901
+ applyPercentageOffsets: applyPercentageOffsets,
50902
+ applyPixelOffsets: applyPixelOffsets,
50903
+ fillOutBbox: fillOutBbox,
50904
+ getAspectRatioArg: getAspectRatioArg
50905
+ });
50906
+
50907
+ // A frame is a viewport, not data, so it can legitimately extend past the
50908
+ // edges of the globe: fitting a near-global extent to a fixed aspect ratio pads
50909
+ // it with whitespace, and that padding can reach beyond a pole. Those
50910
+ // coordinates have no projected equivalent, and projectAndDensifyArcs() drops a
50911
+ // whole arc when any of its vertices fails -- which took out three sides of the
50912
+ // rectangle and left a zero-area bbox. Clamping first keeps the frame
50913
+ // projectable; the off-globe padding is what gets lost.
50914
+ function clampLatLngBbox(bbox) {
50915
+ return [
50916
+ utils.clamp(bbox[0], -180, 180),
50917
+ utils.clamp(bbox[1], -90, 90),
50918
+ utils.clamp(bbox[2], -180, 180),
50919
+ utils.clamp(bbox[3], -90, 90)
50920
+ ];
50921
+ }
50922
+
50923
+ // Project a rectangular frame as a sampled boundary, rather than projecting
50924
+ // only its corners. Curved projections can reach their x/y extrema between
50925
+ // corners, so a corner-only bbox can crop content that was inside the frame.
50926
+ function getProjectedFrameBounds(bbox, project, srcIsLatLng) {
50927
+ var b = new Bounds(srcIsLatLng ? clampLatLngBbox(bbox) : bbox);
50928
+ var xmin = b.xmin;
50929
+ var ymin = b.ymin;
50930
+ var xmax = b.xmax;
50931
+ var ymax = b.ymax;
50932
+ var arcs = new ArcCollection([
50933
+ [[xmin, ymin], [xmax, ymin]],
50934
+ [[xmax, ymin], [xmax, ymax]],
50935
+ [[xmax, ymax], [xmin, ymax]],
50936
+ [[xmin, ymax], [xmin, ymin]]
50937
+ ]);
50938
+ projectAndDensifyArcs(arcs, project);
50939
+ var projected = arcs.getBounds();
50940
+ if (!projected.hasBounds() || projected.area() > 0 === false) {
50941
+ error('Unable to project map frame');
50942
+ }
50943
+ return projected;
50944
+ }
50945
+
50946
+ // Snapshot frame bounds before the dataset arcs are projected.
50947
+ function getFrameProjectionSnapshots(dataset) {
50948
+ return dataset.layers.reduce(function(memo, lyr) {
50949
+ if (isFrameLayer(lyr, dataset.arcs)) {
50950
+ memo.push({
50951
+ layer: lyr,
50952
+ bbox: getLayerBounds(lyr, dataset.arcs).toArray()
50953
+ });
50954
+ }
50955
+ return memo;
50956
+ }, []);
50957
+ }
50958
+
50959
+ // Rebuild a projected frame through the normal topology-aware replacement
50960
+ // path. This is safe when the frame shares an ArcCollection with other layers.
50961
+ function rebuildProjectedFrameLayer(snapshot, dataset, project, srcIsLatLng) {
50962
+ var bounds = getProjectedFrameBounds(snapshot.bbox, project, srcIsLatLng);
50963
+ var rec = getSingleFrameRecord(snapshot.layer) || {};
50964
+ var fixedAspect = utils.isFiniteNumber(rec.frame_aspect_ratio) &&
50965
+ rec.frame_aspect_ratio > 0 ? rec.frame_aspect_ratio : null;
50966
+ var props;
50967
+ if (fixedAspect) {
50968
+ // Projecting a rectangle's boundary and taking its bounds does not preserve
50969
+ // the rectangle's shape, so a frame that declares a ratio has to be padded
50970
+ // back out to it -- otherwise the geometry and the page disagree.
50971
+ var bbox = bounds.toArray();
50972
+ fillOutBbox(bbox, fixedAspect, 1);
50973
+ bounds = new Bounds(bbox);
50974
+ } else {
50975
+ // Height is derived from the extent, so it goes stale when the extent is
50976
+ // reprojected. getFrameLayerData() recomputes it, but the record is what
50977
+ // gets written to an output file.
50978
+ props = {height: Math.round(rec.width * bounds.height() / bounds.width())};
50979
+ }
50980
+ rebuildFrameLayerGeometry(snapshot.layer, dataset, bounds, props);
50981
+ }
50982
+
50983
+ function rebuildFrameLayerGeometry(lyr, dataset, bounds, props) {
50984
+ bounds = bounds instanceof Bounds ? bounds : new Bounds(bounds);
50985
+ var frameDataset = createFrameRectangleDataset(lyr, bounds, props);
50986
+ // The replacement rectangle is imported from bare GeoJSON, so it carries no
50987
+ // CRS. Merging it into the host dataset runs a projected/unprojected
50988
+ // compatibility check, and during -proj the host still reports its source CRS
50989
+ // while the new rectangle already holds projected coordinates -- so the
50990
+ // check has to compare like with like.
50991
+ setDatasetCrsInfo(frameDataset, getDatasetCrsInfo(dataset));
50992
+ replaceLayerContents(lyr, dataset, frameDataset);
50993
+ }
50994
+
50995
+ // Return an isolated rectangular display layer. Dynamic display projection
50996
+ // must not mutate source shapes or the ArcCollection shared by other layers.
50997
+ function getProjectedFrameDisplayLayer(lyr, arcs, project, srcIsLatLng) {
50998
+ if (!isFrameLayer(lyr, arcs)) return null;
50999
+ var bbox = getLayerBounds(lyr, arcs).toArray();
51000
+ var bounds = getProjectedFrameBounds(bbox, project, srcIsLatLng);
51001
+ var dataset = createFrameRectangleDataset(lyr, bounds);
51002
+ return {
51003
+ layer: dataset.layers[0],
51004
+ arcs: dataset.arcs
51005
+ };
51006
+ }
51007
+
51008
+ function createFrameRectangleDataset(lyr, bounds, props) {
51009
+ var record = lyr.data && lyr.data.getReadOnlyRecordAt(0);
51010
+ var feature = {
51011
+ type: 'Feature',
51012
+ properties: Object.assign({}, record || {}, props || {}),
51013
+ geometry: bboxToPolygon(bounds.toArray())
51014
+ };
51015
+ var dataset = importGeoJSON(feature);
51016
+ dataset.layers[0].name = lyr.name;
51017
+ return dataset;
51018
+ }
51019
+
51020
+ var FrameProjection = /*#__PURE__*/Object.freeze({
51021
+ __proto__: null,
51022
+ getFrameProjectionSnapshots: getFrameProjectionSnapshots,
51023
+ getProjectedFrameBounds: getProjectedFrameBounds,
51024
+ getProjectedFrameDisplayLayer: getProjectedFrameDisplayLayer,
51025
+ rebuildFrameLayerGeometry: rebuildFrameLayerGeometry,
51026
+ rebuildProjectedFrameLayer: rebuildProjectedFrameLayer
51027
+ });
51028
+
51029
+ var OPERATION$1 = 'update-frame';
51030
+
51031
+ function updateFrame(targetLayers, dataset, opts) {
51032
+ if (!targetLayers || targetLayers.length != 1) {
51033
+ stop$1('-update-frame expects a single target layer');
51034
+ }
51035
+ var lyr = targetLayers[0];
51036
+ if (!isFrameLayer(lyr, dataset.arcs)) {
51037
+ stop$1('Target layer is not a map frame:', lyr.name || '[unnamed layer]');
51038
+ }
51039
+ if (opts.remove) {
51040
+ if (hasUpdateOptions(opts)) {
51041
+ stop$1('The remove option cannot be combined with frame update options');
51042
+ }
51043
+ demoteFrameLayer(lyr, OPERATION$1);
51044
+ return;
51045
+ }
51046
+ if (!hasUpdateOptions(opts)) {
51047
+ stop$1('Missing frame update option(s)');
51048
+ }
51049
+ if (opts.aspect_ratio !== undefined && opts.auto_aspect) {
51050
+ stop$1('aspect-ratio= and auto-aspect are mutually exclusive');
51051
+ }
51052
+ if (opts.fix_scale && (opts.width !== undefined || opts.height !== undefined)) {
51053
+ stop$1('fix-scale cannot be combined with width= or height=');
51054
+ }
51055
+
51056
+ var rec = lyr.data.getRecords()[0];
51057
+ var frame = getFrameLayerData(lyr, dataset.arcs);
51058
+ var bbox = (opts.bbox || frame.bbox).slice();
51059
+ var offsetArg = opts.offset || opts.offsets;
51060
+ var fixedAspect = getFixedAspect(rec);
51061
+ var width = frame.width;
51062
+ var units = rec.frame_units || 'px';
51063
+ var widthSize, heightSize, height, effectiveAspect;
51064
+
51065
+ // 1. Aspect mode, resolved first because whether the page shape is fixed
51066
+ // decides whether it may constrain the extent while offsets are applied.
51067
+ if (opts.auto_aspect) {
51068
+ fixedAspect = null;
51069
+ } else if (opts.aspect_ratio !== undefined) {
51070
+ if (!utils.isFiniteNumber(opts.aspect_ratio) || opts.aspect_ratio <= 0) {
51071
+ stop$1('Invalid aspect-ratio parameter:', opts.aspect_ratio);
51072
+ }
51073
+ fixedAspect = opts.aspect_ratio;
51074
+ }
51075
+
51076
+ // 2. Extent
51077
+ if (offsetArg) {
51078
+ applyPercentageOffsets(bbox, offsetArg);
51079
+ // Pass a page height only when the shape is fixed. A derived height
51080
+ // follows the extent, so letting it pad the bbox here would hold a
51081
+ // re-fitted frame to its old shape instead of its new bounds.
51082
+ applyPixelOffsets(bbox, frame.width,
51083
+ fixedAspect ? frame.width / fixedAspect : null, offsetArg);
51084
+ }
51085
+ requireValidBbox(bbox);
51086
+ if (fixedAspect) {
51087
+ fillOutBbox(bbox, fixedAspect, 1);
51088
+ }
51089
+
51090
+ // 3. Nominal size
51091
+ if (opts.width !== undefined) {
51092
+ widthSize = parseFrameSize(opts.width);
51093
+ requirePositiveSize(widthSize.valuePx, 'width', opts.width);
51094
+ }
51095
+ if (opts.height !== undefined) {
51096
+ heightSize = parseFrameSize(opts.height);
51097
+ requirePositiveSize(heightSize.valuePx, 'height', opts.height);
51098
+ }
51099
+ if (widthSize && heightSize) {
51100
+ effectiveAspect = widthSize.valuePx / heightSize.valuePx;
51101
+ if (opts.aspect_ratio !== undefined &&
51102
+ ratiosDiffer(effectiveAspect, opts.aspect_ratio)) {
51103
+ stop$1('Contradictory width, height and aspect-ratio values');
51104
+ }
51105
+ width = widthSize.valuePx;
51106
+ units = widthSize.units;
51107
+ fixedAspect = effectiveAspect;
51108
+ fillOutBbox(bbox, fixedAspect, 1);
51109
+ } else if (widthSize) {
51110
+ width = widthSize.valuePx;
51111
+ units = widthSize.units;
51112
+ } else if (heightSize) {
51113
+ effectiveAspect = fixedAspect || getBboxAspect(bbox);
51114
+ width = heightSize.valuePx * effectiveAspect;
51115
+ units = heightSize.units;
51116
+ } else if (opts.fix_scale) {
51117
+ // Hold ground units per output pixel, so the page grows and shrinks with
51118
+ // the extent instead of the scale changing to fit the extent on it.
51119
+ width = frame.width * getBboxWidth(bbox) / getBboxWidth(frame.bbox);
51120
+ requirePositiveSize(width, 'fix-scale', width);
51121
+ }
51122
+
51123
+ requireValidBbox(bbox);
51124
+ effectiveAspect = fixedAspect || getBboxAspect(bbox);
51125
+ height = Math.round(width / effectiveAspect);
51126
+
51127
+ noteLayerWillChange(lyr, {operation: OPERATION$1, unit: 'shapes'});
51128
+ lyr.data.captureTableBefore({operation: OPERATION$1});
51129
+ rec.width = width;
51130
+ rec.height = height;
51131
+ rec.frame_units = units;
51132
+ if (fixedAspect) {
51133
+ rec.frame_aspect_ratio = fixedAspect;
51134
+ } else {
51135
+ delete rec.frame_aspect_ratio;
51136
+ }
51137
+ lyr.data.markChanged({operation: OPERATION$1});
51138
+ rebuildFrameLayerGeometry(lyr, dataset, new Bounds(bbox));
51139
+ markLayerChanged(lyr, {operation: OPERATION$1, unit: 'shapes'});
51140
+ }
51141
+
51142
+ function hasUpdateOptions(opts) {
51143
+ return opts.bbox !== undefined ||
51144
+ opts.width !== undefined ||
51145
+ opts.height !== undefined ||
51146
+ opts.aspect_ratio !== undefined ||
51147
+ opts.auto_aspect ||
51148
+ opts.fix_scale ||
51149
+ opts.offset !== undefined ||
51150
+ opts.offsets !== undefined;
51151
+ }
51152
+
51153
+ function getBboxWidth(bbox) {
51154
+ return bbox[2] - bbox[0];
51155
+ }
51156
+
51157
+ function getFixedAspect(rec) {
51158
+ return utils.isFiniteNumber(rec.frame_aspect_ratio) &&
51159
+ rec.frame_aspect_ratio > 0 ? rec.frame_aspect_ratio : null;
51160
+ }
51161
+
51162
+ function getBboxAspect(bbox) {
51163
+ return (bbox[2] - bbox[0]) / (bbox[3] - bbox[1]);
51164
+ }
51165
+
51166
+ function requireValidBbox(bbox) {
51167
+ if (!bbox.every(utils.isFiniteNumber) ||
51168
+ bbox[2] - bbox[0] <= 0 ||
51169
+ bbox[3] - bbox[1] <= 0) {
51170
+ stop$1('Frame has a collapsed bbox');
51171
+ }
51172
+ }
51173
+
51174
+ function requirePositiveSize(value, name, arg) {
51175
+ if (!utils.isFiniteNumber(value) || value <= 0) {
51176
+ stop$1('Invalid ' + name + ' parameter:', arg);
51177
+ }
51178
+ }
51179
+
51180
+ function ratiosDiffer(a, b) {
51181
+ return Math.abs(a - b) > 1e-10 * Math.max(1, Math.abs(a), Math.abs(b));
51182
+ }
51183
+
51184
+ cmd.updateFrame = updateFrame;
51185
+
51186
+ var OPERATION = 'update-label';
51187
+
51188
+ cmd.updateLabel = updateLabel;
51189
+
51190
+ // Rewrites the knots of a label that already exists.
51191
+ //
51192
+ // -add-label creates and -style sets properties, so neither can move a label:
51193
+ // a label's knots are its geometry, not an attribute. This is the command the
51194
+ // GUI emits when a knot or an anchor is dragged, so that an interactive move is
51195
+ // an ordinary command with undo and session history rather than a direct
51196
+ // mutation with hand-rolled undo.
51197
+ //
51198
+ // See docs/development/label-tool-design.md.
51199
+ function updateLabel(targetLayers, dataset, opts) {
51200
+ var lyr, id, coords, rec;
51201
+ if (targetLayers.length > 1) {
51202
+ // One coordinate list describes one label, so there is no sensible reading
51203
+ // of this across layers.
51204
+ stop$1('Command expects a single target layer');
51205
+ }
51206
+ lyr = targetLayers[0];
51207
+ id = getTargetId(lyr, opts);
51208
+ coords = parseLabelCoords(opts.coordinates);
51209
+ rec = getLayerDataTable(lyr).getRecords()[id];
51210
+ if (!featureIsLabel(rec)) {
51211
+ // Moving an arbitrary point through a command named -update-label would be
51212
+ // a surprising way to succeed, and in the GUI it would mean the hit test
51213
+ // handed over the wrong feature.
51214
+ stop$1('Feature', id, 'in layer "' + layerName(lyr) + '" is not a label ' +
51215
+ '(it has no label-text property)');
51216
+ }
51217
+ warnIfCurveIsUnprojected(dataset, coords.length);
51218
+ // Undo works from what a command declares it is about to change, so an edit
51219
+ // that does not say so is an edit that cannot be undone -- the shapes array
51220
+ // keeps its identity here, leaving nothing for the transaction to notice
51221
+ // afterwards.
51222
+ noteLayerWillChange(lyr, {operation: OPERATION, unit: 'shapes'});
51223
+ // Point shapes hold their coordinates directly, so this is the whole edit.
51224
+ // The pairs are copied rather than aliased: the GUI drags a live copy of the
51225
+ // knots and would otherwise keep a handle on the layer's own arrays.
51226
+ lyr.shapes[id] = coords.map(function(p) { return [p[0], p[1]]; });
51227
+ markLayerChanged(lyr, {operation: OPERATION, unit: 'shapes'});
51228
+ // Nothing about the label's text is touched, and nothing needs to be: a text
51229
+ // measurement describes the *text*, and moving a knot changes the length of
51230
+ // the *path*. The fit check compares the two at render time, against the
51231
+ // curve as it then stands, so a move can turn a fitting label into an
51232
+ // overflowing one with no measurement going out of date.
51233
+ }
51234
+
51235
+ // Which feature to move. One coordinate list can only describe one label, so
51236
+ // this takes a single id rather than the ids= list that -style accepts.
51237
+ function getTargetId(lyr, opts) {
51238
+ var ids = opts.ids;
51239
+ var id;
51240
+ requirePointLayer(lyr);
51241
+ if (!ids || ids.length === 0) {
51242
+ stop$1('Missing required ids parameter (the feature id of the label to update)');
51243
+ }
51244
+ if (ids.length > 1) {
51245
+ stop$1('Command expects a single feature id; received', ids.join(','));
51246
+ }
51247
+ id = ids[0];
51248
+ if (id >= 0 === false || id >= getFeatureCount(lyr)) {
51249
+ stop$1('Layer "' + layerName(lyr) + '" has no feature with id', id);
51250
+ }
51251
+ if (!lyr.shapes || !lyr.shapes[id]) {
51252
+ stop$1('Feature', id, 'in layer "' + layerName(lyr) + '" has no geometry');
51253
+ }
51254
+ return id;
51255
+ }
51256
+
51257
+ function requirePointLayer(lyr) {
51258
+ if (!lyr || getFeatureCount(lyr) === 0) {
51259
+ stop$1('Missing a target layer containing labels');
51260
+ }
51261
+ if (lyr.geometry_type != 'point') {
51262
+ stop$1('Labels can only be updated in a point layer; target "' +
51263
+ layerName(lyr) + '" contains ' + (lyr.geometry_type || 'no geometry'));
51264
+ }
51265
+ }
51266
+
51267
+ function layerName(lyr) {
51268
+ return lyr && lyr.name || '[unnamed]';
51269
+ }
51270
+
51271
+ cmd.addShape = addShape;
51272
+
51273
+ function addShape(targetLayers, targetDataset, opts) {
51274
+ if (targetLayers.length > 1) {
51275
+ stop$1('Command expects a single target layer');
51276
+ }
51277
+ var targetLyr = targetLayers[0]; // may be undefined
51278
+ var targetType = !opts.no_replace && targetLyr && targetLyr.geometry_type || null;
51279
+ var dataset = importGeoJSON(toFeature(opts, targetType));
51280
+ var outputLyr = mergeDatasetsIntoDataset(targetDataset, [dataset])[0];
51281
+ if (opts.no_replace || !targetLyr) {
51282
+ // create new layer
51283
+ setOutputLayerName(outputLyr, targetLyr && targetLyr.name, null, opts);
51284
+ return [outputLyr];
51285
+ }
51286
+ // merge into target layer
51287
+ return cmd.mergeLayers([targetLyr, outputLyr], {force: true});
51288
+ }
51289
+
51290
+ function toFeature(opts, geomType) {
51291
+ if (opts.geojson) {
51292
+ return parseArg(opts.geojson);
51293
+ }
51294
+
51295
+ var geom = opts.coordinates && parseCoordsAsGeometry(opts.coordinates) || null;
51296
+
51297
+ if (!geom) {
51298
+ stop$1('Missing required shape coordinates');
51299
+ }
51300
+
51301
+ if (geomType == 'point' && geom.type != 'Point') {
51302
+ stop$1('Expected point coordinates, received', geom.type);
51303
+ }
51304
+
51305
+ if (geomType == 'polygon' && geom.type != 'Polygon') {
51306
+ stop$1('Expected polygon coordinates, received', geom.type);
51307
+ }
51308
+
51309
+ if (geomType == 'polyline') {
51310
+ if (geom.type == 'Polygon') {
51311
+ geom.coordinates = geom.coordinates[0];
51312
+ geom.type = 'LineString';
51313
+ } else {
51314
+ stop$1('Expected polyline coordinates, received', geom.type);
51315
+ }
51316
+ }
51317
+
51318
+ return {
51319
+ type: 'Feature',
51320
+ properties: parseProperties(opts.properties),
51321
+ geometry: geom
51322
+ };
51323
+ }
51324
+
51325
+ function parseArg(obj) {
51326
+ return typeof obj == 'string' ? JSON.parse(obj) : obj;
51327
+ }
51328
+
51329
+ function parseProperties(arg) {
51330
+ if (!arg) return null;
51331
+ return parseArg(arg);
51332
+ }
51333
+
51334
+ function isArrayOfNumbers(arr) {
51335
+ return arr.length >= 2 && arr.every(utils.isNumber);
51336
+ }
51337
+
51338
+ function isClosedPath$1(arr) {
51339
+ return isArrayOfPoints(arr) && arr.length > 3 && samePoint$2(arr[0], arr[arr.length - 1]);
51340
+ }
51341
+
51342
+ function samePoint$2(a, b) {
51343
+ return a[0] == b[0] && a[1] == b[1];
51344
+ }
49986
51345
 
49987
51346
  function isArrayOfPoints(arr) {
49988
51347
  return arr.every(isPoint);
@@ -54277,67 +55636,6 @@ ${svg}
54277
55636
 
54278
55637
  }
54279
55638
 
54280
- function DatasetEditor(dataset) {
54281
- var layers = [];
54282
- var arcs = [];
54283
-
54284
- this.done = function() {
54285
- dataset.layers = layers;
54286
- if (arcs.length) {
54287
- noteDatasetWillChange(dataset, {operation: 'DatasetEditor.done', unit: 'arcs'});
54288
- dataset.arcs = new ArcCollection(arcs);
54289
- markDatasetChanged(dataset, {operation: 'DatasetEditor.done', unit: 'arcs'});
54290
- withActiveUndoTransaction(null, function() {
54291
- buildTopology(dataset);
54292
- });
54293
- }
54294
- };
54295
-
54296
- this.editLayer = function(lyr, cb) {
54297
- var type = lyr.geometry_type;
54298
- if (dataset.layers.indexOf(lyr) != layers.length) {
54299
- error('Layer was edited out-of-order');
54300
- }
54301
- if (!type) {
54302
- layers.push(lyr);
54303
- return;
54304
- }
54305
- var shapes = lyr.shapes.map(function(shape, shpId) {
54306
- var shape2 = [], retn, input;
54307
- for (var i=0, n=shape ? shape.length : 0; i<n; i++) {
54308
- input = type == 'point' ? shape[i] : idsToCoords(shape[i]);
54309
- retn = cb(input, i, shape);
54310
- if (!Array.isArray(retn)) continue;
54311
- if (type == 'point') {
54312
- shape2.push(retn);
54313
- } else if (type == 'polygon' || type == 'polyline') {
54314
- extendPathShape(shape2, retn || []);
54315
- }
54316
- }
54317
- return shape2.length > 0 ? shape2 : null;
54318
- });
54319
- noteLayerWillChange(lyr, {operation: 'DatasetEditor.editLayer', unit: 'shapes'});
54320
- layers.push(Object.assign(lyr, {shapes: shapes}));
54321
- markLayerChanged(lyr, {operation: 'DatasetEditor.editLayer', unit: 'shapes'});
54322
- };
54323
-
54324
- function extendPathShape(shape, parts) {
54325
- for (var i=0; i<parts.length; i++) {
54326
- shape.push([arcs.length]);
54327
- arcs.push(parts[i]);
54328
- }
54329
- }
54330
-
54331
- function idsToCoords(ids) {
54332
- var coords = [];
54333
- var iter = dataset.arcs.getShapeIter(ids);
54334
- while (iter.hasNext()) {
54335
- coords.push([iter.x, iter.y]);
54336
- }
54337
- return coords;
54338
- }
54339
- }
54340
-
54341
55639
  function makePolylineBuffer(lyr, dataset, opts) {
54342
55640
  time('buffer');
54343
55641
  var spherical = isLatLngCRS(getDatasetCRS(dataset));
@@ -61481,625 +62779,211 @@ ${svg}
61481
62779
  if (data.pointCount === 0) return [];
61482
62780
  return groupPolygonRings(data.pathData, arcs, false).map(function(paths) {
61483
62781
  return paths.map(function(path) {
61484
- return path.points.map(function(p) {
61485
- return p.concat();
61486
- });
61487
- });
61488
- });
61489
- }
61490
-
61491
- function getPolygonGeometry(shape, arcs) {
61492
- var coords = getPolygonMultiPolygonCoords(shape, arcs);
61493
- return coords.length > 0 ? {
61494
- type: 'MultiPolygon',
61495
- coordinates: coords
61496
- } : null;
61497
- }
61498
-
61499
- function getBufferDataset(coords) {
61500
- return importGeoJSON({
61501
- type: 'GeometryCollection',
61502
- geometries: [{
61503
- type: 'MultiPolygon',
61504
- coordinates: coords
61505
- }]
61506
- }, {type: 'polygon'});
61507
- }
61508
-
61509
- // Union a set of winding-fill offset rings (which self-overlap) into clean,
61510
- // non-self-overlapping MultiPolygon coordinates, via the winding-number
61511
- // dissolve. Used by the topological pipeline to feed an ordinary polygon into
61512
- // the shared mosaic (whose boundary-flood membership cannot resolve the
61513
- // self-overlapping construction ring directly).
61514
- function dissolveOffsetRingsToCoords(coords, opts, outlineDissolve) {
61515
- if (!coords || coords.length === 0) return [];
61516
- var dataset = getBufferDataset(coords);
61517
- if (!dataset.arcs) return [];
61518
- var dissolveOpts = outlineDissolve ?
61519
- getOutlineBufferDissolveOpts(opts) :
61520
- Object.assign({}, opts, {winding_fill: true});
61521
- dissolveBufferDataset2(dataset, dissolveOpts);
61522
- var lyr = dataset.layers[0];
61523
- var shape = lyr.shapes && lyr.shapes[0];
61524
- return shape ? getPolygonMultiPolygonCoords(shape, dataset.arcs) : [];
61525
- }
61526
-
61527
- function getBufferMultiPolygonCoords(paths, distance, bufferMaker) {
61528
- var features, coords = [];
61529
- if (paths.length === 0) return coords;
61530
- features = bufferMaker(paths, distance) || [];
61531
- features.forEach(function(feat) {
61532
- var geom = feat && feat.geometry;
61533
- if (geom && geom.type == 'MultiPolygon') {
61534
- coords = coords.concat(geom.coordinates);
61535
- }
61536
- });
61537
- return coords;
61538
- }
61539
-
61540
- function getPolygonBufferPathData(shape, uniqueArcTest) {
61541
- var data = {paths: [], split: false};
61542
- (shape || []).forEach(function(path) {
61543
- var paths = uniqueArcTest ? splitPathAtSharedArcs(path, uniqueArcTest) :
61544
- [path.concat()];
61545
- if (paths.length != 1 || paths[0].length != path.length ||
61546
- !paths[0].every(function(arcId, i) {
61547
- return arcId == path[i];
61548
- })) {
61549
- data.split = true;
61550
- }
61551
- data.paths = data.paths.concat(paths);
61552
- });
61553
- return data;
61554
- }
61555
-
61556
- function splitPathAtSharedArcs(path, uniqueArcTest) {
61557
- var flags = path.map(uniqueArcTest);
61558
- var firstShared = flags.indexOf(false);
61559
- var chains = [];
61560
- var chain = [];
61561
- var start, i, arcId;
61562
- if (firstShared == -1) return [path.concat()];
61563
- start = (firstShared + 1) % path.length;
61564
- for (i = 0; i < path.length; i++) {
61565
- arcId = path[(start + i) % path.length];
61566
- if (uniqueArcTest(arcId)) {
61567
- chain.push(arcId);
61568
- } else if (chain.length > 0) {
61569
- chains.push(chain);
61570
- chain = [];
61571
- }
61572
- }
61573
- if (chain.length > 0) {
61574
- chains.push(chain);
61575
- }
61576
- return chains;
61577
- }
61578
-
61579
- function getUniqueArcTest(lyr, arcs) {
61580
- var classify = getArcClassifier(lyr, arcs, {reusable: true})(function(a, b) {
61581
- return b == -1 ? 'unique' : null;
61582
- });
61583
- return function(arcId) {
61584
- return !!classify(arcId);
61585
- };
61586
- }
61587
-
61588
- function ringArea(ring) {
61589
- var iter = new PointIter(ring);
61590
- return getSphericalPathArea2(iter);
61591
- }
61592
-
61593
- function makePointBuffer(lyr, dataset, opts) {
61594
- var geojson = makePointBufferGeoJSON(lyr, dataset, opts);
61595
- return importGeoJSON(geojson, {});
61596
- }
61597
-
61598
- // Make a single geodetic circle
61599
- function getCircleGeoJSON(center, radius, vertices, opts) {
61600
- var n = 360;
61601
- var geod = getGeodeticSegmentFunction(parseCrsString$1('wgs84')); // ?
61602
- if (opts.inset) {
61603
- radius -= opts.inset;
61604
- }
61605
- return opts.geometry_type == 'polyline' ?
61606
- getPointBufferLineString([center], radius, n, geod) :
61607
- getPointBufferPolygon([center], radius, n, geod, true);
61608
- }
61609
-
61610
- // Convert a point layer to circles
61611
- function makePointBufferGeoJSON(lyr, dataset, opts) {
61612
- var vertices = opts.vertices || 72;
61613
- var distanceFn = getBufferDistanceFunction(lyr, dataset, opts);
61614
- var crs = getDatasetCRS(dataset);
61615
- var spherical = isLatLngCRS(crs);
61616
- var geod = getGeodeticSegmentFunction(crs);
61617
- var geometries = lyr.shapes.map(function(shape, i) {
61618
- var dist = distanceFn(i);
61619
- if (!dist || !shape) return null;
61620
- return getPointBufferPolygon(shape, dist, vertices, geod, spherical);
61621
- });
61622
- // TODO: make sure that importer supports null geometries (nonstandard GeoJSON);
61623
- return {
61624
- type: 'GeometryCollection',
61625
- geometries: geometries
61626
- };
61627
- }
61628
-
61629
- function getPointBufferPolygon(points, distance, vertices, geod, spherical) {
61630
- var rings = [], coords, coords2;
61631
- if (!points || !points.length) return null;
61632
- for (var i=0; i<points.length; i++) {
61633
- coords = getPointBufferCoordinates(points[i], distance, vertices, geod);
61634
- if (!spherical) {
61635
- rings.push([coords]);
61636
- } else if (countCrosses(coords) > 0) {
61637
- coords2 = removePolygonCrosses([coords]);
61638
- while (coords2.length > 0) rings.push([coords2.pop()]); // geojson polygon coords, no hole
61639
- } else if (ringArea(coords) < 0) {
61640
- // negative spherical area: CCW ring, indicating a circle of >180 degrees
61641
- // that fully encloses both poles and the antimeridian.
61642
- // need to add an enclosure around the entire sphere
61643
- // TODO: compare to distance param as a sanity check
61644
- rings.push([
61645
- [[180, 90], [180, -90], [0, -90], [-180, -90], [-180, 90], [0, 90], [180, 90]],
61646
- coords
61647
- ]);
61648
- } else {
61649
- rings.push([coords]);
61650
- }
61651
- }
61652
- return {
61653
- type: 'MultiPolygon',
61654
- coordinates: rings
61655
- };
61656
- }
61657
-
61658
- function getPointBufferLineString(points, distance, vertices, geod) {
61659
- var rings = [], coords;
61660
- if (!points || !points.length) return null;
61661
- for (var i=0; i<points.length; i++) {
61662
- coords = getPointBufferCoordinates(points[i], distance, vertices, geod);
61663
- coords = removePolylineCrosses(coords);
61664
- while (coords.length > 0) rings.push(coords.pop());
61665
- }
61666
- return rings.length == 1 ? {
61667
- type: 'LineString',
61668
- coordinates: rings[0]
61669
- } : {
61670
- type: 'MultiLineString',
61671
- coordinates: rings
61672
- };
61673
- }
61674
-
61675
- // Returns array of [x, y] coordinates in a closed ring
61676
- function getPointBufferCoordinates(center, meterDist, vertices, geod) {
61677
- var coords = [],
61678
- angle = 360 / vertices,
61679
- theta;
61680
- for (var i=0; i<vertices; i++) {
61681
- // offsetting by half a step so 4 sides are flat, not pointy
61682
- // (looks better on low-vertex circles)
61683
- theta = (i + 0.5) * angle % 360;
61684
- coords.push(geod(center[0], center[1], theta, meterDist));
61685
- }
61686
- coords.push(coords[0].concat());
61687
- return coords;
61688
- }
61689
-
61690
- // Returns number of arcs that were removed
61691
- function editArcs(arcs, onPoint) {
61692
- var nn2 = [],
61693
- xx2 = [],
61694
- yy2 = [],
61695
- errors = 0,
61696
- n;
61697
-
61698
- arcs.forEach(function(arc, i) {
61699
- editArc(arc, onPoint);
61700
- });
61701
- arcs.updateVertexData(nn2, xx2, yy2);
61702
- return errors;
61703
-
61704
- function append(p) {
61705
- if (p) {
61706
- xx2.push(p[0]);
61707
- yy2.push(p[1]);
61708
- n++;
61709
- }
61710
- }
61711
-
61712
- function editArc(arc, cb) {
61713
- var x, y, xp, yp, retn;
61714
- var valid = true;
61715
- var i = 0;
61716
- n = 0;
61717
- while (arc.hasNext()) {
61718
- x = arc.x;
61719
- y = arc.y;
61720
- retn = cb(append, x, y, xp, yp, i++);
61721
- if (retn === false) {
61722
- valid = false;
61723
- // assumes that it's ok for the arc iterator to be interrupted.
61724
- break;
61725
- }
61726
- xp = x;
61727
- yp = y;
61728
- }
61729
- if (valid && n == 1) {
61730
- // only one valid point was added to this arc (invalid)
61731
- // e.g. this could happen during reprojection.
61732
- // making this arc empty
61733
- // error("An invalid arc was created");
61734
- message("An invalid arc was created");
61735
- valid = false;
61736
- }
61737
- if (valid) {
61738
- nn2.push(n);
61739
- } else {
61740
- // remove any points that were added for an invalid arc
61741
- while (n-- > 0) {
61742
- xx2.pop();
61743
- yy2.pop();
61744
- }
61745
- nn2.push(0); // add empty arc (to preserve mapping from paths to arcs)
61746
- errors++;
61747
- }
61748
- }
61749
- }
61750
-
61751
- // Planar densification by an interval
61752
- function densifyPathByInterval(coords, interval, interpolate) {
61753
- if (findMaxPathInterval(coords) < interval) return coords;
61754
- if (!interpolate) {
61755
- interpolate = getIntervalInterpolator(interval);
61756
- }
61757
- var coords2 = [coords[0]], a, b;
61758
- for (var i=1, n=coords.length; i<n; i++) {
61759
- a = coords[i-1];
61760
- b = coords[i];
61761
- if (geom.distance2D(a[0], a[1], b[0], b[1]) > interval + 1e-4) {
61762
- appendArr(coords2, interpolate(a, b));
61763
- }
61764
- coords2.push(b);
61765
- }
61766
- return coords2;
61767
- }
61768
-
61769
- function getIntervalInterpolator(interval) {
61770
- return function(a, b) {
61771
- var points = [];
61772
- // var rev = a[0] == b[0] ? a[1] > b[1] : a[0] > b[0];
61773
- var dist = geom.distance2D(a[0], a[1], b[0], b[1]);
61774
- var n = Math.round(dist / interval) - 1;
61775
- var dx = (b[0] - a[0]) / (n + 1),
61776
- dy = (b[1] - a[1]) / (n + 1);
61777
- for (var i=1; i<=n; i++) {
61778
- points.push([a[0] + dx * i, a[1] + dy * i]);
61779
- }
61780
- return points;
61781
- };
61782
- }
61783
-
61784
-
61785
- // Interpolate the same points regardless of segment direction
61786
- function densifyAntimeridianSegment(a, b, interval) {
61787
- var y1, y2;
61788
- var coords = [];
61789
- var ascending = a[1] < b[1];
61790
- if (a[0] != b[0]) error('Expected an edge segment');
61791
- if (ascending) {
61792
- y1 = a[1];
61793
- y2 = b[1];
61794
- } else {
61795
- y1 = b[1];
61796
- y2 = a[1];
61797
- }
61798
- var y = Math.floor(y1 / interval) * interval + interval;
61799
- while (y < y2) {
61800
- coords.push([a[0], y]);
61801
- y += interval;
61802
- }
61803
- if (!ascending) coords.reverse();
61804
- return coords;
61805
- }
61806
-
61807
- function appendArr(dest, src) {
61808
- for (var i=0; i<src.length; i++) dest.push(src[i]);
61809
- }
61810
-
61811
- function findMaxPathInterval(coords) {
61812
- var maxSq = 0, intSq, a, b;
61813
- for (var i=1, n=coords.length; i<n; i++) {
61814
- a = coords[i-1];
61815
- b = coords[i];
61816
- intSq = geom.distanceSq(a[0], a[1], b[0], b[1]);
61817
- if (intSq > maxSq) maxSq = intSq;
61818
- }
61819
- return Math.sqrt(maxSq);
61820
- }
61821
-
61822
- function projectAndDensifyArcs(arcs, proj) {
61823
- var interval = getDefaultDensifyInterval(arcs, proj);
61824
- var minIntervalSq = interval * interval * 25;
61825
- var p;
61826
- return editArcs(arcs, onPoint);
61827
-
61828
- function onPoint(append, lng, lat, prevLng, prevLat, i) {
61829
- var pp = p;
61830
- p = proj(lng, lat);
61831
- if (!p) return false; // signal that current arc contains an error
61832
-
61833
- // Don't try to densify shorter segments (optimization)
61834
- if (i > 0 && geom.distanceSq(p[0], p[1], pp[0], pp[1]) > minIntervalSq) {
61835
- densifySegment(prevLng, prevLat, pp[0], pp[1], lng, lat, p[0], p[1], proj, interval)
61836
- .forEach(append);
61837
- }
61838
- append(p);
61839
- }
61840
- }
61841
-
61842
- // Use the median of intervals computed by projecting segments.
61843
- // We're probing a number of points, because @proj might only be valid in
61844
- // a sub-region of the dataset bbox (e.g. +proj=tpers)
61845
- function findDensifyInterval(bounds, xy, proj) {
61846
- var steps = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9];
61847
- var points = [];
61848
- for (var i=0; i<steps.length; i++) {
61849
- for (var j=0; j<steps.length; j++) {
61850
- points.push([steps[i], steps[j]]);
61851
- }
61852
- }
61853
- var intervals = points.map(function(pos) {
61854
- var x = bounds.xmin + bounds.width() * pos[0];
61855
- var y = bounds.ymin + bounds.height() * pos[1];
61856
- var a = proj(x, y);
61857
- var b = proj(x + xy[0], y + xy[1]);
61858
- return a && b ? geom.distance2D(a[0], a[1], b[0], b[1]) : Infinity;
61859
- }).filter(function(int) {return int < Infinity;});
61860
- return intervals.length > 0 ? utils.findMedian(intervals) : Infinity;
61861
- }
61862
-
61863
- // Kludgy way to get a useful interval for densifying a bounding box.
61864
- // Uses a fraction of average bbox side length)
61865
- // TODO: improve
61866
- function findDensifyInterval2(bb, proj) {
61867
- var a = proj(bb.centerX(), bb.centerY()),
61868
- c = proj(bb.centerX(), bb.ymin), // right center
61869
- d = proj(bb.xmax, bb.centerY()); // bottom center
61870
- var interval = a && c && d ? (geom.distance2D(a[0], a[1], c[0], c[1]) +
61871
- geom.distance2D(a[0], a[1], d[0], d[1])) / 5000 : Infinity;
61872
- return interval;
61873
- }
61874
-
61875
- // Returns an interval in projected units
61876
- function getDefaultDensifyInterval(arcs, proj) {
61877
- var xy = getAvgSegment2(arcs),
61878
- bb = arcs.getBounds(),
61879
- intervalA = findDensifyInterval(bb, xy, proj),
61880
- intervalB = findDensifyInterval2(bb, proj),
61881
- interval = Math.min(intervalA, intervalB);
61882
- if (interval == Infinity) {
61883
- error('Densification error');
61884
- }
61885
- return interval;
61886
- }
61887
-
61888
- // Interpolate points into a projected line segment if needed to prevent large
61889
- // deviations from path of original unprojected segment.
61890
- // @points (optional) array of accumulated points
61891
- function densifySegment(lng0, lat0, x0, y0, lng2, lat2, x2, y2, proj, interval, points) {
61892
- // Find midpoint between two endpoints and project it (assumes longitude does
61893
- // not wrap). TODO Consider bisecting along great circle path -- although this
61894
- // would not be good for boundaries that follow line of constant latitude.
61895
- var lng1 = (lng0 + lng2) / 2,
61896
- lat1 = (lat0 + lat2) / 2,
61897
- p = proj(lng1, lat1),
61898
- distSq;
61899
- if (!p) return; // TODO: consider if this is adequate for handling proj. errors
61900
- distSq = geom.pointSegDistSq2(p[0], p[1], x0, y0, x2, y2); // sq displacement
61901
- points = points || [];
61902
- // Bisect current segment if the projected midpoint deviates from original
61903
- // segment by more than the @interval parameter.
61904
- // ... but don't bisect very small segments to prevent infinite recursion
61905
- // (e.g. if projection function is discontinuous)
61906
- if (distSq > interval * interval * 0.25 && geom.distance2D(lng0, lat0, lng2, lat2) > 0.01) {
61907
- densifySegment(lng0, lat0, x0, y0, lng1, lat1, p[0], p[1], proj, interval, points);
61908
- points.push(p);
61909
- densifySegment(lng1, lat1, p[0], p[1], lng2, lat2, x2, y2, proj, interval, points);
61910
- }
61911
- return points;
61912
- }
61913
-
61914
- // Create rectangles around each feature in a layer
61915
- cmd.rectangles = function(targetLyr, targetDataset, opts) {
61916
- var crsInfo = getDatasetCrsInfo(targetDataset);
61917
- var records = targetLyr.data ? targetLyr.data.getRecords() : null;
61918
- var geometries;
61919
-
61920
- if (opts.bbox) {
61921
- geometries = bboxExpressionToGeometries(opts.bbox, targetLyr, targetDataset);
61922
-
61923
- } else {
61924
- if (!layerHasGeometry$1(targetLyr)) {
61925
- stop$1("Layer is missing geometric shapes");
61926
- }
61927
- geometries = shapesToBoxGeometries(targetLyr, targetDataset, opts);
61928
- }
61929
-
61930
- var geojson = {
61931
- type: 'FeatureCollection',
61932
- features: geometries.map(function(geom, i) {
61933
- var rec = records && records[i] || null;
61934
- if (rec && opts.no_replace) {
61935
- rec = utils.extend({}, rec); // make a copy
61936
- }
61937
- return {
61938
- type: 'Feature',
61939
- properties: rec,
61940
- geometry: geom
61941
- };
61942
- })
61943
- };
61944
- var dataset = importGeoJSON(geojson, {});
61945
- setDatasetCrsInfo(dataset, crsInfo);
61946
- var outputLayers = mergeDatasetsIntoDataset(targetDataset, [dataset]);
61947
- setOutputLayerName(outputLayers[0], targetLyr, null, opts);
61948
- return outputLayers;
61949
- };
61950
-
61951
-
61952
-
61953
-
61954
- function shapesToBoxGeometries(lyr, dataset, opts) {
61955
- var crsInfo = getDatasetCrsInfo(dataset);
61956
- return lyr.shapes.map(function(shp) {
61957
- var bounds = lyr.geometry_type == 'point' ?
61958
- getPointFeatureBounds(shp) : dataset.arcs.getMultiShapeBounds(shp);
61959
- bounds = applyRectangleOptions(bounds, crsInfo.crs, opts);
61960
- if (!bounds) return null;
61961
- return bboxToPolygon(bounds.toArray(), opts);
62782
+ return path.points.map(function(p) {
62783
+ return p.concat();
62784
+ });
62785
+ });
61962
62786
  });
61963
62787
  }
61964
62788
 
61965
- function bboxExpressionToGeometries(exp, lyr, dataset, opts) {
61966
- var compiled = compileFeatureExpression(exp, lyr, dataset.arcs, {});
61967
- var n = getFeatureCount(lyr);
61968
- var result;
61969
- var geometries = [];
61970
- for (var i=0; i<n; i++) {
61971
- result = compiled(i);
61972
- if (!looksLikeBbox(result)) {
61973
- stop$1('Invalid bbox value (expected a GeoJSON-type bbox):', result);
61974
- }
61975
- geometries.push(bboxToPolygon(result));
61976
- }
61977
- return geometries;
62789
+ function getPolygonGeometry(shape, arcs) {
62790
+ var coords = getPolygonMultiPolygonCoords(shape, arcs);
62791
+ return coords.length > 0 ? {
62792
+ type: 'MultiPolygon',
62793
+ coordinates: coords
62794
+ } : null;
61978
62795
  }
61979
62796
 
61980
- function looksLikeBbox(o) {
61981
- if (!o || o.length != 4) return false;
61982
- if (o.some(isNaN)) return false;
61983
- if (o[0] <= o[2] == false || o[1] <= o[3] == false) return false;
61984
- return true;
62797
+ function getBufferDataset(coords) {
62798
+ return importGeoJSON({
62799
+ type: 'GeometryCollection',
62800
+ geometries: [{
62801
+ type: 'MultiPolygon',
62802
+ coordinates: coords
62803
+ }]
62804
+ }, {type: 'polygon'});
61985
62805
  }
61986
62806
 
61987
- // Create rectangles around one or more target layers
61988
- //
61989
- cmd.rectangle2 = function(target, opts) {
61990
- // if target layer is a rectangle and we're applying frame properties,
61991
- // turn the target into a frame instead of creating a new rectangle
61992
- if (target.layers.length == 1 && opts.width &&
61993
- layerIsRectangle(target.layers[0], target.dataset.arcs)) {
61994
- applyFrameProperties(target.layers[0], opts);
61995
- return;
61996
- }
61997
- var datasets = target.layers.map(function(lyr) {
61998
- var dataset = cmd.rectangle({layer: lyr, dataset: target.dataset}, opts);
61999
- setOutputLayerName(dataset.layers[0], lyr, null, opts);
62000
- if (!opts.no_replace) {
62001
- dataset.layers[0].name = lyr.name || dataset.layers[0].name;
62807
+ // Union a set of winding-fill offset rings (which self-overlap) into clean,
62808
+ // non-self-overlapping MultiPolygon coordinates, via the winding-number
62809
+ // dissolve. Used by the topological pipeline to feed an ordinary polygon into
62810
+ // the shared mosaic (whose boundary-flood membership cannot resolve the
62811
+ // self-overlapping construction ring directly).
62812
+ function dissolveOffsetRingsToCoords(coords, opts, outlineDissolve) {
62813
+ if (!coords || coords.length === 0) return [];
62814
+ var dataset = getBufferDataset(coords);
62815
+ if (!dataset.arcs) return [];
62816
+ var dissolveOpts = outlineDissolve ?
62817
+ getOutlineBufferDissolveOpts(opts) :
62818
+ Object.assign({}, opts, {winding_fill: true});
62819
+ dissolveBufferDataset2(dataset, dissolveOpts);
62820
+ var lyr = dataset.layers[0];
62821
+ var shape = lyr.shapes && lyr.shapes[0];
62822
+ return shape ? getPolygonMultiPolygonCoords(shape, dataset.arcs) : [];
62823
+ }
62824
+
62825
+ function getBufferMultiPolygonCoords(paths, distance, bufferMaker) {
62826
+ var features, coords = [];
62827
+ if (paths.length === 0) return coords;
62828
+ features = bufferMaker(paths, distance) || [];
62829
+ features.forEach(function(feat) {
62830
+ var geom = feat && feat.geometry;
62831
+ if (geom && geom.type == 'MultiPolygon') {
62832
+ coords = coords.concat(geom.coordinates);
62002
62833
  }
62003
- return dataset;
62004
62834
  });
62005
- return mergeDatasetsIntoDataset(target.dataset, datasets);
62006
- };
62835
+ return coords;
62836
+ }
62007
62837
 
62008
- cmd.rectangle = function(target, opts) {
62009
- var bounds, crsInfo;
62010
- if (opts.bbox) {
62011
- bounds = new Bounds(opts.bbox);
62012
- crsInfo = target && getDatasetCrsInfo(target.dataset) ||
62013
- probablyDecimalDegreeBounds(bounds) && getCrsInfo('wgs84') || {};
62014
- } else if (target) {
62015
- bounds = getLayerBounds(target.layer, target.dataset.arcs);
62016
- crsInfo = getDatasetCrsInfo(target.dataset);
62838
+ function getPolygonBufferPathData(shape, uniqueArcTest) {
62839
+ var data = {paths: [], split: false};
62840
+ (shape || []).forEach(function(path) {
62841
+ var paths = uniqueArcTest ? splitPathAtSharedArcs(path, uniqueArcTest) :
62842
+ [path.concat()];
62843
+ if (paths.length != 1 || paths[0].length != path.length ||
62844
+ !paths[0].every(function(arcId, i) {
62845
+ return arcId == path[i];
62846
+ })) {
62847
+ data.split = true;
62848
+ }
62849
+ data.paths = data.paths.concat(paths);
62850
+ });
62851
+ return data;
62852
+ }
62853
+
62854
+ function splitPathAtSharedArcs(path, uniqueArcTest) {
62855
+ var flags = path.map(uniqueArcTest);
62856
+ var firstShared = flags.indexOf(false);
62857
+ var chains = [];
62858
+ var chain = [];
62859
+ var start, i, arcId;
62860
+ if (firstShared == -1) return [path.concat()];
62861
+ start = (firstShared + 1) % path.length;
62862
+ for (i = 0; i < path.length; i++) {
62863
+ arcId = path[(start + i) % path.length];
62864
+ if (uniqueArcTest(arcId)) {
62865
+ chain.push(arcId);
62866
+ } else if (chain.length > 0) {
62867
+ chains.push(chain);
62868
+ chain = [];
62869
+ }
62017
62870
  }
62018
- bounds = bounds && applyRectangleOptions(bounds, crsInfo.crs, opts);
62019
- if (!bounds || !bounds.hasBounds()) {
62020
- stop$1('Missing rectangle extent');
62871
+ if (chain.length > 0) {
62872
+ chains.push(chain);
62021
62873
  }
62022
- var feature = {
62023
- type: 'Feature',
62024
- properties: {},
62025
- geometry: bboxToPolygon(bounds.toArray(), opts)
62874
+ return chains;
62875
+ }
62876
+
62877
+ function getUniqueArcTest(lyr, arcs) {
62878
+ var classify = getArcClassifier(lyr, arcs, {reusable: true})(function(a, b) {
62879
+ return b == -1 ? 'unique' : null;
62880
+ });
62881
+ return function(arcId) {
62882
+ return !!classify(arcId);
62026
62883
  };
62027
- var dataset = importGeoJSON(feature, {});
62028
- applyFrameProperties(dataset.layers[0], opts);
62029
- dataset.layers[0].name = opts.name || 'rectangle';
62030
- setDatasetCrsInfo(dataset, crsInfo);
62031
- return dataset;
62032
- };
62884
+ }
62033
62885
 
62034
- function applyFrameProperties(lyr, opts) {
62035
- if (!opts.width) return;
62036
- if (!lyr.data) initDataTable(lyr);
62037
- var d = lyr.data.getRecords()[0] || {};
62038
- d.width = parseSizeParam(opts.width);
62039
- d.type = 'frame';
62886
+ function ringArea(ring) {
62887
+ var iter = new PointIter(ring);
62888
+ return getSphericalPathArea2(iter);
62040
62889
  }
62041
62890
 
62042
- function applyRectangleOptions(bounds, crs, opts) {
62043
- var isGeoBox = probablyDecimalDegreeBounds(bounds);
62044
- if (opts.offset) {
62045
- bounds = applyBoundsOffset(opts.offset, bounds, crs);
62046
- }
62047
- if (bounds.area() > 0 === false) return null;
62048
- if (opts.aspect_ratio) {
62049
- bounds = applyAspectRatio(opts.aspect_ratio, bounds);
62050
- }
62051
- if (isGeoBox) {
62052
- bounds = clampToWorldBounds(bounds);
62053
- }
62054
- return bounds;
62891
+ function makePointBuffer(lyr, dataset, opts) {
62892
+ var geojson = makePointBufferGeoJSON(lyr, dataset, opts);
62893
+ return importGeoJSON(geojson, {});
62055
62894
  }
62056
62895
 
62057
- // opt: aspect ratio as a single number or a range (e.g. "1,2");
62058
- function applyAspectRatio(opt, bounds) {
62059
- var range = String(opt).split(',').map(parseFloat),
62060
- aspectRatio = bounds.width() / bounds.height(),
62061
- min, max; // min is height limit, max is width limit
62062
- if (range.length == 1) {
62063
- range.push(range[0]);
62064
- } else if (range[0] > range[1]) {
62065
- range.reverse();
62066
- }
62067
- min = range[0];
62068
- max = range[1];
62069
- if (!min && !max) return bounds;
62070
- if (!min) min = -Infinity;
62071
- if (!max) max = Infinity;
62072
- if (aspectRatio < min) {
62073
- bounds.fillOut(min);
62074
- } else if (aspectRatio > max) {
62075
- bounds.fillOut(max);
62896
+ // Make a single geodetic circle
62897
+ function getCircleGeoJSON(center, radius, vertices, opts) {
62898
+ var n = 360;
62899
+ var geod = getGeodeticSegmentFunction(parseCrsString$1('wgs84')); // ?
62900
+ if (opts.inset) {
62901
+ radius -= opts.inset;
62076
62902
  }
62077
- return bounds;
62903
+ return opts.geometry_type == 'polyline' ?
62904
+ getPointBufferLineString([center], radius, n, geod) :
62905
+ getPointBufferPolygon([center], radius, n, geod, true);
62078
62906
  }
62079
62907
 
62080
- function applyBoundsOffset(offsetOpt, bounds, crs) {
62081
- var offsets = convertFourSides(offsetOpt, crs, bounds);
62082
- bounds.padBounds(offsets[0], offsets[1], offsets[2], offsets[3]);
62083
- return bounds;
62908
+ // Convert a point layer to circles
62909
+ function makePointBufferGeoJSON(lyr, dataset, opts) {
62910
+ var vertices = opts.vertices || 72;
62911
+ var distanceFn = getBufferDistanceFunction(lyr, dataset, opts);
62912
+ var crs = getDatasetCRS(dataset);
62913
+ var spherical = isLatLngCRS(crs);
62914
+ var geod = getGeodeticSegmentFunction(crs);
62915
+ var geometries = lyr.shapes.map(function(shape, i) {
62916
+ var dist = distanceFn(i);
62917
+ if (!dist || !shape) return null;
62918
+ return getPointBufferPolygon(shape, dist, vertices, geod, spherical);
62919
+ });
62920
+ // TODO: make sure that importer supports null geometries (nonstandard GeoJSON);
62921
+ return {
62922
+ type: 'GeometryCollection',
62923
+ geometries: geometries
62924
+ };
62084
62925
  }
62085
62926
 
62086
- function bboxToPolygon(bbox, optsArg) {
62087
- var opts = optsArg || {};
62088
- var coords = bboxToCoords(bbox);
62089
- if (opts.interval > 0) {
62090
- coords = densifyPathByInterval(coords, opts.interval);
62927
+ function getPointBufferPolygon(points, distance, vertices, geod, spherical) {
62928
+ var rings = [], coords, coords2;
62929
+ if (!points || !points.length) return null;
62930
+ for (var i=0; i<points.length; i++) {
62931
+ coords = getPointBufferCoordinates(points[i], distance, vertices, geod);
62932
+ if (!spherical) {
62933
+ rings.push([coords]);
62934
+ } else if (countCrosses(coords) > 0) {
62935
+ coords2 = removePolygonCrosses([coords]);
62936
+ while (coords2.length > 0) rings.push([coords2.pop()]); // geojson polygon coords, no hole
62937
+ } else if (ringArea(coords) < 0) {
62938
+ // negative spherical area: CCW ring, indicating a circle of >180 degrees
62939
+ // that fully encloses both poles and the antimeridian.
62940
+ // need to add an enclosure around the entire sphere
62941
+ // TODO: compare to distance param as a sanity check
62942
+ rings.push([
62943
+ [[180, 90], [180, -90], [0, -90], [-180, -90], [-180, 90], [0, 90], [180, 90]],
62944
+ coords
62945
+ ]);
62946
+ } else {
62947
+ rings.push([coords]);
62948
+ }
62091
62949
  }
62092
62950
  return {
62093
- type: 'Polygon',
62094
- coordinates: [coords]
62951
+ type: 'MultiPolygon',
62952
+ coordinates: rings
62095
62953
  };
62096
62954
  }
62097
62955
 
62098
- var Rectangle = /*#__PURE__*/Object.freeze({
62099
- __proto__: null,
62100
- applyAspectRatio: applyAspectRatio,
62101
- bboxToPolygon: bboxToPolygon
62102
- });
62956
+ function getPointBufferLineString(points, distance, vertices, geod) {
62957
+ var rings = [], coords;
62958
+ if (!points || !points.length) return null;
62959
+ for (var i=0; i<points.length; i++) {
62960
+ coords = getPointBufferCoordinates(points[i], distance, vertices, geod);
62961
+ coords = removePolylineCrosses(coords);
62962
+ while (coords.length > 0) rings.push(coords.pop());
62963
+ }
62964
+ return rings.length == 1 ? {
62965
+ type: 'LineString',
62966
+ coordinates: rings[0]
62967
+ } : {
62968
+ type: 'MultiLineString',
62969
+ coordinates: rings
62970
+ };
62971
+ }
62972
+
62973
+ // Returns array of [x, y] coordinates in a closed ring
62974
+ function getPointBufferCoordinates(center, meterDist, vertices, geod) {
62975
+ var coords = [],
62976
+ angle = 360 / vertices,
62977
+ theta;
62978
+ for (var i=0; i<vertices; i++) {
62979
+ // offsetting by half a step so 4 sides are flat, not pointy
62980
+ // (looks better on low-vertex circles)
62981
+ theta = (i + 0.5) * angle % 360;
62982
+ coords.push(geod(center[0], center[1], theta, meterDist));
62983
+ }
62984
+ coords.push(coords[0].concat());
62985
+ return coords;
62986
+ }
62103
62987
 
62104
62988
  function getSemiMinorAxis(P) {
62105
62989
  return P.a * Math.sqrt(1 - (P.es || 0));
@@ -63342,6 +64226,22 @@ ${svg}
63342
64226
  // TODO: add more projections
63343
64227
  //
63344
64228
  function expandProjDefn(str, dataset, targetLayers) {
64229
+ return expandProjDefnWithBounds(str, function() {
64230
+ return getBBox(dataset, targetLayers); // TODO: support projected datasets
64231
+ });
64232
+ }
64233
+
64234
+ // Variant for a command with more than one target dataset. Auto-fitted
64235
+ // parameters describe the combined extent of the targets, so that a single
64236
+ // -proj command produces one destination CRS instead of fitting a different
64237
+ // projection to each dataset it happens to visit.
64238
+ function expandProjDefnForTargets(str, targets) {
64239
+ return expandProjDefnWithBounds(str, function() {
64240
+ return getTargetsBBox(targets);
64241
+ });
64242
+ }
64243
+
64244
+ function expandProjDefnWithBounds(str, getTargetBBox) {
63345
64245
  var mproj = require$1('mproj');
63346
64246
  var proj4, params, bbox, isConic2SP, isCentered, isUtm, decimals;
63347
64247
  if (str in mproj.internal.pj_list === false) {
@@ -63353,7 +64253,7 @@ ${svg}
63353
64253
  isUtm = str == 'utm';
63354
64254
  proj4 = '+proj=' + str;
63355
64255
  if (isConic2SP || isCentered || isUtm) {
63356
- bbox = getBBox(dataset, targetLayers); // TODO: support projected datasets
64256
+ bbox = getTargetBBox();
63357
64257
  decimals = getBoundsPrecisionForDisplay(bbox);
63358
64258
  if (isUtm) {
63359
64259
  params = getUtmParams(bbox);
@@ -63366,6 +64266,17 @@ ${svg}
63366
64266
  return proj4;
63367
64267
  }
63368
64268
 
64269
+ function getTargetsBBox(targets) {
64270
+ var bounds = new Bounds();
64271
+ targets.forEach(function(targ) {
64272
+ bounds.mergeBounds(getBBox(targ.dataset, targ.layers));
64273
+ });
64274
+ if (!bounds.hasBounds()) {
64275
+ stop$1('Command target is missing geographical bounds');
64276
+ }
64277
+ return bounds.toArray();
64278
+ }
64279
+
63369
64280
  function getBBox(dataset, targetLayers) {
63370
64281
  var source = targetLayers && targetLayers.length > 0 ? {
63371
64282
  arcs: dataset.arcs,
@@ -63475,6 +64386,7 @@ ${svg}
63475
64386
  var ProjectionParams = /*#__PURE__*/Object.freeze({
63476
64387
  __proto__: null,
63477
64388
  expandProjDefn: expandProjDefn,
64389
+ expandProjDefnForTargets: expandProjDefnForTargets,
63478
64390
  getAutoFitBBox: getAutoFitBBox,
63479
64391
  getCenterParams: getCenterParams,
63480
64392
  getConicParams: getConicParams,
@@ -64936,32 +65848,112 @@ ${svg}
64936
65848
  return a && b && a[0] == b[0] && a[1] == b[1];
64937
65849
  }
64938
65850
 
64939
- cmd.proj = function(dataset, catalog, opts, targetLayers) {
64940
- var srcInfo, destInfo, destStr;
65851
+ // Run -proj over all the targets of a single command.
65852
+ // The destination CRS is resolved once here rather than separately for each
65853
+ // target, for two reasons. Auto-fitted parameters (e.g. the standard parallels
65854
+ // that "lcc" expands to) must describe the whole command target, not whichever
65855
+ // dataset happens to be projected first. And the map frame must receive the
65856
+ // same CRS as the content instead of re-deriving parameters from its own extent.
65857
+ cmd.projTargets = function(targets, catalog, opts) {
65858
+ var destInfo;
65859
+ targets.forEach(function(targ) {
65860
+ applyProjInit(targ.dataset, catalog, opts);
65861
+ });
65862
+ destInfo = resolveProjDestInfo(targets, catalog, opts);
65863
+ targets.forEach(function(targ) {
65864
+ projectTarget(targ.dataset, opts, targ.layers, destInfo);
65865
+ });
65866
+ if (destInfo && destInfo.crs) {
65867
+ projectMapFrame(targets, catalog, opts, destInfo);
65868
+ warnAboutUnprojectedLayers(catalog, destInfo);
65869
+ }
65870
+ };
65871
+
65872
+ cmd.proj = function(dataset, catalog, opts, targetLayers, destInfo) {
65873
+ applyProjInit(dataset, catalog, opts);
65874
+ if (!destInfo) {
65875
+ destInfo = resolveProjDestInfo([{dataset: dataset, layers: targetLayers}], catalog, opts);
65876
+ }
65877
+ projectTarget(dataset, opts, targetLayers, destInfo);
65878
+ };
65879
+
65880
+ function projectTarget(dataset, opts, targetLayers, destInfo) {
64941
65881
  var implicitlyProjectedNames = getImplicitlyTargetedLayerNames(dataset, targetLayers, layerHasGeometry$1);
64942
65882
  // A preserved GeoJSON "crs" member becomes invalid once the CRS changes.
64943
65883
  deleteGeoJSONMetadataCRS(dataset);
64944
- if (opts.init) {
64945
- srcInfo = fetchCrsInfo(opts.init, catalog);
64946
- if (!srcInfo.crs) stop$1("Unknown projection source:", opts.init);
64947
- setDatasetCrsInfo(dataset, srcInfo);
65884
+ if (!destInfo) return;
65885
+ var didProject = projCmd(dataset, destInfo, opts);
65886
+ if (didProject && implicitlyProjectedNames.length > 0) {
65887
+ message(
65888
+ 'Also projected non-target layer' + utils.pluralSuffix(implicitlyProjectedNames.length) +
65889
+ ' from the same dataset: ' + implicitlyProjectedNames.join(', ')
65890
+ );
64948
65891
  }
65892
+ }
65893
+
65894
+ function applyProjInit(dataset, catalog, opts) {
65895
+ if (!opts.init) return;
65896
+ var srcInfo = fetchCrsInfo(opts.init, catalog);
65897
+ if (!srcInfo.crs) stop$1("Unknown projection source:", opts.init);
65898
+ setDatasetCrsInfo(dataset, srcInfo);
65899
+ }
65900
+
65901
+ // @targets [{dataset, layers}, ...] -- all the targets of one -proj command
65902
+ function resolveProjDestInfo(targets, catalog, opts) {
64949
65903
  if (opts.match) {
64950
- destInfo = fetchCrsInfo(opts.match, catalog);
64951
- } else if (opts.crs) {
64952
- destStr = expandProjDefn(opts.crs, dataset, targetLayers);
64953
- destInfo = getCrsInfo(destStr);
64954
- }
64955
- if (destInfo) {
64956
- var didProject = projCmd(dataset, destInfo, opts);
64957
- if (didProject && implicitlyProjectedNames.length > 0) {
64958
- message(
64959
- 'Also projected non-target layer' + utils.pluralSuffix(implicitlyProjectedNames.length) +
64960
- ' from the same dataset: ' + implicitlyProjectedNames.join(', ')
64961
- );
64962
- }
65904
+ return fetchCrsInfo(opts.match, catalog);
65905
+ }
65906
+ if (!opts.crs) return null;
65907
+ return getCrsInfo(expandProjDefnForTargets(opts.crs, targets));
65908
+ }
65909
+
65910
+ // A map frame is composition state, not content: its rectangle describes a
65911
+ // region of the map's coordinate space, so it has to follow the map's CRS even
65912
+ // when the command targeted only a data layer. The GUI deliberately keeps the
65913
+ // frame out of default targets, so nothing else reaches it.
65914
+ function projectMapFrame(targets, catalog, opts, destInfo) {
65915
+ var frame = catalog ? getActiveFrame(catalog) : null;
65916
+ var frameOpts = opts;
65917
+ var frameCrs;
65918
+ if (!frame) return;
65919
+ if (targets.some(function(targ) { return targ.dataset == frame.dataset; })) {
65920
+ return; // already projected as an ordinary target
65921
+ }
65922
+ frameCrs = getDatasetCrsInfo(frame.dataset).crs;
65923
+ if (frameCrs && crsAreEqual(frameCrs, destInfo.crs)) {
65924
+ return; // already in the destination CRS; say nothing
65925
+ }
65926
+ if (!frameCrs) {
65927
+ warn('Unable to reproject the map frame: its coordinate system is unknown.');
65928
+ return;
64963
65929
  }
64964
- };
65930
+ if (opts.init) {
65931
+ // init= describes the CRS of the targeted data, not the frame's.
65932
+ frameOpts = Object.assign({}, opts);
65933
+ delete frameOpts.init;
65934
+ }
65935
+ cmd.proj(frame.dataset, catalog, frameOpts, frame.dataset.layers, destInfo);
65936
+ message('Also reprojected the map frame.');
65937
+ }
65938
+
65939
+ // -proj only applies to the command's targets, so layers in other datasets are
65940
+ // left behind in the old CRS. That usually surfaces much later as a blank
65941
+ // export or an "unable to combine" error, so name them while the cause is clear.
65942
+ function warnAboutUnprojectedLayers(catalog, destInfo) {
65943
+ var names = [];
65944
+ if (!catalog) return;
65945
+ catalog.getLayers().forEach(function(o) {
65946
+ var crs;
65947
+ if (!layerHasGeometry$1(o.layer) && !layerHasRaster(o.layer)) return;
65948
+ crs = getDatasetCrsInfo(o.dataset).crs;
65949
+ if (crs && !crsAreEqual(crs, destInfo.crs)) {
65950
+ names.push(o.layer.name || '[unnamed]');
65951
+ }
65952
+ });
65953
+ if (names.length === 0) return;
65954
+ warn(`Layer${utils.pluralSuffix(names.length)} not projected by this command: ` +
65955
+ names.join(', ') + '. Use target=* to project all layers.');
65956
+ }
64965
65957
 
64966
65958
  function projCmd(dataset, destInfo, opts) {
64967
65959
  // modify copy of coordinate data when running in web UI, so original shapes
@@ -65082,6 +66074,7 @@ ${svg}
65082
66074
  stop$1('Unable to project from a coordinate system that has no inverse transform');
65083
66075
  }
65084
66076
  var proj = getProjTransform2(src, dest); // v2 returns null points instead of throwing an error
66077
+ var frameSnapshots = getFrameProjectionSnapshots(dataset);
65085
66078
  var badArcs = 0;
65086
66079
  var badPoints = 0;
65087
66080
  var healAntimeridian = isLatLngCRS(src) &&
@@ -65115,6 +66108,10 @@ ${svg}
65115
66108
  cleanProjectedPathLayers(dataset, {heal_antimeridian: healAntimeridian});
65116
66109
  }
65117
66110
 
66111
+ frameSnapshots.forEach(function(snapshot) {
66112
+ rebuildProjectedFrameLayer(snapshot, dataset, proj, isLatLngCRS(src));
66113
+ });
66114
+
65118
66115
  if (badArcs > 0 && !opts.quiet) {
65119
66116
  message(`Removed ${badArcs} ${badArcs == 1 ? 'path' : 'paths'} containing unprojectable vertices.`);
65120
66117
  }
@@ -73064,185 +74061,6 @@ ${svg}
73064
74061
  return parsed[0];
73065
74062
  }
73066
74063
 
73067
- cmd.frame = function(catalog, targets, opts) {
73068
- var widthPx, heightPx, aspectRatio, bbox;
73069
- if (opts.width) {
73070
- widthPx = parseSizeParam(opts.width);
73071
- if (widthPx > 0 === false) {
73072
- stop$1('Invalid width parameter:', opts.width);
73073
- }
73074
- }
73075
- if (opts.height) {
73076
- heightPx = parseSizeParam(opts.height);
73077
- if (heightPx > 0 === false) {
73078
- stop$1('Invalid height parameter:', opts.height);
73079
- }
73080
- }
73081
- if (!widthPx && !heightPx) {
73082
- widthPx = 800;
73083
- message('Using default 800px frame width');
73084
- }
73085
-
73086
- if (opts.aspect_ratio) {
73087
- if (opts.aspect_ratio > 0 === false) {
73088
- stop$1('Invalid aspect-ratio parameter:', opts.aspect_ratio);
73089
- }
73090
- if (!heightPx) {
73091
- heightPx = roundToDigits(widthPx / opts.aspect_ratio, 1);
73092
- } else if (!widthPx) {
73093
- widthPx = roundToDigits(heightPx * opts.aspect_ratio, 1);
73094
- }
73095
- }
73096
-
73097
- if (opts.bbox) {
73098
- bbox = opts.bbox;
73099
- // TODO: validate
73100
- } else {
73101
- var datasets = utils.pluck(targets, 'dataset');
73102
- requireDatasetsHaveCompatibleCRS(datasets, 'Targets include both projected and unprojected coordinates');
73103
- bbox = getTargetBbox(targets);
73104
- if (!bbox) {
73105
- stop$1('Command target is missing geographical bounds');
73106
- }
73107
- }
73108
-
73109
- applyPercentageOffsets(bbox, opts.offset || opts.offsets);
73110
- applyPixelOffsets(bbox, widthPx, heightPx, opts.offset || opts.offsets);
73111
-
73112
- if (bbox[3] - bbox[1] > 0 === false || bbox[2] - bbox[0] > 0 === false) {
73113
- stop$1('Frame has a collapsed bbox');
73114
- }
73115
-
73116
- aspectRatio = (bbox[2] - bbox[0]) / (bbox[3] - bbox[1]);
73117
- if (!widthPx) {
73118
- widthPx = roundToDigits(heightPx * aspectRatio, 1);
73119
- } else if (!heightPx) {
73120
- heightPx = roundToDigits(widthPx / aspectRatio, 1);
73121
- }
73122
-
73123
- var feature = {
73124
- type: 'Feature',
73125
- properties: {type: 'frame', width: widthPx, height: heightPx},
73126
- geometry: bboxToPolygon(bbox)
73127
- };
73128
- var frameDataset = importGeoJSON(feature);
73129
- // set CRS from target dataset
73130
- // TODO: handle case: targets have different projections
73131
- // TODO: handle case: first target is missing CRS
73132
- if (targets.length > 0) {
73133
- var crsInfo = getDatasetCrsInfo(targets[0].dataset);
73134
- setDatasetCrsInfo(frameDataset, crsInfo);
73135
- }
73136
- frameDataset.layers[0].name = opts.name || 'frame';
73137
- catalog.addDataset(frameDataset);
73138
- };
73139
-
73140
- function fillOutBbox(bbox, widthPx, heightPx) {
73141
- var hpad = 0, vpad = 0;
73142
- var w = bbox[2] - bbox[0];
73143
- var h = bbox[3] - bbox[1];
73144
- if (widthPx / heightPx > w / h) { // need to add horizontal padding
73145
- hpad = h * widthPx / heightPx - w;
73146
- } else {
73147
- vpad = w * heightPx / widthPx - h;
73148
- }
73149
- bbox[0] -= hpad / 2;
73150
- bbox[1] -= vpad / 2;
73151
- bbox[2] += hpad / 2;
73152
- bbox[3] += vpad / 2;
73153
- }
73154
-
73155
- function applyPercentageOffsets(bbox, arg) {
73156
- var sides = getPctOffsets(arg);
73157
- var l = sides[0],
73158
- b = sides[1],
73159
- r = sides[2],
73160
- t = sides[3],
73161
- w2 = (bbox[2] - bbox[0]) / (1 - l - r),
73162
- h2 = (bbox[3] - bbox[1]) / (1 - t - b);
73163
- bbox[0] -= l * w2;
73164
- bbox[1] -= b * h2;
73165
- bbox[2] += r * w2;
73166
- bbox[3] += t * h2;
73167
- }
73168
-
73169
- function applyPixelOffsets(bbox, widthPx, heightPx, arg) {
73170
- var sides = getPixelOffsets(arg);
73171
- var l = sides[0],
73172
- b = sides[1],
73173
- r = sides[2],
73174
- t = sides[3],
73175
- scale, w;
73176
-
73177
- if (widthPx && heightPx) {
73178
- // add padding to bbox to match pixel dimensions, if needed
73179
- fillOutBbox(bbox, widthPx, heightPx);
73180
- }
73181
-
73182
- w = bbox[2] - bbox[0];
73183
- bbox[3] - bbox[1];
73184
-
73185
- if (widthPx) {
73186
- scale = w / (widthPx - l - r);
73187
- } else {
73188
- scale = w / (heightPx - t - b);
73189
- }
73190
-
73191
- bbox[0] -= scale * l;
73192
- bbox[1] -= scale * b;
73193
- bbox[2] += scale * r;
73194
- bbox[3] += scale * t;
73195
- return scale;
73196
- }
73197
-
73198
- function getPctOffsets(arg) {
73199
- return adjustOffsetsArg(arg).map(str => {
73200
- return str.includes('%') ? parsePercent(str) : 0;
73201
- });
73202
- }
73203
-
73204
- function getPixelOffsets(arg) {
73205
- return adjustOffsetsArg(arg).map(str => {
73206
- return str.includes('%') ? 0 : parseSizeParam(str);
73207
- });
73208
- }
73209
-
73210
- function adjustOffsetsArg(arg) {
73211
- if (!arg) arg = ['0'];
73212
- if (arg.length == 1) {
73213
- return [arg[0], arg[0], arg[0], arg[0]];
73214
- }
73215
- if (arg.length != 4) {
73216
- stop$1('List of offsets should have 4 values');
73217
- }
73218
- return arg;
73219
- }
73220
-
73221
- function getTargetBbox(targets) {
73222
- var expanded = expandCommandTargets(targets);
73223
- var bounds = expanded.reduce(function(memo, o) {
73224
- return memo.mergeBounds(getLayerBounds(o.layer, o.dataset.arcs));
73225
- }, new Bounds());
73226
- return bounds.hasBounds() ? bounds.toArray() : null;
73227
- }
73228
-
73229
- // Convert width and height args to aspect ratio arg for the rectangle() function
73230
- function getAspectRatioArg(widthArg, heightArg) {
73231
- // heightArg is a string containing either a number or a
73232
- // comma-sep. pair of numbers (range);
73233
- return heightArg.split(',').map(function(opt) {
73234
- var height = Number(opt),
73235
- width = Number(widthArg);
73236
- if (!opt) return '';
73237
- return width / height;
73238
- }).reverse().join(',');
73239
- }
73240
-
73241
- var Frame = /*#__PURE__*/Object.freeze({
73242
- __proto__: null,
73243
- getAspectRatioArg: getAspectRatioArg
73244
- });
73245
-
73246
74064
  // Remove intricate sub-scale detail from a single arc so that -smooth has a
73247
74065
  // clean line to work with, WITHOUT thinning the rest of the line: smooth makes a
73248
74066
  // better approximation of the original when it has more detail to work with, so
@@ -78009,7 +78827,7 @@ ${svg}
78009
78827
  if (lyr) return getFrameLayerData(lyr, dataset.arcs);
78010
78828
  // -frame adds the frame it creates to the catalog as a separate dataset, so a
78011
78829
  // catalog-wide search is needed to find it (-scalebar does the same).
78012
- target = catalog ? findFrame(catalog) : null;
78830
+ target = catalog ? getActiveFrame(catalog) : null;
78013
78831
  return target ? getFrameLayerData(target.layer, target.dataset.arcs) : null;
78014
78832
  }
78015
78833
 
@@ -81404,6 +82222,9 @@ ${svg}
81404
82222
  job.catalog.captureCatalogBefore({operation: 'addLayer'});
81405
82223
  job.catalog.addDataset(cmd.addLayer(targetDataset, opts));
81406
82224
 
82225
+ } else if (name == 'update-frame') {
82226
+ cmd.updateFrame(targetLayers, targetDataset, opts);
82227
+
81407
82228
  } else if (name == 'update-label') {
81408
82229
  cmd.updateLabel(targetLayers, targetDataset, opts);
81409
82230
 
@@ -81625,9 +82446,7 @@ ${svg}
81625
82446
  } else if (name == 'proj') {
81626
82447
  await initProjLibrary(opts);
81627
82448
  job.resumeCommand();
81628
- targets.forEach(function(targ) {
81629
- cmd.proj(targ.dataset, job.catalog, opts, targ.layers);
81630
- });
82449
+ cmd.projTargets(targets, job.catalog, opts);
81631
82450
 
81632
82451
  } else if (name == 'rectangle') {
81633
82452
  if (source || opts.bbox || targets.length === 0) {
@@ -81733,7 +82552,15 @@ ${svg}
81733
82552
  }
81734
82553
 
81735
82554
  if (outputDataset) {
81736
- job.catalog.addDataset(outputDataset); // also sets default target
82555
+ var removedLayers = targetLayers && !opts.no_replace ? targetLayers : [];
82556
+ assertSingleFrameUpdate(
82557
+ job.catalog,
82558
+ outputDataset.layers.map(function(lyr) {
82559
+ return {layer: lyr, dataset: outputDataset};
82560
+ }),
82561
+ removedLayers
82562
+ );
82563
+ job.catalog.addDataset(outputDataset, {removeLayers: removedLayers}); // also sets default target
81737
82564
  outputLayers = outputDataset.layers;
81738
82565
  if (targetLayers && !opts.no_replace) {
81739
82566
  // remove target layers from target dataset
@@ -81743,6 +82570,13 @@ ${svg}
81743
82570
  }
81744
82571
  } else if (outputLayers && targetDataset && outputLayers != targetDataset.layers) {
81745
82572
  // integrate output layers into the target dataset
82573
+ assertSingleFrameUpdate(
82574
+ job.catalog,
82575
+ outputLayers.map(function(lyr) {
82576
+ return {layer: lyr, dataset: targetDataset};
82577
+ }),
82578
+ opts.no_replace ? [] : targetLayers
82579
+ );
81746
82580
  if (opts.no_replace) {
81747
82581
  // make sure commands do not return input layers with 'no_replace' option
81748
82582
  if (!outputLayersAreDifferent(outputLayers, targetLayers || [])) {
@@ -81955,7 +82789,7 @@ ${svg}
81955
82789
  return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
81956
82790
  }
81957
82791
 
81958
- var version = "0.7.62";
82792
+ var version = "0.7.64";
81959
82793
 
81960
82794
  // Parse command line args into commands and run them
81961
82795
  // Function takes an optional Node-style callback. A Promise is returned if no callback is given.
@@ -84639,6 +85473,7 @@ ${svg}
84639
85473
  FileTypes,
84640
85474
  FilterGeom,
84641
85475
  Frame,
85476
+ FrameProjection,
84642
85477
  FrameUtils,
84643
85478
  Furniture,
84644
85479
  Geodesic,