mapshaper 0.5.113 → 0.5.116

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.116";
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,186 @@
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;
14927
+ furnitureRenderers.scalebar = renderScalebar;
14928
+
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 (!frame.crs) {
14946
+ message('Unable to render a scalebar: unknown CRS.');
14947
+ return [];
14707
14948
  }
14708
- height = width / aspectRatio;
14709
- return [Math.round(width), Math.round(height)];
14710
- }
14711
14949
 
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);
14950
+ if (labelPos == 'top') {
14951
+ anchorY = -labelOffs;
14952
+ dy += Math.round(labelOffs + fontSize * 0.8);
14717
14953
  }
14718
- return getDatasetBounds(dataset);
14719
- }
14720
14954
 
14721
- // @lyr dataset layer
14722
- function isFrameLayer(lyr) {
14723
- return getFurnitureLayerType(lyr) == 'frame';
14955
+ if (width > 0 === false) {
14956
+ stop("Null scalebar length");
14957
+ }
14958
+ var barObj = {
14959
+ tag: 'rect',
14960
+ properties: {
14961
+ fill: 'black',
14962
+ x: 0,
14963
+ y: 0,
14964
+ width: width,
14965
+ height: barHeight
14966
+ }
14967
+ };
14968
+ var labelOpts = {
14969
+ 'label-text': label,
14970
+ 'font-size': fontSize,
14971
+ 'text-anchor': pos.halign == 'left' ? 'start': 'end',
14972
+ 'dominant-baseline': labelPos == 'top' ? 'auto' : 'hanging'
14973
+ //// 'dominant-baseline': labelPos == 'top' ? 'text-after-edge' : 'text-before-edge'
14974
+ // 'text-after-edge' is buggy in Safari and unsupported by Illustrator,
14975
+ // so I'm using 'hanging' and 'auto', which seem to be well supported.
14976
+ // downside: requires a kludgy multiplier to calculate scalebar height (see above)
14977
+ };
14978
+ var labelObj = symbolRenderers.label(labelOpts, anchorX, anchorY);
14979
+ var g = {
14980
+ tag: 'g',
14981
+ children: [barObj, labelObj],
14982
+ properties: {
14983
+ transform: 'translate(' + dx + ' ' + dy + ')'
14984
+ }
14985
+ };
14986
+ return [g];
14724
14987
  }
14725
14988
 
14726
- function findFrameLayerInDataset(dataset) {
14727
- return utils.find(dataset.layers, function(lyr) {
14728
- return isFrameLayer(lyr);
14729
- });
14989
+ function getAutoScalebarLabel(mapWidth, metersPerPx) {
14990
+ var minWidth = 70; // 100; // TODO: vary min size based on map width
14991
+ var minKm = metersPerPx * minWidth / 1000;
14992
+ // note: removed 1.5 12 and 1,200
14993
+ var options = ('1/8 1/5 1/4 1/2 1 2 3 4 5 8 10 15 20 25 30 40 50 75 ' +
14994
+ '100 150 200 250 300 350 400 500 750 1,000 1,500 2,000 ' +
14995
+ '2,500 3,000 4,000 5,000').split(' ');
14996
+ return options.reduce(function(memo, str) {
14997
+ if (memo) return memo;
14998
+ var label = formatDistanceLabelAsMiles(str);
14999
+ if (parseScalebarLabelToKm(label) > minKm) {
15000
+ return label;
15001
+ }
15002
+ }, null) || '';
14730
15003
  }
14731
15004
 
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;
15005
+ function formatDistanceLabelAsMiles(str) {
15006
+ var num = parseScalebarNumber(str);
15007
+ return str + (num > 1 ? ' MILES' : ' MILE');
14737
15008
  }
14738
15009
 
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;
15010
+ // See test/mapshaper-scalebar.js for examples of supported formats
15011
+ function parseScalebarLabelToKm(str) {
15012
+ var units = parseScalebarUnits(str);
15013
+ var value = parseScalebarNumber(str);
15014
+ if (!units || !value) return NaN;
15015
+ return units == 'mile' ? value * 1.60934 : value;
14744
15016
  }
14745
15017
 
14746
- function getFrameLayerBounds(lyr) {
14747
- return new Bounds(getFurnitureLayerData(lyr).bbox);
15018
+ function parseScalebarUnits(str) {
15019
+ var isMiles = /miles?$/.test(str.toLowerCase());
15020
+ var isKm = /(km|kilometers?|kilometres?)$/.test(str.toLowerCase());
15021
+ return isMiles && 'mile' || isKm && 'km' || '';
14748
15022
  }
14749
15023
 
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;
15024
+ function parseScalebarNumber(str) {
15025
+ var fractionRxp = /^([0-9]+) ?\/ ?([0-9]+)/;
15026
+ var match, value;
15027
+ str = str.replace(/[\s]/g, '').replace(/,/g, '');
15028
+ if (fractionRxp.test(str)) {
15029
+ match = fractionRxp.exec(str);
15030
+ value = +match[1] / +match[2];
14761
15031
  } 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;
15032
+ value = parseFloat(str);
14766
15033
  }
14767
- metersPerPixel = bounds.width() / k * toMeters / data.width;
14768
- return metersPerPixel;
15034
+ return value > 0 && value < Infinity ? value : NaN;
14769
15035
  }
14770
15036
 
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({
15037
+ var Scalebar = /*#__PURE__*/Object.freeze({
14787
15038
  __proto__: null,
14788
- getAspectRatioArg: getAspectRatioArg,
14789
- getFrameSize: getFrameSize,
14790
- findFrameLayerInDataset: findFrameLayerInDataset,
14791
- findFrameDataset: findFrameDataset,
14792
- findFrameLayer: findFrameLayer,
14793
- getFrameLayerBounds: getFrameLayerBounds,
14794
- getMapFrameMetersPerPixel: getMapFrameMetersPerPixel
15039
+ getScalebarLayer: getScalebarLayer,
15040
+ renderScalebar: renderScalebar,
15041
+ formatDistanceLabelAsMiles: formatDistanceLabelAsMiles,
15042
+ parseScalebarLabelToKm: parseScalebarLabelToKm
14795
15043
  });
14796
15044
 
14797
15045
  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];
15046
+ var frame = getFrameData(dataset, opts);
15047
+ fitDatasetToFrame(dataset, frame, opts);
15048
+ return [frame.width, frame.height];
14815
15049
  }
14816
15050
 
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
15051
+ function fitDatasetToFrame(dataset, frame, opts) {
15052
+ var bounds = new Bounds(frame.bbox);
15053
+ var bounds2 = new Bounds(0, 0, frame.width, frame.height);
15054
+ var fwd = bounds.getTransform(bounds2, opts.invert_y);
15055
+ transformPoints(dataset, function(x, y) {
15056
+ return fwd.transform(x, y);
14826
15057
  });
14827
15058
  }
14828
15059
 
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
15060
  var PixelTransform = /*#__PURE__*/Object.freeze({
14923
15061
  __proto__: null,
14924
15062
  transformDatasetToPixels: transformDatasetToPixels,
14925
- parseMarginOption: parseMarginOption,
14926
- calcOutputSizeInPixels: calcOutputSizeInPixels
15063
+ fitDatasetToFrame: fitDatasetToFrame
14927
15064
  });
14928
15065
 
14929
15066
  function stringify(obj) {
@@ -14980,6 +15117,9 @@
14980
15117
  if (key == 'href') {
14981
15118
  key = 'xlink:href';
14982
15119
  }
15120
+ if (key == 'css') {
15121
+ key = 'style'; // inline style
15122
+ }
14983
15123
  return memo + ' ' + key + '="' + stringEscape(strval) + '"';
14984
15124
  }, '');
14985
15125
  }
@@ -15147,12 +15287,11 @@
15147
15287
  }
15148
15288
  */
15149
15289
 
15150
- //
15151
15290
  //
15152
15291
  function exportSVG(dataset, opts) {
15153
15292
  var namespace = 'xmlns="http://www.w3.org/2000/svg"';
15154
15293
  var defs = [];
15155
- var size, svg;
15294
+ var frame, svg, layers;
15156
15295
  var style = '';
15157
15296
 
15158
15297
  // kludge for map keys
@@ -15169,15 +15308,27 @@
15169
15308
  } else {
15170
15309
  dataset = copyDataset(dataset); // Modify a copy of the dataset
15171
15310
  }
15311
+
15172
15312
  // invert_y setting for screen coordinates and geojson polygon generation
15173
15313
  utils.extend(opts, {invert_y: true});
15174
- size = transformCoordsForSVG(dataset, opts);
15314
+ frame = getFrameData(dataset, opts);
15315
+ fitDatasetToFrame(dataset, frame, opts);
15316
+ setCoordinatePrecision(dataset, opts.precision || 0.0001);
15175
15317
 
15176
15318
  // error if one or more svg_data fields are not present in any layers
15177
15319
  if (opts.svg_data) validateSvgDataFields(dataset.layers, opts.svg_data);
15178
15320
 
15179
- svg = dataset.layers.map(function(lyr) {
15180
- var obj = exportLayerForSVG(lyr, dataset, opts);
15321
+ layers = dataset.layers;
15322
+ if (opts.scalebar) {
15323
+ layers.push(getScalebarLayer({})); // default options
15324
+ }
15325
+ svg = layers.map(function(lyr) {
15326
+ var obj;
15327
+ if (layerHasFurniture(lyr)) {
15328
+ obj = exportFurnitureForSVG(lyr, frame, opts);
15329
+ } else {
15330
+ obj = exportLayerForSVG(lyr, dataset, opts);
15331
+ }
15181
15332
  convertPropertiesToDefinitions(obj, defs);
15182
15333
  return stringify(obj);
15183
15334
  }).join('\n');
@@ -15202,39 +15353,25 @@
15202
15353
  <svg ${namespace} version="1.2" baseProfile="tiny" width="%d" height="%d" viewBox="%s %s %s %s" ${lineProps}>${style}
15203
15354
  ${svg}
15204
15355
  </svg>`;
15205
- svg = utils.format(template,size[0], size[1], 0, 0, size[0], size[1]);
15356
+ svg = utils.format(template, frame.width, frame.height, 0, 0, frame.width, frame.height);
15206
15357
  return [{
15207
15358
  content: svg,
15208
15359
  filename: opts.file || getOutputFileBase(dataset) + '.svg'
15209
15360
  }];
15210
15361
  }
15211
15362
 
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;
15363
+ function exportFurnitureForSVG(lyr, frame, opts) {
15364
+ var layerObj = getEmptyLayerForSVG(lyr, opts);
15365
+ layerObj.children = importFurniture(getFurnitureLayerData(lyr), frame);
15366
+ return layerObj;
15217
15367
  }
15218
15368
 
15219
15369
  function exportLayerForSVG(lyr, dataset, opts) {
15220
15370
  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
- }
15371
+ layerObj.children = exportSymbolsForSVG(lyr, dataset, opts);
15226
15372
  return layerObj;
15227
15373
  }
15228
15374
 
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
15375
  function exportSymbolsForSVG(lyr, dataset, opts) {
15239
15376
  // TODO: convert geojson features one at a time
15240
15377
  var d = utils.defaults({layers: [lyr]}, dataset);
@@ -15389,6 +15526,7 @@ ${svg}
15389
15526
  var Svg = /*#__PURE__*/Object.freeze({
15390
15527
  __proto__: null,
15391
15528
  exportSVG: exportSVG,
15529
+ exportFurnitureForSVG: exportFurnitureForSVG,
15392
15530
  exportLayerForSVG: exportLayerForSVG,
15393
15531
  validateSvgDataFields: validateSvgDataFields,
15394
15532
  exportDataAttributesForSVG: exportDataAttributesForSVG,
@@ -15868,7 +16006,9 @@ ${svg}
15868
16006
 
15869
16007
  function initStringField(info, arr, name, encoding) {
15870
16008
  var formatter = encoding == 'ascii' ? encodeValueAsAscii : getStringWriterEncoded(encoding);
15871
- var size = 0;
16009
+ // Set minimum field size to 1 byte, for interoperability with PostGIS
16010
+ // (see https://github.com/mbloch/mapshaper/issues/541)
16011
+ var size = 1;
15872
16012
  var truncated = 0;
15873
16013
  var buffers = arr.map(function(rec) {
15874
16014
  var strval = convertValueToString(rec[name]);
@@ -19284,6 +19424,10 @@ ${svg}
19284
19424
  .option('id-prefix', {
19285
19425
  describe: '[SVG] prefix for namespacing layer and feature ids'
19286
19426
  })
19427
+ .option('scalebar', {
19428
+ type: 'flag',
19429
+ // describe: '[SVG] add a scalebar showing scale at the center of the map'
19430
+ })
19287
19431
  .option('delimiter', {
19288
19432
  describe: '[CSV] field delimiter'
19289
19433
  })
@@ -32534,6 +32678,80 @@ ${svg}
32534
32678
  return parsed[0];
32535
32679
  }
32536
32680
 
32681
+ /* require mapshaper-rectangle, mapshaper-furniture */
32682
+
32683
+ cmd.frame = function(catalog, source, opts) {
32684
+ var size, bounds, tmp, dataset;
32685
+ if (+opts.width > 0 === false && +opts.pixels > 0 === false) {
32686
+ stop("Missing a width or area");
32687
+ }
32688
+ if (opts.width && opts.height) {
32689
+ opts = utils.extend({}, opts);
32690
+ // Height is a string containing either a number or a
32691
+ // comma-sep. pair of numbers (range); here we convert height to
32692
+ // an aspect-ratio parameter for the rectangle() function
32693
+ opts.aspect_ratio = getAspectRatioArg(opts.width, opts.height);
32694
+ // TODO: currently returns max,min aspect ratio, should return in min,max order
32695
+ // (rectangle() function should handle max,min argument correctly now anyway)
32696
+ }
32697
+ tmp = cmd.rectangle(source, opts);
32698
+ bounds = getDatasetBounds(tmp);
32699
+ if (probablyDecimalDegreeBounds(bounds)) {
32700
+ stop('Frames require projected, not geographical coordinates');
32701
+ } else if (!getDatasetCRS(tmp)) {
32702
+ message('Warning: missing projection data. Assuming coordinates are meters and k (scale factor) is 1');
32703
+ }
32704
+ size = getFrameSize(bounds, opts);
32705
+ if (size[0] > 0 === false) {
32706
+ stop('Missing a valid frame width');
32707
+ }
32708
+ if (size[1] > 0 === false) {
32709
+ stop('Missing a valid frame height');
32710
+ }
32711
+ dataset = {info: {}, layers:[{
32712
+ name: opts.name || 'frame',
32713
+ data: new DataTable([{
32714
+ width: size[0],
32715
+ height: size[1],
32716
+ bbox: bounds.toArray(),
32717
+ type: 'frame'
32718
+ }])
32719
+ }]};
32720
+ catalog.addDataset(dataset);
32721
+ };
32722
+
32723
+ // Convert width and height args to aspect ratio arg for the rectangle() function
32724
+ function getAspectRatioArg(widthArg, heightArg) {
32725
+ // heightArg is a string containing either a number or a
32726
+ // comma-sep. pair of numbers (range);
32727
+ return heightArg.split(',').map(function(opt) {
32728
+ var height = Number(opt),
32729
+ width = Number(widthArg);
32730
+ if (!opt) return '';
32731
+ return width / height;
32732
+ }).reverse().join(',');
32733
+ }
32734
+
32735
+ furnitureRenderers.frame = function(d) {
32736
+ var lineWidth = 1,
32737
+ // inset stroke by half of line width
32738
+ off = lineWidth / 2,
32739
+ obj = importPolygon([[[off, off], [off, d.height - off],
32740
+ [d.width - off, d.height - off],
32741
+ [d.width - off, off], [off, off]]]);
32742
+ utils.extend(obj.properties, {
32743
+ fill: 'none',
32744
+ stroke: d.stroke || 'black',
32745
+ 'stroke-width': d['stroke-width'] || lineWidth
32746
+ });
32747
+ return [obj];
32748
+ };
32749
+
32750
+ var Frame = /*#__PURE__*/Object.freeze({
32751
+ __proto__: null,
32752
+ getAspectRatioArg: getAspectRatioArg
32753
+ });
32754
+
32537
32755
  cmd.filterGeom = function(lyr, arcs, opts) {
32538
32756
  if (!layerHasGeometry(lyr)) {
32539
32757
  stop("Layer is missing geometry");
@@ -37421,153 +37639,6 @@ ${svg}
37421
37639
  }
37422
37640
  };
37423
37641
 
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
37642
  cmd.shape = function(targetDataset, opts) {
37572
37643
  var geojson, dataset;
37573
37644
  if (opts.coordinates) {
@@ -40869,6 +40940,7 @@ ${svg}
40869
40940
  FileTypes,
40870
40941
  FilterGeom,
40871
40942
  Frame,
40943
+ FrameData,
40872
40944
  Furniture,
40873
40945
  Geodesic,
40874
40946
  GeojsonExport,