mapshaper 0.5.113 → 0.5.114

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
@@ -1,6 +1,6 @@
1
1
  (function () {
2
2
 
3
- var VERSION = "0.5.113";
3
+ var VERSION = "0.5.114";
4
4
 
5
5
 
6
6
  var utils = /*#__PURE__*/Object.freeze({
@@ -13607,6 +13607,224 @@
13607
13607
  importFurniture: importFurniture
13608
13608
  });
13609
13609
 
13610
+ /*
13611
+ {
13612
+ width: size[0],
13613
+ height: size[1],
13614
+ bbox: bounds.toArray(),
13615
+ type: 'frame'
13616
+ }
13617
+ */
13618
+ function getFrameData(dataset, exportOpts) {
13619
+ var frameLyr = findFrameLayerInDataset(dataset);
13620
+ var frameData;
13621
+ if (frameLyr) {
13622
+ frameData = Object.assign({}, getFrameLayerData(frameLyr));
13623
+ } else {
13624
+ frameData = calcFrameData(dataset, exportOpts);
13625
+ }
13626
+ frameData.crs = getDatasetCRS(dataset);
13627
+ return frameData;
13628
+ }
13629
+
13630
+ function calcFrameData(dataset, opts) {
13631
+ var bounds = getDatasetBounds(dataset);
13632
+ var bounds2 = calcOutputSizeInPixels(bounds, opts);
13633
+ return {
13634
+ bbox: bounds.toArray(),
13635
+ width: Math.round(bounds2.width()),
13636
+ height: Math.round(bounds2.height()) || 1,
13637
+ type: 'frame'
13638
+ };
13639
+ }
13640
+
13641
+ function getFrameLayerData(lyr) {
13642
+ return lyr.data && lyr.data.getReadOnlyRecordAt(0);
13643
+ }
13644
+
13645
+ // Used by mapshaper-frame and mapshaper-pixel-transform. TODO: refactor
13646
+ function getFrameSize(bounds, opts) {
13647
+ var aspectRatio = bounds.width() / bounds.height();
13648
+ var height, width;
13649
+ if (opts.pixels) {
13650
+ width = Math.sqrt(+opts.pixels * aspectRatio);
13651
+ } else {
13652
+ width = +opts.width;
13653
+ }
13654
+ height = width / aspectRatio;
13655
+ return [Math.round(width), Math.round(height)];
13656
+ }
13657
+
13658
+
13659
+ // @lyr dataset layer
13660
+ function isFrameLayer(lyr) {
13661
+ return getFurnitureLayerType(lyr) == 'frame';
13662
+ }
13663
+
13664
+ function findFrameLayerInDataset(dataset) {
13665
+ return utils.find(dataset.layers, function(lyr) {
13666
+ return isFrameLayer(lyr);
13667
+ });
13668
+ }
13669
+
13670
+ function findFrameDataset(catalog) {
13671
+ var target = utils.find(catalog.getLayers(), function(o) {
13672
+ return isFrameLayer(o.layer);
13673
+ });
13674
+ return target ? target.dataset : null;
13675
+ }
13676
+
13677
+ function findFrameLayer(catalog) {
13678
+ var target = utils.find(catalog.getLayers(), function(o) {
13679
+ return isFrameLayer(o.layer);
13680
+ });
13681
+ return target && target.layer || null;
13682
+ }
13683
+
13684
+ function getFrameLayerBounds(lyr) {
13685
+ return new Bounds(getFrameLayerData(lyr).bbox);
13686
+ }
13687
+
13688
+ // @data frame data, including crs property if available
13689
+ // Returns a single value: the ratio or
13690
+ function getMapFrameMetersPerPixel(data) {
13691
+ var bounds = new Bounds(data.bbox);
13692
+ var k, toMeters, metersPerPixel;
13693
+ if (data.crs) {
13694
+ // TODO: handle CRS without inverse projections
13695
+ // scale factor is the ratio of coordinate distance to true distance at a point
13696
+ k = getScaleFactorAtXY(bounds.centerX(), bounds.centerY(), data.crs);
13697
+ toMeters = data.crs.to_meter;
13698
+ } else {
13699
+ // Assuming coordinates are meters and k is 1 (not safe)
13700
+ // A warning should be displayed when relevant furniture element is created
13701
+ k = 1;
13702
+ toMeters = 1;
13703
+ }
13704
+ metersPerPixel = bounds.width() / k * toMeters / data.width;
13705
+ return metersPerPixel;
13706
+ }
13707
+
13708
+
13709
+ // bounds: Bounds object containing bounds of content in geographic coordinates
13710
+ // returns Bounds object containing bounds of pixel output
13711
+ // side effect: bounds param is modified to match the output frame
13712
+ function calcOutputSizeInPixels(bounds, opts) {
13713
+ var padX = 0,
13714
+ padY = 0,
13715
+ offX = 0,
13716
+ offY = 0,
13717
+ width = bounds.width(),
13718
+ height = bounds.height(),
13719
+ margins = parseMarginOption(opts.margin),
13720
+ marginX = margins[0] + margins[2],
13721
+ marginY = margins[1] + margins[3],
13722
+ // TODO: add option to tweak alignment of content when both width and height are given
13723
+ wx = 0.5, // how padding is distributed horizontally (0: left aligned, 0.5: centered, 1: right aligned)
13724
+ wy = 0.5, // vertical padding distribution
13725
+ widthPx, heightPx, size, kx, ky;
13726
+
13727
+ if (opts.fit_bbox) {
13728
+ // scale + shift content to fit within a bbox
13729
+ offX = opts.fit_bbox[0];
13730
+ offY = opts.fit_bbox[1];
13731
+ widthPx = opts.fit_bbox[2] - offX;
13732
+ heightPx = opts.fit_bbox[3] - offY;
13733
+ if (width / height > widthPx / heightPx) {
13734
+ // data is wider than fit box...
13735
+ // scale the data to fit widthwise
13736
+ heightPx = 0;
13737
+ } else {
13738
+ widthPx = 0; // fit the data to the height
13739
+ }
13740
+ marginX = marginY = 0; // TODO: support margins
13741
+
13742
+ } else if (opts.svg_scale > 0) {
13743
+ // alternative to using a fixed width (e.g. when generating multiple files
13744
+ // at a consistent geographic scale)
13745
+ widthPx = width / opts.svg_scale + marginX;
13746
+ heightPx = 0;
13747
+ } else if (+opts.pixels) {
13748
+ size = getFrameSize(bounds, opts);
13749
+ widthPx = size[0];
13750
+ heightPx = size[1];
13751
+ } else {
13752
+ heightPx = opts.height || 0;
13753
+ widthPx = opts.width || (heightPx > 0 ? 0 : 800); // 800 is default width
13754
+ }
13755
+
13756
+ if (heightPx > 0) {
13757
+ // vertical meters per pixel to fit height param
13758
+ ky = (height || width || 1) / (heightPx - marginY);
13759
+ }
13760
+ if (widthPx > 0) {
13761
+ // horizontal meters per pixel to fit width param
13762
+ kx = (width || height || 1) / (widthPx - marginX);
13763
+ }
13764
+
13765
+ if (!widthPx) { // heightPx and ky are defined, set width to match
13766
+ kx = ky;
13767
+ widthPx = width > 0 ? marginX + width / kx : heightPx; // export square graphic if content has 0 width (reconsider this?)
13768
+ } else if (!heightPx) { // widthPx and kx are set, set height to match
13769
+ ky = kx;
13770
+ heightPx = height > 0 ? marginY + height / ky : widthPx;
13771
+ // limit height if max_height is defined
13772
+ if (opts.max_height > 0 && heightPx > opts.max_height) {
13773
+ ky = kx * heightPx / opts.max_height;
13774
+ heightPx = opts.max_height;
13775
+ }
13776
+ }
13777
+
13778
+ if (kx > ky) { // content is wide -- need to pad vertically
13779
+ ky = kx;
13780
+ padY = ky * (heightPx - marginY) - height;
13781
+ } else if (ky > kx) { // content is tall -- need to pad horizontally
13782
+ kx = ky;
13783
+ padX = kx * (widthPx - marginX) - width;
13784
+ }
13785
+
13786
+ bounds.padBounds(
13787
+ margins[0] * kx + padX * wx,
13788
+ margins[1] * ky + padY * wy,
13789
+ margins[2] * kx + padX * (1 - wx),
13790
+ margins[3] * ky + padY * (1 - wy));
13791
+
13792
+ if (!(widthPx > 0 && heightPx > 0)) {
13793
+ error("Missing valid height and width parameters");
13794
+ }
13795
+ if (!(kx === ky && kx > 0)) {
13796
+ error("Missing valid margin parameters");
13797
+ }
13798
+
13799
+ return new Bounds(offX, offY, widthPx + offX, heightPx + offY);
13800
+ }
13801
+
13802
+ function parseMarginOption(opt) {
13803
+ var str = utils.isNumber(opt) ? String(opt) : opt || '';
13804
+ var margins = str.trim().split(/[, ] */);
13805
+ if (margins.length == 1) margins.push(margins[0]);
13806
+ if (margins.length == 2) margins.push(margins[0], margins[1]);
13807
+ if (margins.length == 3) margins.push(margins[2]);
13808
+ return margins.map(function(str) {
13809
+ var px = parseFloat(str);
13810
+ return isNaN(px) ? 1 : px; // 1 is default
13811
+ });
13812
+ }
13813
+
13814
+ var FrameData = /*#__PURE__*/Object.freeze({
13815
+ __proto__: null,
13816
+ getFrameData: getFrameData,
13817
+ getFrameLayerData: getFrameLayerData,
13818
+ getFrameSize: getFrameSize,
13819
+ findFrameLayerInDataset: findFrameLayerInDataset,
13820
+ findFrameDataset: findFrameDataset,
13821
+ findFrameLayer: findFrameLayer,
13822
+ getFrameLayerBounds: getFrameLayerBounds,
13823
+ getMapFrameMetersPerPixel: getMapFrameMetersPerPixel,
13824
+ calcOutputSizeInPixels: calcOutputSizeInPixels,
13825
+ parseMarginOption: parseMarginOption
13826
+ });
13827
+
13610
13828
  // Apply rotation, scale and/or shift to some or all of the features in a dataset
13611
13829
  //
13612
13830
  cmd.affine = function(targetLayers, dataset, opts) {
@@ -14032,6 +14250,7 @@
14032
14250
  // null values indicate the lack of a function for parsing/identifying this property
14033
14251
  // (in which case a heuristic is used for distinguishing a string literal from an expression)
14034
14252
  var stylePropertyTypes = {
14253
+ css: null,
14035
14254
  class: 'classname',
14036
14255
  dx: 'measure',
14037
14256
  dy: 'measure',
@@ -14081,7 +14300,7 @@
14081
14300
  effect: null // e.g. "fade"
14082
14301
  }, stylePropertyTypes);
14083
14302
 
14084
- var commonProperties = 'class,opacity,stroke,stroke-width,stroke-dasharray,stroke-opacity,fill-opacity'.split(',');
14303
+ var commonProperties = 'css,class,opacity,stroke,stroke-width,stroke-dasharray,stroke-opacity,fill-opacity'.split(',');
14085
14304
 
14086
14305
  var propertiesBySymbolType = {
14087
14306
  polygon: utils.arrayToIndex(commonProperties.concat('fill', 'fill-pattern')),
@@ -14182,7 +14401,6 @@
14182
14401
  // treating the string as a literal value
14183
14402
  literalVal = strVal;
14184
14403
  }
14185
- // console.log("literalVal:", mightBeExpression(strVal, fields), strVal, fields)
14186
14404
  if (accessor) return accessor;
14187
14405
  if (literalVal !== null) return function(id) {return literalVal;};
14188
14406
  stop('Unexpected value for', svgName + ':', strVal);
@@ -14278,6 +14496,138 @@
14278
14496
  isSvgColor: isSvgColor
14279
14497
  });
14280
14498
 
14499
+ var geojsonImporters = {
14500
+ Point: importPoint,
14501
+ Polygon: importPolygon,
14502
+ LineString: importLineString,
14503
+ MultiPoint: importMultiPoint,
14504
+ MultiLineString: importMultiLineString,
14505
+ MultiPolygon: importMultiPolygon
14506
+ };
14507
+
14508
+ function importGeoJSONFeatures(features, opts) {
14509
+ opts = opts || {};
14510
+ return features.map(function(obj, i) {
14511
+ var geom = obj.type == 'Feature' ? obj.geometry : obj; // could be null
14512
+ var geomType = geom && geom.type;
14513
+ var msType = GeoJSON.translateGeoJSONType(geomType);
14514
+ var d = obj.properties || {};
14515
+ var svgObj = null;
14516
+ if (geomType && geom.coordinates) {
14517
+ svgObj = geojsonImporters[geomType](geom.coordinates, d);
14518
+ }
14519
+ if (!svgObj) {
14520
+ return {tag: 'g'}; // empty element
14521
+ } else if (msType == 'polyline' || msType == 'polygon') {
14522
+ applyStyleAttributes(svgObj, msType, d);
14523
+ } else if (msType == 'point' && isSimpleCircle(d)) {
14524
+ // kludge -- maintains bw compatibility/passes tests -- style attributes
14525
+ // are applied to the <g> container, 'r' property is applied to circle
14526
+ applyStyleAttributes(svgObj, msType, d, simpleCircleFilter);
14527
+ } else {
14528
+ // other point symbols: attributes are complicated, added downstream
14529
+ }
14530
+ if ('id' in obj) {
14531
+ if (!svgObj.properties) {
14532
+ svgObj.properties = {};
14533
+ }
14534
+ svgObj.properties.id = (opts.id_prefix || '') + obj.id;
14535
+ }
14536
+ return svgObj;
14537
+ });
14538
+ }
14539
+
14540
+ function importPoint(coords, rec) {
14541
+ rec = rec || {};
14542
+ if (isSimpleCircle(rec)) {
14543
+ return {
14544
+ tag: 'circle',
14545
+ properties: {
14546
+ cx: coords[0],
14547
+ cy: coords[1],
14548
+ r: rec.r
14549
+ }
14550
+ };
14551
+ }
14552
+ var o = renderPoint(rec, coords);
14553
+ if (o) o.properties.transform = getTransform(coords);
14554
+ return o;
14555
+ }
14556
+
14557
+ function simpleCircleFilter(k) {
14558
+ return k != 'r';
14559
+ }
14560
+
14561
+ // just a dot, no label or icon
14562
+ function isSimpleCircle(rec) {
14563
+ return rec && (rec.r > 0 && !rec['svg-symbol'] && !rec['label-text']);
14564
+ }
14565
+
14566
+ function importMultiPoint(coords, rec) {
14567
+ var children = [], p;
14568
+ for (var i=0; i<coords.length; i++) {
14569
+ p = importPoint(coords[i], rec);
14570
+ if (!p) continue;
14571
+ if (p.tag == 'g' && p.children) {
14572
+ children = children.concat(p.children);
14573
+ } else {
14574
+ children.push(p);
14575
+ }
14576
+ }
14577
+ return children.length > 0 ? {tag: 'g', children: children} : null;
14578
+ }
14579
+
14580
+ function importLineString(coords) {
14581
+ var d = stringifyLineStringCoords(coords);
14582
+ return {
14583
+ tag: 'path',
14584
+ properties: {d: d}
14585
+ };
14586
+ }
14587
+
14588
+ function importMultiLineString(coords) {
14589
+ var d = coords.map(stringifyLineStringCoords).join(' ');
14590
+ return {
14591
+ tag: 'path',
14592
+ properties: {d: d}
14593
+ };
14594
+ }
14595
+
14596
+ function importMultiPolygon(coords) {
14597
+ return importPolygon(flattenMultiPolygonCoords(coords));
14598
+ }
14599
+
14600
+ function flattenMultiPolygonCoords(coords) {
14601
+ return coords.reduce(function(memo, poly) {
14602
+ return memo.concat(poly);
14603
+ }, []);
14604
+ }
14605
+
14606
+ function importPolygon(coords) {
14607
+ if (coords.length === 0) return null;
14608
+ var o = {
14609
+ tag: 'path',
14610
+ properties: {
14611
+ d: stringifyPolygonCoords(coords)
14612
+ }
14613
+ };
14614
+ if (coords.length > 1) {
14615
+ o.properties['fill-rule'] = 'evenodd'; // support polygons with holes
14616
+ }
14617
+ return o;
14618
+ }
14619
+
14620
+ var GeojsonToSvg = /*#__PURE__*/Object.freeze({
14621
+ __proto__: null,
14622
+ importGeoJSONFeatures: importGeoJSONFeatures,
14623
+ importPoint: importPoint,
14624
+ importLineString: importLineString,
14625
+ importMultiLineString: importMultiLineString,
14626
+ importMultiPolygon: importMultiPolygon,
14627
+ flattenMultiPolygonCoords: flattenMultiPolygonCoords,
14628
+ importPolygon: importPolygon
14629
+ });
14630
+
14281
14631
  function toLabelString(val) {
14282
14632
  if (val || val === 0 || val === false) return String(val);
14283
14633
  return '';
@@ -14341,7 +14691,6 @@
14341
14691
 
14342
14692
  // convert data records (properties like svg-symbol, label-text, fill, r) to svg symbols
14343
14693
 
14344
-
14345
14694
  function getTransform(xy, scale) {
14346
14695
  var str = 'translate(' + roundToTenths(xy[0]) + ' ' + roundToTenths(xy[1]) + ')';
14347
14696
  if (scale && scale != 1) {
@@ -14358,7 +14707,8 @@
14358
14707
  square: square,
14359
14708
  image: image,
14360
14709
  group: group,
14361
- label: label
14710
+ label: label,
14711
+ offset: offset
14362
14712
  };
14363
14713
 
14364
14714
  // render label and/or point symbol
@@ -14426,8 +14776,9 @@
14426
14776
  function label(d, x, y) {
14427
14777
  var o = renderStyledLabel(d);
14428
14778
  if (x || y) {
14429
- o.properties.x = x || 0;
14430
- o.properties.y = y || 0;
14779
+ // set x, y here, rather than adding to dx, dy -- so dy, dy can
14780
+ // have CSS units (like ems)
14781
+ o.properties.transform = getTransform([x, y]);
14431
14782
  }
14432
14783
  return o;
14433
14784
  }
@@ -14487,21 +14838,40 @@
14487
14838
  return o;
14488
14839
  }
14489
14840
 
14841
+ function offset(d, x, y) {
14842
+ var dx = (x || 0) + (d.dx || 0);
14843
+ var dy = (y || 0) + (d.dy || 0);
14844
+ return {
14845
+ tag: 'g',
14846
+ properties: {
14847
+ transform: getTransform([dx, dy])
14848
+ },
14849
+ children: []
14850
+ };
14851
+ }
14852
+
14490
14853
  function group(d, x, y) {
14491
- var parts = (d.parts || []).map(function(o) {
14854
+ var o = {
14855
+ tag: 'g',
14856
+ children: []
14857
+ };
14858
+ var children = o.children;
14859
+ (d.parts || []).forEach(function(o) {
14492
14860
  var sym = renderComplexSymbol(o, x, y);
14493
- if (d.chained) {
14861
+ children.push(sym);
14862
+ //if (d.chained) {
14494
14863
  //if (o.chained) {
14864
+ if (o.type == 'line') {
14495
14865
  x += (o.dx || 0);
14496
14866
  y += (o.dy || 0);
14497
14867
  }
14498
- return sym;
14868
+ if (o.type == 'offset') {
14869
+ children = sym.children;
14870
+ x = y = 0;
14871
+ }
14499
14872
  });
14500
- if (parts.length == 1) return parts[0];
14501
- return {
14502
- tag: 'g',
14503
- children: parts
14504
- };
14873
+ if (o.children.length == 1 && !o.properties) return o.children[0];
14874
+ return o;
14505
14875
  }
14506
14876
 
14507
14877
  var SvgSymbols = /*#__PURE__*/Object.freeze({
@@ -14511,419 +14881,180 @@
14511
14881
  renderPoint: renderPoint
14512
14882
  });
14513
14883
 
14514
- var geojsonImporters = {
14515
- Point: importPoint,
14516
- Polygon: importPolygon,
14517
- LineString: importLineString,
14518
- MultiPoint: importMultiPoint,
14519
- MultiLineString: importMultiLineString,
14520
- MultiPolygon: importMultiPolygon
14521
- };
14522
-
14523
- function importGeoJSONFeatures(features, opts) {
14524
- opts = opts || {};
14525
- return features.map(function(obj, i) {
14526
- var geom = obj.type == 'Feature' ? obj.geometry : obj; // could be null
14527
- var geomType = geom && geom.type;
14528
- var msType = GeoJSON.translateGeoJSONType(geomType);
14529
- var d = obj.properties || {};
14530
- var svgObj = null;
14531
- if (geomType && geom.coordinates) {
14532
- svgObj = geojsonImporters[geomType](geom.coordinates, d);
14533
- }
14534
- if (!svgObj) {
14535
- return {tag: 'g'}; // empty element
14536
- } else if (msType == 'polyline' || msType == 'polygon') {
14537
- applyStyleAttributes(svgObj, msType, d);
14538
- } else if (msType == 'point' && isSimpleCircle(d)) {
14539
- // kludge -- maintains bw compatibility/passes tests -- style attributes
14540
- // are applied to the <g> container, 'r' property is applied to circle
14541
- applyStyleAttributes(svgObj, msType, d, simpleCircleFilter);
14542
- } else {
14543
- // other point symbols: attributes are complicated, added downstream
14544
- }
14545
- if ('id' in obj) {
14546
- if (!svgObj.properties) {
14547
- svgObj.properties = {};
14548
- }
14549
- svgObj.properties.id = (opts.id_prefix || '') + obj.id;
14550
- }
14551
- return svgObj;
14552
- });
14553
- }
14554
-
14555
- function importPoint(coords, rec) {
14556
- rec = rec || {};
14557
- if (isSimpleCircle(rec)) {
14558
- return {
14559
- tag: 'circle',
14560
- properties: {
14561
- cx: coords[0],
14562
- cy: coords[1],
14563
- r: rec.r
14564
- }
14565
- };
14566
- }
14567
- var o = renderPoint(rec, coords);
14568
- if (o) o.properties.transform = getTransform(coords);
14569
- return o;
14570
- }
14571
-
14572
- function simpleCircleFilter(k) {
14573
- return k != 'r';
14574
- }
14575
-
14576
- // just a dot, no label or icon
14577
- function isSimpleCircle(rec) {
14578
- return rec && (rec.r > 0 && !rec['svg-symbol'] && !rec['label-text']);
14579
- }
14580
-
14581
- function importMultiPoint(coords, rec) {
14582
- var children = [], p;
14583
- for (var i=0; i<coords.length; i++) {
14584
- p = importPoint(coords[i], rec);
14585
- if (!p) continue;
14586
- if (p.tag == 'g' && p.children) {
14587
- children = children.concat(p.children);
14588
- } else {
14589
- children.push(p);
14590
- }
14884
+ cmd.scalebar = function(catalog, opts) {
14885
+ var frame = findFrameDataset(catalog);
14886
+ var obj, lyr;
14887
+ if (!frame) {
14888
+ stop('Missing a map frame');
14591
14889
  }
14592
- return children.length > 0 ? {tag: 'g', children: children} : null;
14593
- }
14890
+ lyr = getScalebarLayer(opts);
14891
+ frame.layers.push(lyr);
14892
+ };
14594
14893
 
14595
- function importLineString(coords) {
14596
- var d = stringifyLineStringCoords(coords);
14894
+ function getScalebarLayer(opts) {
14895
+ var obj = utils.defaults({type: 'scalebar'}, opts);
14597
14896
  return {
14598
- tag: 'path',
14599
- properties: {d: d}
14600
- };
14601
- }
14602
-
14603
- function importMultiLineString(coords) {
14604
- var d = coords.map(stringifyLineStringCoords).join(' ');
14605
- return {
14606
- tag: 'path',
14607
- properties: {d: d}
14897
+ name: opts.name || 'scalebar',
14898
+ data: new DataTable([obj])
14608
14899
  };
14609
14900
  }
14610
14901
 
14611
- function importMultiPolygon(coords) {
14612
- return importPolygon(flattenMultiPolygonCoords(coords));
14613
- }
14614
-
14615
- function flattenMultiPolygonCoords(coords) {
14616
- return coords.reduce(function(memo, poly) {
14617
- return memo.concat(poly);
14618
- }, []);
14619
- }
14620
-
14621
- function importPolygon(coords) {
14622
- if (coords.length === 0) return null;
14623
- var o = {
14624
- tag: 'path',
14625
- properties: {
14626
- d: stringifyPolygonCoords(coords)
14627
- }
14902
+ // TODO: generalize to other kinds of furniture as they are developed
14903
+ function getScalebarPosition(d) {
14904
+ var opts = { // defaults
14905
+ valign: 'top',
14906
+ halign: 'left',
14907
+ voffs: 10,
14908
+ hoffs: 10
14628
14909
  };
14629
- if (coords.length > 1) {
14630
- o.properties['fill-rule'] = 'evenodd'; // support polygons with holes
14631
- }
14632
- return o;
14633
- }
14634
-
14635
- var GeojsonToSvg = /*#__PURE__*/Object.freeze({
14636
- __proto__: null,
14637
- importGeoJSONFeatures: importGeoJSONFeatures,
14638
- importPoint: importPoint,
14639
- importLineString: importLineString,
14640
- importMultiLineString: importMultiLineString,
14641
- importMultiPolygon: importMultiPolygon,
14642
- flattenMultiPolygonCoords: flattenMultiPolygonCoords,
14643
- importPolygon: importPolygon
14644
- });
14645
-
14646
- /* require mapshaper-rectangle, mapshaper-furniture */
14647
-
14648
- cmd.frame = function(catalog, source, opts) {
14649
- var size, bounds, tmp, dataset;
14650
- if (+opts.width > 0 === false && +opts.pixels > 0 === false) {
14651
- stop("Missing a width or area");
14652
- }
14653
- if (opts.width && opts.height) {
14654
- opts = utils.extend({}, opts);
14655
- // Height is a string containing either a number or a
14656
- // comma-sep. pair of numbers (range); here we convert height to
14657
- // an aspect-ratio parameter for the rectangle() function
14658
- opts.aspect_ratio = getAspectRatioArg(opts.width, opts.height);
14659
- // TODO: currently returns max,min aspect ratio, should return in min,max order
14660
- // (rectangle() function should handle max,min argument correctly now anyway)
14910
+ if (+d.left > 0) {
14911
+ opts.hoffs = +d.left;
14661
14912
  }
14662
- tmp = cmd.rectangle(source, opts);
14663
- bounds = getDatasetBounds(tmp);
14664
- if (probablyDecimalDegreeBounds(bounds)) {
14665
- stop('Frames require projected, not geographical coordinates');
14666
- } else if (!getDatasetCRS(tmp)) {
14667
- message('Warning: missing projection data. Assuming coordinates are meters and k (scale factor) is 1');
14913
+ if (+d.top > 0) {
14914
+ opts.voffs = +d.top;
14668
14915
  }
14669
- size = getFrameSize(bounds, opts);
14670
- if (size[0] > 0 === false) {
14671
- stop('Missing a valid frame width');
14916
+ if (+d.right > 0) {
14917
+ opts.hoffs = +d.right;
14918
+ opts.halign = 'right';
14672
14919
  }
14673
- if (size[1] > 0 === false) {
14674
- stop('Missing a valid frame height');
14920
+ if (+d.bottom > 0) {
14921
+ opts.voffs = +d.bottom;
14922
+ opts.valign = 'bottom';
14675
14923
  }
14676
- dataset = {info: {}, layers:[{
14677
- name: opts.name || 'frame',
14678
- data: new DataTable([{
14679
- width: size[0],
14680
- height: size[1],
14681
- bbox: bounds.toArray(),
14682
- type: 'frame'
14683
- }])
14684
- }]};
14685
- catalog.addDataset(dataset);
14686
- };
14687
-
14688
- // Convert width and height args to aspect ratio arg for the rectangle() function
14689
- function getAspectRatioArg(widthArg, heightArg) {
14690
- // heightArg is a string containing either a number or a
14691
- // comma-sep. pair of numbers (range);
14692
- return heightArg.split(',').map(function(opt) {
14693
- var height = Number(opt),
14694
- width = Number(widthArg);
14695
- if (!opt) return '';
14696
- return width / height;
14697
- }).reverse().join(',');
14924
+ return opts;
14698
14925
  }
14699
14926
 
14700
- function getFrameSize(bounds, opts) {
14701
- var aspectRatio = bounds.width() / bounds.height();
14702
- var height, width;
14703
- if (opts.pixels) {
14704
- width = Math.sqrt(+opts.pixels * aspectRatio);
14705
- } else {
14706
- width = +opts.width;
14707
- }
14708
- height = width / aspectRatio;
14709
- return [Math.round(width), Math.round(height)];
14710
- }
14927
+ furnitureRenderers.scalebar = renderScalebar;
14711
14928
 
14712
- function getDatasetDisplayBounds(dataset) {
14713
- var frameLyr = findFrameLayerInDataset(dataset);
14714
- if (frameLyr) {
14715
- // TODO: check for coordinate issues (non-intersection with other layers, etc)
14716
- return getFrameLayerBounds(frameLyr);
14929
+ function renderScalebar(d, frame) {
14930
+ var pos = getScalebarPosition(d);
14931
+ var metersPerPx = getMapFrameMetersPerPixel(frame);
14932
+ var label = d.label_text || getAutoScalebarLabel(frame.width, metersPerPx);
14933
+ var scalebarKm = parseScalebarLabelToKm(label);
14934
+ var barHeight = 3;
14935
+ var labelOffs = 4;
14936
+ var fontSize = +d.font_size || 12;
14937
+ var width = Math.round(scalebarKm / metersPerPx * 1000);
14938
+ var height = Math.round(barHeight + labelOffs + fontSize * 0.8);
14939
+ var labelPos = d.label_position == 'top' ? 'top' : 'bottom';
14940
+ var anchorX = pos.halign == 'left' ? 0 : width;
14941
+ var anchorY = barHeight + labelOffs;
14942
+ var dx = pos.halign == 'right' ? frame.width - width - pos.hoffs : pos.hoffs;
14943
+ var dy = pos.valign == 'bottom' ? frame.height - height - pos.voffs : pos.voffs;
14944
+
14945
+ if (labelPos == 'top') {
14946
+ anchorY = -labelOffs;
14947
+ dy += Math.round(labelOffs + fontSize * 0.8);
14717
14948
  }
14718
- return getDatasetBounds(dataset);
14719
- }
14720
14949
 
14721
- // @lyr dataset layer
14722
- function isFrameLayer(lyr) {
14723
- return getFurnitureLayerType(lyr) == 'frame';
14950
+ if (width > 0 === false) {
14951
+ stop("Null scalebar length");
14952
+ }
14953
+ var barObj = {
14954
+ tag: 'rect',
14955
+ properties: {
14956
+ fill: 'black',
14957
+ x: 0,
14958
+ y: 0,
14959
+ width: width,
14960
+ height: barHeight
14961
+ }
14962
+ };
14963
+ var labelOpts = {
14964
+ 'label-text': label,
14965
+ 'font-size': fontSize,
14966
+ 'text-anchor': pos.halign == 'left' ? 'start': 'end',
14967
+ 'dominant-baseline': labelPos == 'top' ? 'auto' : 'hanging'
14968
+ //// 'dominant-baseline': labelPos == 'top' ? 'text-after-edge' : 'text-before-edge'
14969
+ // 'text-after-edge' is buggy in Safari and unsupported by Illustrator,
14970
+ // so I'm using 'hanging' and 'auto', which seem to be well supported.
14971
+ // downside: requires a kludgy multiplier to calculate scalebar height (see above)
14972
+ };
14973
+ var labelObj = symbolRenderers.label(labelOpts, anchorX, anchorY);
14974
+ var g = {
14975
+ tag: 'g',
14976
+ children: [barObj, labelObj],
14977
+ properties: {
14978
+ transform: 'translate(' + dx + ' ' + dy + ')'
14979
+ }
14980
+ };
14981
+ return [g];
14724
14982
  }
14725
14983
 
14726
- function findFrameLayerInDataset(dataset) {
14727
- return utils.find(dataset.layers, function(lyr) {
14728
- return isFrameLayer(lyr);
14729
- });
14984
+ function getAutoScalebarLabel(mapWidth, metersPerPx) {
14985
+ var minWidth = 75; // 100; // TODO: vary min size based on map width
14986
+ var minKm = metersPerPx * minWidth / 1000;
14987
+ var options = ('1/8 1/5 1/4 1/2 1 1.5 2 3 4 5 8 10 12 15 20 25 30 40 50 75 ' +
14988
+ '100 150 200 250 300 350 400 500 750 1,000 1,200 1,500 2,000 ' +
14989
+ '2,500 3,000 4,000 5,000').split(' ');
14990
+ return options.reduce(function(memo, str) {
14991
+ if (memo) return memo;
14992
+ var label = formatDistanceLabelAsMiles(str);
14993
+ if (parseScalebarLabelToKm(label) > minKm) {
14994
+ return label;
14995
+ }
14996
+ }, null) || '';
14730
14997
  }
14731
14998
 
14732
- function findFrameDataset(catalog) {
14733
- var target = utils.find(catalog.getLayers(), function(o) {
14734
- return isFrameLayer(o.layer);
14735
- });
14736
- return target ? target.dataset : null;
14999
+ function formatDistanceLabelAsMiles(str) {
15000
+ var num = parseScalebarNumber(str);
15001
+ return str + (num > 1 ? ' MILES' : ' MILE');
14737
15002
  }
14738
15003
 
14739
- function findFrameLayer(catalog) {
14740
- var target = utils.find(catalog.getLayers(), function(o) {
14741
- return isFrameLayer(o.layer);
14742
- });
14743
- return target && target.layer || null;
15004
+ // See test/mapshaper-scalebar.js for examples of supported formats
15005
+ function parseScalebarLabelToKm(str) {
15006
+ var units = parseScalebarUnits(str);
15007
+ var value = parseScalebarNumber(str);
15008
+ if (!units || !value) return NaN;
15009
+ return units == 'mile' ? value * 1.60934 : value;
14744
15010
  }
14745
15011
 
14746
- function getFrameLayerBounds(lyr) {
14747
- return new Bounds(getFurnitureLayerData(lyr).bbox);
15012
+ function parseScalebarUnits(str) {
15013
+ var isMiles = /miles?$/.test(str.toLowerCase());
15014
+ var isKm = /(km|kilometers?|kilometres?)$/.test(str.toLowerCase());
15015
+ return isMiles && 'mile' || isKm && 'km' || '';
14748
15016
  }
14749
15017
 
14750
-
14751
- // @data frame data, including crs property if available
14752
- // Returns a single value: the ratio or
14753
- function getMapFrameMetersPerPixel(data) {
14754
- var bounds = new Bounds(data.bbox);
14755
- var k, toMeters, metersPerPixel;
14756
- if (data.crs) {
14757
- // TODO: handle CRS without inverse projections
14758
- // scale factor is the ratio of coordinate distance to true distance at a point
14759
- k = getScaleFactorAtXY(bounds.centerX(), bounds.centerY(), data.crs);
14760
- toMeters = data.crs.to_meter;
15018
+ function parseScalebarNumber(str) {
15019
+ var fractionRxp = /^([0-9]+) ?\/ ?([0-9]+)/;
15020
+ var match, value;
15021
+ str = str.replace(/[\s]/g, '').replace(/,/g, '');
15022
+ if (fractionRxp.test(str)) {
15023
+ match = fractionRxp.exec(str);
15024
+ value = +match[1] / +match[2];
14761
15025
  } else {
14762
- // Assuming coordinates are meters and k is 1 (not safe)
14763
- // A warning should be displayed when relevant furniture element is created
14764
- k = 1;
14765
- toMeters = 1;
15026
+ value = parseFloat(str);
14766
15027
  }
14767
- metersPerPixel = bounds.width() / k * toMeters / data.width;
14768
- return metersPerPixel;
15028
+ return value > 0 && value < Infinity ? value : NaN;
14769
15029
  }
14770
15030
 
14771
- furnitureRenderers.frame = function(d) {
14772
- var lineWidth = 1,
14773
- // inset stroke by half of line width
14774
- off = lineWidth / 2,
14775
- obj = importPolygon([[[off, off], [off, d.height - off],
14776
- [d.width - off, d.height - off],
14777
- [d.width - off, off], [off, off]]]);
14778
- utils.extend(obj.properties, {
14779
- fill: 'none',
14780
- stroke: d.stroke || 'black',
14781
- 'stroke-width': d['stroke-width'] || lineWidth
14782
- });
14783
- return [obj];
14784
- };
14785
-
14786
- var Frame = /*#__PURE__*/Object.freeze({
15031
+ var Scalebar = /*#__PURE__*/Object.freeze({
14787
15032
  __proto__: null,
14788
- getAspectRatioArg: getAspectRatioArg,
14789
- getFrameSize: getFrameSize,
14790
- findFrameLayerInDataset: findFrameLayerInDataset,
14791
- findFrameDataset: findFrameDataset,
14792
- findFrameLayer: findFrameLayer,
14793
- getFrameLayerBounds: getFrameLayerBounds,
14794
- getMapFrameMetersPerPixel: getMapFrameMetersPerPixel
15033
+ getScalebarLayer: getScalebarLayer,
15034
+ renderScalebar: renderScalebar,
15035
+ formatDistanceLabelAsMiles: formatDistanceLabelAsMiles,
15036
+ parseScalebarLabelToKm: parseScalebarLabelToKm
14795
15037
  });
14796
15038
 
14797
15039
  function transformDatasetToPixels(dataset, opts) {
14798
- var frameLyr = findFrameLayerInDataset(dataset);
14799
- var bounds, bounds2, fwd, frameData;
14800
- if (frameLyr) {
14801
- // TODO: handle options like width, height margin when a frame is present
14802
- // TODO: check that aspect ratios match
14803
- frameData = getFurnitureLayerData(frameLyr);
14804
- bounds = new Bounds(frameData.bbox);
14805
- bounds2 = new Bounds(0, 0, frameData.width, frameData.height);
14806
- } else {
14807
- bounds = getDatasetBounds(dataset);
14808
- bounds2 = calcOutputSizeInPixels(bounds, opts);
14809
- }
14810
- fwd = bounds.getTransform(bounds2, opts.invert_y);
14811
- transformPoints(dataset, function(x, y) {
14812
- return fwd.transform(x, y);
14813
- });
14814
- return [Math.round(bounds2.width()), Math.round(bounds2.height()) || 1];
15040
+ var frame = getFrameData(dataset, opts);
15041
+ fitDatasetToFrame(dataset, frame, opts);
15042
+ return [frame.width, frame.height];
14815
15043
  }
14816
15044
 
14817
- function parseMarginOption(opt) {
14818
- var str = utils.isNumber(opt) ? String(opt) : opt || '';
14819
- var margins = str.trim().split(/[, ] */);
14820
- if (margins.length == 1) margins.push(margins[0]);
14821
- if (margins.length == 2) margins.push(margins[0], margins[1]);
14822
- if (margins.length == 3) margins.push(margins[2]);
14823
- return margins.map(function(str) {
14824
- var px = parseFloat(str);
14825
- return isNaN(px) ? 1 : px; // 1 is default
15045
+ function fitDatasetToFrame(dataset, frame, opts) {
15046
+ var bounds = new Bounds(frame.bbox);
15047
+ var bounds2 = new Bounds(0, 0, frame.width, frame.height);
15048
+ var fwd = bounds.getTransform(bounds2, opts.invert_y);
15049
+ transformPoints(dataset, function(x, y) {
15050
+ return fwd.transform(x, y);
14826
15051
  });
14827
15052
  }
14828
15053
 
14829
- // bounds: Bounds object containing bounds of content in geographic coordinates
14830
- // returns Bounds object containing bounds of pixel output
14831
- // side effect: bounds param is modified to match the output frame
14832
- function calcOutputSizeInPixels(bounds, opts) {
14833
- var padX = 0,
14834
- padY = 0,
14835
- offX = 0,
14836
- offY = 0,
14837
- width = bounds.width(),
14838
- height = bounds.height(),
14839
- margins = parseMarginOption(opts.margin),
14840
- marginX = margins[0] + margins[2],
14841
- marginY = margins[1] + margins[3],
14842
- // TODO: add option to tweak alignment of content when both width and height are given
14843
- wx = 0.5, // how padding is distributed horizontally (0: left aligned, 0.5: centered, 1: right aligned)
14844
- wy = 0.5, // vertical padding distribution
14845
- widthPx, heightPx, size, kx, ky;
14846
-
14847
- if (opts.fit_bbox) {
14848
- // scale + shift content to fit within a bbox
14849
- offX = opts.fit_bbox[0];
14850
- offY = opts.fit_bbox[1];
14851
- widthPx = opts.fit_bbox[2] - offX;
14852
- heightPx = opts.fit_bbox[3] - offY;
14853
- if (width / height > widthPx / heightPx) {
14854
- // data is wider than fit box...
14855
- // scale the data to fit widthwise
14856
- heightPx = 0;
14857
- } else {
14858
- widthPx = 0; // fit the data to the height
14859
- }
14860
- marginX = marginY = 0; // TODO: support margins
14861
-
14862
- } else if (opts.svg_scale > 0) {
14863
- // alternative to using a fixed width (e.g. when generating multiple files
14864
- // at a consistent geographic scale)
14865
- widthPx = width / opts.svg_scale + marginX;
14866
- heightPx = 0;
14867
- } else if (+opts.pixels) {
14868
- size = getFrameSize(bounds, opts);
14869
- widthPx = size[0];
14870
- heightPx = size[1];
14871
- } else {
14872
- heightPx = opts.height || 0;
14873
- widthPx = opts.width || (heightPx > 0 ? 0 : 800); // 800 is default width
14874
- }
14875
-
14876
- if (heightPx > 0) {
14877
- // vertical meters per pixel to fit height param
14878
- ky = (height || width || 1) / (heightPx - marginY);
14879
- }
14880
- if (widthPx > 0) {
14881
- // horizontal meters per pixel to fit width param
14882
- kx = (width || height || 1) / (widthPx - marginX);
14883
- }
14884
-
14885
- if (!widthPx) { // heightPx and ky are defined, set width to match
14886
- kx = ky;
14887
- widthPx = width > 0 ? marginX + width / kx : heightPx; // export square graphic if content has 0 width (reconsider this?)
14888
- } else if (!heightPx) { // widthPx and kx are set, set height to match
14889
- ky = kx;
14890
- heightPx = height > 0 ? marginY + height / ky : widthPx;
14891
- // limit height if max_height is defined
14892
- if (opts.max_height > 0 && heightPx > opts.max_height) {
14893
- ky = kx * heightPx / opts.max_height;
14894
- heightPx = opts.max_height;
14895
- }
14896
- }
14897
-
14898
- if (kx > ky) { // content is wide -- need to pad vertically
14899
- ky = kx;
14900
- padY = ky * (heightPx - marginY) - height;
14901
- } else if (ky > kx) { // content is tall -- need to pad horizontally
14902
- kx = ky;
14903
- padX = kx * (widthPx - marginX) - width;
14904
- }
14905
-
14906
- bounds.padBounds(
14907
- margins[0] * kx + padX * wx,
14908
- margins[1] * ky + padY * wy,
14909
- margins[2] * kx + padX * (1 - wx),
14910
- margins[3] * ky + padY * (1 - wy));
14911
-
14912
- if (!(widthPx > 0 && heightPx > 0)) {
14913
- error("Missing valid height and width parameters");
14914
- }
14915
- if (!(kx === ky && kx > 0)) {
14916
- error("Missing valid margin parameters");
14917
- }
14918
-
14919
- return new Bounds(offX, offY, widthPx + offX, heightPx + offY);
14920
- }
14921
-
14922
15054
  var PixelTransform = /*#__PURE__*/Object.freeze({
14923
15055
  __proto__: null,
14924
15056
  transformDatasetToPixels: transformDatasetToPixels,
14925
- parseMarginOption: parseMarginOption,
14926
- calcOutputSizeInPixels: calcOutputSizeInPixels
15057
+ fitDatasetToFrame: fitDatasetToFrame
14927
15058
  });
14928
15059
 
14929
15060
  function stringify(obj) {
@@ -14980,6 +15111,9 @@
14980
15111
  if (key == 'href') {
14981
15112
  key = 'xlink:href';
14982
15113
  }
15114
+ if (key == 'css') {
15115
+ key = 'style'; // inline style
15116
+ }
14983
15117
  return memo + ' ' + key + '="' + stringEscape(strval) + '"';
14984
15118
  }, '');
14985
15119
  }
@@ -15147,12 +15281,11 @@
15147
15281
  }
15148
15282
  */
15149
15283
 
15150
- //
15151
15284
  //
15152
15285
  function exportSVG(dataset, opts) {
15153
15286
  var namespace = 'xmlns="http://www.w3.org/2000/svg"';
15154
15287
  var defs = [];
15155
- var size, svg;
15288
+ var frame, svg, layers;
15156
15289
  var style = '';
15157
15290
 
15158
15291
  // kludge for map keys
@@ -15169,15 +15302,27 @@
15169
15302
  } else {
15170
15303
  dataset = copyDataset(dataset); // Modify a copy of the dataset
15171
15304
  }
15305
+
15172
15306
  // invert_y setting for screen coordinates and geojson polygon generation
15173
15307
  utils.extend(opts, {invert_y: true});
15174
- size = transformCoordsForSVG(dataset, opts);
15308
+ frame = getFrameData(dataset, opts);
15309
+ fitDatasetToFrame(dataset, frame, opts);
15310
+ setCoordinatePrecision(dataset, opts.precision || 0.0001);
15175
15311
 
15176
15312
  // error if one or more svg_data fields are not present in any layers
15177
15313
  if (opts.svg_data) validateSvgDataFields(dataset.layers, opts.svg_data);
15178
15314
 
15179
- svg = dataset.layers.map(function(lyr) {
15180
- var obj = exportLayerForSVG(lyr, dataset, opts);
15315
+ layers = dataset.layers;
15316
+ if (opts.scalebar) {
15317
+ layers.push(getScalebarLayer({})); // default options
15318
+ }
15319
+ svg = layers.map(function(lyr) {
15320
+ var obj;
15321
+ if (layerHasFurniture(lyr)) {
15322
+ obj = exportFurnitureForSVG(lyr, frame, opts);
15323
+ } else {
15324
+ obj = exportLayerForSVG(lyr, dataset, opts);
15325
+ }
15181
15326
  convertPropertiesToDefinitions(obj, defs);
15182
15327
  return stringify(obj);
15183
15328
  }).join('\n');
@@ -15202,39 +15347,25 @@
15202
15347
  <svg ${namespace} version="1.2" baseProfile="tiny" width="%d" height="%d" viewBox="%s %s %s %s" ${lineProps}>${style}
15203
15348
  ${svg}
15204
15349
  </svg>`;
15205
- svg = utils.format(template,size[0], size[1], 0, 0, size[0], size[1]);
15350
+ svg = utils.format(template, frame.width, frame.height, 0, 0, frame.width, frame.height);
15206
15351
  return [{
15207
15352
  content: svg,
15208
15353
  filename: opts.file || getOutputFileBase(dataset) + '.svg'
15209
15354
  }];
15210
15355
  }
15211
15356
 
15212
- function transformCoordsForSVG(dataset, opts) {
15213
- var size = transformDatasetToPixels(dataset, opts);
15214
- var precision = opts.precision || 0.0001;
15215
- setCoordinatePrecision(dataset, precision);
15216
- return size;
15357
+ function exportFurnitureForSVG(lyr, frame, opts) {
15358
+ var layerObj = getEmptyLayerForSVG(lyr, opts);
15359
+ layerObj.children = importFurniture(getFurnitureLayerData(lyr), frame);
15360
+ return layerObj;
15217
15361
  }
15218
15362
 
15219
15363
  function exportLayerForSVG(lyr, dataset, opts) {
15220
15364
  var layerObj = getEmptyLayerForSVG(lyr, opts);
15221
- if (layerHasFurniture(lyr)) {
15222
- layerObj.children = exportFurnitureForSVG(lyr, dataset, opts);
15223
- } else {
15224
- layerObj.children = exportSymbolsForSVG(lyr, dataset, opts);
15225
- }
15365
+ layerObj.children = exportSymbolsForSVG(lyr, dataset, opts);
15226
15366
  return layerObj;
15227
15367
  }
15228
15368
 
15229
- function exportFurnitureForSVG(lyr, dataset, opts) {
15230
- var frameLyr = findFrameLayerInDataset(dataset);
15231
- var frameData;
15232
- if (!frameLyr) return [];
15233
- frameData = getFurnitureLayerData(frameLyr);
15234
- frameData.crs = getDatasetCRS(dataset); // required by e.g. scalebar
15235
- return importFurniture(getFurnitureLayerData(lyr), frameData);
15236
- }
15237
-
15238
15369
  function exportSymbolsForSVG(lyr, dataset, opts) {
15239
15370
  // TODO: convert geojson features one at a time
15240
15371
  var d = utils.defaults({layers: [lyr]}, dataset);
@@ -15389,6 +15520,7 @@ ${svg}
15389
15520
  var Svg = /*#__PURE__*/Object.freeze({
15390
15521
  __proto__: null,
15391
15522
  exportSVG: exportSVG,
15523
+ exportFurnitureForSVG: exportFurnitureForSVG,
15392
15524
  exportLayerForSVG: exportLayerForSVG,
15393
15525
  validateSvgDataFields: validateSvgDataFields,
15394
15526
  exportDataAttributesForSVG: exportDataAttributesForSVG,
@@ -19284,6 +19416,10 @@ ${svg}
19284
19416
  .option('id-prefix', {
19285
19417
  describe: '[SVG] prefix for namespacing layer and feature ids'
19286
19418
  })
19419
+ .option('scalebar', {
19420
+ type: 'flag',
19421
+ // describe: '[SVG] add a scalebar showing scale at the center of the map'
19422
+ })
19287
19423
  .option('delimiter', {
19288
19424
  describe: '[CSV] field delimiter'
19289
19425
  })
@@ -32534,6 +32670,80 @@ ${svg}
32534
32670
  return parsed[0];
32535
32671
  }
32536
32672
 
32673
+ /* require mapshaper-rectangle, mapshaper-furniture */
32674
+
32675
+ cmd.frame = function(catalog, source, opts) {
32676
+ var size, bounds, tmp, dataset;
32677
+ if (+opts.width > 0 === false && +opts.pixels > 0 === false) {
32678
+ stop("Missing a width or area");
32679
+ }
32680
+ if (opts.width && opts.height) {
32681
+ opts = utils.extend({}, opts);
32682
+ // Height is a string containing either a number or a
32683
+ // comma-sep. pair of numbers (range); here we convert height to
32684
+ // an aspect-ratio parameter for the rectangle() function
32685
+ opts.aspect_ratio = getAspectRatioArg(opts.width, opts.height);
32686
+ // TODO: currently returns max,min aspect ratio, should return in min,max order
32687
+ // (rectangle() function should handle max,min argument correctly now anyway)
32688
+ }
32689
+ tmp = cmd.rectangle(source, opts);
32690
+ bounds = getDatasetBounds(tmp);
32691
+ if (probablyDecimalDegreeBounds(bounds)) {
32692
+ stop('Frames require projected, not geographical coordinates');
32693
+ } else if (!getDatasetCRS(tmp)) {
32694
+ message('Warning: missing projection data. Assuming coordinates are meters and k (scale factor) is 1');
32695
+ }
32696
+ size = getFrameSize(bounds, opts);
32697
+ if (size[0] > 0 === false) {
32698
+ stop('Missing a valid frame width');
32699
+ }
32700
+ if (size[1] > 0 === false) {
32701
+ stop('Missing a valid frame height');
32702
+ }
32703
+ dataset = {info: {}, layers:[{
32704
+ name: opts.name || 'frame',
32705
+ data: new DataTable([{
32706
+ width: size[0],
32707
+ height: size[1],
32708
+ bbox: bounds.toArray(),
32709
+ type: 'frame'
32710
+ }])
32711
+ }]};
32712
+ catalog.addDataset(dataset);
32713
+ };
32714
+
32715
+ // Convert width and height args to aspect ratio arg for the rectangle() function
32716
+ function getAspectRatioArg(widthArg, heightArg) {
32717
+ // heightArg is a string containing either a number or a
32718
+ // comma-sep. pair of numbers (range);
32719
+ return heightArg.split(',').map(function(opt) {
32720
+ var height = Number(opt),
32721
+ width = Number(widthArg);
32722
+ if (!opt) return '';
32723
+ return width / height;
32724
+ }).reverse().join(',');
32725
+ }
32726
+
32727
+ furnitureRenderers.frame = function(d) {
32728
+ var lineWidth = 1,
32729
+ // inset stroke by half of line width
32730
+ off = lineWidth / 2,
32731
+ obj = importPolygon([[[off, off], [off, d.height - off],
32732
+ [d.width - off, d.height - off],
32733
+ [d.width - off, off], [off, off]]]);
32734
+ utils.extend(obj.properties, {
32735
+ fill: 'none',
32736
+ stroke: d.stroke || 'black',
32737
+ 'stroke-width': d['stroke-width'] || lineWidth
32738
+ });
32739
+ return [obj];
32740
+ };
32741
+
32742
+ var Frame = /*#__PURE__*/Object.freeze({
32743
+ __proto__: null,
32744
+ getAspectRatioArg: getAspectRatioArg
32745
+ });
32746
+
32537
32747
  cmd.filterGeom = function(lyr, arcs, opts) {
32538
32748
  if (!layerHasGeometry(lyr)) {
32539
32749
  stop("Layer is missing geometry");
@@ -37421,153 +37631,6 @@ ${svg}
37421
37631
  }
37422
37632
  };
37423
37633
 
37424
- cmd.scalebar = function(catalog, opts) {
37425
- var frame = findFrameDataset(catalog);
37426
- var obj, lyr;
37427
- if (!frame) {
37428
- stop('Missing a map frame');
37429
- }
37430
- obj = utils.defaults({type: 'scalebar'}, opts);
37431
- lyr = {
37432
- name: opts.name || 'scalebar',
37433
- data: new DataTable([obj])
37434
- };
37435
- frame.layers.push(lyr);
37436
- };
37437
-
37438
- // TODO: generalize to other kinds of furniture as they are developed
37439
- function getScalebarPosition(d) {
37440
- var opts = { // defaults
37441
- valign: 'top',
37442
- halign: 'left',
37443
- voffs: 10,
37444
- hoffs: 10
37445
- };
37446
- if (+d.left > 0) {
37447
- opts.hoffs = +d.left;
37448
- }
37449
- if (+d.top > 0) {
37450
- opts.voffs = +d.top;
37451
- }
37452
- if (+d.right > 0) {
37453
- opts.hoffs = +d.right;
37454
- opts.halign = 'right';
37455
- }
37456
- if (+d.bottom > 0) {
37457
- opts.voffs = +d.bottom;
37458
- opts.valign = 'bottom';
37459
- }
37460
- return opts;
37461
- }
37462
-
37463
- furnitureRenderers.scalebar = function(d, frame) {
37464
- var pos = getScalebarPosition(d);
37465
- var metersPerPx = getMapFrameMetersPerPixel(frame);
37466
- var label = d.label_text || getAutoScalebarLabel(frame.width, metersPerPx);
37467
- var scalebarKm = parseScalebarLabelToKm(label);
37468
- var barHeight = 3;
37469
- var labelOffs = 4;
37470
- var fontSize = +d.font_size || 13;
37471
- var width = Math.round(scalebarKm / metersPerPx * 1000);
37472
- var height = Math.round(barHeight + labelOffs + fontSize * 0.8);
37473
- var labelPos = d.label_position == 'top' ? 'top' : 'bottom';
37474
- var anchorX = pos.halign == 'left' ? 0 : width;
37475
- var anchorY = barHeight + labelOffs;
37476
- var dx = pos.halign == 'right' ? frame.width - width - pos.hoffs : pos.hoffs;
37477
- var dy = pos.valign == 'bottom' ? frame.height - height - pos.voffs : pos.voffs;
37478
-
37479
- if (labelPos == 'top') {
37480
- anchorY = -labelOffs;
37481
- dy += Math.round(labelOffs + fontSize * 0.8);
37482
- }
37483
-
37484
- if (width > 0 === false) {
37485
- stop("Null scalebar length");
37486
- }
37487
- var barObj = {
37488
- tag: 'rect',
37489
- properties: {
37490
- fill: 'black',
37491
- x: 0,
37492
- y: 0,
37493
- width: width,
37494
- height: barHeight
37495
- }
37496
- };
37497
- var labelOpts = {
37498
- 'label-text': label,
37499
- 'font-size': fontSize,
37500
- 'text-anchor': pos.halign == 'left' ? 'start': 'end',
37501
- 'dominant-baseline': labelPos == 'top' ? 'auto' : 'hanging'
37502
- //// 'dominant-baseline': labelPos == 'top' ? 'text-after-edge' : 'text-before-edge'
37503
- // 'text-after-edge' is buggy in Safari and unsupported by Illustrator,
37504
- // so I'm using 'hanging' and 'auto', which seem to be well supported.
37505
- // downside: requires a kludgy multiplier to calculate scalebar height (see above)
37506
- };
37507
- var labelObj = symbolRenderers.label(labelOpts, anchorX, anchorY);
37508
- var g = {
37509
- tag: 'g',
37510
- children: [barObj, labelObj],
37511
- properties: {
37512
- transform: 'translate(' + dx + ' ' + dy + ')'
37513
- }
37514
- };
37515
- return [g];
37516
- };
37517
-
37518
- function getAutoScalebarLabel(mapWidth, metersPerPx) {
37519
- var minWidth = 100; // TODO: vary min size based on map width
37520
- var minKm = metersPerPx * minWidth / 1000;
37521
- var options = ('1/8 1/5 1/4 1/2 1 1.5 2 3 4 5 8 10 12 15 20 25 30 40 50 75 ' +
37522
- '100 150 200 250 300 350 400 500 750 1,000 1,200 1,500 2,000 ' +
37523
- '2,500 3,000 4,000 5,000').split(' ');
37524
- return options.reduce(function(memo, str) {
37525
- if (memo) return memo;
37526
- var label = formatDistanceLabelAsMiles(str);
37527
- if (parseScalebarLabelToKm(label) > minKm) {
37528
- return label;
37529
- }
37530
- }, null) || '';
37531
- }
37532
-
37533
- function formatDistanceLabelAsMiles(str) {
37534
- var num = parseScalebarNumber(str);
37535
- return str + (num > 1 ? ' MILES' : ' MILE');
37536
- }
37537
-
37538
- // See test/mapshaper-scalebar.js for examples of supported formats
37539
- function parseScalebarLabelToKm(str) {
37540
- var units = parseScalebarUnits(str);
37541
- var value = parseScalebarNumber(str);
37542
- if (!units || !value) return NaN;
37543
- return units == 'mile' ? value * 1.60934 : value;
37544
- }
37545
-
37546
- function parseScalebarUnits(str) {
37547
- var isMiles = /miles?$/.test(str.toLowerCase());
37548
- var isKm = /(km|kilometers?|kilometres?)$/.test(str.toLowerCase());
37549
- return isMiles && 'mile' || isKm && 'km' || '';
37550
- }
37551
-
37552
- function parseScalebarNumber(str) {
37553
- var fractionRxp = /^([0-9]+) ?\/ ?([0-9]+)/;
37554
- var match, value;
37555
- str = str.replace(/[\s]/g, '').replace(/,/g, '');
37556
- if (fractionRxp.test(str)) {
37557
- match = fractionRxp.exec(str);
37558
- value = +match[1] / +match[2];
37559
- } else {
37560
- value = parseFloat(str);
37561
- }
37562
- return value > 0 && value < Infinity ? value : NaN;
37563
- }
37564
-
37565
- var Scalebar = /*#__PURE__*/Object.freeze({
37566
- __proto__: null,
37567
- formatDistanceLabelAsMiles: formatDistanceLabelAsMiles,
37568
- parseScalebarLabelToKm: parseScalebarLabelToKm
37569
- });
37570
-
37571
37634
  cmd.shape = function(targetDataset, opts) {
37572
37635
  var geojson, dataset;
37573
37636
  if (opts.coordinates) {
@@ -40869,6 +40932,7 @@ ${svg}
40869
40932
  FileTypes,
40870
40933
  FilterGeom,
40871
40934
  Frame,
40935
+ FrameData,
40872
40936
  Furniture,
40873
40937
  Geodesic,
40874
40938
  GeojsonExport,