mapshaper 0.7.68 → 0.7.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/www/mapshaper.js CHANGED
@@ -25424,6 +25424,9 @@
25424
25424
  // type/data accessors -- doesn't form an import cycle with the
25425
25425
  // scalebar-aware renderer registry that lives in furniture.mjs.
25426
25426
 
25427
+ // Must match the renderers registered in mapshaper-furniture.mjs
25428
+ var furnitureTypes = ['scalebar'];
25429
+
25427
25430
  // @lyr dataset layer
25428
25431
  function getFurnitureLayerType(lyr) {
25429
25432
  var rec = lyr.data && lyr.data.getReadOnlyRecordAt(0);
@@ -25434,6 +25437,12 @@
25434
25437
  return lyr.data && lyr.data.getReadOnlyRecordAt(0);
25435
25438
  }
25436
25439
 
25440
+ // A data-only layer holding the settings of a map element such as a scalebar
25441
+ function layerIsFurniture(lyr) {
25442
+ return !!lyr && !lyr.geometry_type &&
25443
+ furnitureTypes.includes(getFurnitureLayerType(lyr));
25444
+ }
25445
+
25437
25446
  /*
25438
25447
  {
25439
25448
  width: size[0],
@@ -25629,6 +25638,27 @@
25629
25638
  });
25630
25639
  }
25631
25640
 
25641
+ // Furniture (scalebars, etc.) belongs to a frame by sharing its dataset.
25642
+ // Furniture in a dataset without a frame is an ordinary standalone layer.
25643
+ function getFrameFurnitureLayers(dataset) {
25644
+ if (!dataset || !findFrameLayerInDataset(dataset)) return [];
25645
+ return dataset.layers.filter(layerIsFurniture);
25646
+ }
25647
+
25648
+ function findFrameFurnitureLayer(dataset, type) {
25649
+ return getFrameFurnitureLayers(dataset).find(function(lyr) {
25650
+ return getFurnitureLayerType(lyr) == type;
25651
+ }) || null;
25652
+ }
25653
+
25654
+ // True for a map frame and for the furniture that belongs to it: layers that
25655
+ // describe the map layout rather than holding map content.
25656
+ function isFrameComponentLayer(lyr, dataset) {
25657
+ if (!dataset) return false;
25658
+ if (isFrameLayer(lyr, dataset.arcs)) return true;
25659
+ return layerIsFurniture(lyr) && !!findFrameLayerInDataset(dataset);
25660
+ }
25661
+
25632
25662
  function findFrames(catalog) {
25633
25663
  return catalog.getLayers().filter(function(o) {
25634
25664
  return isFrameLayer(o.layer, o.dataset.arcs);
@@ -25855,6 +25885,7 @@
25855
25885
  demoteFrameLayer: demoteFrameLayer,
25856
25886
  findFrame: findFrame,
25857
25887
  findFrameDataset: findFrameDataset,
25888
+ findFrameFurnitureLayer: findFrameFurnitureLayer,
25858
25889
  findFrameLayer: findFrameLayer,
25859
25890
  findFrameLayerInDataset: findFrameLayerInDataset,
25860
25891
  findFrames: findFrames,
@@ -25863,11 +25894,13 @@
25863
25894
  frameReservedFields: frameReservedFields,
25864
25895
  getActiveFrame: getActiveFrame,
25865
25896
  getFrameData: getFrameData,
25897
+ getFrameFurnitureLayers: getFrameFurnitureLayers,
25866
25898
  getFrameLayerBounds: getFrameLayerBounds,
25867
25899
  getFrameLayerData: getFrameLayerData,
25868
25900
  getFrameSize: getFrameSize,
25869
25901
  getMapFrameMetersPerPixel: getMapFrameMetersPerPixel,
25870
25902
  getSingleFrameRecord: getSingleFrameRecord,
25903
+ isFrameComponentLayer: isFrameComponentLayer,
25871
25904
  isFrameLayer: isFrameLayer,
25872
25905
  isFrameReservedField: isFrameReservedField,
25873
25906
  parseFrameSize: parseFrameSize,
@@ -25875,12 +25908,53 @@
25875
25908
  resolveExportFrame: resolveExportFrame
25876
25909
  });
25877
25910
 
25911
+ // Adds a furniture layer as a standalone dataset, without changing the
25912
+ // default target (so a following -o still exports the content layers)
25878
25913
  function addFurnitureLayer(lyr, catalog) {
25879
25914
  var o = {
25880
25915
  info: {},
25881
25916
  layers: [lyr]
25882
25917
  };
25918
+ catalog.captureCatalogBefore({operation: 'addFurnitureLayer'});
25883
25919
  catalog.getDatasets().push(o);
25920
+ catalog.markCatalogChanged({operation: 'addFurnitureLayer'});
25921
+ }
25922
+
25923
+ // Adds a furniture layer to a frame's dataset, replacing any existing
25924
+ // furniture of the same type. Passing a null layer removes it.
25925
+ // Returns the replaced or removed layer, if any
25926
+ function setFrameFurnitureLayer(frameDataset, type, lyr) {
25927
+ var layers = frameDataset.layers;
25928
+ var idx = layers.findIndex(function(o) {
25929
+ return layerIsFurniture(o) && getFurnitureLayerType(o) == type;
25930
+ });
25931
+ var prev = idx > -1 ? layers[idx] : null;
25932
+ if (!prev && !lyr) return null;
25933
+ noteDatasetWillChange(frameDataset, {operation: 'setFrameFurniture', unit: 'layers'});
25934
+ layers = layers.slice();
25935
+ if (prev && lyr) {
25936
+ lyr.name = prev.name;
25937
+ layers[idx] = lyr;
25938
+ } else if (prev) {
25939
+ layers.splice(idx, 1);
25940
+ } else {
25941
+ layers.push(lyr);
25942
+ }
25943
+ frameDataset.layers = layers;
25944
+ markDatasetChanged(frameDataset, {operation: 'setFrameFurniture', unit: 'layers'});
25945
+ return prev;
25946
+ }
25947
+
25948
+ // Moves a replaced frame's furniture into the dataset of the frame replacing it
25949
+ function moveFrameFurniture(fromDataset, toDataset) {
25950
+ var furniture = fromDataset.layers.filter(layerIsFurniture);
25951
+ if (furniture.length === 0) return;
25952
+ noteDatasetWillChange(fromDataset, {operation: 'moveFrameFurniture', unit: 'layers'});
25953
+ fromDataset.layers = fromDataset.layers.filter(function(lyr) {
25954
+ return !furniture.includes(lyr);
25955
+ });
25956
+ markDatasetChanged(fromDataset, {operation: 'moveFrameFurniture', unit: 'layers'});
25957
+ toDataset.layers = toDataset.layers.concat(furniture);
25884
25958
  }
25885
25959
 
25886
25960
  // Pure geometry-to-SVG-path primitives. Extracted from geojson-to-svg.mjs
@@ -28425,22 +28499,60 @@
28425
28499
  symbolRenderers: symbolRenderers
28426
28500
  });
28427
28501
 
28502
+ // Command options that are stored in the scalebar's data record
28503
+ var scalebarFields = ['label', 'units', 'style', 'font_size', 'font_family',
28504
+ 'font_style', 'font_weight', 'color', 'tic_length', 'bar_width', 'label_offset',
28505
+ 'position', 'label_position', 'dual_units', 'margin'];
28506
+
28507
+ // With a map frame, the scalebar belongs to the frame, and running the
28508
+ // command again replaces it. Without a frame, each run adds a standalone
28509
+ // scalebar layer, which is exported if it is included in the output layers.
28428
28510
  cmd.scalebar = function(catalog, opts) {
28511
+ var frame = getActiveFrame(catalog);
28512
+ if (opts.remove) {
28513
+ if (!frame || !setFrameFurnitureLayer(frame.dataset, 'scalebar', null)) {
28514
+ stop$1('The map frame has no scalebar to remove');
28515
+ }
28516
+ return;
28517
+ }
28518
+ validateScalebarOpts(opts);
28429
28519
  var lyr = getScalebarLayer(opts);
28430
- if (opts.label && !parseScalebarUnits(opts.label)) {
28431
- stop$1(`Expected units of km or miles in scalebar label (received ${opts.label})`);
28520
+ if (frame) {
28521
+ setFrameFurnitureLayer(frame.dataset, 'scalebar', lyr);
28522
+ } else {
28523
+ addFurnitureLayer(lyr, catalog);
28432
28524
  }
28433
- addFurnitureLayer(lyr, catalog);
28434
28525
  };
28435
28526
 
28436
28527
  function getScalebarLayer(opts) {
28437
- var obj = utils.defaults({type: 'scalebar'}, opts);
28528
+ var rec = {type: 'scalebar'};
28529
+ scalebarFields.forEach(function(k) {
28530
+ if (opts[k] !== undefined && opts[k] !== null && opts[k] !== '') {
28531
+ rec[k] = opts[k];
28532
+ }
28533
+ });
28438
28534
  return {
28439
- name: opts.name || 'scalebar',
28440
- data: new DataTable([obj])
28535
+ name: 'scalebar',
28536
+ data: new DataTable([rec])
28441
28537
  };
28442
28538
  }
28443
28539
 
28540
+ function validateScalebarOpts(opts) {
28541
+ if (opts.label) {
28542
+ splitScalebarLabel(opts.label).forEach(function(str) {
28543
+ if (!parseScalebarUnits(str) && !isBareDistance(str)) {
28544
+ stop$1(`Expected a distance or units of km, meters, miles or feet in scalebar label (received ${str})`);
28545
+ }
28546
+ });
28547
+ }
28548
+ if (opts.units && !parseLabelUnits(opts.units)) {
28549
+ stop$1('Unsupported scalebar units:', opts.units);
28550
+ }
28551
+ if (opts.style && !/^[ab]$/i.test(opts.style)) {
28552
+ stop$1('Unsupported scalebar style:', opts.style);
28553
+ }
28554
+ }
28555
+
28444
28556
  function renderScalebar(d, frame) {
28445
28557
  if (!frame.crs) {
28446
28558
  message('Unable to render scalebar: unknown CRS.');
@@ -28453,14 +28565,15 @@
28453
28565
  var opts = getScalebarOpts(d);
28454
28566
  var metersPerPx = getMapFrameMetersPerPixel(frame);
28455
28567
  var frameWidthPx = frame.width;
28456
- var labels = d.label ? d.label.split(',') : null;
28457
- var label1 = labels && labels[0] || null;
28458
- var props1 = parseScalebarLabel(label1 || getAutoScalebarLabel(frameWidthPx, metersPerPx, 'mile'));
28459
- var label2 = opts.style == 'b' && labels && labels[1] || null;
28568
+ var labels = d.label ? splitScalebarLabel(d.label) : [];
28569
+ var units = parseLabelUnits(d.units) || 'mile';
28570
+ var label1 = getScalebarLabel(labels[0], frameWidthPx, metersPerPx, units);
28571
+ var props1 = parseScalebarLabel(label1);
28572
+ var label2 = null;
28460
28573
  var props2, length2;
28461
28574
 
28462
28575
  if (props1.km > 0 === false) {
28463
- message('Unusable scalebar label:', label1);
28576
+ message('Unusable scalebar label:', labels[0] || '');
28464
28577
  return [];
28465
28578
  }
28466
28579
 
@@ -28469,6 +28582,11 @@
28469
28582
  stop$1("Null scalebar length");
28470
28583
  }
28471
28584
 
28585
+ if (opts.style == 'b' && labels[1]) {
28586
+ label2 = completeLabel(labels[1], getOtherUnits(props1.units));
28587
+ } else if (opts.style == 'b' && opts.dual_units) {
28588
+ label2 = getAutoSecondLabel(length1, metersPerPx, getOtherUnits(props1.units));
28589
+ }
28472
28590
  if (label2) {
28473
28591
  props2 = parseScalebarLabel(label2);
28474
28592
  length2 = Math.round(props2.km / metersPerPx * 1000);
@@ -28521,8 +28639,13 @@
28521
28639
  };
28522
28640
 
28523
28641
  function getScalebarOpts(d) {
28524
- var style = d.style == 'b' || d.style == 'B' ? 'b' : 'a';
28525
- return Object.assign({}, defaultOpts, styleOpts[style], d, {style: style});
28642
+ var style = d.style == 'b' || d.style == 'B' || !d.style && d.dual_units ? 'b' : 'a';
28643
+ var opts = Object.assign({}, defaultOpts, styleOpts[style]);
28644
+ Object.keys(d).forEach(function(k) {
28645
+ if (d[k] !== undefined && d[k] !== null && d[k] !== '') opts[k] = d[k];
28646
+ });
28647
+ opts.style = style;
28648
+ return opts;
28526
28649
  }
28527
28650
 
28528
28651
  function renderAsSvg(length, text, length2, text2, opts) {
@@ -28540,12 +28663,11 @@
28540
28663
  };
28541
28664
  }
28542
28665
 
28543
- // TODO: generalize to other kinds of furniture as they are developed
28544
28666
  function getScalebarPosition(opts) {
28545
28667
  var pos = opts.position || 'top-left';
28546
28668
  return {
28547
- ypos: pos.includes('top') ? 'top' : 'bottom',
28548
- xpos: pos.includes('left') ? 'left' : 'right'
28669
+ ypos: pos.includes('bottom') ? 'bottom' : 'top',
28670
+ xpos: pos.includes('right') ? 'right' : 'left'
28549
28671
  };
28550
28672
  }
28551
28673
 
@@ -28559,12 +28681,16 @@
28559
28681
  'label-text': text,
28560
28682
  'font-size': opts.font_size,
28561
28683
  'text-anchor': labelPos.anchor,
28562
- 'dominant-baseline': labelPos.ypos == 'top' ? 'auto' : 'hanging'
28684
+ 'dominant-baseline': labelPos.ypos == 'top' ? 'auto' : 'hanging',
28563
28685
  //// 'dominant-baseline': labelPos == 'top' ? 'text-after-edge' : 'text-before-edge'
28564
28686
  // 'text-after-edge' is buggy in Safari and unsupported by Illustrator,
28565
28687
  // so I'm using 'hanging' and 'auto', which seem to be well supported.
28566
28688
  // downside: requires a kludgy multiplier to calculate scalebar height (see above)
28567
28689
  };
28690
+ if (opts.color) labelOpts.fill = opts.color;
28691
+ if (opts.font_family) labelOpts['font-family'] = opts.font_family;
28692
+ if (opts.font_style) labelOpts['font-style'] = opts.font_style;
28693
+ if (opts.font_weight) labelOpts['font-weight'] = opts.font_weight;
28568
28694
  return symbolRenderers.label(labelOpts, anchorX, anchorY);
28569
28695
  }
28570
28696
 
@@ -28624,7 +28750,7 @@
28624
28750
  }
28625
28751
  var bar = importMultiLineString(coords);
28626
28752
  Object.assign(bar.properties, {
28627
- stroke: 'black',
28753
+ stroke: opts.color || 'black',
28628
28754
  fill: 'none',
28629
28755
  'stroke-width': opts.bar_width,
28630
28756
  'stroke-linecap': 'butt',
@@ -28633,35 +28759,132 @@
28633
28759
  return bar;
28634
28760
  }
28635
28761
 
28636
- // unit: 'km' || 'mile'
28762
+ // Kilometers per unit
28763
+ var unitSizes = {
28764
+ km: 1,
28765
+ m: 0.001,
28766
+ mile: 1.60934,
28767
+ ft: 0.0003048
28768
+ };
28769
+
28770
+ // Candidate lengths for automatic labels, in ascending order of distance
28771
+ var autoLengths = {
28772
+ imperial: [
28773
+ ['ft', '10 20 25 50 100 200 250 500'],
28774
+ // note: removed 1.5 12 and 1,200
28775
+ ['mile', '1/8 1/5 1/4 1/2 1 2 3 4 5 8 10 15 20 25 30 40 50 75 100 150 200 250 ' +
28776
+ '300 350 400 500 750 1,000 1,500 2,000 2,500 3,000 4,000 5,000']
28777
+ ],
28778
+ metric: [
28779
+ ['m', '5 10 20 25 50 100 200 250 500'],
28780
+ ['km', '1 2 3 4 5 8 10 15 20 25 30 40 50 75 100 150 200 250 300 350 ' +
28781
+ '400 500 750 1,000 1,500 2,000 2,500 3,000 4,000 5,000']
28782
+ ]
28783
+ };
28784
+
28785
+ function getAutoLabels(system) {
28786
+ return autoLengths[system].reduce(function(memo, group) {
28787
+ return memo.concat(group[1].split(' ').map(function(str) {
28788
+ return formatDistanceLabel(str, group[0]);
28789
+ }));
28790
+ }, []);
28791
+ }
28792
+
28793
+ function getUnitSystem(units) {
28794
+ return units == 'km' || units == 'm' ? 'metric' : 'imperial';
28795
+ }
28796
+
28797
+ function getOtherUnits(units) {
28798
+ return getUnitSystem(units) == 'metric' ? 'mile' : 'km';
28799
+ }
28800
+
28801
+ // The unit system of the units= option: returns 'km', 'mile' or ''
28802
+ function parseUnitsOption(str) {
28803
+ var units = parseLabelUnits(str);
28804
+ return units ? (getUnitSystem(units) == 'metric' ? 'km' : 'mile') : '';
28805
+ }
28806
+
28807
+ // Accepts the units= option, which also gives the units of a label that is a
28808
+ // bare number; returns 'km', 'm', 'mile', 'ft' or ''
28809
+ function parseLabelUnits(str) {
28810
+ var s = String(str || '').toLowerCase();
28811
+ if (/^(km|kilomet(er|re)s?|metric)$/.test(s)) return 'km';
28812
+ if (/^(m|meters?|metres?)$/.test(s)) return 'm';
28813
+ if (/^(mi|miles?|imperial)$/.test(s)) return 'mile';
28814
+ if (/^(ft|feet)$/.test(s)) return 'ft';
28815
+ return '';
28816
+ }
28817
+
28818
+ // Splits a dual-unit label like "100 km,50 miles". Only a comma after the units
28819
+ // separates two distances, so "1,000 km" and a bare "1,500" stay whole.
28820
+ function splitScalebarLabel(label) {
28821
+ return String(label).split(/(?<=[^\d\s]),\s*/).map(function(str) {
28822
+ return str.trim();
28823
+ }).filter(Boolean);
28824
+ }
28825
+
28826
+ // A distance with no units, e.g. "150" or "1/2" or "1,000"
28827
+ function isBareDistance(str) {
28828
+ return /^[\d\s.,/]+$/.test(str) && parseScalebarNumber(str) > 0;
28829
+ }
28830
+
28831
+ // Adds units to a label that is a bare number
28832
+ function completeLabel(label, units) {
28833
+ return isBareDistance(label) ? formatDistanceLabel(label.trim(), units) : label;
28834
+ }
28835
+
28836
+ // label: a label from the label= option, or empty for an automatic label
28837
+ function getScalebarLabel(label, frameWidthPx, metersPerPx, defaultUnits) {
28838
+ var units;
28839
+ if (label && parseScalebarNumber(label) > 0) return completeLabel(label, defaultUnits);
28840
+ // A label with units but no number gets an automatic length in those units
28841
+ units = label && parseScalebarUnits(label) || defaultUnits;
28842
+ return getAutoScalebarLabel(frameWidthPx, metersPerPx, units);
28843
+ }
28844
+
28845
+ // unit: 'km' || 'mile' (or 'm', 'ft' for the same systems)
28846
+ // The longest length that fits within 20% of the map's width, or the shortest
28847
+ // that is at least 70px long if the map is too narrow for that
28637
28848
  function getAutoScalebarLabel(mapWidth, metersPerPx, unit) {
28638
- var minWidth = 70; // 100; // TODO: vary min size based on map width
28639
- var minKm = metersPerPx * minWidth / 1000;
28640
- // note: removed 1.5 12 and 1,200
28641
- var options = ('1/8 1/5 1/4 1/2 1 2 3 4 5 8 10 15 20 25 30 40 50 75 ' +
28642
- '100 150 200 250 300 350 400 500 750 1,000 1,500 2,000 ' +
28643
- '2,500 3,000 4,000 5,000').split(' ');
28644
- return options.reduce(function(memo, str) {
28645
- if (memo) return memo;
28646
- var label = formatDistanceLabel(str, unit);
28647
- if (parseScalebarLabelToKm(label) > minKm) {
28648
- return label;
28649
- }
28650
- }, null) || '';
28849
+ var minKm = metersPerPx * 70 / 1000;
28850
+ var maxKm = metersPerPx * mapWidth * 0.2 / 1000;
28851
+ var labels = getAutoLabels(getUnitSystem(unit));
28852
+ var longest = labels.filter(function(label) {
28853
+ var km = parseScalebarLabelToKm(label);
28854
+ return km >= minKm && km <= maxKm;
28855
+ }).pop();
28856
+ return longest || labels.find(function(label) {
28857
+ return parseScalebarLabelToKm(label) >= minKm;
28858
+ }) || '';
28859
+ }
28860
+
28861
+ // The longest automatic length in the given units that fits within the
28862
+ // first part of a dual-unit scalebar
28863
+ function getAutoSecondLabel(maxPx, metersPerPx, unit) {
28864
+ var maxKm = maxPx * metersPerPx / 1000;
28865
+ var minKm = maxKm / 4;
28866
+ var labels = getAutoLabels(getUnitSystem(unit)).filter(function(label) {
28867
+ var km = parseScalebarLabelToKm(label);
28868
+ return km <= maxKm && km >= minKm;
28869
+ });
28870
+ return labels.pop() || null;
28651
28871
  }
28652
28872
 
28653
28873
  function formatDistanceLabel(numStr, unit) {
28654
28874
  var num = parseScalebarNumber(numStr);
28655
- var unitStr = unit == 'km' && 'KM' || num > 1 && 'MILES' || 'MILE';
28875
+ var unitStr = unit == 'km' && 'KM' ||
28876
+ unit == 'm' && (num > 1 ? 'METERS' : 'METER') ||
28877
+ unit == 'ft' && (num > 1 ? 'FEET' : 'FOOT') ||
28878
+ num > 1 && 'MILES' || 'MILE';
28656
28879
  return numStr + ' ' + unitStr;
28657
28880
  }
28658
28881
 
28659
- // See test/mapshaper-scalebar.js for examples of supported formats
28882
+ // See test/scalebar-test.mjs for examples of supported formats
28660
28883
  function parseScalebarLabelToKm(str) {
28661
28884
  var units = parseScalebarUnits(str);
28662
28885
  var value = parseScalebarNumber(str);
28663
28886
  if (!units || !value) return NaN;
28664
- return units == 'mile' ? value * 1.60934 : value;
28887
+ return value * unitSizes[units];
28665
28888
  }
28666
28889
 
28667
28890
  function parseScalebarLabel(label) {
@@ -28669,7 +28892,7 @@
28669
28892
  var units = label ? parseScalebarUnits(label) : 'mile';
28670
28893
  var km = NaN;
28671
28894
  if (units && num) {
28672
- km = units == 'mile' ? num * 1.60934 : num;
28895
+ km = num * unitSizes[units];
28673
28896
  }
28674
28897
  return {
28675
28898
  number: num,
@@ -28678,11 +28901,14 @@
28678
28901
  };
28679
28902
  }
28680
28903
 
28904
+ // Returns 'km', 'm', 'mile', 'ft' or ''
28681
28905
  function parseScalebarUnits(str) {
28682
- var isMiles = /(miles?|mi[.]?|英里)$/.test(str.toLowerCase());
28683
- var isKm = /(k\.m\.|km|kilometers?|kilom.tres?|公里)$/.test(str.toLowerCase());
28684
- var units = isMiles && 'mile' || isKm && 'km' || '';
28685
- return units;
28906
+ var s = String(str).trim().toLowerCase();
28907
+ if (/(miles?|mi[.]?|英里)$/.test(s)) return 'mile';
28908
+ if (/(k\.m\.|km|kilomet(er|re)s?|kilom.tres?|公里)$/.test(s)) return 'km';
28909
+ if (/(^|[\d\s.])(feet|foot|ft[.]?|英尺)$/.test(s)) return 'ft';
28910
+ if (/(^|[\d\s.])(m|m\.|meters?|metres?|mètres?|米)$/.test(s)) return 'm';
28911
+ return '';
28686
28912
  }
28687
28913
 
28688
28914
  function parseScalebarNumber(str) {
@@ -28702,8 +28928,12 @@
28702
28928
  __proto__: null,
28703
28929
  formatDistanceLabel: formatDistanceLabel,
28704
28930
  getScalebarLayer: getScalebarLayer,
28931
+ parseLabelUnits: parseLabelUnits,
28705
28932
  parseScalebarLabelToKm: parseScalebarLabelToKm,
28706
- renderScalebar: renderScalebar
28933
+ parseScalebarUnits: parseScalebarUnits,
28934
+ parseUnitsOption: parseUnitsOption,
28935
+ renderScalebar: renderScalebar,
28936
+ splitScalebarLabel: splitScalebarLabel
28707
28937
  });
28708
28938
 
28709
28939
  var furnitureRenderers = {
@@ -28725,24 +28955,33 @@
28725
28955
  function renderFurnitureLayer(lyr, frame) {
28726
28956
  var d = getFurnitureLayerData(lyr);
28727
28957
  var renderer = furnitureRenderers[d.type];
28958
+ var problem;
28728
28959
  if (!renderer) {
28729
28960
  stop$1('Missing renderer for', d.type, 'element');
28730
28961
  }
28731
- if (!frame.crs) {
28732
- stop$1(`Unable to render ${d.type} (unknown map projection)`);
28733
- }
28734
- if (!isProjectedCRS(frame.crs)) {
28735
- stop$1(`Unable to render ${d.type} (map is unprojected)`);
28962
+ problem = getFurnitureFrameProblem(frame);
28963
+ if (problem) {
28964
+ stop$1(`Unable to render ${d.type} (${problem})`);
28736
28965
  }
28737
28966
  return renderer(d, frame) || [];
28738
28967
  }
28739
28968
 
28969
+ // Returns a reason why furniture can't be drawn in a frame, or null
28970
+ function getFurnitureFrameProblem(frame) {
28971
+ if (!frame) return 'missing map frame';
28972
+ if (!frame.crs) return 'unknown map projection';
28973
+ if (!isProjectedCRS(frame.crs)) return 'map is unprojected';
28974
+ return null;
28975
+ }
28976
+
28740
28977
  var Furniture = /*#__PURE__*/Object.freeze({
28741
28978
  __proto__: null,
28979
+ getFurnitureFrameProblem: getFurnitureFrameProblem,
28742
28980
  getFurnitureLayerData: getFurnitureLayerData,
28743
28981
  getFurnitureLayerType: getFurnitureLayerType,
28744
28982
  isFurnitureLayer: isFurnitureLayer,
28745
28983
  layerHasFurniture: layerHasFurniture,
28984
+ layerIsFurniture: layerIsFurniture,
28746
28985
  renderFurnitureLayer: renderFurnitureLayer
28747
28986
  });
28748
28987
 
@@ -39199,6 +39438,9 @@ ${svg}
39199
39438
  async function exportDatasets(datasets, opts) {
39200
39439
  var format = getOutputFormat(datasets[0], opts);
39201
39440
  var files;
39441
+ if (format != 'svg' && format != PACKAGE_EXT) {
39442
+ datasets = removeFurnitureLayers(datasets);
39443
+ }
39202
39444
  validateRasterExportFormat(datasets, format);
39203
39445
  if (format != 'geoparquet' && opts.level !== undefined) {
39204
39446
  error('The level= option only applies to GeoParquet output');
@@ -39274,6 +39516,25 @@ ${svg}
39274
39516
  return files;
39275
39517
  }
39276
39518
 
39519
+ // Scalebars and other map furniture only have a meaning in SVG output
39520
+ function removeFurnitureLayers(datasets) {
39521
+ var hasFurniture = datasets.some(function(dataset) {
39522
+ return dataset.layers.some(layerIsFurniture);
39523
+ });
39524
+ if (!hasFurniture) return datasets;
39525
+ datasets = datasets.map(function(dataset) {
39526
+ return utils.defaults({
39527
+ layers: dataset.layers.filter(function(lyr) { return !layerIsFurniture(lyr); })
39528
+ }, dataset);
39529
+ }).filter(function(dataset) {
39530
+ return dataset.layers.length > 0;
39531
+ });
39532
+ if (datasets.length === 0) {
39533
+ stop$1('Map furniture layers (e.g. scalebars) can only be exported as SVG');
39534
+ }
39535
+ return datasets;
39536
+ }
39537
+
39277
39538
  function validateRasterExportFormat(datasets, format) {
39278
39539
  if (format == 'geotiff') {
39279
39540
  if (datasetsHaveVectorLayers(datasets)) {
@@ -44131,10 +44392,13 @@ ${svg}
44131
44392
  .option('target', targetOpt);
44132
44393
 
44133
44394
  parser.command('scalebar')
44134
- .describe('add a simple scale bar to SVG output')
44395
+ .describe('add a scale bar to the map frame and SVG output')
44135
44396
  .option('label', {
44136
44397
  DEFAULT: true,
44137
- describe: 'distance label, e.g. "35 miles"'
44398
+ describe: 'distance label, e.g. "35 miles" (default is automatic)'
44399
+ })
44400
+ .option('units', {
44401
+ describe: 'units of a bare-number or automatic label: km, meters, miles or feet (default is miles)'
44138
44402
  })
44139
44403
  .option('style', {
44140
44404
  describe: 'two options: a or b'
@@ -44142,6 +44406,18 @@ ${svg}
44142
44406
  .option('font-size', {
44143
44407
  type: 'number'
44144
44408
  })
44409
+ .option('font-family', {
44410
+ describe: 'font family of labels'
44411
+ })
44412
+ .option('font-style', {
44413
+ describe: 'font style of labels, e.g. italic'
44414
+ })
44415
+ .option('font-weight', {
44416
+ describe: 'font weight of labels, e.g. bold'
44417
+ })
44418
+ .option('color', {
44419
+ describe: 'color of bar and labels (default is black)'
44420
+ })
44145
44421
  .option('tic-length', {
44146
44422
  describe: 'length of tic marks (style b)',
44147
44423
  type: 'number'
@@ -44154,18 +44430,22 @@ ${svg}
44154
44430
  type: 'number'
44155
44431
  })
44156
44432
  .option('position', {
44157
- describe: 'e.g. bottom-right (default is top-left)'
44433
+ describe: 'top-left (default), top-right, bottom-left or bottom-right'
44158
44434
  })
44159
44435
  .option('label-position', {
44160
44436
  describe: 'top, bottom, top-center (style a), etc'
44161
44437
  })
44162
44438
  .option('dual-units', {
44163
- // describe: 'display both metric and imperial units',
44439
+ describe: 'show both metric and imperial units (style b)',
44164
44440
  type: 'flag'
44165
44441
  })
44166
44442
  .option('margin', {
44167
44443
  describe: 'offset in pixels from edge of map',
44168
44444
  type: 'number'
44445
+ })
44446
+ .option('remove', {
44447
+ describe: 'remove the scale bar from the map frame',
44448
+ type: 'flag'
44169
44449
  });
44170
44450
 
44171
44451
  parser.command('shape')
@@ -52161,8 +52441,10 @@ ${svg}
52161
52441
  // May return NaN for a bbox the frame can't be fitted to.
52162
52442
  // @opts.ignore_symbols fit to the coordinates alone
52163
52443
  function getFrameContentBbox(targets, getScale, opts) {
52444
+ // a data-only layer has no bounds
52164
52445
  var bounds = targets.reduce(function(memo, o) {
52165
- return memo.mergeBounds(getLayerBounds(o.layer, o.dataset.arcs));
52446
+ var b = getLayerBounds(o.layer, o.dataset.arcs);
52447
+ return b ? memo.mergeBounds(b) : memo;
52166
52448
  }, new Bounds());
52167
52449
  var raw, items, bbox;
52168
52450
  if (!bounds.hasBounds()) return null;
@@ -52309,6 +52591,11 @@ ${svg}
52309
52591
  var extent = getFrameExtent(contentBbox, widthPx, heightPx, opts);
52310
52592
  return getFrameScale(extent.bbox, extent.width, getFixedAspect$1(extent, opts));
52311
52593
  }, opts);
52594
+ if (!bbox && !expandCommandTargets(targets).some(function(o) {
52595
+ return layerHasGeometry$1(o.layer);
52596
+ })) {
52597
+ stop$1('Unable to fit a frame to a layer with no geometry. Target a layer containing shapes, or use the bbox= option.');
52598
+ }
52312
52599
  if (!bbox) {
52313
52600
  stop$1('Command target is missing geographical bounds');
52314
52601
  }
@@ -52332,14 +52619,17 @@ ${svg}
52332
52619
  var crsInfo = getDatasetCrsInfo(targets[0].dataset);
52333
52620
  setDatasetCrsInfo(frameDataset, crsInfo);
52334
52621
  }
52335
- frameDataset.layers[0].name = opts.name || 'frame';
52622
+ var frameLyr = frameDataset.layers[0];
52623
+ frameLyr.name = opts.name || 'frame';
52336
52624
  if (existingFrame) {
52337
52625
  if (!opts.replace) {
52338
52626
  stop$1('A map frame already exists:', existingFrame.layer.name || '[unnamed frame]');
52339
52627
  }
52628
+ moveFrameFurniture(existingFrame.dataset, frameDataset);
52340
52629
  demoteFrameLayer(existingFrame.layer);
52341
52630
  }
52342
- catalog.addDataset(frameDataset);
52631
+ // target the frame, not any furniture that came with it
52632
+ catalog.setDefaultTarget([frameLyr], frameDataset);
52343
52633
  };
52344
52634
 
52345
52635
  // The frame's extent and nominal size, from the extent of its content and the
@@ -84111,7 +84401,7 @@ ${svg}
84111
84401
  return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
84112
84402
  }
84113
84403
 
84114
- var version = "0.7.68";
84404
+ var version = "0.7.69";
84115
84405
 
84116
84406
  // Parse command line args into commands and run them
84117
84407
  // Function takes an optional Node-style callback. A Promise is returned if no callback is given.