mapshaper 0.5.111 → 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/www/mapshaper.js CHANGED
@@ -1,6 +1,6 @@
1
1
  (function () {
2
2
 
3
- var VERSION = "0.5.111";
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) {
@@ -13744,6 +13962,14 @@
13744
13962
  return p.length > 2 && p[2] == 'C';
13745
13963
  }
13746
13964
 
13965
+ function stringifyPolygonCoords(coords) {
13966
+ var parts = [];
13967
+ for (var i=0; i<coords.length; i++) {
13968
+ parts.push(stringifyLineStringCoords(coords[i]) + ' Z');
13969
+ }
13970
+ return parts.length > 0 ? parts.join(' ') : '';
13971
+ }
13972
+
13747
13973
  function stringifyLineStringCoords(coords) {
13748
13974
  if (coords.length === 0) return '';
13749
13975
  var d = 'M';
@@ -13774,6 +14000,7 @@
13774
14000
 
13775
14001
  var SvgPathUtils = /*#__PURE__*/Object.freeze({
13776
14002
  __proto__: null,
14003
+ stringifyPolygonCoords: stringifyPolygonCoords,
13777
14004
  stringifyLineStringCoords: stringifyLineStringCoords
13778
14005
  });
13779
14006
 
@@ -14023,6 +14250,7 @@
14023
14250
  // null values indicate the lack of a function for parsing/identifying this property
14024
14251
  // (in which case a heuristic is used for distinguishing a string literal from an expression)
14025
14252
  var stylePropertyTypes = {
14253
+ css: null,
14026
14254
  class: 'classname',
14027
14255
  dx: 'measure',
14028
14256
  dy: 'measure',
@@ -14041,6 +14269,7 @@
14041
14269
  'stroke-dasharray': 'dasharray',
14042
14270
  'stroke-width': 'number',
14043
14271
  'stroke-opacity': 'number',
14272
+ 'stroke-miterlimit': 'number',
14044
14273
  'fill-opacity': 'number',
14045
14274
  'text-anchor': null
14046
14275
  };
@@ -14071,16 +14300,34 @@
14071
14300
  effect: null // e.g. "fade"
14072
14301
  }, stylePropertyTypes);
14073
14302
 
14074
- 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(',');
14075
14304
 
14076
14305
  var propertiesBySymbolType = {
14077
14306
  polygon: utils.arrayToIndex(commonProperties.concat('fill', 'fill-pattern')),
14078
- polyline: utils.arrayToIndex(commonProperties.concat('stroke-linecap', 'stroke-linejoin')),
14307
+ polyline: utils.arrayToIndex(commonProperties.concat('stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit')),
14079
14308
  point: utils.arrayToIndex(commonProperties.concat('fill', 'r')),
14080
14309
  label: utils.arrayToIndex(commonProperties.concat(
14081
- 'fill,r,font-family,font-size,text-anchor,font-weight,font-style,letter-spacing,dominant-baseline'.split(',')))
14310
+ 'fill,font-family,font-size,text-anchor,font-weight,font-style,letter-spacing,dominant-baseline'.split(',')))
14082
14311
  };
14083
14312
 
14313
+ // symType: point, polygon, polyline, label
14314
+ function applyStyleAttributes(svgObj, symType, rec, filter) {
14315
+ var fields = findPropertiesBySymbolGeom(Object.keys(rec || {}), symType);
14316
+ for (var i=0, n=fields.length; i<n; i++) {
14317
+ if (filter && !filter(fields[i])) continue;
14318
+ setAttribute(svgObj, fields[i], rec[fields[i]]);
14319
+ }
14320
+ }
14321
+
14322
+ function setAttribute(obj, k, v) {
14323
+ if (!obj.properties) obj.properties = {};
14324
+ obj.properties[k] = v;
14325
+ if (k == 'stroke-dasharray' && v) {
14326
+ // kludge for cleaner dashes... make butt the default?
14327
+ obj.properties['stroke-linecap'] = 'butt';
14328
+ }
14329
+ }
14330
+
14084
14331
  function isSupportedSvgStyleProperty(name) {
14085
14332
  return name in stylePropertyTypes;
14086
14333
  }
@@ -14154,7 +14401,6 @@
14154
14401
  // treating the string as a literal value
14155
14402
  literalVal = strVal;
14156
14403
  }
14157
- // console.log("literalVal:", mightBeExpression(strVal, fields), strVal, fields)
14158
14404
  if (accessor) return accessor;
14159
14405
  if (literalVal !== null) return function(id) {return literalVal;};
14160
14406
  stop('Unexpected value for', svgName + ':', strVal);
@@ -14236,6 +14482,7 @@
14236
14482
 
14237
14483
  var SvgProperties = /*#__PURE__*/Object.freeze({
14238
14484
  __proto__: null,
14485
+ applyStyleAttributes: applyStyleAttributes,
14239
14486
  isSupportedSvgStyleProperty: isSupportedSvgStyleProperty,
14240
14487
  findPropertiesBySymbolGeom: findPropertiesBySymbolGeom,
14241
14488
  getSymbolDataAccessor: getSymbolDataAccessor,
@@ -14249,37 +14496,36 @@
14249
14496
  isSvgColor: isSvgColor
14250
14497
  });
14251
14498
 
14252
- var symbolRenderers = {};
14253
-
14254
- function getTransform(xy, scale) {
14255
- var str = 'translate(' + roundToTenths(xy[0]) + ' ' + roundToTenths(xy[1]) + ')';
14256
- if (scale && scale != 1) {
14257
- str += ' scale(' + scale + ')';
14258
- }
14259
- return str;
14260
- }
14261
-
14262
- var SvgCommon = /*#__PURE__*/Object.freeze({
14263
- __proto__: null,
14264
- symbolRenderers: symbolRenderers,
14265
- getTransform: getTransform
14266
- });
14499
+ var geojsonImporters = {
14500
+ Point: importPoint,
14501
+ Polygon: importPolygon,
14502
+ LineString: importLineString,
14503
+ MultiPoint: importMultiPoint,
14504
+ MultiLineString: importMultiLineString,
14505
+ MultiPolygon: importMultiPolygon
14506
+ };
14267
14507
 
14268
14508
  function importGeoJSONFeatures(features, opts) {
14269
14509
  opts = opts || {};
14270
14510
  return features.map(function(obj, i) {
14271
14511
  var geom = obj.type == 'Feature' ? obj.geometry : obj; // could be null
14272
14512
  var geomType = geom && geom.type;
14513
+ var msType = GeoJSON.translateGeoJSONType(geomType);
14514
+ var d = obj.properties || {};
14273
14515
  var svgObj = null;
14274
14516
  if (geomType && geom.coordinates) {
14275
- svgObj = geojsonImporters[geomType](geom.coordinates, obj.properties, opts);
14517
+ svgObj = geojsonImporters[geomType](geom.coordinates, d);
14276
14518
  }
14277
14519
  if (!svgObj) {
14278
14520
  return {tag: 'g'}; // empty element
14279
- }
14280
- // TODO: fix error caused by null svgObj (caused by e.g. MultiPolygon with [] coordinates)
14281
- if (obj.properties) {
14282
- applyStyleAttributes(svgObj, geomType, obj.properties);
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
14283
14529
  }
14284
14530
  if ('id' in obj) {
14285
14531
  if (!svgObj.properties) {
@@ -14291,34 +14537,37 @@
14291
14537
  });
14292
14538
  }
14293
14539
 
14294
- function applyStyleAttributes(svgObj, geomType, rec) {
14295
- var symbolType = GeoJSON.translateGeoJSONType(geomType);
14296
- if (symbolType == 'point' && ('label-text' in rec)) {
14297
- symbolType = 'label';
14298
- }
14299
- var fields = findPropertiesBySymbolGeom(Object.keys(rec), symbolType);
14300
- for (var i=0, n=fields.length; i<n; i++) {
14301
- setAttribute(svgObj, fields[i], rec[fields[i]]);
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
+ };
14302
14551
  }
14552
+ var o = renderPoint(rec, coords);
14553
+ if (o) o.properties.transform = getTransform(coords);
14554
+ return o;
14303
14555
  }
14304
14556
 
14305
- function setAttribute(obj, k, v) {
14306
- if (k == 'r') {
14307
- // assigned by importPoint()
14308
- } else {
14309
- if (!obj.properties) obj.properties = {};
14310
- obj.properties[k] = v;
14311
- if (k == 'stroke-dasharray' && v) {
14312
- // kludge for cleaner dashes... make butt the default?
14313
- obj.properties['stroke-linecap'] = 'butt';
14314
- }
14315
- }
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']);
14316
14564
  }
14317
14565
 
14318
- function importMultiPoint(coords, rec, layerOpts) {
14566
+ function importMultiPoint(coords, rec) {
14319
14567
  var children = [], p;
14320
14568
  for (var i=0; i<coords.length; i++) {
14321
- p = importPoint(coords[i], rec, layerOpts);
14569
+ p = importPoint(coords[i], rec);
14570
+ if (!p) continue;
14322
14571
  if (p.tag == 'g' && p.children) {
14323
14572
  children = children.concat(p.children);
14324
14573
  } else {
@@ -14328,18 +14577,6 @@
14328
14577
  return children.length > 0 ? {tag: 'g', children: children} : null;
14329
14578
  }
14330
14579
 
14331
- function importMultiPath(coords, importer) {
14332
- var o;
14333
- for (var i=0; i<coords.length; i++) {
14334
- if (i === 0) {
14335
- o = importer(coords[i]);
14336
- } else {
14337
- o.properties.d += ' ' + importer(coords[i]).properties.d;
14338
- }
14339
- }
14340
- return o;
14341
- }
14342
-
14343
14580
  function importLineString(coords) {
14344
14581
  var d = stringifyLineStringCoords(coords);
14345
14582
  return {
@@ -14348,7 +14585,6 @@
14348
14585
  };
14349
14586
  }
14350
14587
 
14351
-
14352
14588
  function importMultiLineString(coords) {
14353
14589
  var d = coords.map(stringifyLineStringCoords).join(' ');
14354
14590
  return {
@@ -14357,20 +14593,55 @@
14357
14593
  };
14358
14594
  }
14359
14595
 
14360
- // Kludge for applying fill and other styles to a <text> element
14361
- // (for rendering labels in the GUI with the dot in Canvas, not SVG)
14362
- function importStyledLabel(rec, p) {
14363
- var o = importLabel(rec, p);
14364
- applyStyleAttributes(o, 'Point', rec);
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
+ }
14365
14617
  return o;
14366
14618
  }
14367
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
+
14368
14631
  function toLabelString(val) {
14369
14632
  if (val || val === 0 || val === false) return String(val);
14370
14633
  return '';
14371
14634
  }
14372
14635
 
14373
- function importLabel(rec, p) {
14636
+ // Kludge for applying fill and other styles to a <text> element
14637
+ // (for rendering labels in the GUI with the dot in Canvas, not SVG)
14638
+ function renderStyledLabel(rec) {
14639
+ var o = renderLabel(rec);
14640
+ applyStyleAttributes(o, 'label', rec);
14641
+ return o;
14642
+ }
14643
+
14644
+ function renderLabel(rec) {
14374
14645
  var line = toLabelString(rec['label-text']);
14375
14646
  var morelines, obj;
14376
14647
  // Accepting \n (two chars) as an alternative to the newline character
@@ -14385,9 +14656,6 @@
14385
14656
  y: dy,
14386
14657
  x: dx
14387
14658
  };
14388
- if (p) {
14389
- properties.transform = getTransform(p);
14390
- }
14391
14659
  if (newline.test(line)) {
14392
14660
  morelines = line.split(newline);
14393
14661
  line = morelines.shift();
@@ -14415,406 +14683,378 @@
14415
14683
  return obj;
14416
14684
  }
14417
14685
 
14418
- function getEmptySymbol() {
14419
- return {tag: 'g', properties: {}, children: []};
14420
- }
14686
+ var SvgLabels = /*#__PURE__*/Object.freeze({
14687
+ __proto__: null,
14688
+ renderStyledLabel: renderStyledLabel,
14689
+ renderLabel: renderLabel
14690
+ });
14421
14691
 
14692
+ // convert data records (properties like svg-symbol, label-text, fill, r) to svg symbols
14422
14693
 
14423
- function renderSymbol(d, x, y) {
14424
- var renderer = symbolRenderers[d.type];
14425
- if (!renderer) {
14426
- stop(d.type ? 'Unknown symbol type: ' + d.type : 'Symbol is missing a type property');
14427
- }
14428
- return renderer(d, x || 0, y || 0);
14429
- }
14430
-
14431
- // d: svg-symbol object from feature data object
14432
- function importSymbol(d, xy) {
14433
- var renderer;
14434
- if (!d) {
14435
- return getEmptySymbol();
14436
- }
14437
- if (utils.isString(d)) {
14438
- d = JSON.parse(d);
14694
+ function getTransform(xy, scale) {
14695
+ var str = 'translate(' + roundToTenths(xy[0]) + ' ' + roundToTenths(xy[1]) + ')';
14696
+ if (scale && scale != 1) {
14697
+ str += ' scale(' + scale + ')';
14439
14698
  }
14440
- return {
14441
- tag: 'g',
14442
- properties: {
14443
- 'class': 'mapshaper-svg-symbol',
14444
- transform: xy ? getTransform(xy) : null
14445
- },
14446
- children: renderSymbol(d)
14447
- };
14699
+ return str;
14448
14700
  }
14449
14701
 
14450
- function importPoint(coords, rec, layerOpts) {
14451
- rec = rec || {};
14452
- if ('svg-symbol' in rec) {
14453
- return importSymbol(rec['svg-symbol'], coords);
14454
- }
14455
- return importStandardPoint(coords, rec, layerOpts || {});
14456
- }
14702
+ var symbolRenderers = {
14703
+ line: line,
14704
+ polygon: polygon,
14705
+ polyline: polyline,
14706
+ circle: circle,
14707
+ square: square,
14708
+ image: image,
14709
+ group: group,
14710
+ label: label,
14711
+ offset: offset
14712
+ };
14457
14713
 
14458
- function importPolygon(coords) {
14459
- var d, o;
14460
- for (var i=0; i<coords.length; i++) {
14461
- d = o ? o.properties.d + ' ' : '';
14462
- o = importLineString(coords[i]);
14463
- o.properties.d = d + o.properties.d + ' Z';
14714
+ // render label and/or point symbol
14715
+ function renderPoint(rec) {
14716
+ var children = [];
14717
+ // var halfSize = rec.r || 0; // radius or half of symbol size
14718
+ if (featureHasSvgSymbol(rec)) {
14719
+ children.push(renderSymbol(rec));
14464
14720
  }
14465
- if (coords.length > 1) {
14466
- o.properties['fill-rule'] = 'evenodd'; // support polygons with holes
14721
+ if (featureHasLabel(rec)) {
14722
+ children.push(renderStyledLabel(rec));
14467
14723
  }
14724
+ var o = children.length > 1 ? {tag: 'g', children: children} : children[0];
14725
+ if (!o) return null;
14726
+ o.properties = o.properties || {};
14468
14727
  return o;
14469
14728
  }
14470
14729
 
14471
- function importStandardPoint(coords, rec, layerOpts) {
14472
- var isLabel = 'label-text' in rec;
14473
- var symbolType = layerOpts.point_symbol || '';
14474
- var children = [];
14475
- var halfSize = rec.r || 0; // radius or half of symbol size
14476
- var p;
14477
- // if not a label, create a symbol even without a size
14478
- // (circle radius can be set via CSS)
14479
- if (halfSize > 0 || !isLabel) {
14480
- if (symbolType == 'square') {
14481
- p = {
14482
- tag: 'rect',
14483
- properties: {
14484
- x: coords[0] - halfSize,
14485
- y: coords[1] - halfSize,
14486
- width: halfSize * 2,
14487
- height: halfSize * 2
14488
- }};
14489
- } else { // default is circle
14490
- p = {
14491
- tag: 'circle',
14492
- properties: {
14493
- cx: coords[0],
14494
- cy: coords[1]
14495
- }};
14496
- if (halfSize > 0) {
14497
- p.properties.r = halfSize;
14498
- }
14499
- }
14500
- children.push(p);
14730
+ function renderSymbol(d) {
14731
+ if (d['svg-symbol']) {
14732
+ return renderComplexSymbol(d['svg-symbol']);
14501
14733
  }
14502
- if (isLabel) {
14503
- children.push(importLabel(rec, coords));
14734
+ if (d.r > 0) {
14735
+ return circle(d);
14504
14736
  }
14505
- return children.length > 1 ? {tag: 'g', children: children} : children[0];
14737
+ return empty();
14506
14738
  }
14507
14739
 
14508
- var geojsonImporters = {
14509
- Point: importPoint,
14510
- Polygon: importPolygon,
14511
- LineString: importLineString,
14512
- MultiPoint: function(coords, rec, opts) {
14513
- return importMultiPoint(coords, rec, opts);
14514
- },
14515
- MultiLineString: function(coords) {
14516
- return importMultiPath(coords, importLineString);
14517
- },
14518
- MultiPolygon: function(coords) {
14519
- return importMultiPath(coords, importPolygon);
14520
- }
14521
- };
14522
-
14523
- var GeojsonToSvg = /*#__PURE__*/Object.freeze({
14524
- __proto__: null,
14525
- importGeoJSONFeatures: importGeoJSONFeatures,
14526
- applyStyleAttributes: applyStyleAttributes,
14527
- importLineString: importLineString,
14528
- importMultiLineString: importMultiLineString,
14529
- importStyledLabel: importStyledLabel,
14530
- importLabel: importLabel,
14531
- renderSymbol: renderSymbol,
14532
- importSymbol: importSymbol,
14533
- importPoint: importPoint,
14534
- importPolygon: importPolygon
14535
- });
14536
-
14537
- /* require mapshaper-rectangle, mapshaper-furniture */
14538
-
14539
- cmd.frame = function(catalog, source, opts) {
14540
- var size, bounds, tmp, dataset;
14541
- if (+opts.width > 0 === false && +opts.pixels > 0 === false) {
14542
- stop("Missing a width or area");
14740
+ function renderComplexSymbol(sym, x, y) {
14741
+ if (utils.isString(sym)) {
14742
+ sym = JSON.parse(sym);
14543
14743
  }
14544
- if (opts.width && opts.height) {
14545
- opts = utils.extend({}, opts);
14546
- // Height is a string containing either a number or a
14547
- // comma-sep. pair of numbers (range); here we convert height to
14548
- // an aspect-ratio parameter for the rectangle() function
14549
- opts.aspect_ratio = getAspectRatioArg(opts.width, opts.height);
14550
- // TODO: currently returns max,min aspect ratio, should return in min,max order
14551
- // (rectangle() function should handle max,min argument correctly now anyway)
14744
+ if (sym.tag) {
14745
+ // symbol appears to already use mapshaper's svg notation... pass through
14746
+ return sym;
14552
14747
  }
14553
- tmp = cmd.rectangle(source, opts);
14554
- bounds = getDatasetBounds(tmp);
14555
- if (probablyDecimalDegreeBounds(bounds)) {
14556
- stop('Frames require projected, not geographical coordinates');
14557
- } else if (!getDatasetCRS(tmp)) {
14558
- message('Warning: missing projection data. Assuming coordinates are meters and k (scale factor) is 1');
14559
- }
14560
- size = getFrameSize(bounds, opts);
14561
- if (size[0] > 0 === false) {
14562
- stop('Missing a valid frame width');
14748
+ var renderer = symbolRenderers[sym.type];
14749
+ if (!renderer) {
14750
+ message(sym.type ? 'Unknown symbol type: ' + sym.type : 'Symbol is missing a type property');
14751
+ return empty();
14563
14752
  }
14564
- if (size[1] > 0 === false) {
14565
- stop('Missing a valid frame height');
14753
+ var o = renderer(sym, x || 0, y || 0);
14754
+ if (sym.opacity) {
14755
+ o.properties.opacity = sym.opacity;
14566
14756
  }
14567
- dataset = {info: {}, layers:[{
14568
- name: opts.name || 'frame',
14569
- data: new DataTable([{
14570
- width: size[0],
14571
- height: size[1],
14572
- bbox: bounds.toArray(),
14573
- type: 'frame'
14574
- }])
14575
- }]};
14576
- catalog.addDataset(dataset);
14577
- };
14757
+ return o;
14758
+ }
14578
14759
 
14579
- // Convert width and height args to aspect ratio arg for the rectangle() function
14580
- function getAspectRatioArg(widthArg, heightArg) {
14581
- // heightArg is a string containing either a number or a
14582
- // comma-sep. pair of numbers (range);
14583
- return heightArg.split(',').map(function(opt) {
14584
- var height = Number(opt),
14585
- width = Number(widthArg);
14586
- if (!opt) return '';
14587
- return width / height;
14588
- }).reverse().join(',');
14760
+ function empty() {
14761
+ return {tag: 'g', properties: {}, children: []};
14589
14762
  }
14590
14763
 
14591
- function getFrameSize(bounds, opts) {
14592
- var aspectRatio = bounds.width() / bounds.height();
14593
- var height, width;
14594
- if (opts.pixels) {
14595
- width = Math.sqrt(+opts.pixels * aspectRatio);
14596
- } else {
14597
- width = +opts.width;
14598
- }
14599
- height = width / aspectRatio;
14600
- return [Math.round(width), Math.round(height)];
14764
+ function circle(d, x, y) {
14765
+ var o = {
14766
+ tag: 'circle',
14767
+ properties: {
14768
+ cx: x || 0,
14769
+ cy: y || 0
14770
+ }
14771
+ };
14772
+ applyStyleAttributes(o, 'point', d);
14773
+ return o;
14601
14774
  }
14602
14775
 
14603
- function getDatasetDisplayBounds(dataset) {
14604
- var frameLyr = findFrameLayerInDataset(dataset);
14605
- if (frameLyr) {
14606
- // TODO: check for coordinate issues (non-intersection with other layers, etc)
14607
- return getFrameLayerBounds(frameLyr);
14776
+ function label(d, x, y) {
14777
+ var o = renderStyledLabel(d);
14778
+ if (x || y) {
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]);
14608
14782
  }
14609
- return getDatasetBounds(dataset);
14783
+ return o;
14610
14784
  }
14611
14785
 
14612
- // @lyr dataset layer
14613
- function isFrameLayer(lyr) {
14614
- return getFurnitureLayerType(lyr) == 'frame';
14786
+ function image(d, x, y) {
14787
+ var w = d.width || 20,
14788
+ h = d.height || 20;
14789
+ var o = {
14790
+ tag: 'image',
14791
+ properties: {
14792
+ width: w,
14793
+ height: h,
14794
+ x: (x || 0) - w / 2,
14795
+ y: (y || 0) - h / 2,
14796
+ href: d.href || ''
14797
+ }
14798
+ };
14799
+ return o;
14615
14800
  }
14616
14801
 
14617
- function findFrameLayerInDataset(dataset) {
14618
- return utils.find(dataset.layers, function(lyr) {
14619
- return isFrameLayer(lyr);
14620
- });
14802
+ function square(d, x, y) {
14803
+ var r = d.r || 0;
14804
+ var o = {
14805
+ tag: 'rect',
14806
+ properties: {
14807
+ x: x - r,
14808
+ y: y - r,
14809
+ width: r * 2,
14810
+ height: r * 2
14811
+ }
14812
+ };
14813
+ applyStyleAttributes(o, 'point', d);
14814
+ return o;
14621
14815
  }
14622
14816
 
14623
- function findFrameDataset(catalog) {
14624
- var target = utils.find(catalog.getLayers(), function(o) {
14625
- return isFrameLayer(o.layer);
14626
- });
14627
- return target ? target.dataset : null;
14817
+ function line(d, x, y) {
14818
+ var coords, o;
14819
+ coords = [[x, y], [x + (d.dx || 0), y + (d.dy || 0)]];
14820
+ o = importLineString(coords);
14821
+ applyStyleAttributes(o, 'polyline', d);
14822
+ return o;
14628
14823
  }
14629
14824
 
14630
- function findFrameLayer(catalog) {
14631
- var target = utils.find(catalog.getLayers(), function(o) {
14632
- return isFrameLayer(o.layer);
14633
- });
14634
- return target && target.layer || null;
14825
+ // polyline coords are like GeoJSON MultiLineString coords: an array of 0 or more paths
14826
+ function polyline(d, x, y) {
14827
+ var coords = d.coordinates || [];
14828
+ var o = importMultiLineString(coords);
14829
+ applyStyleAttributes(o, 'polyline', d);
14830
+ return o;
14635
14831
  }
14636
14832
 
14637
- function getFrameLayerBounds(lyr) {
14638
- return new Bounds(getFurnitureLayerData(lyr).bbox);
14833
+ // polygon coords are an array of rings (and holes), like flattened MultiPolygon coords
14834
+ function polygon(d, x, y) {
14835
+ var coords = d.coordinates || [];
14836
+ var o = importPolygon(coords);
14837
+ applyStyleAttributes(o, 'polygon', d);
14838
+ return o;
14639
14839
  }
14640
14840
 
14641
-
14642
- // @data frame data, including crs property if available
14643
- // Returns a single value: the ratio or
14644
- function getMapFrameMetersPerPixel(data) {
14645
- var bounds = new Bounds(data.bbox);
14646
- var k, toMeters, metersPerPixel;
14647
- if (data.crs) {
14648
- // TODO: handle CRS without inverse projections
14649
- // scale factor is the ratio of coordinate distance to true distance at a point
14650
- k = getScaleFactorAtXY(bounds.centerX(), bounds.centerY(), data.crs);
14651
- toMeters = data.crs.to_meter;
14652
- } else {
14653
- // Assuming coordinates are meters and k is 1 (not safe)
14654
- // A warning should be displayed when relevant furniture element is created
14655
- k = 1;
14656
- toMeters = 1;
14657
- }
14658
- metersPerPixel = bounds.width() / k * toMeters / data.width;
14659
- return metersPerPixel;
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
+ };
14660
14851
  }
14661
14852
 
14662
- furnitureRenderers.frame = function(d) {
14663
- var lineWidth = 1,
14664
- // inset stroke by half of line width
14665
- off = lineWidth / 2,
14666
- obj = importPolygon([[[off, off], [off, d.height - off],
14667
- [d.width - off, d.height - off],
14668
- [d.width - off, off], [off, off]]]);
14669
- utils.extend(obj.properties, {
14670
- fill: 'none',
14671
- stroke: d.stroke || 'black',
14672
- 'stroke-width': d['stroke-width'] || lineWidth
14853
+ function group(d, x, y) {
14854
+ var o = {
14855
+ tag: 'g',
14856
+ children: []
14857
+ };
14858
+ var children = o.children;
14859
+ (d.parts || []).forEach(function(o) {
14860
+ var sym = renderComplexSymbol(o, x, y);
14861
+ children.push(sym);
14862
+ //if (d.chained) {
14863
+ //if (o.chained) {
14864
+ if (o.type == 'line') {
14865
+ x += (o.dx || 0);
14866
+ y += (o.dy || 0);
14867
+ }
14868
+ if (o.type == 'offset') {
14869
+ children = sym.children;
14870
+ x = y = 0;
14871
+ }
14673
14872
  });
14674
- return [obj];
14675
- };
14873
+ if (o.children.length == 1 && !o.properties) return o.children[0];
14874
+ return o;
14875
+ }
14676
14876
 
14677
- var Frame = /*#__PURE__*/Object.freeze({
14877
+ var SvgSymbols = /*#__PURE__*/Object.freeze({
14678
14878
  __proto__: null,
14679
- getAspectRatioArg: getAspectRatioArg,
14680
- getFrameSize: getFrameSize,
14681
- findFrameLayerInDataset: findFrameLayerInDataset,
14682
- findFrameDataset: findFrameDataset,
14683
- findFrameLayer: findFrameLayer,
14684
- getFrameLayerBounds: getFrameLayerBounds,
14685
- getMapFrameMetersPerPixel: getMapFrameMetersPerPixel
14879
+ getTransform: getTransform,
14880
+ symbolRenderers: symbolRenderers,
14881
+ renderPoint: renderPoint
14686
14882
  });
14687
14883
 
14688
- function transformDatasetToPixels(dataset, opts) {
14689
- var frameLyr = findFrameLayerInDataset(dataset);
14690
- var bounds, bounds2, fwd, frameData;
14691
- if (frameLyr) {
14692
- // TODO: handle options like width, height margin when a frame is present
14693
- // TODO: check that aspect ratios match
14694
- frameData = getFurnitureLayerData(frameLyr);
14695
- bounds = new Bounds(frameData.bbox);
14696
- bounds2 = new Bounds(0, 0, frameData.width, frameData.height);
14697
- } else {
14698
- bounds = getDatasetBounds(dataset);
14699
- bounds2 = calcOutputSizeInPixels(bounds, opts);
14884
+ cmd.scalebar = function(catalog, opts) {
14885
+ var frame = findFrameDataset(catalog);
14886
+ var obj, lyr;
14887
+ if (!frame) {
14888
+ stop('Missing a map frame');
14700
14889
  }
14701
- fwd = bounds.getTransform(bounds2, opts.invert_y);
14702
- transformPoints(dataset, function(x, y) {
14703
- return fwd.transform(x, y);
14704
- });
14705
- return [Math.round(bounds2.width()), Math.round(bounds2.height()) || 1];
14890
+ lyr = getScalebarLayer(opts);
14891
+ frame.layers.push(lyr);
14892
+ };
14893
+
14894
+ function getScalebarLayer(opts) {
14895
+ var obj = utils.defaults({type: 'scalebar'}, opts);
14896
+ return {
14897
+ name: opts.name || 'scalebar',
14898
+ data: new DataTable([obj])
14899
+ };
14706
14900
  }
14707
14901
 
14708
- function parseMarginOption(opt) {
14709
- var str = utils.isNumber(opt) ? String(opt) : opt || '';
14710
- var margins = str.trim().split(/[, ] */);
14711
- if (margins.length == 1) margins.push(margins[0]);
14712
- if (margins.length == 2) margins.push(margins[0], margins[1]);
14713
- if (margins.length == 3) margins.push(margins[2]);
14714
- return margins.map(function(str) {
14715
- var px = parseFloat(str);
14716
- return isNaN(px) ? 1 : px; // 1 is default
14717
- });
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
14909
+ };
14910
+ if (+d.left > 0) {
14911
+ opts.hoffs = +d.left;
14912
+ }
14913
+ if (+d.top > 0) {
14914
+ opts.voffs = +d.top;
14915
+ }
14916
+ if (+d.right > 0) {
14917
+ opts.hoffs = +d.right;
14918
+ opts.halign = 'right';
14919
+ }
14920
+ if (+d.bottom > 0) {
14921
+ opts.voffs = +d.bottom;
14922
+ opts.valign = 'bottom';
14923
+ }
14924
+ return opts;
14718
14925
  }
14719
14926
 
14720
- // bounds: Bounds object containing bounds of content in geographic coordinates
14721
- // returns Bounds object containing bounds of pixel output
14722
- // side effect: bounds param is modified to match the output frame
14723
- function calcOutputSizeInPixels(bounds, opts) {
14724
- var padX = 0,
14725
- padY = 0,
14726
- offX = 0,
14727
- offY = 0,
14728
- width = bounds.width(),
14729
- height = bounds.height(),
14730
- margins = parseMarginOption(opts.margin),
14731
- marginX = margins[0] + margins[2],
14732
- marginY = margins[1] + margins[3],
14733
- // TODO: add option to tweak alignment of content when both width and height are given
14734
- wx = 0.5, // how padding is distributed horizontally (0: left aligned, 0.5: centered, 1: right aligned)
14735
- wy = 0.5, // vertical padding distribution
14736
- widthPx, heightPx, size, kx, ky;
14927
+ furnitureRenderers.scalebar = renderScalebar;
14737
14928
 
14738
- if (opts.fit_bbox) {
14739
- // scale + shift content to fit within a bbox
14740
- offX = opts.fit_bbox[0];
14741
- offY = opts.fit_bbox[1];
14742
- widthPx = opts.fit_bbox[2] - offX;
14743
- heightPx = opts.fit_bbox[3] - offY;
14744
- if (width / height > widthPx / heightPx) {
14745
- // data is wider than fit box...
14746
- // scale the data to fit widthwise
14747
- heightPx = 0;
14748
- } else {
14749
- widthPx = 0; // fit the data to the height
14750
- }
14751
- marginX = marginY = 0; // TODO: support margins
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;
14752
14944
 
14753
- } else if (opts.svg_scale > 0) {
14754
- // alternative to using a fixed width (e.g. when generating multiple files
14755
- // at a consistent geographic scale)
14756
- widthPx = width / opts.svg_scale + marginX;
14757
- heightPx = 0;
14758
- } else if (+opts.pixels) {
14759
- size = getFrameSize(bounds, opts);
14760
- widthPx = size[0];
14761
- heightPx = size[1];
14762
- } else {
14763
- heightPx = opts.height || 0;
14764
- widthPx = opts.width || (heightPx > 0 ? 0 : 800); // 800 is default width
14945
+ if (labelPos == 'top') {
14946
+ anchorY = -labelOffs;
14947
+ dy += Math.round(labelOffs + fontSize * 0.8);
14765
14948
  }
14766
14949
 
14767
- if (heightPx > 0) {
14768
- // vertical meters per pixel to fit height param
14769
- ky = (height || width || 1) / (heightPx - marginY);
14770
- }
14771
- if (widthPx > 0) {
14772
- // horizontal meters per pixel to fit width param
14773
- kx = (width || height || 1) / (widthPx - marginX);
14950
+ if (width > 0 === false) {
14951
+ stop("Null scalebar length");
14774
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];
14982
+ }
14775
14983
 
14776
- if (!widthPx) { // heightPx and ky are defined, set width to match
14777
- kx = ky;
14778
- widthPx = width > 0 ? marginX + width / kx : heightPx; // export square graphic if content has 0 width (reconsider this?)
14779
- } else if (!heightPx) { // widthPx and kx are set, set height to match
14780
- ky = kx;
14781
- heightPx = height > 0 ? marginY + height / ky : widthPx;
14782
- // limit height if max_height is defined
14783
- if (opts.max_height > 0 && heightPx > opts.max_height) {
14784
- ky = kx * heightPx / opts.max_height;
14785
- heightPx = opts.max_height;
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;
14786
14995
  }
14787
- }
14996
+ }, null) || '';
14997
+ }
14788
14998
 
14789
- if (kx > ky) { // content is wide -- need to pad vertically
14790
- ky = kx;
14791
- padY = ky * (heightPx - marginY) - height;
14792
- } else if (ky > kx) { // content is tall -- need to pad horizontally
14793
- kx = ky;
14794
- padX = kx * (widthPx - marginX) - width;
14795
- }
14999
+ function formatDistanceLabelAsMiles(str) {
15000
+ var num = parseScalebarNumber(str);
15001
+ return str + (num > 1 ? ' MILES' : ' MILE');
15002
+ }
14796
15003
 
14797
- bounds.padBounds(
14798
- margins[0] * kx + padX * wx,
14799
- margins[1] * ky + padY * wy,
14800
- margins[2] * kx + padX * (1 - wx),
14801
- margins[3] * ky + padY * (1 - wy));
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;
15010
+ }
14802
15011
 
14803
- if (!(widthPx > 0 && heightPx > 0)) {
14804
- error("Missing valid height and width parameters");
14805
- }
14806
- if (!(kx === ky && kx > 0)) {
14807
- error("Missing valid margin parameters");
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' || '';
15016
+ }
15017
+
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];
15025
+ } else {
15026
+ value = parseFloat(str);
14808
15027
  }
15028
+ return value > 0 && value < Infinity ? value : NaN;
15029
+ }
14809
15030
 
14810
- return new Bounds(offX, offY, widthPx + offX, heightPx + offY);
15031
+ var Scalebar = /*#__PURE__*/Object.freeze({
15032
+ __proto__: null,
15033
+ getScalebarLayer: getScalebarLayer,
15034
+ renderScalebar: renderScalebar,
15035
+ formatDistanceLabelAsMiles: formatDistanceLabelAsMiles,
15036
+ parseScalebarLabelToKm: parseScalebarLabelToKm
15037
+ });
15038
+
15039
+ function transformDatasetToPixels(dataset, opts) {
15040
+ var frame = getFrameData(dataset, opts);
15041
+ fitDatasetToFrame(dataset, frame, opts);
15042
+ return [frame.width, frame.height];
15043
+ }
15044
+
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);
15051
+ });
14811
15052
  }
14812
15053
 
14813
15054
  var PixelTransform = /*#__PURE__*/Object.freeze({
14814
15055
  __proto__: null,
14815
15056
  transformDatasetToPixels: transformDatasetToPixels,
14816
- parseMarginOption: parseMarginOption,
14817
- calcOutputSizeInPixels: calcOutputSizeInPixels
15057
+ fitDatasetToFrame: fitDatasetToFrame
14818
15058
  });
14819
15059
 
14820
15060
  function stringify(obj) {
@@ -14871,6 +15111,9 @@
14871
15111
  if (key == 'href') {
14872
15112
  key = 'xlink:href';
14873
15113
  }
15114
+ if (key == 'css') {
15115
+ key = 'style'; // inline style
15116
+ }
14874
15117
  return memo + ' ' + key + '="' + stringEscape(strval) + '"';
14875
15118
  }, '');
14876
15119
  }
@@ -14989,6 +15232,7 @@
14989
15232
  return svg;
14990
15233
  }
14991
15234
 
15235
+ // href: A URL or a local path
14992
15236
  // TODO: download SVG files asynchronously
14993
15237
  // (currently, files are downloaded synchronously, which is obviously undesirable)
14994
15238
  //
@@ -14996,7 +15240,7 @@
14996
15240
  var content;
14997
15241
  if (href.indexOf('http') === 0) {
14998
15242
  content = fetchFileSync(href);
14999
- } else if (require('fs').existsSync(href)) { // assume href is a relative path
15243
+ } else if (require('fs').existsSync(href)) {
15000
15244
  content = require('fs').readFileSync(href, 'utf8');
15001
15245
  } else {
15002
15246
  stop("Invalid SVG location:", href);
@@ -15037,12 +15281,11 @@
15037
15281
  }
15038
15282
  */
15039
15283
 
15040
- //
15041
15284
  //
15042
15285
  function exportSVG(dataset, opts) {
15043
15286
  var namespace = 'xmlns="http://www.w3.org/2000/svg"';
15044
15287
  var defs = [];
15045
- var size, svg;
15288
+ var frame, svg, layers;
15046
15289
  var style = '';
15047
15290
 
15048
15291
  // kludge for map keys
@@ -15059,62 +15302,70 @@
15059
15302
  } else {
15060
15303
  dataset = copyDataset(dataset); // Modify a copy of the dataset
15061
15304
  }
15305
+
15062
15306
  // invert_y setting for screen coordinates and geojson polygon generation
15063
15307
  utils.extend(opts, {invert_y: true});
15064
- size = transformCoordsForSVG(dataset, opts);
15308
+ frame = getFrameData(dataset, opts);
15309
+ fitDatasetToFrame(dataset, frame, opts);
15310
+ setCoordinatePrecision(dataset, opts.precision || 0.0001);
15065
15311
 
15066
15312
  // error if one or more svg_data fields are not present in any layers
15067
15313
  if (opts.svg_data) validateSvgDataFields(dataset.layers, opts.svg_data);
15068
15314
 
15069
- svg = dataset.layers.map(function(lyr) {
15070
- 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
+ }
15071
15326
  convertPropertiesToDefinitions(obj, defs);
15072
15327
  return stringify(obj);
15073
15328
  }).join('\n');
15329
+
15074
15330
  if (defs.length > 0) {
15075
15331
  svg = '<defs>\n' + utils.pluck(defs, 'svg').join('') + '</defs>\n' + svg;
15076
15332
  }
15333
+
15077
15334
  if (svg.includes('xlink:')) {
15078
15335
  namespace += ' xmlns:xlink="http://www.w3.org/1999/xlink"';
15079
15336
  }
15337
+
15338
+ // default line style properties
15080
15339
  var capStyle = opts.default_linecap || 'round';
15340
+ var lineProps = `stroke-linecap="${capStyle}" stroke-linejoin="round"`;
15341
+ if (svg.includes('stroke-linejoin="miter"')) {
15342
+ // the default limit in Illustrator seems to be 10 -- too large for mapping
15343
+ // (Mapbox uses 2 as the default in their styles)
15344
+ lineProps += ' stroke-miterlimit="2"';
15345
+ }
15081
15346
  var template = `<?xml version="1.0"?>
15082
- <svg ${namespace} version="1.2" baseProfile="tiny" width="%d" height="%d" viewBox="%s %s %s %s" stroke-linecap="${capStyle}" stroke-linejoin="round">${style}
15347
+ <svg ${namespace} version="1.2" baseProfile="tiny" width="%d" height="%d" viewBox="%s %s %s %s" ${lineProps}>${style}
15083
15348
  ${svg}
15084
15349
  </svg>`;
15085
- 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);
15086
15351
  return [{
15087
15352
  content: svg,
15088
15353
  filename: opts.file || getOutputFileBase(dataset) + '.svg'
15089
15354
  }];
15090
15355
  }
15091
15356
 
15092
- function transformCoordsForSVG(dataset, opts) {
15093
- var size = transformDatasetToPixels(dataset, opts);
15094
- var precision = opts.precision || 0.0001;
15095
- setCoordinatePrecision(dataset, precision);
15096
- return size;
15357
+ function exportFurnitureForSVG(lyr, frame, opts) {
15358
+ var layerObj = getEmptyLayerForSVG(lyr, opts);
15359
+ layerObj.children = importFurniture(getFurnitureLayerData(lyr), frame);
15360
+ return layerObj;
15097
15361
  }
15098
15362
 
15099
15363
  function exportLayerForSVG(lyr, dataset, opts) {
15100
15364
  var layerObj = getEmptyLayerForSVG(lyr, opts);
15101
- if (layerHasFurniture(lyr)) {
15102
- layerObj.children = exportFurnitureForSVG(lyr, dataset, opts);
15103
- } else {
15104
- layerObj.children = exportSymbolsForSVG(lyr, dataset, opts);
15105
- }
15365
+ layerObj.children = exportSymbolsForSVG(lyr, dataset, opts);
15106
15366
  return layerObj;
15107
15367
  }
15108
15368
 
15109
- function exportFurnitureForSVG(lyr, dataset, opts) {
15110
- var frameLyr = findFrameLayerInDataset(dataset);
15111
- var frameData;
15112
- if (!frameLyr) return [];
15113
- frameData = getFurnitureLayerData(frameLyr);
15114
- frameData.crs = getDatasetCRS(dataset); // required by e.g. scalebar
15115
- return importFurniture(getFurnitureLayerData(lyr), frameData);
15116
- }
15117
-
15118
15369
  function exportSymbolsForSVG(lyr, dataset, opts) {
15119
15370
  // TODO: convert geojson features one at a time
15120
15371
  var d = utils.defaults({layers: [lyr]}, dataset);
@@ -15245,6 +15496,15 @@ ${svg}
15245
15496
  return layerObj;
15246
15497
  }
15247
15498
 
15499
+ function featureHasSvgSymbol(d) {
15500
+ return !!(d && (d['svg-symbol'] || d.r));
15501
+ }
15502
+
15503
+ function featureHasLabel(d) {
15504
+ var text = d && d['label-text'];
15505
+ return text || text === 0; // accept numerical 0 as label text
15506
+ }
15507
+
15248
15508
  function layerHasSvgSymbols(lyr) {
15249
15509
  return lyr.geometry_type == 'point' && lyr.data && lyr.data.fieldExists('svg-symbol');
15250
15510
  }
@@ -15260,10 +15520,13 @@ ${svg}
15260
15520
  var Svg = /*#__PURE__*/Object.freeze({
15261
15521
  __proto__: null,
15262
15522
  exportSVG: exportSVG,
15523
+ exportFurnitureForSVG: exportFurnitureForSVG,
15263
15524
  exportLayerForSVG: exportLayerForSVG,
15264
15525
  validateSvgDataFields: validateSvgDataFields,
15265
15526
  exportDataAttributesForSVG: exportDataAttributesForSVG,
15266
15527
  getEmptyLayerForSVG: getEmptyLayerForSVG,
15528
+ featureHasSvgSymbol: featureHasSvgSymbol,
15529
+ featureHasLabel: featureHasLabel,
15267
15530
  layerHasSvgSymbols: layerHasSvgSymbols,
15268
15531
  layerHasLabels: layerHasLabels
15269
15532
  });
@@ -19153,6 +19416,10 @@ ${svg}
19153
19416
  .option('id-prefix', {
19154
19417
  describe: '[SVG] prefix for namespacing layer and feature ids'
19155
19418
  })
19419
+ .option('scalebar', {
19420
+ type: 'flag',
19421
+ // describe: '[SVG] add a scalebar showing scale at the center of the map'
19422
+ })
19156
19423
  .option('delimiter', {
19157
19424
  describe: '[CSV] field delimiter'
19158
19425
  })
@@ -20346,14 +20613,21 @@ ${svg}
20346
20613
  .option('stroke', {})
20347
20614
  .option('stroke-width', {})
20348
20615
  .option('fill', {
20349
- describe: 'symbol fill color'
20616
+ describe: 'symbol fill color (filled symbols only)'
20350
20617
  })
20351
- .option('polygons', {
20352
- describe: 'generate symbols as polygons instead of SVG objects',
20618
+ .option('stroke', {
20619
+ describe: 'symbol line color (linear symbols only)'
20620
+ })
20621
+ .option('stroke-width', {
20622
+ describe: 'symbol line width (linear symbols only)'
20623
+ })
20624
+ .option('geographic', {
20625
+ old_alias: 'polygons',
20626
+ describe: 'make geographic shapes instead of SVG objects',
20353
20627
  type: 'flag'
20354
20628
  })
20355
20629
  .option('pixel-scale', {
20356
- describe: 'set symbol scale in meters-per-pixel (for polygons option)',
20630
+ describe: 'set symbol scale in meters per pixel (geographic option)',
20357
20631
  type: 'number',
20358
20632
  })
20359
20633
  // .option('flipped', {
@@ -20390,6 +20664,9 @@ ${svg}
20390
20664
  .option('radii', {
20391
20665
  describe: '(ring) comma-sep. list of concentric radii, ascending order'
20392
20666
  })
20667
+ .option('arrow-style', {
20668
+ describe: '(arrow) options: stick, standard (default is standard)'
20669
+ })
20393
20670
  .option('length', {
20394
20671
  old_alias: 'arrow-length',
20395
20672
  describe: '(arrow) length of arrow in pixels'
@@ -32393,6 +32670,80 @@ ${svg}
32393
32670
  return parsed[0];
32394
32671
  }
32395
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
+
32396
32747
  cmd.filterGeom = function(lyr, arcs, opts) {
32397
32748
  if (!layerHasGeometry(lyr)) {
32398
32749
  stop("Layer is missing geometry");
@@ -37280,219 +37631,6 @@ ${svg}
37280
37631
  }
37281
37632
  };
37282
37633
 
37283
- symbolRenderers.circle = function(d, x, y) {
37284
- var o = importPoint([x, y], d, {});
37285
- applyStyleAttributes(o, 'Point', d);
37286
- return [o];
37287
- };
37288
-
37289
- symbolRenderers.label = function(d, x, y) {
37290
- var o = importStyledLabel(d, [x, y]);
37291
- return [o];
37292
- };
37293
-
37294
- symbolRenderers.image = function(d, x, y) {
37295
- var w = d.width || 20,
37296
- h = d.height || 20;
37297
- var o = {
37298
- tag: 'image',
37299
- properties: {
37300
- width: w,
37301
- height: h,
37302
- x: x - w / 2,
37303
- y: y - h / 2,
37304
- href: d.href || ''
37305
- }
37306
- };
37307
- return [o];
37308
- };
37309
-
37310
- symbolRenderers.square = function(d, x, y) {
37311
- var o = importPoint([x, y], d, {point_symbol: 'square'});
37312
- applyStyleAttributes(o, 'Point', d);
37313
- return [o];
37314
- };
37315
-
37316
- symbolRenderers.line = function(d, x, y) {
37317
- var coords, o;
37318
- coords = [[x, y], [x + (d.dx || 0), y + (d.dy || 0)]];
37319
- o = importLineString(coords);
37320
- applyStyleAttributes(o, 'LineString', d);
37321
- return [o];
37322
- };
37323
-
37324
- symbolRenderers.polyline = function(d, x, y) {
37325
- var coords = d.coordinates || [];
37326
- var o = importMultiLineString(coords);
37327
- applyStyleAttributes(o, 'LineString', d);
37328
- return [o];
37329
- };
37330
-
37331
- symbolRenderers.polygon = function(d, x, y) {
37332
- var coords = d.coordinates || [];
37333
- var o = importPolygon(coords);
37334
- applyStyleAttributes(o, 'Polygon', d);
37335
- return [o];
37336
- };
37337
-
37338
- symbolRenderers.group = function(d, x, y) {
37339
- return (d.parts || []).reduce(function(memo, o) {
37340
- var sym = renderSymbol(o, x, y);
37341
- if (d.chained) {
37342
- x += (o.dx || 0);
37343
- y += (o.dy || 0);
37344
- }
37345
- return memo.concat(sym);
37346
- }, []);
37347
- };
37348
-
37349
- cmd.scalebar = function(catalog, opts) {
37350
- var frame = findFrameDataset(catalog);
37351
- var obj, lyr;
37352
- if (!frame) {
37353
- stop('Missing a map frame');
37354
- }
37355
- obj = utils.defaults({type: 'scalebar'}, opts);
37356
- lyr = {
37357
- name: opts.name || 'scalebar',
37358
- data: new DataTable([obj])
37359
- };
37360
- frame.layers.push(lyr);
37361
- };
37362
-
37363
- // TODO: generalize to other kinds of furniture as they are developed
37364
- function getScalebarPosition(d) {
37365
- var opts = { // defaults
37366
- valign: 'top',
37367
- halign: 'left',
37368
- voffs: 10,
37369
- hoffs: 10
37370
- };
37371
- if (+d.left > 0) {
37372
- opts.hoffs = +d.left;
37373
- }
37374
- if (+d.top > 0) {
37375
- opts.voffs = +d.top;
37376
- }
37377
- if (+d.right > 0) {
37378
- opts.hoffs = +d.right;
37379
- opts.halign = 'right';
37380
- }
37381
- if (+d.bottom > 0) {
37382
- opts.voffs = +d.bottom;
37383
- opts.valign = 'bottom';
37384
- }
37385
- return opts;
37386
- }
37387
-
37388
- furnitureRenderers.scalebar = function(d, frame) {
37389
- var pos = getScalebarPosition(d);
37390
- var metersPerPx = getMapFrameMetersPerPixel(frame);
37391
- var label = d.label_text || getAutoScalebarLabel(frame.width, metersPerPx);
37392
- var scalebarKm = parseScalebarLabelToKm(label);
37393
- var barHeight = 3;
37394
- var labelOffs = 4;
37395
- var fontSize = +d.font_size || 13;
37396
- var width = Math.round(scalebarKm / metersPerPx * 1000);
37397
- var height = Math.round(barHeight + labelOffs + fontSize * 0.8);
37398
- var labelPos = d.label_position == 'top' ? 'top' : 'bottom';
37399
- var anchorX = pos.halign == 'left' ? 0 : width;
37400
- var anchorY = barHeight + labelOffs;
37401
- var dx = pos.halign == 'right' ? frame.width - width - pos.hoffs : pos.hoffs;
37402
- var dy = pos.valign == 'bottom' ? frame.height - height - pos.voffs : pos.voffs;
37403
-
37404
- if (labelPos == 'top') {
37405
- anchorY = -labelOffs;
37406
- dy += Math.round(labelOffs + fontSize * 0.8);
37407
- }
37408
-
37409
- if (width > 0 === false) {
37410
- stop("Null scalebar length");
37411
- }
37412
- var barObj = {
37413
- tag: 'rect',
37414
- properties: {
37415
- fill: 'black',
37416
- x: 0,
37417
- y: 0,
37418
- width: width,
37419
- height: barHeight
37420
- }
37421
- };
37422
- var labelOpts = {
37423
- 'label-text': label,
37424
- 'font-size': fontSize,
37425
- 'text-anchor': pos.halign == 'left' ? 'start': 'end',
37426
- 'dominant-baseline': labelPos == 'top' ? 'auto' : 'hanging'
37427
- //// 'dominant-baseline': labelPos == 'top' ? 'text-after-edge' : 'text-before-edge'
37428
- // 'text-after-edge' is buggy in Safari and unsupported by Illustrator,
37429
- // so I'm using 'hanging' and 'auto', which seem to be well supported.
37430
- // downside: requires a kludgy multiplier to calculate scalebar height (see above)
37431
- };
37432
- var labelObj = symbolRenderers.label(labelOpts, anchorX, anchorY)[0];
37433
- var g = {
37434
- tag: 'g',
37435
- children: [barObj, labelObj],
37436
- properties: {
37437
- transform: 'translate(' + dx + ' ' + dy + ')'
37438
- }
37439
- };
37440
- return [g];
37441
- };
37442
-
37443
- function getAutoScalebarLabel(mapWidth, metersPerPx) {
37444
- var minWidth = 100; // TODO: vary min size based on map width
37445
- var minKm = metersPerPx * minWidth / 1000;
37446
- 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 ' +
37447
- '100 150 200 250 300 350 400 500 750 1,000 1,200 1,500 2,000 ' +
37448
- '2,500 3,000 4,000 5,000').split(' ');
37449
- return options.reduce(function(memo, str) {
37450
- if (memo) return memo;
37451
- var label = formatDistanceLabelAsMiles(str);
37452
- if (parseScalebarLabelToKm(label) > minKm) {
37453
- return label;
37454
- }
37455
- }, null) || '';
37456
- }
37457
-
37458
- function formatDistanceLabelAsMiles(str) {
37459
- var num = parseScalebarNumber(str);
37460
- return str + (num > 1 ? ' MILES' : ' MILE');
37461
- }
37462
-
37463
- // See test/mapshaper-scalebar.js for examples of supported formats
37464
- function parseScalebarLabelToKm(str) {
37465
- var units = parseScalebarUnits(str);
37466
- var value = parseScalebarNumber(str);
37467
- if (!units || !value) return NaN;
37468
- return units == 'mile' ? value * 1.60934 : value;
37469
- }
37470
-
37471
- function parseScalebarUnits(str) {
37472
- var isMiles = /miles?$/.test(str.toLowerCase());
37473
- var isKm = /(km|kilometers?|kilometres?)$/.test(str.toLowerCase());
37474
- return isMiles && 'mile' || isKm && 'km' || '';
37475
- }
37476
-
37477
- function parseScalebarNumber(str) {
37478
- var fractionRxp = /^([0-9]+) ?\/ ?([0-9]+)/;
37479
- var match, value;
37480
- str = str.replace(/[\s]/g, '').replace(/,/g, '');
37481
- if (fractionRxp.test(str)) {
37482
- match = fractionRxp.exec(str);
37483
- value = +match[1] / +match[2];
37484
- } else {
37485
- value = parseFloat(str);
37486
- }
37487
- return value > 0 && value < Infinity ? value : NaN;
37488
- }
37489
-
37490
- var Scalebar = /*#__PURE__*/Object.freeze({
37491
- __proto__: null,
37492
- formatDistanceLabelAsMiles: formatDistanceLabelAsMiles,
37493
- parseScalebarLabelToKm: parseScalebarLabelToKm
37494
- });
37495
-
37496
37634
  cmd.shape = function(targetDataset, opts) {
37497
37635
  var geojson, dataset;
37498
37636
  if (opts.coordinates) {
@@ -38680,6 +38818,19 @@ ${svg}
38680
38818
 
38681
38819
  var roundCoord = getRoundingFunction(0.01);
38682
38820
 
38821
+ function getSymbolFillColor(d) {
38822
+ return d.fill || 'magenta';
38823
+ }
38824
+
38825
+ function getSymbolStrokeColor(d) {
38826
+ return d.stroke || d.fill || 'magenta';
38827
+ }
38828
+
38829
+ function getSymbolRadius(d) {
38830
+ if (d.radius === 0 || d.length === 0 || d.r === 0) return 0;
38831
+ return d.radius || d.length || d.r || 5; // use a default value
38832
+ }
38833
+
38683
38834
  function forEachSymbolCoord(coords, cb) {
38684
38835
  var isPoint = coords && utils.isNumber(coords[0]);
38685
38836
  var isNested = !isPoint && coords && Array.isArray(coords[0]);
@@ -38748,57 +38899,104 @@ ${svg}
38748
38899
  points.push(p2);
38749
38900
  }
38750
38901
 
38751
- // export function getStickArrowCoords(d, totalLen) {
38752
- // var minStemRatio = getMinStemRatio(d);
38753
- // var headAngle = d['arrow-head-angle'] || 90;
38754
- // var curve = d['arrow-stem-curve'] || 0;
38755
- // var unscaledHeadWidth = d['arrow-head-width'] || 9;
38756
- // var unscaledHeadLen = getHeadLength(unscaledHeadWidth, headAngle);
38757
- // var scale = getScale(totalLen, unscaledHeadLen, minStemRatio);
38758
- // var headWidth = unscaledHeadWidth * scale;
38759
- // var headLen = unscaledHeadLen * scale;
38760
- // var tip = getStickArrowTip(totalLen, curve);
38761
- // var stem = [[0, 0], tip.concat()];
38762
- // if (curve) {
38763
- // addBezierArcControlPoints(stem, curve);
38764
- // }
38765
- // if (!headLen) return [stem];
38766
- // var head = [addPoints([-headWidth / 2, -headLen], tip), tip.concat(), addPoints([headWidth / 2, -headLen], tip)];
38902
+ function getStickArrowCoords(d) {
38903
+ return getArrowCoords(d, 'stick');
38904
+ }
38767
38905
 
38768
- // rotateCoords(stem, d.rotation);
38769
- // rotateCoords(head, d.rotation);
38770
- // return [stem, head];
38771
- // }
38906
+ function getFilledArrowCoords(d) {
38907
+ return getArrowCoords(d, 'standard');
38908
+ }
38772
38909
 
38773
- // function getStickArrowTip(totalLen, curve) {
38774
- // // curve/2 intersects the arrowhead at 90deg (trigonometry)
38775
- // var theta = Math.abs(curve/2) / 180 * Math.PI;
38776
- // var dx = totalLen * Math.sin(theta) * (curve > 0 ? -1 : 1);
38777
- // var dy = totalLen * Math.cos(theta);
38778
- // return [dx, dy];
38779
- // }
38910
+ function getArrowCoords(d, style) {
38911
+ var stickArrow = style == 'stick',
38912
+ // direction = d.rotation || d.direction || 0,
38913
+ direction = d.direction || 0, // rotation is an independent parameter
38914
+ stemTaper = d['stem-taper'] || 0,
38915
+ curvature = d['stem-curve'] || 0,
38916
+ size = calcArrowSize(d, stickArrow);
38917
+ if (!size) return null;
38918
+ var stemLen = size.stemLen,
38919
+ headLen = size.headLen,
38920
+ headDx = size.headWidth / 2,
38921
+ stemDx = size.stemWidth / 2,
38922
+ baseDx = stemDx * (1 - stemTaper),
38923
+ head, stem, coords, dx, dy;
38780
38924
 
38781
- // function addPoints(a, b) {
38782
- // return [a[0] + b[0], a[1] + b[1]];
38783
- // }
38925
+ if (curvature) {
38926
+ // make curved stem
38927
+ if (direction > 0) curvature = -curvature;
38928
+ var theta = Math.abs(curvature) / 180 * Math.PI;
38929
+ var sign = curvature > 0 ? 1 : -1;
38930
+ var ax = baseDx * Math.cos(theta); // rotate arrow base
38931
+ var ay = baseDx * Math.sin(theta) * -sign;
38932
+ dx = stemLen * Math.sin(theta / 2) * sign;
38933
+ dy = stemLen * Math.cos(theta / 2);
38934
+
38935
+ if (stickArrow) {
38936
+ stem = getCurvedStemCoords(-ax, -ay, dx, dy, theta);
38937
+ } else {
38938
+ var leftStem = getCurvedStemCoords(-ax, -ay, -stemDx + dx, dy, theta);
38939
+ var rightStem = getCurvedStemCoords(ax, ay, stemDx + dx, dy, theta);
38940
+ stem = leftStem.concat(rightStem.reverse());
38941
+ }
38942
+
38943
+ } else {
38944
+ // make straight stem
38945
+ dx = 0;
38946
+ dy = stemLen;
38947
+ if (stickArrow) {
38948
+ stem = [[0, 0], [0, stemLen]];
38949
+ } else {
38950
+ stem = [[-baseDx, 0], [baseDx, 0]];
38951
+ }
38952
+ }
38953
+
38954
+ if (stickArrow) {
38955
+ // make stick arrow
38956
+ head = [[-headDx + dx, stemLen - headLen], [dx, stemLen], [headDx + dx, stemLen - headLen]];
38957
+ coords = [stem, head]; // MultiLineString coords
38958
+ } else {
38959
+ // make filled arrow
38960
+ // coordinates go counter clockwise, starting from the leftmost head coordinate
38961
+ head = [[stemDx + dx, dy], [headDx + dx, dy],
38962
+ [dx, headLen + dy], [-headDx + dx, dy], [-stemDx + dx, dy]];
38963
+ coords = stem.concat(head);
38964
+ coords.push(stem[0].concat()); // closed path
38965
+ coords = [coords]; // Polygon coords
38966
+ }
38967
+
38968
+ if (d.anchor == 'end') {
38969
+ scaleAndShiftCoords(coords, 1, [-dx, -dy - headLen]);
38970
+ } else if (d.anchor == 'middle') {
38971
+ // shift midpoint away from the head a bit for a more balanced placement
38972
+ // scaleAndShiftCoords(coords, 1, [-dx/2, (-dy - headLen)/2]);
38973
+ scaleAndShiftCoords(coords, 1, [-dx * 0.5, -dy * 0.5 - headLen * 0.25]);
38974
+ }
38975
+
38976
+ rotateCoords(coords, direction);
38977
+ if (d.flipped) {
38978
+ flipY(coords);
38979
+ }
38980
+ return coords;
38981
+ }
38784
38982
 
38785
38983
  function calcStraightArrowCoords(stemLen, headLen, stemDx, headDx, baseDx) {
38786
38984
  return [[baseDx, 0], [stemDx, stemLen], [headDx, stemLen], [0, stemLen + headLen],
38787
38985
  [-headDx, stemLen], [-stemDx, stemLen], [-baseDx, 0], [baseDx, 0]];
38788
38986
  }
38789
38987
 
38790
- function calcArrowSize(d) {
38988
+ function calcArrowSize(d, stickArrow) {
38791
38989
  // don't display arrows with negative length
38792
38990
  var totalLen = Math.max(d.radius || d.length || d.r || 0, 0),
38793
38991
  scale = 1,
38794
38992
  o = initArrowSize(d); // calc several parameters
38795
38993
 
38796
- if (totalLen > 0) {
38994
+ if (totalLen >= 0) {
38797
38995
  scale = calcScale(totalLen, o.headLen, d);
38798
38996
  o.stemWidth *= scale;
38799
38997
  o.headWidth *= scale;
38800
38998
  o.headLen *= scale;
38801
- o.stemLen = totalLen - o.headLen;
38999
+ o.stemLen = stickArrow ? totalLen : totalLen - o.headLen;
38802
39000
  }
38803
39001
 
38804
39002
  if (o.headWidth < o.stemWidth) {
@@ -38819,7 +39017,7 @@ ${svg}
38819
39017
  } else if (stemLen + headLen > totalLen) {
38820
39018
  scale = totalLen / (stemLen + headLen);
38821
39019
  }
38822
- return scale;
39020
+ return scale || 0;
38823
39021
  }
38824
39022
 
38825
39023
  function initArrowSize(d) {
@@ -38849,56 +39047,6 @@ ${svg}
38849
39047
  return 1 / Math.tan(Math.PI * headAngle / 180 / 2) / 2;
38850
39048
  }
38851
39049
 
38852
- function getFilledArrowCoords(d) {
38853
- var direction = d.rotation || d.direction || 0,
38854
- stemTaper = d['stem-taper'] || 0,
38855
- curvature = d['stem-curve'] || 0,
38856
- size = calcArrowSize(d);
38857
- if (!size) return null;
38858
- var stemLen = size.stemLen,
38859
- headLen = size.headLen,
38860
- headDx = size.headWidth / 2,
38861
- stemDx = size.stemWidth / 2,
38862
- baseDx = stemDx * (1 - stemTaper),
38863
- head, stem, coords, dx, dy;
38864
-
38865
- if (curvature) {
38866
- if (direction > 0) curvature = -curvature;
38867
- var theta = Math.abs(curvature) / 180 * Math.PI;
38868
- var sign = curvature > 0 ? 1 : -1;
38869
- var ax = baseDx * Math.cos(theta); // rotate arrow base
38870
- var ay = baseDx * Math.sin(theta) * -sign;
38871
- dx = stemLen * Math.sin(theta / 2) * sign;
38872
- dy = stemLen * Math.cos(theta / 2);
38873
- var leftStem = getCurvedStemCoords(-ax, -ay, -stemDx + dx, dy, theta);
38874
- var rightStem = getCurvedStemCoords(ax, ay, stemDx + dx, dy, theta);
38875
- stem = leftStem.concat(rightStem.reverse());
38876
-
38877
- } else {
38878
- dx = 0;
38879
- dy = stemLen;
38880
- stem = [[-baseDx, 0], [baseDx, 0], [baseDx, 0]];
38881
- }
38882
-
38883
- // coordinates go counter clockwise, starting from the leftmost head coordinate
38884
- head = [[stemDx + dx, dy], [headDx + dx, dy],
38885
- [dx, headLen + dy], [-headDx + dx, dy], [-stemDx + dx, dy]];
38886
-
38887
- coords = stem.concat(head);
38888
- if (d.anchor == 'end') {
38889
- scaleAndShiftCoords(coords, 1, [-dx, -dy - headLen]);
38890
- } else if (d.anchor == 'middle') {
38891
- // shift midpoint away from the head a bit for a more balanced placement
38892
- // scaleAndShiftCoords(coords, 1, [-dx/2, (-dy - headLen)/2]);
38893
- scaleAndShiftCoords(coords, 1, [-dx * 0.5, -dy * 0.5 - headLen * 0.25]);
38894
- }
38895
-
38896
- rotateCoords(coords, direction);
38897
- if (d.flipped) {
38898
- flipY(coords);
38899
- }
38900
- return [coords];
38901
- }
38902
39050
 
38903
39051
  // ax, ay: point on the base
38904
39052
  // bx, by: point on the stem
@@ -38934,8 +39082,19 @@ ${svg}
38934
39082
  return coords;
38935
39083
  }
38936
39084
 
39085
+ function makeCircleSymbol(d, opts) {
39086
+ var radius = getSymbolRadius(d);
39087
+ // TODO: remove duplication with svg-symbols.js
39088
+ if (+opts.scale) radius *= +opts.scale;
39089
+ return {
39090
+ type: 'circle',
39091
+ fill: getSymbolFillColor(d),
39092
+ r: radius
39093
+ };
39094
+ }
39095
+
38937
39096
  function getPolygonCoords(d) {
38938
- var radius = d.radius || d.length || d.r,
39097
+ var radius = getSymbolRadius(d),
38939
39098
  sides = +d.sides || getSidesByType(d.type),
38940
39099
  rotated = sides % 2 == 1,
38941
39100
  coords = [],
@@ -38972,7 +39131,7 @@ ${svg}
38972
39131
  }
38973
39132
 
38974
39133
  function getStarCoords(d) {
38975
- var radius = d.radius || d.length || d.r,
39134
+ var radius = getSymbolRadius(d),
38976
39135
  points = d.points || d.sides && d.sides / 2 || 5,
38977
39136
  sides = points * 2,
38978
39137
  minorRadius = getMinorRadius(points) * radius,
@@ -39017,6 +39176,35 @@ ${svg}
39017
39176
  return 180 - centerAngle;
39018
39177
  }
39019
39178
 
39179
+ // Returns a svg-symbol object
39180
+ function makeRingSymbol(d, opts) {
39181
+ var scale = +opts.scale || 1;
39182
+ var radii = parseRings(d.radii || '2').map(function(r) { return r * scale; });
39183
+ var solidCenter = utils.isOdd(radii.length);
39184
+ var color = getSymbolFillColor(d);
39185
+ var parts = [];
39186
+ if (solidCenter) {
39187
+ parts.push({
39188
+ type: 'circle',
39189
+ fill: color,
39190
+ r: radii.shift()
39191
+ });
39192
+ }
39193
+ for (var i=0; i<radii.length; i+= 2) {
39194
+ parts.push({
39195
+ type: 'circle',
39196
+ fill: 'none', // TODO remove default black fill so this is not needed
39197
+ stroke: color,
39198
+ 'stroke-width': roundToTenths(radii[i+1] - radii[i]),
39199
+ r: roundToTenths(radii[i+1] * 0.5 + radii[i] * 0.5)
39200
+ });
39201
+ }
39202
+ return {
39203
+ type: 'group',
39204
+ parts: parts
39205
+ };
39206
+ }
39207
+
39020
39208
  // Returns GeoJSON MultiPolygon coords
39021
39209
  function getRingCoords(d) {
39022
39210
  var radii = parseRings(d.radii || '2');
@@ -39048,13 +39236,36 @@ ${svg}
39048
39236
  return utils.uniq(arr);
39049
39237
  }
39050
39238
 
39239
+ // Returns an svg-symbol data object for one symbol
39240
+ function makePathSymbol(coords, properties, geojsonType) {
39241
+ var sym;
39242
+ if (geojsonType == 'MultiPolygon' || geojsonType == 'Polygon') {
39243
+ sym = {
39244
+ type: 'polygon',
39245
+ fill: getSymbolFillColor(properties),
39246
+ coordinates: geojsonType == 'Polygon' ? coords : flattenMultiPolygonCoords(coords)
39247
+ };
39248
+ } else if (geojsonType == 'LineString' || geojsonType == 'MultiLineString') {
39249
+ sym = {
39250
+ type: 'polyline',
39251
+ stroke: getSymbolStrokeColor(properties),
39252
+ 'stroke-width': properties['stroke-width'] || 2,
39253
+ coordinates: geojsonType == 'LineString' ? [coords] : coords
39254
+ };
39255
+ } else {
39256
+ error('Unsupported type:', geojsonType);
39257
+ }
39258
+ roundCoordsForSVG(sym.coordinates);
39259
+ return sym;
39260
+ }
39261
+
39051
39262
  // TODO: refactor to remove duplication in mapshaper-svg-style.js
39052
39263
  cmd.symbols = function(inputLyr, dataset, opts) {
39053
39264
  requireSinglePointLayer(inputLyr);
39054
39265
  var lyr = opts.no_replace ? copyLayer(inputLyr) : inputLyr;
39055
- var polygonMode = !!opts.polygons;
39266
+ var shapeMode = !!opts.geographic;
39056
39267
  var metersPerPx;
39057
- if (polygonMode) {
39268
+ if (shapeMode) {
39058
39269
  requireProjectedDataset(dataset);
39059
39270
  metersPerPx = opts.pixel_scale || getMetersPerPixel(lyr, dataset);
39060
39271
  }
@@ -39064,9 +39275,24 @@ ${svg}
39064
39275
  if (!shp) return null;
39065
39276
  var d = getSymbolData(i);
39066
39277
  var rec = records[i] || {};
39278
+
39279
+ // non-polygon symbols
39280
+ if (!shapeMode && d.type == 'circle') {
39281
+ rec['svg-symbol'] = makeCircleSymbol(d, opts);
39282
+ return;
39283
+ }
39284
+ if (!shapeMode && d.type == 'ring') {
39285
+ rec['svg-symbol'] = makeRingSymbol(d, opts);
39286
+ return;
39287
+ }
39288
+
39067
39289
  var geojsonType = 'Polygon';
39068
39290
  var coords;
39069
- if (d.type == 'arrow') {
39291
+ // these symbols get converted to polygon shapes
39292
+ if (d.type == 'arrow' && opts.arrow_style == 'stick') {
39293
+ coords = getStickArrowCoords(d);
39294
+ geojsonType = 'MultiLineString';
39295
+ } else if (d.type == 'arrow') {
39070
39296
  coords = getFilledArrowCoords(d);
39071
39297
  } else if (d.type == 'ring') {
39072
39298
  coords = getRingCoords(d);
@@ -39078,23 +39304,24 @@ ${svg}
39078
39304
  }
39079
39305
  if (!coords) return null;
39080
39306
  rotateCoords(coords, +d.rotation || 0);
39081
- if (!polygonMode) {
39307
+ if (!shapeMode) {
39082
39308
  flipY(coords);
39083
39309
  }
39084
39310
  if (+opts.scale) {
39085
39311
  scaleAndShiftCoords(coords, +opts.scale, [0, 0]);
39086
39312
  }
39087
- if (polygonMode) {
39313
+ if (shapeMode) {
39088
39314
  scaleAndShiftCoords(coords, metersPerPx, shp[0]);
39089
- if (d.tfill) rec.fill = d.fill;
39315
+ if (d.fill) rec.fill = d.fill;
39316
+ if (d.stroke) rec.stroke = d.stroke;
39090
39317
  return createGeometry(coords, geojsonType);
39091
39318
  } else {
39092
- rec['svg-symbol'] = makeSvgPolygonSymbol(coords, d, geojsonType);
39319
+ rec['svg-symbol'] = makePathSymbol(coords, d, geojsonType);
39093
39320
  }
39094
39321
  });
39095
39322
 
39096
39323
  var outputLyr, dataset2;
39097
- if (polygonMode) {
39324
+ if (shapeMode) {
39098
39325
  dataset2 = importGeometries(geometries, records);
39099
39326
  outputLyr = mergeOutputLayerIntoDataset(inputLyr, dataset, dataset2, opts);
39100
39327
  outputLyr.data = lyr.data;
@@ -39128,31 +39355,10 @@ ${svg}
39128
39355
  }
39129
39356
 
39130
39357
  function getMetersPerPixel(lyr, dataset) {
39131
-
39132
- // TODO: handle single point, no extent
39133
39358
  var bounds = getLayerBounds(lyr);
39134
- return bounds.width() / 800;
39135
- }
39136
-
39137
- // Returns an svg-symbol data object for one symbol
39138
- function makeSvgPolygonSymbol(coords, properties, geojsonType) {
39139
- if (geojsonType == 'MultiPolygon') {
39140
- coords = convertMultiPolygonCoords(coords);
39141
- } else if (geojsonType != 'Polygon') {
39142
- error('Unsupported type:', geojsonType);
39143
- }
39144
- roundCoordsForSVG(coords);
39145
- return {
39146
- type: 'polygon',
39147
- coordinates: coords,
39148
- fill: properties.fill || 'magenta'
39149
- };
39150
- }
39151
-
39152
- function convertMultiPolygonCoords(coords) {
39153
- return coords.reduce(function(memo, poly) {
39154
- return memo.concat(poly);
39155
- }, []);
39359
+ // TODO: need a better way to handle a single point with no extent
39360
+ var extent = bounds.width() || bounds.height() || 1000;
39361
+ return extent / 800;
39156
39362
  }
39157
39363
 
39158
39364
  var Symbols = /*#__PURE__*/Object.freeze({
@@ -40661,7 +40867,8 @@ ${svg}
40661
40867
  // export only functions called by the GUI.
40662
40868
 
40663
40869
  var internal = {};
40664
- internal.svg = Object.assign({}, SvgCommon, SvgStringify, SvgPathUtils, GeojsonToSvg);
40870
+
40871
+ internal.svg = Object.assign({}, SvgStringify, SvgPathUtils, GeojsonToSvg, SvgLabels, SvgSymbols);
40665
40872
 
40666
40873
  // Assign functions and objects exported from modules to the 'internal' namespace
40667
40874
  // to maintain compatibility with tests and to expose (some of) them to the GUI.
@@ -40725,6 +40932,7 @@ ${svg}
40725
40932
  FileTypes,
40726
40933
  FilterGeom,
40727
40934
  Frame,
40935
+ FrameData,
40728
40936
  Furniture,
40729
40937
  Geodesic,
40730
40938
  GeojsonExport,