mapshaper 0.7.62 → 0.7.63

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) {
@@ -28649,7 +28838,8 @@
28649
28838
  function exportSVG(dataset, opts) {
28650
28839
  var namespace = 'xmlns="http://www.w3.org/2000/svg"';
28651
28840
  var defs = [];
28652
- var frame, svg, layers, metadataJSON, files, svgFile;
28841
+ var frame, frameLyr, frameBackground, frameNeatline;
28842
+ var svg, layers, metadataJSON, files, svgFile;
28653
28843
  var style = '';
28654
28844
 
28655
28845
  // kludge for map keys
@@ -28686,6 +28876,21 @@
28686
28876
  if (opts.scalebar) {
28687
28877
  layers.push(getScalebarLayer({})); // default options
28688
28878
  }
28879
+ frameLyr = layers.find(function(lyr) {
28880
+ return isFrameLayer(lyr, dataset.arcs);
28881
+ });
28882
+ if (frameLyr) {
28883
+ frameBackground = exportFrameStylePhase(frameLyr, dataset, opts, 'background');
28884
+ frameNeatline = exportFrameStylePhase(frameLyr, dataset, opts, 'neatline');
28885
+ layers = layers.filter(function(lyr) { return lyr != frameLyr; });
28886
+ } else if (opts.gui_frame?.style) {
28887
+ frameBackground = exportExplicitFrameStyle(
28888
+ frame, opts.gui_frame, 'background'
28889
+ );
28890
+ frameNeatline = exportExplicitFrameStyle(
28891
+ frame, opts.gui_frame, 'neatline'
28892
+ );
28893
+ }
28689
28894
  svg = layers.map(function(lyr) {
28690
28895
  var obj;
28691
28896
  if (layerHasFurniture(lyr)) {
@@ -28698,6 +28903,8 @@
28698
28903
  convertPropertiesToDefinitions(obj, defs);
28699
28904
  return stringify(obj);
28700
28905
  }).join('\n');
28906
+ if (frameBackground) svg = frameBackground + '\n' + svg;
28907
+ if (frameNeatline) svg += '\n' + frameNeatline;
28701
28908
 
28702
28909
  if (metadataJSON) {
28703
28910
  svg = getMetadataBlock(metadataJSON, [0, 0, frame.width, frame.height]) + svg;
@@ -28732,6 +28939,65 @@ ${svg}
28732
28939
  return files;
28733
28940
  }
28734
28941
 
28942
+ function exportFrameStylePhase(lyr, dataset, opts, phase) {
28943
+ var copy = copyLayer(lyr);
28944
+ var rec = copy.data && copy.data.getRecords()[0];
28945
+ var obj;
28946
+ if (!rec) return '';
28947
+ if (phase == 'background') {
28948
+ if (!rec.fill || rec.fill == 'none') return '';
28949
+ removeStylePrefix(rec, 'stroke');
28950
+ copy.name = (lyr.name || 'frame') + '-background';
28951
+ } else {
28952
+ if (!rec.stroke || rec.stroke == 'none' ||
28953
+ Number(rec['stroke-width']) <= 0) return '';
28954
+ removeStylePrefix(rec, 'fill');
28955
+ rec.fill = 'none';
28956
+ copy.name = (lyr.name || 'frame') + '-neatline';
28957
+ }
28958
+ obj = exportLayerForSVG(copy, dataset, opts);
28959
+ return stringify(obj);
28960
+ }
28961
+
28962
+ function removeStylePrefix(rec, prefix) {
28963
+ Object.keys(rec).forEach(function(key) {
28964
+ if (key == prefix || key.indexOf(prefix + '-') === 0) {
28965
+ delete rec[key];
28966
+ }
28967
+ });
28968
+ }
28969
+
28970
+ function exportExplicitFrameStyle(frame, context, phase) {
28971
+ var style = context.style || {};
28972
+ var properties = {
28973
+ id: (context.name || 'frame') + '-' + phase,
28974
+ x: 0,
28975
+ y: 0,
28976
+ width: frame.width,
28977
+ height: frame.height
28978
+ };
28979
+ if (phase == 'background') {
28980
+ if (!style.fill || style.fill == 'none') return '';
28981
+ copyProperties(properties, style,
28982
+ ['fill', 'fill-opacity', 'fill-rule', 'opacity']);
28983
+ } else {
28984
+ if (!style.stroke || style.stroke == 'none' ||
28985
+ Number(style['stroke-width']) <= 0) return '';
28986
+ properties.fill = 'none';
28987
+ copyProperties(properties, style, [
28988
+ 'stroke', 'stroke-width', 'stroke-opacity', 'stroke-linecap',
28989
+ 'stroke-linejoin', 'stroke-dasharray', 'opacity'
28990
+ ]);
28991
+ }
28992
+ return stringify({tag: 'rect', properties: properties});
28993
+ }
28994
+
28995
+ function copyProperties(dest, src, names) {
28996
+ names.forEach(function(name) {
28997
+ if (src[name] !== undefined) dest[name] = src[name];
28998
+ });
28999
+ }
29000
+
28735
29001
  function getSvgFileBase(dataset, opts) {
28736
29002
  var file = opts.file || getOutputFileBase(dataset) + '.svg';
28737
29003
  return file.replace(/\.svg$/i, '');
@@ -28807,6 +29073,10 @@ ${svg}
28807
29073
 
28808
29074
  function exportLayerForSVG(lyr, dataset, opts) {
28809
29075
  var layerObj = getEmptyLayerForSVG(lyr, opts);
29076
+ if (isFrameLayer(lyr, dataset.arcs) && !frameHasExportStyle(lyr)) {
29077
+ layerObj.children = [];
29078
+ return layerObj;
29079
+ }
28810
29080
  if (layerIsRectangle(lyr, dataset.arcs)) {
28811
29081
  lyr = copyLayer(lyr);
28812
29082
  adjustRectangleStyle(lyr);
@@ -28815,6 +29085,14 @@ ${svg}
28815
29085
  return layerObj;
28816
29086
  }
28817
29087
 
29088
+ function frameHasExportStyle(lyr) {
29089
+ var rec = lyr.data && lyr.data.getReadOnlyRecordAt(0);
29090
+ var fields = findStylePropertiesBySymbolGeom(Object.keys(rec || {}), 'polygon');
29091
+ return fields.some(function(name) {
29092
+ return rec[name] !== null && rec[name] !== undefined && rec[name] !== '';
29093
+ });
29094
+ }
29095
+
28818
29096
  function exportRasterLayerForSVG(lyr, frame, opts) {
28819
29097
  var raster = lyr.raster;
28820
29098
  var rendered = renderRasterForSVG(raster, frame, opts);
@@ -42582,6 +42860,45 @@ ${svg}
42582
42860
  // projected coordinate is not read as lat-long
42583
42861
  .option('target', targetOpt);
42584
42862
 
42863
+ // used by GUI
42864
+ parser.command('update-frame')
42865
+ // .describe('update an existing map frame layer')
42866
+ .option('bbox', {
42867
+ describe: 'frame coordinates (xmin,ymin,xmax,ymax)',
42868
+ type: 'bbox'
42869
+ })
42870
+ .option('width', {
42871
+ describe: 'nominal frame width (e.g. 5in, 10cm, 600px)'
42872
+ })
42873
+ .option('height', {
42874
+ describe: 'nominal frame height'
42875
+ })
42876
+ .option('aspect-ratio', {
42877
+ describe: 'fixed page aspect ratio',
42878
+ type: 'number'
42879
+ })
42880
+ .option('auto-aspect', {
42881
+ describe: 'derive page height from the frame extent',
42882
+ type: 'flag'
42883
+ })
42884
+ .option('fix-scale', {
42885
+ describe: 'hold the map scale; derive the size from the new extent',
42886
+ type: 'flag'
42887
+ })
42888
+ .option('offset', {
42889
+ describe: 'padding in display units or pct of width/height, e.g. 5cm 20px 5%',
42890
+ type: 'strings'
42891
+ })
42892
+ .option('offsets', {
42893
+ describe: 'separate offsets for each side, in l,b,r,t order',
42894
+ type: 'strings'
42895
+ })
42896
+ .option('remove', {
42897
+ describe: 'demote the frame to an ordinary rectangle layer',
42898
+ type: 'flag'
42899
+ })
42900
+ .option('target', targetOpt);
42901
+
42585
42902
  // used by GUI
42586
42903
  parser.command('update-label')
42587
42904
  // .describe('move the anchor or curve knots of an existing label')
@@ -42694,7 +43011,7 @@ ${svg}
42694
43011
  type: 'bbox'
42695
43012
  })
42696
43013
  .option('offset', {
42697
- describe: 'padding in display units or pct of width, e.g. 5cm 20px 5%',
43014
+ describe: 'padding in display units or pct of width/height, e.g. 5cm 20px 5%',
42698
43015
  type: 'strings'
42699
43016
  })
42700
43017
  .option('offsets', {
@@ -42702,6 +43019,10 @@ ${svg}
42702
43019
  type: 'strings'
42703
43020
  })
42704
43021
  .option('name', nameOpt)
43022
+ .option('replace', {
43023
+ describe: 'replace the existing map frame',
43024
+ type: 'flag'
43025
+ })
42705
43026
  .option('target', targetOpt);
42706
43027
 
42707
43028
  // .option('height', {
@@ -49327,12 +49648,13 @@ ${svg}
49327
49648
  return layers;
49328
49649
  };
49329
49650
 
49330
- this.addDataset = function(dataset) {
49331
- this.setDefaultTarget(dataset.layers, dataset);
49651
+ this.addDataset = function(dataset, opts) {
49652
+ this.setDefaultTarget(dataset.layers, dataset, opts);
49332
49653
  return this;
49333
49654
  };
49334
49655
 
49335
49656
  this.addDatasets = function(datasets) {
49657
+ assertCatalogCanAddDatasets(this, datasets);
49336
49658
  datasets.forEach(function(dataset) {
49337
49659
  this.addDataset(dataset);
49338
49660
  }, this);
@@ -49361,7 +49683,7 @@ ${svg}
49361
49683
  return defaultTargets;
49362
49684
  };
49363
49685
 
49364
- this.setDefaultTarget = function(layers, dataset) {
49686
+ this.setDefaultTarget = function(layers, dataset, opts) {
49365
49687
  this.setDefaultTargets([{
49366
49688
  // Copy layers array, in case layers is a reference to dataset.layers.
49367
49689
  // This prevents layers that are added to the dataset inside a command from
@@ -49369,12 +49691,19 @@ ${svg}
49369
49691
  // by '-join unmatched unjoined'.
49370
49692
  layers: layers.concat(),
49371
49693
  dataset: dataset
49372
- }]);
49694
+ }], opts);
49373
49695
  };
49374
49696
 
49375
49697
  // arr: array of target objects {layers:[], dataset:{}}
49376
- this.setDefaultTargets = function(arr) {
49698
+ this.setDefaultTargets = function(arr, opts) {
49377
49699
  if (targetsAreSame(defaultTargets, arr)) return;
49700
+ var newDatasets = arr.reduce(function(memo, target) {
49701
+ if (!datasets.includes(target.dataset) && !memo.includes(target.dataset)) {
49702
+ memo.push(target.dataset);
49703
+ }
49704
+ return memo;
49705
+ }, []);
49706
+ assertCatalogCanAddDatasets(this, newDatasets, opts && opts.removeLayers);
49378
49707
  this.captureCatalogBefore({operation: 'setDefaultTargets'});
49379
49708
  arr.forEach(function(target) {
49380
49709
  if (datasets.indexOf(target.dataset) == -1) {
@@ -49824,173 +50153,1132 @@ ${svg}
49824
50153
  return dataset;
49825
50154
  }
49826
50155
 
49827
- var OPERATION = 'update-label';
50156
+ // Returns number of arcs that were removed
50157
+ function editArcs(arcs, onPoint) {
50158
+ var nn2 = [],
50159
+ xx2 = [],
50160
+ yy2 = [],
50161
+ errors = 0,
50162
+ n;
49828
50163
 
49829
- cmd.updateLabel = updateLabel;
50164
+ arcs.forEach(function(arc, i) {
50165
+ editArc(arc, onPoint);
50166
+ });
50167
+ arcs.updateVertexData(nn2, xx2, yy2);
50168
+ return errors;
49830
50169
 
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');
50170
+ function append(p) {
50171
+ if (p) {
50172
+ xx2.push(p[0]);
50173
+ yy2.push(p[1]);
50174
+ n++;
50175
+ }
49846
50176
  }
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)');
50177
+
50178
+ function editArc(arc, cb) {
50179
+ var x, y, xp, yp, retn;
50180
+ var valid = true;
50181
+ var i = 0;
50182
+ n = 0;
50183
+ while (arc.hasNext()) {
50184
+ x = arc.x;
50185
+ y = arc.y;
50186
+ retn = cb(append, x, y, xp, yp, i++);
50187
+ if (retn === false) {
50188
+ valid = false;
50189
+ // assumes that it's ok for the arc iterator to be interrupted.
50190
+ break;
50191
+ }
50192
+ xp = x;
50193
+ yp = y;
50194
+ }
50195
+ if (valid && n == 1) {
50196
+ // only one valid point was added to this arc (invalid)
50197
+ // e.g. this could happen during reprojection.
50198
+ // making this arc empty
50199
+ // error("An invalid arc was created");
50200
+ message("An invalid arc was created");
50201
+ valid = false;
50202
+ }
50203
+ if (valid) {
50204
+ nn2.push(n);
50205
+ } else {
50206
+ // remove any points that were added for an invalid arc
50207
+ while (n-- > 0) {
50208
+ xx2.pop();
50209
+ yy2.pop();
50210
+ }
50211
+ nn2.push(0); // add empty arc (to preserve mapping from paths to arcs)
50212
+ errors++;
50213
+ }
49857
50214
  }
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
50215
  }
49875
50216
 
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);
50217
+ function DatasetEditor(dataset) {
50218
+ var layers = [];
50219
+ var arcs = [];
50220
+
50221
+ this.done = function() {
50222
+ dataset.layers = layers;
50223
+ if (arcs.length) {
50224
+ noteDatasetWillChange(dataset, {operation: 'DatasetEditor.done', unit: 'arcs'});
50225
+ dataset.arcs = new ArcCollection(arcs);
50226
+ markDatasetChanged(dataset, {operation: 'DatasetEditor.done', unit: 'arcs'});
50227
+ withActiveUndoTransaction(null, function() {
50228
+ buildTopology(dataset);
50229
+ });
50230
+ }
50231
+ };
50232
+
50233
+ this.editLayer = function(lyr, cb) {
50234
+ var type = lyr.geometry_type;
50235
+ if (dataset.layers.indexOf(lyr) != layers.length) {
50236
+ error('Layer was edited out-of-order');
50237
+ }
50238
+ if (!type) {
50239
+ layers.push(lyr);
50240
+ return;
50241
+ }
50242
+ var shapes = lyr.shapes.map(function(shape, shpId) {
50243
+ var shape2 = [], retn, input;
50244
+ for (var i=0, n=shape ? shape.length : 0; i<n; i++) {
50245
+ input = type == 'point' ? shape[i] : idsToCoords(shape[i]);
50246
+ retn = cb(input, i, shape);
50247
+ if (!Array.isArray(retn)) continue;
50248
+ if (type == 'point') {
50249
+ shape2.push(retn);
50250
+ } else if (type == 'polygon' || type == 'polyline') {
50251
+ extendPathShape(shape2, retn || []);
50252
+ }
50253
+ }
50254
+ return shape2.length > 0 ? shape2 : null;
50255
+ });
50256
+ noteLayerWillChange(lyr, {operation: 'DatasetEditor.editLayer', unit: 'shapes'});
50257
+ layers.push(Object.assign(lyr, {shapes: shapes}));
50258
+ markLayerChanged(lyr, {operation: 'DatasetEditor.editLayer', unit: 'shapes'});
50259
+ };
50260
+
50261
+ function extendPathShape(shape, parts) {
50262
+ for (var i=0; i<parts.length; i++) {
50263
+ shape.push([arcs.length]);
50264
+ arcs.push(parts[i]);
50265
+ }
49891
50266
  }
49892
- if (!lyr.shapes || !lyr.shapes[id]) {
49893
- stop$1('Feature', id, 'in layer "' + layerName(lyr) + '" has no geometry');
50267
+
50268
+ function idsToCoords(ids) {
50269
+ var coords = [];
50270
+ var iter = dataset.arcs.getShapeIter(ids);
50271
+ while (iter.hasNext()) {
50272
+ coords.push([iter.x, iter.y]);
50273
+ }
50274
+ return coords;
49894
50275
  }
49895
- return id;
49896
50276
  }
49897
50277
 
49898
- function requirePointLayer(lyr) {
49899
- if (!lyr || getFeatureCount(lyr) === 0) {
49900
- stop$1('Missing a target layer containing labels');
50278
+ // Planar densification by an interval
50279
+ function densifyPathByInterval(coords, interval, interpolate) {
50280
+ if (findMaxPathInterval(coords) < interval) return coords;
50281
+ if (!interpolate) {
50282
+ interpolate = getIntervalInterpolator(interval);
49901
50283
  }
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'));
50284
+ var coords2 = [coords[0]], a, b;
50285
+ for (var i=1, n=coords.length; i<n; i++) {
50286
+ a = coords[i-1];
50287
+ b = coords[i];
50288
+ if (geom.distance2D(a[0], a[1], b[0], b[1]) > interval + 1e-4) {
50289
+ appendArr(coords2, interpolate(a, b));
50290
+ }
50291
+ coords2.push(b);
49905
50292
  }
50293
+ return coords2;
49906
50294
  }
49907
50295
 
49908
- function layerName(lyr) {
49909
- return lyr && lyr.name || '[unnamed]';
50296
+ function getIntervalInterpolator(interval) {
50297
+ return function(a, b) {
50298
+ var points = [];
50299
+ // var rev = a[0] == b[0] ? a[1] > b[1] : a[0] > b[0];
50300
+ var dist = geom.distance2D(a[0], a[1], b[0], b[1]);
50301
+ var n = Math.round(dist / interval) - 1;
50302
+ var dx = (b[0] - a[0]) / (n + 1),
50303
+ dy = (b[1] - a[1]) / (n + 1);
50304
+ for (var i=1; i<=n; i++) {
50305
+ points.push([a[0] + dx * i, a[1] + dy * i]);
50306
+ }
50307
+ return points;
50308
+ };
49910
50309
  }
49911
50310
 
49912
- cmd.addShape = addShape;
49913
50311
 
49914
- function addShape(targetLayers, targetDataset, opts) {
49915
- if (targetLayers.length > 1) {
49916
- stop$1('Command expects a single target layer');
50312
+ // Interpolate the same points regardless of segment direction
50313
+ function densifyAntimeridianSegment(a, b, interval) {
50314
+ var y1, y2;
50315
+ var coords = [];
50316
+ var ascending = a[1] < b[1];
50317
+ if (a[0] != b[0]) error('Expected an edge segment');
50318
+ if (ascending) {
50319
+ y1 = a[1];
50320
+ y2 = b[1];
50321
+ } else {
50322
+ y1 = b[1];
50323
+ y2 = a[1];
49917
50324
  }
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];
50325
+ var y = Math.floor(y1 / interval) * interval + interval;
50326
+ while (y < y2) {
50327
+ coords.push([a[0], y]);
50328
+ y += interval;
49926
50329
  }
49927
- // merge into target layer
49928
- return cmd.mergeLayers([targetLyr, outputLyr], {force: true});
50330
+ if (!ascending) coords.reverse();
50331
+ return coords;
49929
50332
  }
49930
50333
 
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;
50334
+ function appendArr(dest, src) {
50335
+ for (var i=0; i<src.length; i++) dest.push(src[i]);
50336
+ }
49937
50337
 
49938
- if (!geom) {
49939
- stop$1('Missing required shape coordinates');
50338
+ function findMaxPathInterval(coords) {
50339
+ var maxSq = 0, intSq, a, b;
50340
+ for (var i=1, n=coords.length; i<n; i++) {
50341
+ a = coords[i-1];
50342
+ b = coords[i];
50343
+ intSq = geom.distanceSq(a[0], a[1], b[0], b[1]);
50344
+ if (intSq > maxSq) maxSq = intSq;
49940
50345
  }
50346
+ return Math.sqrt(maxSq);
50347
+ }
49941
50348
 
49942
- if (geomType == 'point' && geom.type != 'Point') {
49943
- stop$1('Expected point coordinates, received', geom.type);
49944
- }
50349
+ function projectAndDensifyArcs(arcs, proj) {
50350
+ var interval = getDefaultDensifyInterval(arcs, proj);
50351
+ var minIntervalSq = interval * interval * 25;
50352
+ var p;
50353
+ return editArcs(arcs, onPoint);
49945
50354
 
49946
- if (geomType == 'polygon' && geom.type != 'Polygon') {
49947
- stop$1('Expected polygon coordinates, received', geom.type);
49948
- }
50355
+ function onPoint(append, lng, lat, prevLng, prevLat, i) {
50356
+ var pp = p;
50357
+ p = proj(lng, lat);
50358
+ if (!p) return false; // signal that current arc contains an error
49949
50359
 
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);
50360
+ // Don't try to densify shorter segments (optimization)
50361
+ if (i > 0 && geom.distanceSq(p[0], p[1], pp[0], pp[1]) > minIntervalSq) {
50362
+ densifySegment(prevLng, prevLat, pp[0], pp[1], lng, lat, p[0], p[1], proj, interval)
50363
+ .forEach(append);
49956
50364
  }
50365
+ append(p);
49957
50366
  }
49958
-
49959
- return {
49960
- type: 'Feature',
49961
- properties: parseProperties(opts.properties),
49962
- geometry: geom
49963
- };
49964
50367
  }
49965
50368
 
49966
- function parseArg(obj) {
49967
- return typeof obj == 'string' ? JSON.parse(obj) : obj;
50369
+ // Use the median of intervals computed by projecting segments.
50370
+ // We're probing a number of points, because @proj might only be valid in
50371
+ // a sub-region of the dataset bbox (e.g. +proj=tpers)
50372
+ function findDensifyInterval(bounds, xy, proj) {
50373
+ var steps = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9];
50374
+ var points = [];
50375
+ for (var i=0; i<steps.length; i++) {
50376
+ for (var j=0; j<steps.length; j++) {
50377
+ points.push([steps[i], steps[j]]);
50378
+ }
50379
+ }
50380
+ var intervals = points.map(function(pos) {
50381
+ var x = bounds.xmin + bounds.width() * pos[0];
50382
+ var y = bounds.ymin + bounds.height() * pos[1];
50383
+ var a = proj(x, y);
50384
+ var b = proj(x + xy[0], y + xy[1]);
50385
+ return a && b ? geom.distance2D(a[0], a[1], b[0], b[1]) : Infinity;
50386
+ }).filter(function(int) {return int < Infinity;});
50387
+ return intervals.length > 0 ? utils.findMedian(intervals) : Infinity;
49968
50388
  }
49969
50389
 
49970
- function parseProperties(arg) {
49971
- if (!arg) return null;
49972
- return parseArg(arg);
50390
+ // Kludgy way to get a useful interval for densifying a bounding box.
50391
+ // Uses a fraction of average bbox side length)
50392
+ // TODO: improve
50393
+ function findDensifyInterval2(bb, proj) {
50394
+ var a = proj(bb.centerX(), bb.centerY()),
50395
+ c = proj(bb.centerX(), bb.ymin), // right center
50396
+ d = proj(bb.xmax, bb.centerY()); // bottom center
50397
+ var interval = a && c && d ? (geom.distance2D(a[0], a[1], c[0], c[1]) +
50398
+ geom.distance2D(a[0], a[1], d[0], d[1])) / 5000 : Infinity;
50399
+ return interval;
49973
50400
  }
49974
50401
 
49975
- function isArrayOfNumbers(arr) {
49976
- return arr.length >= 2 && arr.every(utils.isNumber);
50402
+ // Returns an interval in projected units
50403
+ function getDefaultDensifyInterval(arcs, proj) {
50404
+ var xy = getAvgSegment2(arcs),
50405
+ bb = arcs.getBounds(),
50406
+ intervalA = findDensifyInterval(bb, xy, proj),
50407
+ intervalB = findDensifyInterval2(bb, proj),
50408
+ interval = Math.min(intervalA, intervalB);
50409
+ if (interval == Infinity) {
50410
+ error('Densification error');
50411
+ }
50412
+ return interval;
49977
50413
  }
49978
50414
 
49979
- function isClosedPath$1(arr) {
49980
- return isArrayOfPoints(arr) && arr.length > 3 && samePoint$2(arr[0], arr[arr.length - 1]);
50415
+ // Interpolate points into a projected line segment if needed to prevent large
50416
+ // deviations from path of original unprojected segment.
50417
+ // @points (optional) array of accumulated points
50418
+ function densifySegment(lng0, lat0, x0, y0, lng2, lat2, x2, y2, proj, interval, points) {
50419
+ // Find midpoint between two endpoints and project it (assumes longitude does
50420
+ // not wrap). TODO Consider bisecting along great circle path -- although this
50421
+ // would not be good for boundaries that follow line of constant latitude.
50422
+ var lng1 = (lng0 + lng2) / 2,
50423
+ lat1 = (lat0 + lat2) / 2,
50424
+ p = proj(lng1, lat1),
50425
+ distSq;
50426
+ if (!p) return; // TODO: consider if this is adequate for handling proj. errors
50427
+ distSq = geom.pointSegDistSq2(p[0], p[1], x0, y0, x2, y2); // sq displacement
50428
+ points = points || [];
50429
+ // Bisect current segment if the projected midpoint deviates from original
50430
+ // segment by more than the @interval parameter.
50431
+ // ... but don't bisect very small segments to prevent infinite recursion
50432
+ // (e.g. if projection function is discontinuous)
50433
+ if (distSq > interval * interval * 0.25 && geom.distance2D(lng0, lat0, lng2, lat2) > 0.01) {
50434
+ densifySegment(lng0, lat0, x0, y0, lng1, lat1, p[0], p[1], proj, interval, points);
50435
+ points.push(p);
50436
+ densifySegment(lng1, lat1, p[0], p[1], lng2, lat2, x2, y2, proj, interval, points);
50437
+ }
50438
+ return points;
49981
50439
  }
49982
50440
 
49983
- function samePoint$2(a, b) {
49984
- return a[0] == b[0] && a[1] == b[1];
49985
- }
50441
+ // Create rectangles around each feature in a layer
50442
+ cmd.rectangles = function(targetLyr, targetDataset, opts) {
50443
+ var crsInfo = getDatasetCrsInfo(targetDataset);
50444
+ var records = targetLyr.data ? targetLyr.data.getRecords() : null;
50445
+ var geometries;
49986
50446
 
49987
- function isArrayOfPoints(arr) {
49988
- return arr.every(isPoint);
49989
- }
50447
+ if (opts.bbox) {
50448
+ geometries = bboxExpressionToGeometries(opts.bbox, targetLyr, targetDataset);
49990
50449
 
49991
- function isPoint(arr) {
49992
- return arr && arr.length == 2 && isArrayOfNumbers(arr);
49993
- }
50450
+ } else {
50451
+ if (!layerHasGeometry$1(targetLyr)) {
50452
+ stop$1("Layer is missing geometric shapes");
50453
+ }
50454
+ geometries = shapesToBoxGeometries(targetLyr, targetDataset, opts);
50455
+ }
50456
+
50457
+ var geojson = {
50458
+ type: 'FeatureCollection',
50459
+ features: geometries.map(function(geom, i) {
50460
+ var rec = records && records[i] || null;
50461
+ if (rec && opts.no_replace) {
50462
+ rec = utils.extend({}, rec); // make a copy
50463
+ }
50464
+ return {
50465
+ type: 'Feature',
50466
+ properties: rec,
50467
+ geometry: geom
50468
+ };
50469
+ })
50470
+ };
50471
+ var dataset = importGeoJSON(geojson, {});
50472
+ setDatasetCrsInfo(dataset, crsInfo);
50473
+ var outputLayers = mergeDatasetsIntoDataset(targetDataset, [dataset]);
50474
+ setOutputLayerName(outputLayers[0], targetLyr, null, opts);
50475
+ return outputLayers;
50476
+ };
50477
+
50478
+
50479
+
50480
+
50481
+ function shapesToBoxGeometries(lyr, dataset, opts) {
50482
+ var crsInfo = getDatasetCrsInfo(dataset);
50483
+ return lyr.shapes.map(function(shp) {
50484
+ var bounds = lyr.geometry_type == 'point' ?
50485
+ getPointFeatureBounds(shp) : dataset.arcs.getMultiShapeBounds(shp);
50486
+ bounds = applyRectangleOptions(bounds, crsInfo.crs, opts);
50487
+ if (!bounds) return null;
50488
+ return bboxToPolygon(bounds.toArray(), opts);
50489
+ });
50490
+ }
50491
+
50492
+ function bboxExpressionToGeometries(exp, lyr, dataset, opts) {
50493
+ var compiled = compileFeatureExpression(exp, lyr, dataset.arcs, {});
50494
+ var n = getFeatureCount(lyr);
50495
+ var result;
50496
+ var geometries = [];
50497
+ for (var i=0; i<n; i++) {
50498
+ result = compiled(i);
50499
+ if (!looksLikeBbox(result)) {
50500
+ stop$1('Invalid bbox value (expected a GeoJSON-type bbox):', result);
50501
+ }
50502
+ geometries.push(bboxToPolygon(result));
50503
+ }
50504
+ return geometries;
50505
+ }
50506
+
50507
+ function looksLikeBbox(o) {
50508
+ if (!o || o.length != 4) return false;
50509
+ if (o.some(isNaN)) return false;
50510
+ if (o[0] <= o[2] == false || o[1] <= o[3] == false) return false;
50511
+ return true;
50512
+ }
50513
+
50514
+ // Create rectangles around one or more target layers
50515
+ //
50516
+ cmd.rectangle2 = function(target, opts) {
50517
+ // if target layer is a rectangle and we're applying frame properties,
50518
+ // turn the target into a frame instead of creating a new rectangle
50519
+ if (target.layers.length == 1 && opts.width &&
50520
+ layerIsRectangle(target.layers[0], target.dataset.arcs)) {
50521
+ applyFrameProperties(target.layers[0], opts);
50522
+ return;
50523
+ }
50524
+ var datasets = target.layers.map(function(lyr) {
50525
+ var dataset = cmd.rectangle({layer: lyr, dataset: target.dataset}, opts);
50526
+ setOutputLayerName(dataset.layers[0], lyr, null, opts);
50527
+ if (!opts.no_replace) {
50528
+ dataset.layers[0].name = lyr.name || dataset.layers[0].name;
50529
+ }
50530
+ return dataset;
50531
+ });
50532
+ return mergeDatasetsIntoDataset(target.dataset, datasets);
50533
+ };
50534
+
50535
+ cmd.rectangle = function(target, opts) {
50536
+ var bounds, crsInfo;
50537
+ if (opts.bbox) {
50538
+ bounds = new Bounds(opts.bbox);
50539
+ crsInfo = target && getDatasetCrsInfo(target.dataset) ||
50540
+ probablyDecimalDegreeBounds(bounds) && getCrsInfo('wgs84') || {};
50541
+ } else if (target) {
50542
+ bounds = getLayerBounds(target.layer, target.dataset.arcs);
50543
+ crsInfo = getDatasetCrsInfo(target.dataset);
50544
+ }
50545
+ bounds = bounds && applyRectangleOptions(bounds, crsInfo.crs, opts);
50546
+ if (!bounds || !bounds.hasBounds()) {
50547
+ stop$1('Missing rectangle extent');
50548
+ }
50549
+ var feature = {
50550
+ type: 'Feature',
50551
+ properties: {},
50552
+ geometry: bboxToPolygon(bounds.toArray(), opts)
50553
+ };
50554
+ var dataset = importGeoJSON(feature, {});
50555
+ applyFrameProperties(dataset.layers[0], opts);
50556
+ dataset.layers[0].name = opts.name || 'rectangle';
50557
+ setDatasetCrsInfo(dataset, crsInfo);
50558
+ return dataset;
50559
+ };
50560
+
50561
+ function applyFrameProperties(lyr, opts) {
50562
+ if (!opts.width) return;
50563
+ if (!lyr.data) initDataTable(lyr);
50564
+ var d = lyr.data.getRecords()[0] || {};
50565
+ var size = parseFrameSize(opts.width);
50566
+ d.width = size.valuePx;
50567
+ d.type = 'frame';
50568
+ d.frame_units = size.units;
50569
+ }
50570
+
50571
+ function applyRectangleOptions(bounds, crs, opts) {
50572
+ var isGeoBox = probablyDecimalDegreeBounds(bounds);
50573
+ if (opts.offset) {
50574
+ bounds = applyBoundsOffset(opts.offset, bounds, crs);
50575
+ }
50576
+ if (bounds.area() > 0 === false) return null;
50577
+ if (opts.aspect_ratio) {
50578
+ bounds = applyAspectRatio(opts.aspect_ratio, bounds);
50579
+ }
50580
+ if (isGeoBox) {
50581
+ bounds = clampToWorldBounds(bounds);
50582
+ }
50583
+ return bounds;
50584
+ }
50585
+
50586
+ // opt: aspect ratio as a single number or a range (e.g. "1,2");
50587
+ function applyAspectRatio(opt, bounds) {
50588
+ var range = String(opt).split(',').map(parseFloat),
50589
+ aspectRatio = bounds.width() / bounds.height(),
50590
+ min, max; // min is height limit, max is width limit
50591
+ if (range.length == 1) {
50592
+ range.push(range[0]);
50593
+ } else if (range[0] > range[1]) {
50594
+ range.reverse();
50595
+ }
50596
+ min = range[0];
50597
+ max = range[1];
50598
+ if (!min && !max) return bounds;
50599
+ if (!min) min = -Infinity;
50600
+ if (!max) max = Infinity;
50601
+ if (aspectRatio < min) {
50602
+ bounds.fillOut(min);
50603
+ } else if (aspectRatio > max) {
50604
+ bounds.fillOut(max);
50605
+ }
50606
+ return bounds;
50607
+ }
50608
+
50609
+ function applyBoundsOffset(offsetOpt, bounds, crs) {
50610
+ var offsets = convertFourSides(offsetOpt, crs, bounds);
50611
+ bounds.padBounds(offsets[0], offsets[1], offsets[2], offsets[3]);
50612
+ return bounds;
50613
+ }
50614
+
50615
+ function bboxToPolygon(bbox, optsArg) {
50616
+ var opts = optsArg || {};
50617
+ var coords = bboxToCoords(bbox);
50618
+ if (opts.interval > 0) {
50619
+ coords = densifyPathByInterval(coords, opts.interval);
50620
+ }
50621
+ return {
50622
+ type: 'Polygon',
50623
+ coordinates: [coords]
50624
+ };
50625
+ }
50626
+
50627
+ var Rectangle = /*#__PURE__*/Object.freeze({
50628
+ __proto__: null,
50629
+ applyAspectRatio: applyAspectRatio,
50630
+ bboxToPolygon: bboxToPolygon
50631
+ });
50632
+
50633
+ cmd.frame = function(catalog, targets, opts) {
50634
+ var widthPx, heightPx, aspectRatio, bbox;
50635
+ var existingFrame = getActiveFrame(catalog);
50636
+ if (opts.width) {
50637
+ widthPx = parseFrameSize(opts.width).valuePx;
50638
+ if (widthPx > 0 === false) {
50639
+ stop$1('Invalid width parameter:', opts.width);
50640
+ }
50641
+ }
50642
+ if (opts.height) {
50643
+ heightPx = parseFrameSize(opts.height).valuePx;
50644
+ if (heightPx > 0 === false) {
50645
+ stop$1('Invalid height parameter:', opts.height);
50646
+ }
50647
+ }
50648
+ if (!widthPx && !heightPx) {
50649
+ widthPx = 800;
50650
+ message('Using default 800px frame width');
50651
+ }
50652
+
50653
+ if (opts.aspect_ratio) {
50654
+ if (opts.aspect_ratio > 0 === false) {
50655
+ stop$1('Invalid aspect-ratio parameter:', opts.aspect_ratio);
50656
+ }
50657
+ if (!heightPx) {
50658
+ heightPx = roundToDigits(widthPx / opts.aspect_ratio, 1);
50659
+ } else if (!widthPx) {
50660
+ widthPx = roundToDigits(heightPx * opts.aspect_ratio, 1);
50661
+ }
50662
+ }
50663
+
50664
+ if (opts.bbox) {
50665
+ bbox = opts.bbox;
50666
+ // TODO: validate
50667
+ } else {
50668
+ var datasets = utils.pluck(targets, 'dataset');
50669
+ requireDatasetsHaveCompatibleCRS(datasets, 'Targets include both projected and unprojected coordinates');
50670
+ bbox = getTargetBbox(targets);
50671
+ if (!bbox) {
50672
+ stop$1('Command target is missing geographical bounds');
50673
+ }
50674
+ }
50675
+
50676
+ applyPercentageOffsets(bbox, opts.offset || opts.offsets);
50677
+ applyPixelOffsets(bbox, widthPx, heightPx, opts.offset || opts.offsets);
50678
+
50679
+ if (bbox[3] - bbox[1] > 0 === false || bbox[2] - bbox[0] > 0 === false) {
50680
+ stop$1('Frame has a collapsed bbox');
50681
+ }
50682
+
50683
+ aspectRatio = (bbox[2] - bbox[0]) / (bbox[3] - bbox[1]);
50684
+ if (!widthPx) {
50685
+ widthPx = roundToDigits(heightPx * aspectRatio, 1);
50686
+ } else if (!heightPx) {
50687
+ heightPx = roundToDigits(widthPx / aspectRatio, 1);
50688
+ }
50689
+
50690
+ var feature = {
50691
+ type: 'Feature',
50692
+ properties: getFrameProperties(widthPx, heightPx, opts),
50693
+ geometry: bboxToPolygon(bbox)
50694
+ };
50695
+ var frameDataset = importGeoJSON(feature);
50696
+ // set CRS from target dataset
50697
+ // TODO: handle case: targets have different projections
50698
+ // TODO: handle case: first target is missing CRS
50699
+ if (targets.length > 0) {
50700
+ var crsInfo = getDatasetCrsInfo(targets[0].dataset);
50701
+ setDatasetCrsInfo(frameDataset, crsInfo);
50702
+ }
50703
+ frameDataset.layers[0].name = opts.name || 'frame';
50704
+ if (existingFrame) {
50705
+ if (!opts.replace) {
50706
+ stop$1('A map frame already exists:', existingFrame.layer.name || '[unnamed frame]');
50707
+ }
50708
+ demoteFrameLayer(existingFrame.layer);
50709
+ }
50710
+ catalog.addDataset(frameDataset);
50711
+ };
50712
+
50713
+ function getFrameProperties(width, height, opts) {
50714
+ var properties = {
50715
+ type: 'frame',
50716
+ width: width,
50717
+ height: height,
50718
+ frame_units: opts.width || opts.height ?
50719
+ parseFrameSize(opts.width || opts.height).units : 'px'
50720
+ };
50721
+ if (opts.aspect_ratio > 0 || opts.width && opts.height) {
50722
+ properties.frame_aspect_ratio = width / height;
50723
+ }
50724
+ return properties;
50725
+ }
50726
+
50727
+ function fillOutBbox(bbox, widthPx, heightPx) {
50728
+ var hpad = 0, vpad = 0;
50729
+ var w = bbox[2] - bbox[0];
50730
+ var h = bbox[3] - bbox[1];
50731
+ if (widthPx / heightPx > w / h) { // need to add horizontal padding
50732
+ hpad = h * widthPx / heightPx - w;
50733
+ } else {
50734
+ vpad = w * heightPx / widthPx - h;
50735
+ }
50736
+ bbox[0] -= hpad / 2;
50737
+ bbox[1] -= vpad / 2;
50738
+ bbox[2] += hpad / 2;
50739
+ bbox[3] += vpad / 2;
50740
+ }
50741
+
50742
+ function applyPercentageOffsets(bbox, arg) {
50743
+ var sides = getPctOffsets(arg);
50744
+ var l = sides[0],
50745
+ b = sides[1],
50746
+ r = sides[2],
50747
+ t = sides[3],
50748
+ w2 = (bbox[2] - bbox[0]) / (1 - l - r),
50749
+ h2 = (bbox[3] - bbox[1]) / (1 - t - b);
50750
+ bbox[0] -= l * w2;
50751
+ bbox[1] -= b * h2;
50752
+ bbox[2] += r * w2;
50753
+ bbox[3] += t * h2;
50754
+ }
50755
+
50756
+ function applyPixelOffsets(bbox, widthPx, heightPx, arg) {
50757
+ var sides = getPixelOffsets(arg);
50758
+ var l = sides[0],
50759
+ b = sides[1],
50760
+ r = sides[2],
50761
+ t = sides[3],
50762
+ scale, w;
50763
+
50764
+ if (widthPx && heightPx) {
50765
+ // add padding to bbox to match pixel dimensions, if needed
50766
+ fillOutBbox(bbox, widthPx, heightPx);
50767
+ }
50768
+
50769
+ w = bbox[2] - bbox[0];
50770
+ bbox[3] - bbox[1];
50771
+
50772
+ if (widthPx) {
50773
+ scale = w / (widthPx - l - r);
50774
+ } else {
50775
+ scale = w / (heightPx - t - b);
50776
+ }
50777
+
50778
+ bbox[0] -= scale * l;
50779
+ bbox[1] -= scale * b;
50780
+ bbox[2] += scale * r;
50781
+ bbox[3] += scale * t;
50782
+ return scale;
50783
+ }
50784
+
50785
+ function getPctOffsets(arg) {
50786
+ return adjustOffsetsArg(arg).map(str => {
50787
+ return str.includes('%') ? parsePercent(str) : 0;
50788
+ });
50789
+ }
50790
+
50791
+ function getPixelOffsets(arg) {
50792
+ return adjustOffsetsArg(arg).map(str => {
50793
+ return str.includes('%') ? 0 : parseSizeParam(str);
50794
+ });
50795
+ }
50796
+
50797
+ function adjustOffsetsArg(arg) {
50798
+ if (!arg) arg = ['0'];
50799
+ if (arg.length == 1) {
50800
+ return [arg[0], arg[0], arg[0], arg[0]];
50801
+ }
50802
+ if (arg.length != 4) {
50803
+ stop$1('List of offsets should have 4 values');
50804
+ }
50805
+ return arg;
50806
+ }
50807
+
50808
+ function getTargetBbox(targets) {
50809
+ var expanded = expandCommandTargets(targets);
50810
+ var bounds = expanded.reduce(function(memo, o) {
50811
+ return memo.mergeBounds(getLayerBounds(o.layer, o.dataset.arcs));
50812
+ }, new Bounds());
50813
+ return bounds.hasBounds() ? bounds.toArray() : null;
50814
+ }
50815
+
50816
+ // Convert width and height args to aspect ratio arg for the rectangle() function
50817
+ function getAspectRatioArg(widthArg, heightArg) {
50818
+ // heightArg is a string containing either a number or a
50819
+ // comma-sep. pair of numbers (range);
50820
+ return heightArg.split(',').map(function(opt) {
50821
+ var height = Number(opt),
50822
+ width = Number(widthArg);
50823
+ if (!opt) return '';
50824
+ return width / height;
50825
+ }).reverse().join(',');
50826
+ }
50827
+
50828
+ var Frame = /*#__PURE__*/Object.freeze({
50829
+ __proto__: null,
50830
+ applyPercentageOffsets: applyPercentageOffsets,
50831
+ applyPixelOffsets: applyPixelOffsets,
50832
+ fillOutBbox: fillOutBbox,
50833
+ getAspectRatioArg: getAspectRatioArg
50834
+ });
50835
+
50836
+ // A frame is a viewport, not data, so it can legitimately extend past the
50837
+ // edges of the globe: fitting a near-global extent to a fixed aspect ratio pads
50838
+ // it with whitespace, and that padding can reach beyond a pole. Those
50839
+ // coordinates have no projected equivalent, and projectAndDensifyArcs() drops a
50840
+ // whole arc when any of its vertices fails -- which took out three sides of the
50841
+ // rectangle and left a zero-area bbox. Clamping first keeps the frame
50842
+ // projectable; the off-globe padding is what gets lost.
50843
+ function clampLatLngBbox(bbox) {
50844
+ return [
50845
+ utils.clamp(bbox[0], -180, 180),
50846
+ utils.clamp(bbox[1], -90, 90),
50847
+ utils.clamp(bbox[2], -180, 180),
50848
+ utils.clamp(bbox[3], -90, 90)
50849
+ ];
50850
+ }
50851
+
50852
+ // Project a rectangular frame as a sampled boundary, rather than projecting
50853
+ // only its corners. Curved projections can reach their x/y extrema between
50854
+ // corners, so a corner-only bbox can crop content that was inside the frame.
50855
+ function getProjectedFrameBounds(bbox, project, srcIsLatLng) {
50856
+ var b = new Bounds(srcIsLatLng ? clampLatLngBbox(bbox) : bbox);
50857
+ var xmin = b.xmin;
50858
+ var ymin = b.ymin;
50859
+ var xmax = b.xmax;
50860
+ var ymax = b.ymax;
50861
+ var arcs = new ArcCollection([
50862
+ [[xmin, ymin], [xmax, ymin]],
50863
+ [[xmax, ymin], [xmax, ymax]],
50864
+ [[xmax, ymax], [xmin, ymax]],
50865
+ [[xmin, ymax], [xmin, ymin]]
50866
+ ]);
50867
+ projectAndDensifyArcs(arcs, project);
50868
+ var projected = arcs.getBounds();
50869
+ if (!projected.hasBounds() || projected.area() > 0 === false) {
50870
+ error('Unable to project map frame');
50871
+ }
50872
+ return projected;
50873
+ }
50874
+
50875
+ // Snapshot frame bounds before the dataset arcs are projected.
50876
+ function getFrameProjectionSnapshots(dataset) {
50877
+ return dataset.layers.reduce(function(memo, lyr) {
50878
+ if (isFrameLayer(lyr, dataset.arcs)) {
50879
+ memo.push({
50880
+ layer: lyr,
50881
+ bbox: getLayerBounds(lyr, dataset.arcs).toArray()
50882
+ });
50883
+ }
50884
+ return memo;
50885
+ }, []);
50886
+ }
50887
+
50888
+ // Rebuild a projected frame through the normal topology-aware replacement
50889
+ // path. This is safe when the frame shares an ArcCollection with other layers.
50890
+ function rebuildProjectedFrameLayer(snapshot, dataset, project, srcIsLatLng) {
50891
+ var bounds = getProjectedFrameBounds(snapshot.bbox, project, srcIsLatLng);
50892
+ var rec = getSingleFrameRecord(snapshot.layer) || {};
50893
+ var fixedAspect = utils.isFiniteNumber(rec.frame_aspect_ratio) &&
50894
+ rec.frame_aspect_ratio > 0 ? rec.frame_aspect_ratio : null;
50895
+ var props;
50896
+ if (fixedAspect) {
50897
+ // Projecting a rectangle's boundary and taking its bounds does not preserve
50898
+ // the rectangle's shape, so a frame that declares a ratio has to be padded
50899
+ // back out to it -- otherwise the geometry and the page disagree.
50900
+ var bbox = bounds.toArray();
50901
+ fillOutBbox(bbox, fixedAspect, 1);
50902
+ bounds = new Bounds(bbox);
50903
+ } else {
50904
+ // Height is derived from the extent, so it goes stale when the extent is
50905
+ // reprojected. getFrameLayerData() recomputes it, but the record is what
50906
+ // gets written to an output file.
50907
+ props = {height: Math.round(rec.width * bounds.height() / bounds.width())};
50908
+ }
50909
+ rebuildFrameLayerGeometry(snapshot.layer, dataset, bounds, props);
50910
+ }
50911
+
50912
+ function rebuildFrameLayerGeometry(lyr, dataset, bounds, props) {
50913
+ bounds = bounds instanceof Bounds ? bounds : new Bounds(bounds);
50914
+ var frameDataset = createFrameRectangleDataset(lyr, bounds, props);
50915
+ // The replacement rectangle is imported from bare GeoJSON, so it carries no
50916
+ // CRS. Merging it into the host dataset runs a projected/unprojected
50917
+ // compatibility check, and during -proj the host still reports its source CRS
50918
+ // while the new rectangle already holds projected coordinates -- so the
50919
+ // check has to compare like with like.
50920
+ setDatasetCrsInfo(frameDataset, getDatasetCrsInfo(dataset));
50921
+ replaceLayerContents(lyr, dataset, frameDataset);
50922
+ }
50923
+
50924
+ // Return an isolated rectangular display layer. Dynamic display projection
50925
+ // must not mutate source shapes or the ArcCollection shared by other layers.
50926
+ function getProjectedFrameDisplayLayer(lyr, arcs, project, srcIsLatLng) {
50927
+ if (!isFrameLayer(lyr, arcs)) return null;
50928
+ var bbox = getLayerBounds(lyr, arcs).toArray();
50929
+ var bounds = getProjectedFrameBounds(bbox, project, srcIsLatLng);
50930
+ var dataset = createFrameRectangleDataset(lyr, bounds);
50931
+ return {
50932
+ layer: dataset.layers[0],
50933
+ arcs: dataset.arcs
50934
+ };
50935
+ }
50936
+
50937
+ function createFrameRectangleDataset(lyr, bounds, props) {
50938
+ var record = lyr.data && lyr.data.getReadOnlyRecordAt(0);
50939
+ var feature = {
50940
+ type: 'Feature',
50941
+ properties: Object.assign({}, record || {}, props || {}),
50942
+ geometry: bboxToPolygon(bounds.toArray())
50943
+ };
50944
+ var dataset = importGeoJSON(feature);
50945
+ dataset.layers[0].name = lyr.name;
50946
+ return dataset;
50947
+ }
50948
+
50949
+ var FrameProjection = /*#__PURE__*/Object.freeze({
50950
+ __proto__: null,
50951
+ getFrameProjectionSnapshots: getFrameProjectionSnapshots,
50952
+ getProjectedFrameBounds: getProjectedFrameBounds,
50953
+ getProjectedFrameDisplayLayer: getProjectedFrameDisplayLayer,
50954
+ rebuildFrameLayerGeometry: rebuildFrameLayerGeometry,
50955
+ rebuildProjectedFrameLayer: rebuildProjectedFrameLayer
50956
+ });
50957
+
50958
+ var OPERATION$1 = 'update-frame';
50959
+
50960
+ function updateFrame(targetLayers, dataset, opts) {
50961
+ if (!targetLayers || targetLayers.length != 1) {
50962
+ stop$1('-update-frame expects a single target layer');
50963
+ }
50964
+ var lyr = targetLayers[0];
50965
+ if (!isFrameLayer(lyr, dataset.arcs)) {
50966
+ stop$1('Target layer is not a map frame:', lyr.name || '[unnamed layer]');
50967
+ }
50968
+ if (opts.remove) {
50969
+ if (hasUpdateOptions(opts)) {
50970
+ stop$1('The remove option cannot be combined with frame update options');
50971
+ }
50972
+ demoteFrameLayer(lyr, OPERATION$1);
50973
+ return;
50974
+ }
50975
+ if (!hasUpdateOptions(opts)) {
50976
+ stop$1('Missing frame update option(s)');
50977
+ }
50978
+ if (opts.aspect_ratio !== undefined && opts.auto_aspect) {
50979
+ stop$1('aspect-ratio= and auto-aspect are mutually exclusive');
50980
+ }
50981
+ if (opts.fix_scale && (opts.width !== undefined || opts.height !== undefined)) {
50982
+ stop$1('fix-scale cannot be combined with width= or height=');
50983
+ }
50984
+
50985
+ var rec = lyr.data.getRecords()[0];
50986
+ var frame = getFrameLayerData(lyr, dataset.arcs);
50987
+ var bbox = (opts.bbox || frame.bbox).slice();
50988
+ var offsetArg = opts.offset || opts.offsets;
50989
+ var fixedAspect = getFixedAspect(rec);
50990
+ var width = frame.width;
50991
+ var units = rec.frame_units || 'px';
50992
+ var widthSize, heightSize, height, effectiveAspect;
50993
+
50994
+ // 1. Aspect mode, resolved first because whether the page shape is fixed
50995
+ // decides whether it may constrain the extent while offsets are applied.
50996
+ if (opts.auto_aspect) {
50997
+ fixedAspect = null;
50998
+ } else if (opts.aspect_ratio !== undefined) {
50999
+ if (!utils.isFiniteNumber(opts.aspect_ratio) || opts.aspect_ratio <= 0) {
51000
+ stop$1('Invalid aspect-ratio parameter:', opts.aspect_ratio);
51001
+ }
51002
+ fixedAspect = opts.aspect_ratio;
51003
+ }
51004
+
51005
+ // 2. Extent
51006
+ if (offsetArg) {
51007
+ applyPercentageOffsets(bbox, offsetArg);
51008
+ // Pass a page height only when the shape is fixed. A derived height
51009
+ // follows the extent, so letting it pad the bbox here would hold a
51010
+ // re-fitted frame to its old shape instead of its new bounds.
51011
+ applyPixelOffsets(bbox, frame.width,
51012
+ fixedAspect ? frame.width / fixedAspect : null, offsetArg);
51013
+ }
51014
+ requireValidBbox(bbox);
51015
+ if (fixedAspect) {
51016
+ fillOutBbox(bbox, fixedAspect, 1);
51017
+ }
51018
+
51019
+ // 3. Nominal size
51020
+ if (opts.width !== undefined) {
51021
+ widthSize = parseFrameSize(opts.width);
51022
+ requirePositiveSize(widthSize.valuePx, 'width', opts.width);
51023
+ }
51024
+ if (opts.height !== undefined) {
51025
+ heightSize = parseFrameSize(opts.height);
51026
+ requirePositiveSize(heightSize.valuePx, 'height', opts.height);
51027
+ }
51028
+ if (widthSize && heightSize) {
51029
+ effectiveAspect = widthSize.valuePx / heightSize.valuePx;
51030
+ if (opts.aspect_ratio !== undefined &&
51031
+ ratiosDiffer(effectiveAspect, opts.aspect_ratio)) {
51032
+ stop$1('Contradictory width, height and aspect-ratio values');
51033
+ }
51034
+ width = widthSize.valuePx;
51035
+ units = widthSize.units;
51036
+ fixedAspect = effectiveAspect;
51037
+ fillOutBbox(bbox, fixedAspect, 1);
51038
+ } else if (widthSize) {
51039
+ width = widthSize.valuePx;
51040
+ units = widthSize.units;
51041
+ } else if (heightSize) {
51042
+ effectiveAspect = fixedAspect || getBboxAspect(bbox);
51043
+ width = heightSize.valuePx * effectiveAspect;
51044
+ units = heightSize.units;
51045
+ } else if (opts.fix_scale) {
51046
+ // Hold ground units per output pixel, so the page grows and shrinks with
51047
+ // the extent instead of the scale changing to fit the extent on it.
51048
+ width = frame.width * getBboxWidth(bbox) / getBboxWidth(frame.bbox);
51049
+ requirePositiveSize(width, 'fix-scale', width);
51050
+ }
51051
+
51052
+ requireValidBbox(bbox);
51053
+ effectiveAspect = fixedAspect || getBboxAspect(bbox);
51054
+ height = Math.round(width / effectiveAspect);
51055
+
51056
+ noteLayerWillChange(lyr, {operation: OPERATION$1, unit: 'shapes'});
51057
+ lyr.data.captureTableBefore({operation: OPERATION$1});
51058
+ rec.width = width;
51059
+ rec.height = height;
51060
+ rec.frame_units = units;
51061
+ if (fixedAspect) {
51062
+ rec.frame_aspect_ratio = fixedAspect;
51063
+ } else {
51064
+ delete rec.frame_aspect_ratio;
51065
+ }
51066
+ lyr.data.markChanged({operation: OPERATION$1});
51067
+ rebuildFrameLayerGeometry(lyr, dataset, new Bounds(bbox));
51068
+ markLayerChanged(lyr, {operation: OPERATION$1, unit: 'shapes'});
51069
+ }
51070
+
51071
+ function hasUpdateOptions(opts) {
51072
+ return opts.bbox !== undefined ||
51073
+ opts.width !== undefined ||
51074
+ opts.height !== undefined ||
51075
+ opts.aspect_ratio !== undefined ||
51076
+ opts.auto_aspect ||
51077
+ opts.fix_scale ||
51078
+ opts.offset !== undefined ||
51079
+ opts.offsets !== undefined;
51080
+ }
51081
+
51082
+ function getBboxWidth(bbox) {
51083
+ return bbox[2] - bbox[0];
51084
+ }
51085
+
51086
+ function getFixedAspect(rec) {
51087
+ return utils.isFiniteNumber(rec.frame_aspect_ratio) &&
51088
+ rec.frame_aspect_ratio > 0 ? rec.frame_aspect_ratio : null;
51089
+ }
51090
+
51091
+ function getBboxAspect(bbox) {
51092
+ return (bbox[2] - bbox[0]) / (bbox[3] - bbox[1]);
51093
+ }
51094
+
51095
+ function requireValidBbox(bbox) {
51096
+ if (!bbox.every(utils.isFiniteNumber) ||
51097
+ bbox[2] - bbox[0] <= 0 ||
51098
+ bbox[3] - bbox[1] <= 0) {
51099
+ stop$1('Frame has a collapsed bbox');
51100
+ }
51101
+ }
51102
+
51103
+ function requirePositiveSize(value, name, arg) {
51104
+ if (!utils.isFiniteNumber(value) || value <= 0) {
51105
+ stop$1('Invalid ' + name + ' parameter:', arg);
51106
+ }
51107
+ }
51108
+
51109
+ function ratiosDiffer(a, b) {
51110
+ return Math.abs(a - b) > 1e-10 * Math.max(1, Math.abs(a), Math.abs(b));
51111
+ }
51112
+
51113
+ cmd.updateFrame = updateFrame;
51114
+
51115
+ var OPERATION = 'update-label';
51116
+
51117
+ cmd.updateLabel = updateLabel;
51118
+
51119
+ // Rewrites the knots of a label that already exists.
51120
+ //
51121
+ // -add-label creates and -style sets properties, so neither can move a label:
51122
+ // a label's knots are its geometry, not an attribute. This is the command the
51123
+ // GUI emits when a knot or an anchor is dragged, so that an interactive move is
51124
+ // an ordinary command with undo and session history rather than a direct
51125
+ // mutation with hand-rolled undo.
51126
+ //
51127
+ // See docs/development/label-tool-design.md.
51128
+ function updateLabel(targetLayers, dataset, opts) {
51129
+ var lyr, id, coords, rec;
51130
+ if (targetLayers.length > 1) {
51131
+ // One coordinate list describes one label, so there is no sensible reading
51132
+ // of this across layers.
51133
+ stop$1('Command expects a single target layer');
51134
+ }
51135
+ lyr = targetLayers[0];
51136
+ id = getTargetId(lyr, opts);
51137
+ coords = parseLabelCoords(opts.coordinates);
51138
+ rec = getLayerDataTable(lyr).getRecords()[id];
51139
+ if (!featureIsLabel(rec)) {
51140
+ // Moving an arbitrary point through a command named -update-label would be
51141
+ // a surprising way to succeed, and in the GUI it would mean the hit test
51142
+ // handed over the wrong feature.
51143
+ stop$1('Feature', id, 'in layer "' + layerName(lyr) + '" is not a label ' +
51144
+ '(it has no label-text property)');
51145
+ }
51146
+ warnIfCurveIsUnprojected(dataset, coords.length);
51147
+ // Undo works from what a command declares it is about to change, so an edit
51148
+ // that does not say so is an edit that cannot be undone -- the shapes array
51149
+ // keeps its identity here, leaving nothing for the transaction to notice
51150
+ // afterwards.
51151
+ noteLayerWillChange(lyr, {operation: OPERATION, unit: 'shapes'});
51152
+ // Point shapes hold their coordinates directly, so this is the whole edit.
51153
+ // The pairs are copied rather than aliased: the GUI drags a live copy of the
51154
+ // knots and would otherwise keep a handle on the layer's own arrays.
51155
+ lyr.shapes[id] = coords.map(function(p) { return [p[0], p[1]]; });
51156
+ markLayerChanged(lyr, {operation: OPERATION, unit: 'shapes'});
51157
+ // Nothing about the label's text is touched, and nothing needs to be: a text
51158
+ // measurement describes the *text*, and moving a knot changes the length of
51159
+ // the *path*. The fit check compares the two at render time, against the
51160
+ // curve as it then stands, so a move can turn a fitting label into an
51161
+ // overflowing one with no measurement going out of date.
51162
+ }
51163
+
51164
+ // Which feature to move. One coordinate list can only describe one label, so
51165
+ // this takes a single id rather than the ids= list that -style accepts.
51166
+ function getTargetId(lyr, opts) {
51167
+ var ids = opts.ids;
51168
+ var id;
51169
+ requirePointLayer(lyr);
51170
+ if (!ids || ids.length === 0) {
51171
+ stop$1('Missing required ids parameter (the feature id of the label to update)');
51172
+ }
51173
+ if (ids.length > 1) {
51174
+ stop$1('Command expects a single feature id; received', ids.join(','));
51175
+ }
51176
+ id = ids[0];
51177
+ if (id >= 0 === false || id >= getFeatureCount(lyr)) {
51178
+ stop$1('Layer "' + layerName(lyr) + '" has no feature with id', id);
51179
+ }
51180
+ if (!lyr.shapes || !lyr.shapes[id]) {
51181
+ stop$1('Feature', id, 'in layer "' + layerName(lyr) + '" has no geometry');
51182
+ }
51183
+ return id;
51184
+ }
51185
+
51186
+ function requirePointLayer(lyr) {
51187
+ if (!lyr || getFeatureCount(lyr) === 0) {
51188
+ stop$1('Missing a target layer containing labels');
51189
+ }
51190
+ if (lyr.geometry_type != 'point') {
51191
+ stop$1('Labels can only be updated in a point layer; target "' +
51192
+ layerName(lyr) + '" contains ' + (lyr.geometry_type || 'no geometry'));
51193
+ }
51194
+ }
51195
+
51196
+ function layerName(lyr) {
51197
+ return lyr && lyr.name || '[unnamed]';
51198
+ }
51199
+
51200
+ cmd.addShape = addShape;
51201
+
51202
+ function addShape(targetLayers, targetDataset, opts) {
51203
+ if (targetLayers.length > 1) {
51204
+ stop$1('Command expects a single target layer');
51205
+ }
51206
+ var targetLyr = targetLayers[0]; // may be undefined
51207
+ var targetType = !opts.no_replace && targetLyr && targetLyr.geometry_type || null;
51208
+ var dataset = importGeoJSON(toFeature(opts, targetType));
51209
+ var outputLyr = mergeDatasetsIntoDataset(targetDataset, [dataset])[0];
51210
+ if (opts.no_replace || !targetLyr) {
51211
+ // create new layer
51212
+ setOutputLayerName(outputLyr, targetLyr && targetLyr.name, null, opts);
51213
+ return [outputLyr];
51214
+ }
51215
+ // merge into target layer
51216
+ return cmd.mergeLayers([targetLyr, outputLyr], {force: true});
51217
+ }
51218
+
51219
+ function toFeature(opts, geomType) {
51220
+ if (opts.geojson) {
51221
+ return parseArg(opts.geojson);
51222
+ }
51223
+
51224
+ var geom = opts.coordinates && parseCoordsAsGeometry(opts.coordinates) || null;
51225
+
51226
+ if (!geom) {
51227
+ stop$1('Missing required shape coordinates');
51228
+ }
51229
+
51230
+ if (geomType == 'point' && geom.type != 'Point') {
51231
+ stop$1('Expected point coordinates, received', geom.type);
51232
+ }
51233
+
51234
+ if (geomType == 'polygon' && geom.type != 'Polygon') {
51235
+ stop$1('Expected polygon coordinates, received', geom.type);
51236
+ }
51237
+
51238
+ if (geomType == 'polyline') {
51239
+ if (geom.type == 'Polygon') {
51240
+ geom.coordinates = geom.coordinates[0];
51241
+ geom.type = 'LineString';
51242
+ } else {
51243
+ stop$1('Expected polyline coordinates, received', geom.type);
51244
+ }
51245
+ }
51246
+
51247
+ return {
51248
+ type: 'Feature',
51249
+ properties: parseProperties(opts.properties),
51250
+ geometry: geom
51251
+ };
51252
+ }
51253
+
51254
+ function parseArg(obj) {
51255
+ return typeof obj == 'string' ? JSON.parse(obj) : obj;
51256
+ }
51257
+
51258
+ function parseProperties(arg) {
51259
+ if (!arg) return null;
51260
+ return parseArg(arg);
51261
+ }
51262
+
51263
+ function isArrayOfNumbers(arr) {
51264
+ return arr.length >= 2 && arr.every(utils.isNumber);
51265
+ }
51266
+
51267
+ function isClosedPath$1(arr) {
51268
+ return isArrayOfPoints(arr) && arr.length > 3 && samePoint$2(arr[0], arr[arr.length - 1]);
51269
+ }
51270
+
51271
+ function samePoint$2(a, b) {
51272
+ return a[0] == b[0] && a[1] == b[1];
51273
+ }
51274
+
51275
+ function isArrayOfPoints(arr) {
51276
+ return arr.every(isPoint);
51277
+ }
51278
+
51279
+ function isPoint(arr) {
51280
+ return arr && arr.length == 2 && isArrayOfNumbers(arr);
51281
+ }
49994
51282
 
49995
51283
  function transposeCoords(arr) {
49996
51284
  var coords = [];
@@ -54277,67 +55565,6 @@ ${svg}
54277
55565
 
54278
55566
  }
54279
55567
 
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
55568
  function makePolylineBuffer(lyr, dataset, opts) {
54342
55569
  time('buffer');
54343
55570
  var spherical = isLatLngCRS(getDatasetCRS(dataset));
@@ -61481,625 +62708,211 @@ ${svg}
61481
62708
  if (data.pointCount === 0) return [];
61482
62709
  return groupPolygonRings(data.pathData, arcs, false).map(function(paths) {
61483
62710
  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);
62711
+ return path.points.map(function(p) {
62712
+ return p.concat();
62713
+ });
62714
+ });
61962
62715
  });
61963
62716
  }
61964
62717
 
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;
62718
+ function getPolygonGeometry(shape, arcs) {
62719
+ var coords = getPolygonMultiPolygonCoords(shape, arcs);
62720
+ return coords.length > 0 ? {
62721
+ type: 'MultiPolygon',
62722
+ coordinates: coords
62723
+ } : null;
61978
62724
  }
61979
62725
 
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;
62726
+ function getBufferDataset(coords) {
62727
+ return importGeoJSON({
62728
+ type: 'GeometryCollection',
62729
+ geometries: [{
62730
+ type: 'MultiPolygon',
62731
+ coordinates: coords
62732
+ }]
62733
+ }, {type: 'polygon'});
61985
62734
  }
61986
62735
 
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;
62736
+ // Union a set of winding-fill offset rings (which self-overlap) into clean,
62737
+ // non-self-overlapping MultiPolygon coordinates, via the winding-number
62738
+ // dissolve. Used by the topological pipeline to feed an ordinary polygon into
62739
+ // the shared mosaic (whose boundary-flood membership cannot resolve the
62740
+ // self-overlapping construction ring directly).
62741
+ function dissolveOffsetRingsToCoords(coords, opts, outlineDissolve) {
62742
+ if (!coords || coords.length === 0) return [];
62743
+ var dataset = getBufferDataset(coords);
62744
+ if (!dataset.arcs) return [];
62745
+ var dissolveOpts = outlineDissolve ?
62746
+ getOutlineBufferDissolveOpts(opts) :
62747
+ Object.assign({}, opts, {winding_fill: true});
62748
+ dissolveBufferDataset2(dataset, dissolveOpts);
62749
+ var lyr = dataset.layers[0];
62750
+ var shape = lyr.shapes && lyr.shapes[0];
62751
+ return shape ? getPolygonMultiPolygonCoords(shape, dataset.arcs) : [];
62752
+ }
62753
+
62754
+ function getBufferMultiPolygonCoords(paths, distance, bufferMaker) {
62755
+ var features, coords = [];
62756
+ if (paths.length === 0) return coords;
62757
+ features = bufferMaker(paths, distance) || [];
62758
+ features.forEach(function(feat) {
62759
+ var geom = feat && feat.geometry;
62760
+ if (geom && geom.type == 'MultiPolygon') {
62761
+ coords = coords.concat(geom.coordinates);
62002
62762
  }
62003
- return dataset;
62004
62763
  });
62005
- return mergeDatasetsIntoDataset(target.dataset, datasets);
62006
- };
62764
+ return coords;
62765
+ }
62007
62766
 
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);
62767
+ function getPolygonBufferPathData(shape, uniqueArcTest) {
62768
+ var data = {paths: [], split: false};
62769
+ (shape || []).forEach(function(path) {
62770
+ var paths = uniqueArcTest ? splitPathAtSharedArcs(path, uniqueArcTest) :
62771
+ [path.concat()];
62772
+ if (paths.length != 1 || paths[0].length != path.length ||
62773
+ !paths[0].every(function(arcId, i) {
62774
+ return arcId == path[i];
62775
+ })) {
62776
+ data.split = true;
62777
+ }
62778
+ data.paths = data.paths.concat(paths);
62779
+ });
62780
+ return data;
62781
+ }
62782
+
62783
+ function splitPathAtSharedArcs(path, uniqueArcTest) {
62784
+ var flags = path.map(uniqueArcTest);
62785
+ var firstShared = flags.indexOf(false);
62786
+ var chains = [];
62787
+ var chain = [];
62788
+ var start, i, arcId;
62789
+ if (firstShared == -1) return [path.concat()];
62790
+ start = (firstShared + 1) % path.length;
62791
+ for (i = 0; i < path.length; i++) {
62792
+ arcId = path[(start + i) % path.length];
62793
+ if (uniqueArcTest(arcId)) {
62794
+ chain.push(arcId);
62795
+ } else if (chain.length > 0) {
62796
+ chains.push(chain);
62797
+ chain = [];
62798
+ }
62017
62799
  }
62018
- bounds = bounds && applyRectangleOptions(bounds, crsInfo.crs, opts);
62019
- if (!bounds || !bounds.hasBounds()) {
62020
- stop$1('Missing rectangle extent');
62800
+ if (chain.length > 0) {
62801
+ chains.push(chain);
62021
62802
  }
62022
- var feature = {
62023
- type: 'Feature',
62024
- properties: {},
62025
- geometry: bboxToPolygon(bounds.toArray(), opts)
62803
+ return chains;
62804
+ }
62805
+
62806
+ function getUniqueArcTest(lyr, arcs) {
62807
+ var classify = getArcClassifier(lyr, arcs, {reusable: true})(function(a, b) {
62808
+ return b == -1 ? 'unique' : null;
62809
+ });
62810
+ return function(arcId) {
62811
+ return !!classify(arcId);
62026
62812
  };
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
- };
62813
+ }
62033
62814
 
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';
62815
+ function ringArea(ring) {
62816
+ var iter = new PointIter(ring);
62817
+ return getSphericalPathArea2(iter);
62040
62818
  }
62041
62819
 
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;
62820
+ function makePointBuffer(lyr, dataset, opts) {
62821
+ var geojson = makePointBufferGeoJSON(lyr, dataset, opts);
62822
+ return importGeoJSON(geojson, {});
62055
62823
  }
62056
62824
 
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);
62825
+ // Make a single geodetic circle
62826
+ function getCircleGeoJSON(center, radius, vertices, opts) {
62827
+ var n = 360;
62828
+ var geod = getGeodeticSegmentFunction(parseCrsString$1('wgs84')); // ?
62829
+ if (opts.inset) {
62830
+ radius -= opts.inset;
62076
62831
  }
62077
- return bounds;
62832
+ return opts.geometry_type == 'polyline' ?
62833
+ getPointBufferLineString([center], radius, n, geod) :
62834
+ getPointBufferPolygon([center], radius, n, geod, true);
62078
62835
  }
62079
62836
 
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;
62837
+ // Convert a point layer to circles
62838
+ function makePointBufferGeoJSON(lyr, dataset, opts) {
62839
+ var vertices = opts.vertices || 72;
62840
+ var distanceFn = getBufferDistanceFunction(lyr, dataset, opts);
62841
+ var crs = getDatasetCRS(dataset);
62842
+ var spherical = isLatLngCRS(crs);
62843
+ var geod = getGeodeticSegmentFunction(crs);
62844
+ var geometries = lyr.shapes.map(function(shape, i) {
62845
+ var dist = distanceFn(i);
62846
+ if (!dist || !shape) return null;
62847
+ return getPointBufferPolygon(shape, dist, vertices, geod, spherical);
62848
+ });
62849
+ // TODO: make sure that importer supports null geometries (nonstandard GeoJSON);
62850
+ return {
62851
+ type: 'GeometryCollection',
62852
+ geometries: geometries
62853
+ };
62084
62854
  }
62085
62855
 
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);
62856
+ function getPointBufferPolygon(points, distance, vertices, geod, spherical) {
62857
+ var rings = [], coords, coords2;
62858
+ if (!points || !points.length) return null;
62859
+ for (var i=0; i<points.length; i++) {
62860
+ coords = getPointBufferCoordinates(points[i], distance, vertices, geod);
62861
+ if (!spherical) {
62862
+ rings.push([coords]);
62863
+ } else if (countCrosses(coords) > 0) {
62864
+ coords2 = removePolygonCrosses([coords]);
62865
+ while (coords2.length > 0) rings.push([coords2.pop()]); // geojson polygon coords, no hole
62866
+ } else if (ringArea(coords) < 0) {
62867
+ // negative spherical area: CCW ring, indicating a circle of >180 degrees
62868
+ // that fully encloses both poles and the antimeridian.
62869
+ // need to add an enclosure around the entire sphere
62870
+ // TODO: compare to distance param as a sanity check
62871
+ rings.push([
62872
+ [[180, 90], [180, -90], [0, -90], [-180, -90], [-180, 90], [0, 90], [180, 90]],
62873
+ coords
62874
+ ]);
62875
+ } else {
62876
+ rings.push([coords]);
62877
+ }
62091
62878
  }
62092
62879
  return {
62093
- type: 'Polygon',
62094
- coordinates: [coords]
62880
+ type: 'MultiPolygon',
62881
+ coordinates: rings
62095
62882
  };
62096
62883
  }
62097
62884
 
62098
- var Rectangle = /*#__PURE__*/Object.freeze({
62099
- __proto__: null,
62100
- applyAspectRatio: applyAspectRatio,
62101
- bboxToPolygon: bboxToPolygon
62102
- });
62885
+ function getPointBufferLineString(points, distance, vertices, geod) {
62886
+ var rings = [], coords;
62887
+ if (!points || !points.length) return null;
62888
+ for (var i=0; i<points.length; i++) {
62889
+ coords = getPointBufferCoordinates(points[i], distance, vertices, geod);
62890
+ coords = removePolylineCrosses(coords);
62891
+ while (coords.length > 0) rings.push(coords.pop());
62892
+ }
62893
+ return rings.length == 1 ? {
62894
+ type: 'LineString',
62895
+ coordinates: rings[0]
62896
+ } : {
62897
+ type: 'MultiLineString',
62898
+ coordinates: rings
62899
+ };
62900
+ }
62901
+
62902
+ // Returns array of [x, y] coordinates in a closed ring
62903
+ function getPointBufferCoordinates(center, meterDist, vertices, geod) {
62904
+ var coords = [],
62905
+ angle = 360 / vertices,
62906
+ theta;
62907
+ for (var i=0; i<vertices; i++) {
62908
+ // offsetting by half a step so 4 sides are flat, not pointy
62909
+ // (looks better on low-vertex circles)
62910
+ theta = (i + 0.5) * angle % 360;
62911
+ coords.push(geod(center[0], center[1], theta, meterDist));
62912
+ }
62913
+ coords.push(coords[0].concat());
62914
+ return coords;
62915
+ }
62103
62916
 
62104
62917
  function getSemiMinorAxis(P) {
62105
62918
  return P.a * Math.sqrt(1 - (P.es || 0));
@@ -63342,6 +64155,22 @@ ${svg}
63342
64155
  // TODO: add more projections
63343
64156
  //
63344
64157
  function expandProjDefn(str, dataset, targetLayers) {
64158
+ return expandProjDefnWithBounds(str, function() {
64159
+ return getBBox(dataset, targetLayers); // TODO: support projected datasets
64160
+ });
64161
+ }
64162
+
64163
+ // Variant for a command with more than one target dataset. Auto-fitted
64164
+ // parameters describe the combined extent of the targets, so that a single
64165
+ // -proj command produces one destination CRS instead of fitting a different
64166
+ // projection to each dataset it happens to visit.
64167
+ function expandProjDefnForTargets(str, targets) {
64168
+ return expandProjDefnWithBounds(str, function() {
64169
+ return getTargetsBBox(targets);
64170
+ });
64171
+ }
64172
+
64173
+ function expandProjDefnWithBounds(str, getTargetBBox) {
63345
64174
  var mproj = require$1('mproj');
63346
64175
  var proj4, params, bbox, isConic2SP, isCentered, isUtm, decimals;
63347
64176
  if (str in mproj.internal.pj_list === false) {
@@ -63353,7 +64182,7 @@ ${svg}
63353
64182
  isUtm = str == 'utm';
63354
64183
  proj4 = '+proj=' + str;
63355
64184
  if (isConic2SP || isCentered || isUtm) {
63356
- bbox = getBBox(dataset, targetLayers); // TODO: support projected datasets
64185
+ bbox = getTargetBBox();
63357
64186
  decimals = getBoundsPrecisionForDisplay(bbox);
63358
64187
  if (isUtm) {
63359
64188
  params = getUtmParams(bbox);
@@ -63366,6 +64195,17 @@ ${svg}
63366
64195
  return proj4;
63367
64196
  }
63368
64197
 
64198
+ function getTargetsBBox(targets) {
64199
+ var bounds = new Bounds();
64200
+ targets.forEach(function(targ) {
64201
+ bounds.mergeBounds(getBBox(targ.dataset, targ.layers));
64202
+ });
64203
+ if (!bounds.hasBounds()) {
64204
+ stop$1('Command target is missing geographical bounds');
64205
+ }
64206
+ return bounds.toArray();
64207
+ }
64208
+
63369
64209
  function getBBox(dataset, targetLayers) {
63370
64210
  var source = targetLayers && targetLayers.length > 0 ? {
63371
64211
  arcs: dataset.arcs,
@@ -63475,6 +64315,7 @@ ${svg}
63475
64315
  var ProjectionParams = /*#__PURE__*/Object.freeze({
63476
64316
  __proto__: null,
63477
64317
  expandProjDefn: expandProjDefn,
64318
+ expandProjDefnForTargets: expandProjDefnForTargets,
63478
64319
  getAutoFitBBox: getAutoFitBBox,
63479
64320
  getCenterParams: getCenterParams,
63480
64321
  getConicParams: getConicParams,
@@ -64936,32 +65777,112 @@ ${svg}
64936
65777
  return a && b && a[0] == b[0] && a[1] == b[1];
64937
65778
  }
64938
65779
 
64939
- cmd.proj = function(dataset, catalog, opts, targetLayers) {
64940
- var srcInfo, destInfo, destStr;
65780
+ // Run -proj over all the targets of a single command.
65781
+ // The destination CRS is resolved once here rather than separately for each
65782
+ // target, for two reasons. Auto-fitted parameters (e.g. the standard parallels
65783
+ // that "lcc" expands to) must describe the whole command target, not whichever
65784
+ // dataset happens to be projected first. And the map frame must receive the
65785
+ // same CRS as the content instead of re-deriving parameters from its own extent.
65786
+ cmd.projTargets = function(targets, catalog, opts) {
65787
+ var destInfo;
65788
+ targets.forEach(function(targ) {
65789
+ applyProjInit(targ.dataset, catalog, opts);
65790
+ });
65791
+ destInfo = resolveProjDestInfo(targets, catalog, opts);
65792
+ targets.forEach(function(targ) {
65793
+ projectTarget(targ.dataset, opts, targ.layers, destInfo);
65794
+ });
65795
+ if (destInfo && destInfo.crs) {
65796
+ projectMapFrame(targets, catalog, opts, destInfo);
65797
+ warnAboutUnprojectedLayers(catalog, destInfo);
65798
+ }
65799
+ };
65800
+
65801
+ cmd.proj = function(dataset, catalog, opts, targetLayers, destInfo) {
65802
+ applyProjInit(dataset, catalog, opts);
65803
+ if (!destInfo) {
65804
+ destInfo = resolveProjDestInfo([{dataset: dataset, layers: targetLayers}], catalog, opts);
65805
+ }
65806
+ projectTarget(dataset, opts, targetLayers, destInfo);
65807
+ };
65808
+
65809
+ function projectTarget(dataset, opts, targetLayers, destInfo) {
64941
65810
  var implicitlyProjectedNames = getImplicitlyTargetedLayerNames(dataset, targetLayers, layerHasGeometry$1);
64942
65811
  // A preserved GeoJSON "crs" member becomes invalid once the CRS changes.
64943
65812
  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);
65813
+ if (!destInfo) return;
65814
+ var didProject = projCmd(dataset, destInfo, opts);
65815
+ if (didProject && implicitlyProjectedNames.length > 0) {
65816
+ message(
65817
+ 'Also projected non-target layer' + utils.pluralSuffix(implicitlyProjectedNames.length) +
65818
+ ' from the same dataset: ' + implicitlyProjectedNames.join(', ')
65819
+ );
64948
65820
  }
65821
+ }
65822
+
65823
+ function applyProjInit(dataset, catalog, opts) {
65824
+ if (!opts.init) return;
65825
+ var srcInfo = fetchCrsInfo(opts.init, catalog);
65826
+ if (!srcInfo.crs) stop$1("Unknown projection source:", opts.init);
65827
+ setDatasetCrsInfo(dataset, srcInfo);
65828
+ }
65829
+
65830
+ // @targets [{dataset, layers}, ...] -- all the targets of one -proj command
65831
+ function resolveProjDestInfo(targets, catalog, opts) {
64949
65832
  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
- }
65833
+ return fetchCrsInfo(opts.match, catalog);
65834
+ }
65835
+ if (!opts.crs) return null;
65836
+ return getCrsInfo(expandProjDefnForTargets(opts.crs, targets));
65837
+ }
65838
+
65839
+ // A map frame is composition state, not content: its rectangle describes a
65840
+ // region of the map's coordinate space, so it has to follow the map's CRS even
65841
+ // when the command targeted only a data layer. The GUI deliberately keeps the
65842
+ // frame out of default targets, so nothing else reaches it.
65843
+ function projectMapFrame(targets, catalog, opts, destInfo) {
65844
+ var frame = catalog ? getActiveFrame(catalog) : null;
65845
+ var frameOpts = opts;
65846
+ var frameCrs;
65847
+ if (!frame) return;
65848
+ if (targets.some(function(targ) { return targ.dataset == frame.dataset; })) {
65849
+ return; // already projected as an ordinary target
65850
+ }
65851
+ frameCrs = getDatasetCrsInfo(frame.dataset).crs;
65852
+ if (frameCrs && crsAreEqual(frameCrs, destInfo.crs)) {
65853
+ return; // already in the destination CRS; say nothing
65854
+ }
65855
+ if (!frameCrs) {
65856
+ warn('Unable to reproject the map frame: its coordinate system is unknown.');
65857
+ return;
64963
65858
  }
64964
- };
65859
+ if (opts.init) {
65860
+ // init= describes the CRS of the targeted data, not the frame's.
65861
+ frameOpts = Object.assign({}, opts);
65862
+ delete frameOpts.init;
65863
+ }
65864
+ cmd.proj(frame.dataset, catalog, frameOpts, frame.dataset.layers, destInfo);
65865
+ message('Also reprojected the map frame.');
65866
+ }
65867
+
65868
+ // -proj only applies to the command's targets, so layers in other datasets are
65869
+ // left behind in the old CRS. That usually surfaces much later as a blank
65870
+ // export or an "unable to combine" error, so name them while the cause is clear.
65871
+ function warnAboutUnprojectedLayers(catalog, destInfo) {
65872
+ var names = [];
65873
+ if (!catalog) return;
65874
+ catalog.getLayers().forEach(function(o) {
65875
+ var crs;
65876
+ if (!layerHasGeometry$1(o.layer) && !layerHasRaster(o.layer)) return;
65877
+ crs = getDatasetCrsInfo(o.dataset).crs;
65878
+ if (crs && !crsAreEqual(crs, destInfo.crs)) {
65879
+ names.push(o.layer.name || '[unnamed]');
65880
+ }
65881
+ });
65882
+ if (names.length === 0) return;
65883
+ warn(`Layer${utils.pluralSuffix(names.length)} not projected by this command: ` +
65884
+ names.join(', ') + '. Use target=* to project all layers.');
65885
+ }
64965
65886
 
64966
65887
  function projCmd(dataset, destInfo, opts) {
64967
65888
  // modify copy of coordinate data when running in web UI, so original shapes
@@ -65082,6 +66003,7 @@ ${svg}
65082
66003
  stop$1('Unable to project from a coordinate system that has no inverse transform');
65083
66004
  }
65084
66005
  var proj = getProjTransform2(src, dest); // v2 returns null points instead of throwing an error
66006
+ var frameSnapshots = getFrameProjectionSnapshots(dataset);
65085
66007
  var badArcs = 0;
65086
66008
  var badPoints = 0;
65087
66009
  var healAntimeridian = isLatLngCRS(src) &&
@@ -65115,6 +66037,10 @@ ${svg}
65115
66037
  cleanProjectedPathLayers(dataset, {heal_antimeridian: healAntimeridian});
65116
66038
  }
65117
66039
 
66040
+ frameSnapshots.forEach(function(snapshot) {
66041
+ rebuildProjectedFrameLayer(snapshot, dataset, proj, isLatLngCRS(src));
66042
+ });
66043
+
65118
66044
  if (badArcs > 0 && !opts.quiet) {
65119
66045
  message(`Removed ${badArcs} ${badArcs == 1 ? 'path' : 'paths'} containing unprojectable vertices.`);
65120
66046
  }
@@ -73064,185 +73990,6 @@ ${svg}
73064
73990
  return parsed[0];
73065
73991
  }
73066
73992
 
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
73993
  // Remove intricate sub-scale detail from a single arc so that -smooth has a
73247
73994
  // clean line to work with, WITHOUT thinning the rest of the line: smooth makes a
73248
73995
  // better approximation of the original when it has more detail to work with, so
@@ -78009,7 +78756,7 @@ ${svg}
78009
78756
  if (lyr) return getFrameLayerData(lyr, dataset.arcs);
78010
78757
  // -frame adds the frame it creates to the catalog as a separate dataset, so a
78011
78758
  // catalog-wide search is needed to find it (-scalebar does the same).
78012
- target = catalog ? findFrame(catalog) : null;
78759
+ target = catalog ? getActiveFrame(catalog) : null;
78013
78760
  return target ? getFrameLayerData(target.layer, target.dataset.arcs) : null;
78014
78761
  }
78015
78762
 
@@ -81404,6 +82151,9 @@ ${svg}
81404
82151
  job.catalog.captureCatalogBefore({operation: 'addLayer'});
81405
82152
  job.catalog.addDataset(cmd.addLayer(targetDataset, opts));
81406
82153
 
82154
+ } else if (name == 'update-frame') {
82155
+ cmd.updateFrame(targetLayers, targetDataset, opts);
82156
+
81407
82157
  } else if (name == 'update-label') {
81408
82158
  cmd.updateLabel(targetLayers, targetDataset, opts);
81409
82159
 
@@ -81625,9 +82375,7 @@ ${svg}
81625
82375
  } else if (name == 'proj') {
81626
82376
  await initProjLibrary(opts);
81627
82377
  job.resumeCommand();
81628
- targets.forEach(function(targ) {
81629
- cmd.proj(targ.dataset, job.catalog, opts, targ.layers);
81630
- });
82378
+ cmd.projTargets(targets, job.catalog, opts);
81631
82379
 
81632
82380
  } else if (name == 'rectangle') {
81633
82381
  if (source || opts.bbox || targets.length === 0) {
@@ -81733,7 +82481,15 @@ ${svg}
81733
82481
  }
81734
82482
 
81735
82483
  if (outputDataset) {
81736
- job.catalog.addDataset(outputDataset); // also sets default target
82484
+ var removedLayers = targetLayers && !opts.no_replace ? targetLayers : [];
82485
+ assertSingleFrameUpdate(
82486
+ job.catalog,
82487
+ outputDataset.layers.map(function(lyr) {
82488
+ return {layer: lyr, dataset: outputDataset};
82489
+ }),
82490
+ removedLayers
82491
+ );
82492
+ job.catalog.addDataset(outputDataset, {removeLayers: removedLayers}); // also sets default target
81737
82493
  outputLayers = outputDataset.layers;
81738
82494
  if (targetLayers && !opts.no_replace) {
81739
82495
  // remove target layers from target dataset
@@ -81743,6 +82499,13 @@ ${svg}
81743
82499
  }
81744
82500
  } else if (outputLayers && targetDataset && outputLayers != targetDataset.layers) {
81745
82501
  // integrate output layers into the target dataset
82502
+ assertSingleFrameUpdate(
82503
+ job.catalog,
82504
+ outputLayers.map(function(lyr) {
82505
+ return {layer: lyr, dataset: targetDataset};
82506
+ }),
82507
+ opts.no_replace ? [] : targetLayers
82508
+ );
81746
82509
  if (opts.no_replace) {
81747
82510
  // make sure commands do not return input layers with 'no_replace' option
81748
82511
  if (!outputLayersAreDifferent(outputLayers, targetLayers || [])) {
@@ -81955,7 +82718,7 @@ ${svg}
81955
82718
  return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
81956
82719
  }
81957
82720
 
81958
- var version = "0.7.62";
82721
+ var version = "0.7.63";
81959
82722
 
81960
82723
  // Parse command line args into commands and run them
81961
82724
  // Function takes an optional Node-style callback. A Promise is returned if no callback is given.
@@ -84639,6 +85402,7 @@ ${svg}
84639
85402
  FileTypes,
84640
85403
  FilterGeom,
84641
85404
  Frame,
85405
+ FrameProjection,
84642
85406
  FrameUtils,
84643
85407
  Furniture,
84644
85408
  Geodesic,