mapshaper 0.7.58 → 0.7.60

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
@@ -18168,10 +18168,29 @@
18168
18168
  parseSizeParam: parseSizeParam
18169
18169
  });
18170
18170
 
18171
- // Default gap-width as a fraction of the layer's median polygon-ring segment
18172
- // length. Shared by interior fill and exterior close-outer-gaps.
18171
+ // Conservative width as a fraction of the layer's median polygon-ring segment
18172
+ // length. Used for partition min-width and duplicate-boundary search — not for
18173
+ // the default gap-width=auto fill threshold (see getDefaultGapWidth).
18173
18174
  var GAP_WIDTH_SEGMENT_FRACTION = 0.01;
18174
18175
 
18176
+ // Inject gap_width=auto unless the caller already chose a width or a legacy
18177
+ // area/sliver option. Used by -clean, -filter-slivers, and gap partition.
18178
+ function applyDefaultGapWidthOpts(opts) {
18179
+ opts = Object.assign({}, opts);
18180
+ if (opts.gap_width != null) return opts;
18181
+ // Legacy area/sliver options: keep the historical gap_fill_area=auto default.
18182
+ if (opts.gap_fill_area != null || opts.min_gap_area != null ||
18183
+ opts.min_area != null || opts.sliver_control != null) {
18184
+ if (opts.gap_fill_area == null && opts.min_gap_area == null &&
18185
+ opts.min_area == null) {
18186
+ opts.gap_fill_area = 'auto';
18187
+ }
18188
+ return opts;
18189
+ }
18190
+ opts.gap_width = 'auto';
18191
+ return opts;
18192
+ }
18193
+
18175
18194
  // Used by -clean -dissolve -filter-slivers -filter-islands to generate filters
18176
18195
  // for removing small polygon rings / filling mosaic gaps.
18177
18196
  // Prefers gap_width= (including 'auto'). Legacy gap_fill_area / sliver_control
@@ -18317,9 +18336,10 @@
18317
18336
  };
18318
18337
  }
18319
18338
 
18320
- // Median polygon-ring segment length * GAP_WIDTH_SEGMENT_FRACTION.
18339
+ // Width equivalent of the legacy gap-fill-area=auto + sliver-control=1 test:
18340
+ // W = sqrt(A0 / π). The two filters accept the same rings (see getGapWidthTest).
18321
18341
  function getDefaultGapWidth(lyr, arcs) {
18322
- return getMedianPolygonSegmentLength(lyr, arcs) * GAP_WIDTH_SEGMENT_FRACTION;
18342
+ return Math.sqrt(getDefaultSliverThreshold(lyr, arcs) / Math.PI);
18323
18343
  }
18324
18344
 
18325
18345
  function getMedianPolygonSegmentLength(lyr, arcs) {
@@ -18400,6 +18420,7 @@
18400
18420
  var Slivers = /*#__PURE__*/Object.freeze({
18401
18421
  __proto__: null,
18402
18422
  GAP_WIDTH_SEGMENT_FRACTION: GAP_WIDTH_SEGMENT_FRACTION,
18423
+ applyDefaultGapWidthOpts: applyDefaultGapWidthOpts,
18403
18424
  calcMaxSliverArea: calcMaxSliverArea,
18404
18425
  getDefaultGapWidth: getDefaultGapWidth,
18405
18426
  getDefaultSliverThreshold: getDefaultSliverThreshold,
@@ -23467,8 +23488,7 @@
23467
23488
  var spherical = isLatLngCRS(crs);
23468
23489
  var medianSeg = measureMedianSegment(lyr, arcs);
23469
23490
  // Same width as interior gap filling, from the same automatic default.
23470
- var distance = resolveCloseDistance(opts, crs,
23471
- medianSeg * GAP_WIDTH_SEGMENT_FRACTION);
23491
+ var distance = resolveCloseDistance(opts, crs, getDefaultGapWidth(lyr, arcs));
23472
23492
  if (!(distance > 0)) return 0;
23473
23493
 
23474
23494
  profileStart('cg.outsideFacingArcs');
@@ -23518,7 +23538,7 @@
23518
23538
  return {seams: seams, seeds: seeds, arcsById: arcsById, footpoints: footpoints};
23519
23539
  }
23520
23540
 
23521
- // Same segment median as interior gap-width=auto (all polygon-ring segments).
23541
+ // Median of every polygon-ring segment; used for min-seam length.
23522
23542
  function measureMedianSegment(lyr, arcs) {
23523
23543
  return profileWrap('cg.medianSegment', function() {
23524
23544
  return getMedianPolygonSegmentLength(lyr, arcs);
@@ -24154,18 +24174,7 @@
24154
24174
  function partitionPolygonMosaicGaps(lyr, dataset, nodes, opts) {
24155
24175
  if (!lyr.shapes || lyr.shapes.length < MIN_GAP_OWNERS) return false;
24156
24176
  var mosaicIndex = new MosaicIndex(lyr, nodes, {flat: true});
24157
- var sliverOpts = utils.extend({}, opts);
24158
- if (sliverOpts.gap_width == null) {
24159
- if (sliverOpts.gap_fill_area != null || sliverOpts.min_gap_area != null ||
24160
- sliverOpts.min_area != null || sliverOpts.sliver_control != null) {
24161
- if (sliverOpts.gap_fill_area == null && sliverOpts.min_gap_area == null &&
24162
- sliverOpts.min_area == null) {
24163
- sliverOpts.gap_fill_area = 'auto';
24164
- }
24165
- } else {
24166
- sliverOpts.gap_width = 'auto';
24167
- }
24168
- }
24177
+ var sliverOpts = applyDefaultGapWidthOpts(opts);
24169
24178
  var filter = getSliverFilter(lyr, dataset, sliverOpts).filter;
24170
24179
  var gaps = mosaicIndex.getUnusedTileData(filter).filter(function(gap) {
24171
24180
  return countOwners(gap.boundary) >= MIN_GAP_OWNERS;
@@ -24601,10 +24610,11 @@
24601
24610
  // several features with hairline slivers, which is worse for having more of them.
24602
24611
  //
24603
24612
  // A layer's median segment length is the scale at which its boundaries were
24604
- // drawn, so the same fraction of it that serves as the default gap-width -- what
24605
- // mapshaper already treats as too narrow to be intended -- marks where dividing
24606
- // stops being worth it. This is deliberately independent of the gap-width in
24607
- // effect: raising that to fill wider gaps should not coarsen the data's own scale.
24613
+ // drawn. A small fraction of that scale marks where dividing a gap stops being
24614
+ // worth it: the displacement would be smaller than the data's own detail, and
24615
+ // the pieces would be hairline slivers. This is deliberately independent of
24616
+ // the gap-width in effect: raising that to fill wider gaps should not coarsen
24617
+ // the data's own scale.
24608
24618
  function getMinDivisibleGapWidth(lyr, arcs) {
24609
24619
  var width = getMedianPolygonSegmentLength(lyr, arcs) *
24610
24620
  GAP_WIDTH_SEGMENT_FRACTION;
@@ -24741,7 +24751,7 @@
24741
24751
 
24742
24752
  function cleanPolygonLayerGeometry(lyr, dataset, opts) {
24743
24753
  // clean polygons by apply the 'dissolve2' function to each feature
24744
- opts = withDefaultGapWidth(opts);
24754
+ opts = applyDefaultGapWidthOpts(opts);
24745
24755
  var groups = lyr.shapes.map(function(shp, i) {
24746
24756
  return [i];
24747
24757
  });
@@ -24750,22 +24760,6 @@
24750
24760
  markLayerChanged(lyr, {operation: 'cleanPolygonLayerGeometry', unit: 'shapes'});
24751
24761
  }
24752
24762
 
24753
- function withDefaultGapWidth(opts) {
24754
- opts = Object.assign({}, opts);
24755
- if (opts.gap_width != null) return opts;
24756
- // Legacy area/sliver options: keep the historical gap_fill_area=auto default.
24757
- if (opts.gap_fill_area != null || opts.min_gap_area != null ||
24758
- opts.min_area != null || opts.sliver_control != null) {
24759
- if (opts.gap_fill_area == null && opts.min_gap_area == null &&
24760
- opts.min_area == null) {
24761
- opts.gap_fill_area = 'auto';
24762
- }
24763
- return opts;
24764
- }
24765
- opts.gap_width = 'auto';
24766
- return opts;
24767
- }
24768
-
24769
24763
  function collectArcIds(shapes) {
24770
24764
  var ids = {};
24771
24765
  traversePaths(shapes, function(o) {
@@ -40329,12 +40323,13 @@ ${svg}
40329
40323
 
40330
40324
  parser.command('filter-slivers')
40331
40325
  .describe('remove small polygon rings')
40326
+ .option('gap-width', gapWidthOpt)
40332
40327
  .option('min-area', {
40333
40328
  type: 'area',
40334
- describe: 'area threshold (e.g. 2sqkm)'
40329
+ describe: '(deprecated) use gap-width= instead'
40335
40330
  })
40336
40331
  .option('sliver-control', {
40337
- describe: 'boost area threshold of slivers (0-1, default is 1)',
40332
+ describe: '(deprecated) use gap-width= instead',
40338
40333
  type: 'number'
40339
40334
  })
40340
40335
  .option('weighted', {
@@ -40383,9 +40378,9 @@ ${svg}
40383
40378
  .option('no-replace', noReplaceOpt);
40384
40379
 
40385
40380
  parser.command('grid')
40386
- .describe('create a grid of square, hexagonal, rhombus or triangle polygons')
40381
+ .describe('create a grid of square, hexagonal, rhombus, triangle or cairo polygons')
40387
40382
  .option('type', {
40388
- describe: 'square, square2, hex, hex2, rhombus, rhombus2, triangle or triangle2 (default is square)'
40383
+ describe: 'square, square2, hex, hex2, rhombus, rhombus2, triangle, triangle2 or cairo (default is square)'
40389
40384
  })
40390
40385
  .option('interval', {
40391
40386
  describe: 'side length (e.g. 500m, 12km)',
@@ -40407,6 +40402,10 @@ ${svg}
40407
40402
  describe: 'scale factor for cells, between 0 and 2',
40408
40403
  type: 'number'
40409
40404
  })
40405
+ .option('rotate', {
40406
+ describe: 'rotation angle in degrees (default is 0)',
40407
+ type: 'number'
40408
+ })
40410
40409
  // .option('bbox', {
40411
40410
  // type: 'bbox',
40412
40411
  // describe: 'xmin,ymin,xmax,ymax (default is bbox of data)'
@@ -56569,7 +56568,13 @@ ${svg}
56569
56568
  };
56570
56569
 
56571
56570
  function filterSlivers(lyr, dataset, optsArg) {
56572
- var opts = utils.extend({sliver_control: 1}, optsArg);
56571
+ var opts = applyDefaultGapWidthOpts(optsArg);
56572
+ // Legacy area path only: keep the historical sliver-control=1 default.
56573
+ // Do not inject it before applyDefaultGapWidthOpts, or the implicit default
56574
+ // would lock the command onto the area path.
56575
+ if (opts.gap_width == null && opts.sliver_control == null) {
56576
+ opts.sliver_control = 1;
56577
+ }
56573
56578
  var filterData = getSliverFilter(lyr, dataset, opts);
56574
56579
  var ringTest = filterData.filter;
56575
56580
  var removed = 0;
@@ -74555,7 +74560,7 @@ ${svg}
74555
74560
  cmd.polygonGrid = function(targetLayers, targetDataset, opts) {
74556
74561
  requireProjectedDataset(targetDataset);
74557
74562
  var params = getGridParams(targetLayers, targetDataset, opts);
74558
- var gridDataset = makeGridDataset(params); // grid is a new dataset
74563
+ var gridDataset = makeGridDataset(params, opts); // grid is a new dataset
74559
74564
  gridDataset.info = copyDatasetInfo(targetDataset.info);
74560
74565
  setOutputLayerName(gridDataset.layers[0], null, 'grid', opts);
74561
74566
  if (opts.debug) gridDataset.layers.push(cmd.pointGrid2(targetLayers, targetDataset, opts));
@@ -74619,35 +74624,67 @@ ${svg}
74619
74624
 
74620
74625
  function makeGridDataset(params, opts) {
74621
74626
  var geojson, dataset;
74627
+ var rotation = opts.rotate || 0;
74628
+ var genParams = rotation ? getRotatedGridParams(params, rotation) : params;
74622
74629
  if (params.type == 'square') {
74623
- geojson = getSquareGridGeoJSON(getSquareGridCoordinates(params));
74630
+ geojson = getSquareGridGeoJSON(getSquareGridCoordinates(genParams));
74624
74631
  } else if (params.type == 'square2') {
74625
- geojson = getRotatedSquareGridGeoJSON(params);
74632
+ geojson = getRotatedSquareGridGeoJSON(genParams);
74626
74633
  } else if (params.type == 'hex') {
74627
- geojson = getHexGridGeoJSON(getHexGridCoordinates(params));
74634
+ geojson = getHexGridGeoJSON(getHexGridCoordinates(genParams));
74628
74635
  } else if (params.type == 'hex2') {
74629
74636
  // use rotated grid
74630
- geojson = getHexGridGeoJSON(getHexGridCoordinates(swapGridParams(params)));
74637
+ geojson = getHexGridGeoJSON(getHexGridCoordinates(swapGridParams(genParams)));
74631
74638
  swapPolygonCoords(geojson);
74632
74639
  } else if (params.type == 'rhombus') {
74633
- geojson = getRhombusGridGeoJSON(params, false);
74640
+ geojson = getRhombusGridGeoJSON(genParams, false);
74634
74641
  } else if (params.type == 'rhombus2') {
74635
- geojson = getRhombusGridGeoJSON(params, true);
74642
+ geojson = getRhombusGridGeoJSON(genParams, true);
74636
74643
  } else if (params.type == 'triangle') {
74637
- geojson = getTriangleGridGeoJSON(params, false);
74644
+ geojson = getTriangleGridGeoJSON(genParams, false);
74638
74645
  } else if (params.type == 'triangle2') {
74639
- geojson = getTriangleGridGeoJSON(params, true);
74646
+ geojson = getTriangleGridGeoJSON(genParams, true);
74647
+ } else if (params.type == 'cairo') {
74648
+ geojson = getCairoGridGeoJSON(genParams);
74640
74649
  } else {
74641
74650
  stop$1('Unsupported grid type');
74642
74651
  }
74643
74652
  scaleGridCells(geojson, params.cellScale);
74644
74653
  alignGridToBounds(geojson, params.bbox);
74654
+ if (rotation) {
74655
+ rotateGridCells(geojson, rotation, params.bbox);
74656
+ }
74645
74657
  cullGridCells(geojson, params.bbox);
74646
74658
  dataset = importGeoJSON(geojson, {});
74647
74659
  buildTopology(dataset);
74648
74660
  return dataset;
74649
74661
  }
74650
74662
 
74663
+ // Expand the generation rectangle so that, after rotation around the shared
74664
+ // center, the grid still covers the original target bbox.
74665
+ function getRotatedGridParams(params, rotation) {
74666
+ var angle = rotation * Math.PI / 180;
74667
+ var cos = Math.abs(Math.cos(angle));
74668
+ var sin = Math.abs(Math.sin(angle));
74669
+ return utils.defaults({
74670
+ width: params.width * cos + params.height * sin,
74671
+ height: params.width * sin + params.height * cos
74672
+ }, params);
74673
+ }
74674
+
74675
+ function rotateGridCells(geojson, rotation, bbox) {
74676
+ var cx = (bbox[0] + bbox[2]) / 2;
74677
+ var cy = (bbox[1] + bbox[3]) / 2;
74678
+ var transform = getAffineTransform(rotation, 1, [0, 0], [cx, cy]);
74679
+ geojson.geometries.forEach(function(geom) {
74680
+ if (geom.type == 'Polygon') {
74681
+ geom.coordinates[0] = geom.coordinates[0].map(function(xy) {
74682
+ return transform(xy[0], xy[1]);
74683
+ });
74684
+ }
74685
+ });
74686
+ }
74687
+
74651
74688
  function swapGridParams(params) {
74652
74689
  var bbox = params.bbox;
74653
74690
  return utils.defaults({
@@ -74729,6 +74766,7 @@ ${svg}
74729
74766
  if (type == 'hex' || type == 'hex2') return 3 * Math.sqrt(3) / 2;
74730
74767
  if (type == 'rhombus' || type == 'rhombus2') return Math.sqrt(3) / 2;
74731
74768
  if (type == 'triangle' || type == 'triangle2') return Math.sqrt(3) / 4;
74769
+ if (type == 'cairo') return (4 + Math.sqrt(7)) / 4;
74732
74770
  stop$1('Unsupported grid type');
74733
74771
  }
74734
74772
 
@@ -74882,6 +74920,95 @@ ${svg}
74882
74920
  return geojson;
74883
74921
  }
74884
74922
 
74923
+ // Equilateral type-4 Cairo tessellation. Four-valent (90°) vertices sit on a
74924
+ // square lattice of spacing S = interval * (√7+1)/2; each lattice square
74925
+ // contains a central bar of length `interval`, alternating horizontal and
74926
+ // vertical. Two pentagons share each bar.
74927
+ //
74928
+ // Vertices are computed once and reused. Algebraically equal expressions
74929
+ // such as i*S and (i-1)*S + S are not bit-identical, and independently
74930
+ // computed copies of the same 4-way junction become T-intersections.
74931
+ function getCairoGridGeoJSON(params) {
74932
+ var side = params.interval;
74933
+ var spacing = side * (Math.sqrt(7) + 1) / 2;
74934
+ var halfBar = side / 2;
74935
+ var iMin = -1;
74936
+ var jMin = -1;
74937
+ var iMax = Math.ceil(params.width / spacing) + 1;
74938
+ var jMax = Math.ceil(params.height / spacing) + 1;
74939
+ var lattice = [];
74940
+ var bars = [];
74941
+ var geometries = [];
74942
+ var i, j, y, cx, cy;
74943
+
74944
+ for (j = jMin - 1; j <= jMax + 1; j++) {
74945
+ y = j * spacing;
74946
+ lattice[j] = [];
74947
+ for (i = iMin - 1; i <= iMax + 1; i++) {
74948
+ lattice[j][i] = [i * spacing, y];
74949
+ }
74950
+ }
74951
+
74952
+ for (j = jMin - 1; j < jMax + 1; j++) {
74953
+ bars[j] = [];
74954
+ for (i = iMin - 1; i < iMax + 1; i++) {
74955
+ cx = (i + 0.5) * spacing;
74956
+ cy = (j + 0.5) * spacing;
74957
+ bars[j][i] = (i + j) % 2 === 0 ? {
74958
+ a: [cx - halfBar, cy],
74959
+ b: [cx + halfBar, cy]
74960
+ } : {
74961
+ a: [cx, cy - halfBar],
74962
+ b: [cx, cy + halfBar]
74963
+ };
74964
+ }
74965
+ }
74966
+
74967
+ for (j = jMin; j < jMax; j++) {
74968
+ for (i = iMin; i < iMax; i++) {
74969
+ if ((i + j) % 2 === 0) {
74970
+ appendCairoPentagon(geometries, [
74971
+ lattice[j][i],
74972
+ bars[j - 1][i].b,
74973
+ lattice[j][i + 1],
74974
+ bars[j][i].b,
74975
+ bars[j][i].a
74976
+ ]);
74977
+ appendCairoPentagon(geometries, [
74978
+ lattice[j + 1][i],
74979
+ bars[j][i].a,
74980
+ bars[j][i].b,
74981
+ lattice[j + 1][i + 1],
74982
+ bars[j + 1][i].a
74983
+ ]);
74984
+ } else {
74985
+ appendCairoPentagon(geometries, [
74986
+ lattice[j][i],
74987
+ bars[j][i].a,
74988
+ bars[j][i].b,
74989
+ lattice[j + 1][i],
74990
+ bars[j][i - 1].b
74991
+ ]);
74992
+ appendCairoPentagon(geometries, [
74993
+ lattice[j][i + 1],
74994
+ bars[j][i + 1].a,
74995
+ lattice[j + 1][i + 1],
74996
+ bars[j][i].b,
74997
+ bars[j][i].a
74998
+ ]);
74999
+ }
75000
+ }
75001
+ }
75002
+ return {type: 'GeometryCollection', geometries: geometries};
75003
+ }
75004
+
75005
+ function appendCairoPentagon(geometries, verts) {
75006
+ geometries.push({
75007
+ type: 'Polygon',
75008
+ coordinates: [verts.concat([verts[0]])]
75009
+ });
75010
+ }
75011
+
74885
75012
  function subdivideHexagon(coords) {
74886
75013
  var center = getPolygonRingCenter(coords);
74887
75014
  return [
@@ -80084,7 +80211,7 @@ ${svg}
80084
80211
  return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
80085
80212
  }
80086
80213
 
80087
- var version = "0.7.58";
80214
+ var version = "0.7.60";
80088
80215
 
80089
80216
  // Parse command line args into commands and run them
80090
80217
  // Function takes an optional Node-style callback. A Promise is returned if no callback is given.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mapshaper",
3
- "version": "0.7.58",
3
+ "version": "0.7.60",
4
4
  "description": "A tool for editing geospatial data for mapping and GIS.",
5
5
  "keywords": [
6
6
  "shapefile",
package/www/basemap.js CHANGED
@@ -3,7 +3,7 @@ window.mapboxParams = {
3
3
  // css: 'https://api.mapbox.com/mapbox-gl-js/v2.11.1/mapbox-gl.css',
4
4
  js: 'https://api.mapbox.com/mapbox-gl-js/v3.15.0/mapbox-gl.js',
5
5
  css: 'https://api.mapbox.com/mapbox-gl-js/v3.15.0/mapbox-gl.css',
6
- production_key: 'pk.eyJ1IjoiZ3JhbW1hdGEiLCJhIjoiY21jZ2RlbGk1MGhvNzJrcHV2dTYzZWE2byJ9.btkRnRrPW75c0AlQ5lH2cg',
6
+ production_key: 'pk.eyJ1IjoiZ3JhbW1hdGEiLCJhIjoiY210cjg2NTk5MDBiMTJ4b3dvbDNwMjdudSJ9.EohCQlad59ntK8CqWlopKw',
7
7
  localhost_key: 'pk.eyJ1IjoiZ3JhbW1hdGEiLCJhIjoiY21jZ2Rna2hrMGo2ZjJqcHFxMHAycDlyMSJ9.q8FchhB-NAN_FGBHN0AJeg',
8
8
  styles: [{
9
9
  name: 'Reference map',
@@ -11109,6 +11109,8 @@
11109
11109
 
11110
11110
  var openMenu;
11111
11111
  var openMenuId;
11112
+ // One menu element per parent element, created on demand.
11113
+ var menus = new WeakMap();
11112
11114
  // Prefixes shown to the left of a menu item. The checkmark is what a copyable
11113
11115
  // item shows after it has been copied.
11114
11116
  var BULLET = '• &nbsp;';
@@ -11132,14 +11134,26 @@
11132
11134
  }
11133
11135
  }
11134
11136
 
11137
+ // Menus are re-used, rather than created per click: a discarded menu stays in
11138
+ // the DOM, and an empty one is a 10px sliver at the bottom of the page, which
11139
+ // is enough to make the page scrollable (see the note in ContextMenu()).
11140
+ function getContextMenu(parentArg) {
11141
+ var parent = parentArg || document.querySelector('body');
11142
+ var menu = menus.get(parent);
11143
+ if (!menu) {
11144
+ menu = new ContextMenu(parent);
11145
+ menus.set(parent, menu);
11146
+ }
11147
+ return menu;
11148
+ }
11149
+
11135
11150
  function openContextMenu(e, lyr, parent) {
11136
- var menu = new ContextMenu(parent);
11137
11151
  if (e.contextMenuId && e.contextMenuId == openMenuId) {
11138
11152
  closeOpenMenu(true);
11139
11153
  return;
11140
11154
  }
11141
11155
  closeOpenMenu(true);
11142
- menu.open(e, lyr);
11156
+ getContextMenu(parent).open(e, lyr);
11143
11157
  openMenuId = e.contextMenuId || null;
11144
11158
  }
11145
11159
 
@@ -11151,6 +11165,14 @@
11151
11165
  var _open = false;
11152
11166
  var _openCount = 0;
11153
11167
 
11168
+ // The menu is hidden until it is opened. A visible empty menu is 10px of
11169
+ // padding and border at the end of the page, and the page is exactly as tall
11170
+ // as the window, so those 10px make the document scrollable. The body is
11171
+ // overflow:hidden, but that only stops the user from scrolling: the browser
11172
+ // still scrolls the page to reveal a focused element or a caret, and once it
11173
+ // does, the whole UI stays shifted up (mbloch/mapshaper#702).
11174
+ menu.hide();
11175
+
11154
11176
  this.isOpen = function() {
11155
11177
  return _open;
11156
11178
  };
@@ -11292,10 +11314,23 @@
11292
11314
  }
11293
11315
  menu.css('top', (e.pageY - offs.top - 15) + 'px');
11294
11316
  menu.show();
11317
+ keepInsideWindow();
11295
11318
 
11296
11319
  _open = true;
11297
11320
  _openCount++;
11298
11321
 
11322
+ // A menu opened near the bottom of the window would otherwise hang off the
11323
+ // screen, out of reach and adding to the page's scrollable overflow.
11324
+ function keepInsideWindow() {
11325
+ var pad = 6;
11326
+ var rect = menu.node().getBoundingClientRect();
11327
+ var overflow = rect.bottom + pad - document.documentElement.clientHeight;
11328
+ var shift = Math.min(overflow, rect.top - pad);
11329
+ if (shift > 0) {
11330
+ menu.css('top', (parseFloat(menu.node().style.top) - shift) + 'px');
11331
+ }
11332
+ }
11333
+
11299
11334
  function getParentOffset() { // crossbrowser version
11300
11335
  if (parent == body) {
11301
11336
  return {top: 0, left: 0};
@@ -25418,7 +25453,7 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
25418
25453
  gui.messages = new MessageControl(gui);
25419
25454
  gui.basemap = new Basemap(gui);
25420
25455
  gui.session = new SessionHistory(gui);
25421
- gui.contextMenu = new ContextMenu();
25456
+ gui.contextMenu = getContextMenu();
25422
25457
  gui.undo = new Undo(gui);
25423
25458
  gui.map = new MshpMap(gui);
25424
25459
  // gui.project = new ProjectOptions(gui);
package/www/mapshaper.js CHANGED
@@ -18168,10 +18168,29 @@
18168
18168
  parseSizeParam: parseSizeParam
18169
18169
  });
18170
18170
 
18171
- // Default gap-width as a fraction of the layer's median polygon-ring segment
18172
- // length. Shared by interior fill and exterior close-outer-gaps.
18171
+ // Conservative width as a fraction of the layer's median polygon-ring segment
18172
+ // length. Used for partition min-width and duplicate-boundary search — not for
18173
+ // the default gap-width=auto fill threshold (see getDefaultGapWidth).
18173
18174
  var GAP_WIDTH_SEGMENT_FRACTION = 0.01;
18174
18175
 
18176
+ // Inject gap_width=auto unless the caller already chose a width or a legacy
18177
+ // area/sliver option. Used by -clean, -filter-slivers, and gap partition.
18178
+ function applyDefaultGapWidthOpts(opts) {
18179
+ opts = Object.assign({}, opts);
18180
+ if (opts.gap_width != null) return opts;
18181
+ // Legacy area/sliver options: keep the historical gap_fill_area=auto default.
18182
+ if (opts.gap_fill_area != null || opts.min_gap_area != null ||
18183
+ opts.min_area != null || opts.sliver_control != null) {
18184
+ if (opts.gap_fill_area == null && opts.min_gap_area == null &&
18185
+ opts.min_area == null) {
18186
+ opts.gap_fill_area = 'auto';
18187
+ }
18188
+ return opts;
18189
+ }
18190
+ opts.gap_width = 'auto';
18191
+ return opts;
18192
+ }
18193
+
18175
18194
  // Used by -clean -dissolve -filter-slivers -filter-islands to generate filters
18176
18195
  // for removing small polygon rings / filling mosaic gaps.
18177
18196
  // Prefers gap_width= (including 'auto'). Legacy gap_fill_area / sliver_control
@@ -18317,9 +18336,10 @@
18317
18336
  };
18318
18337
  }
18319
18338
 
18320
- // Median polygon-ring segment length * GAP_WIDTH_SEGMENT_FRACTION.
18339
+ // Width equivalent of the legacy gap-fill-area=auto + sliver-control=1 test:
18340
+ // W = sqrt(A0 / π). The two filters accept the same rings (see getGapWidthTest).
18321
18341
  function getDefaultGapWidth(lyr, arcs) {
18322
- return getMedianPolygonSegmentLength(lyr, arcs) * GAP_WIDTH_SEGMENT_FRACTION;
18342
+ return Math.sqrt(getDefaultSliverThreshold(lyr, arcs) / Math.PI);
18323
18343
  }
18324
18344
 
18325
18345
  function getMedianPolygonSegmentLength(lyr, arcs) {
@@ -18400,6 +18420,7 @@
18400
18420
  var Slivers = /*#__PURE__*/Object.freeze({
18401
18421
  __proto__: null,
18402
18422
  GAP_WIDTH_SEGMENT_FRACTION: GAP_WIDTH_SEGMENT_FRACTION,
18423
+ applyDefaultGapWidthOpts: applyDefaultGapWidthOpts,
18403
18424
  calcMaxSliverArea: calcMaxSliverArea,
18404
18425
  getDefaultGapWidth: getDefaultGapWidth,
18405
18426
  getDefaultSliverThreshold: getDefaultSliverThreshold,
@@ -23467,8 +23488,7 @@
23467
23488
  var spherical = isLatLngCRS(crs);
23468
23489
  var medianSeg = measureMedianSegment(lyr, arcs);
23469
23490
  // Same width as interior gap filling, from the same automatic default.
23470
- var distance = resolveCloseDistance(opts, crs,
23471
- medianSeg * GAP_WIDTH_SEGMENT_FRACTION);
23491
+ var distance = resolveCloseDistance(opts, crs, getDefaultGapWidth(lyr, arcs));
23472
23492
  if (!(distance > 0)) return 0;
23473
23493
 
23474
23494
  profileStart('cg.outsideFacingArcs');
@@ -23518,7 +23538,7 @@
23518
23538
  return {seams: seams, seeds: seeds, arcsById: arcsById, footpoints: footpoints};
23519
23539
  }
23520
23540
 
23521
- // Same segment median as interior gap-width=auto (all polygon-ring segments).
23541
+ // Median of every polygon-ring segment; used for min-seam length.
23522
23542
  function measureMedianSegment(lyr, arcs) {
23523
23543
  return profileWrap('cg.medianSegment', function() {
23524
23544
  return getMedianPolygonSegmentLength(lyr, arcs);
@@ -24154,18 +24174,7 @@
24154
24174
  function partitionPolygonMosaicGaps(lyr, dataset, nodes, opts) {
24155
24175
  if (!lyr.shapes || lyr.shapes.length < MIN_GAP_OWNERS) return false;
24156
24176
  var mosaicIndex = new MosaicIndex(lyr, nodes, {flat: true});
24157
- var sliverOpts = utils.extend({}, opts);
24158
- if (sliverOpts.gap_width == null) {
24159
- if (sliverOpts.gap_fill_area != null || sliverOpts.min_gap_area != null ||
24160
- sliverOpts.min_area != null || sliverOpts.sliver_control != null) {
24161
- if (sliverOpts.gap_fill_area == null && sliverOpts.min_gap_area == null &&
24162
- sliverOpts.min_area == null) {
24163
- sliverOpts.gap_fill_area = 'auto';
24164
- }
24165
- } else {
24166
- sliverOpts.gap_width = 'auto';
24167
- }
24168
- }
24177
+ var sliverOpts = applyDefaultGapWidthOpts(opts);
24169
24178
  var filter = getSliverFilter(lyr, dataset, sliverOpts).filter;
24170
24179
  var gaps = mosaicIndex.getUnusedTileData(filter).filter(function(gap) {
24171
24180
  return countOwners(gap.boundary) >= MIN_GAP_OWNERS;
@@ -24601,10 +24610,11 @@
24601
24610
  // several features with hairline slivers, which is worse for having more of them.
24602
24611
  //
24603
24612
  // A layer's median segment length is the scale at which its boundaries were
24604
- // drawn, so the same fraction of it that serves as the default gap-width -- what
24605
- // mapshaper already treats as too narrow to be intended -- marks where dividing
24606
- // stops being worth it. This is deliberately independent of the gap-width in
24607
- // effect: raising that to fill wider gaps should not coarsen the data's own scale.
24613
+ // drawn. A small fraction of that scale marks where dividing a gap stops being
24614
+ // worth it: the displacement would be smaller than the data's own detail, and
24615
+ // the pieces would be hairline slivers. This is deliberately independent of
24616
+ // the gap-width in effect: raising that to fill wider gaps should not coarsen
24617
+ // the data's own scale.
24608
24618
  function getMinDivisibleGapWidth(lyr, arcs) {
24609
24619
  var width = getMedianPolygonSegmentLength(lyr, arcs) *
24610
24620
  GAP_WIDTH_SEGMENT_FRACTION;
@@ -24741,7 +24751,7 @@
24741
24751
 
24742
24752
  function cleanPolygonLayerGeometry(lyr, dataset, opts) {
24743
24753
  // clean polygons by apply the 'dissolve2' function to each feature
24744
- opts = withDefaultGapWidth(opts);
24754
+ opts = applyDefaultGapWidthOpts(opts);
24745
24755
  var groups = lyr.shapes.map(function(shp, i) {
24746
24756
  return [i];
24747
24757
  });
@@ -24750,22 +24760,6 @@
24750
24760
  markLayerChanged(lyr, {operation: 'cleanPolygonLayerGeometry', unit: 'shapes'});
24751
24761
  }
24752
24762
 
24753
- function withDefaultGapWidth(opts) {
24754
- opts = Object.assign({}, opts);
24755
- if (opts.gap_width != null) return opts;
24756
- // Legacy area/sliver options: keep the historical gap_fill_area=auto default.
24757
- if (opts.gap_fill_area != null || opts.min_gap_area != null ||
24758
- opts.min_area != null || opts.sliver_control != null) {
24759
- if (opts.gap_fill_area == null && opts.min_gap_area == null &&
24760
- opts.min_area == null) {
24761
- opts.gap_fill_area = 'auto';
24762
- }
24763
- return opts;
24764
- }
24765
- opts.gap_width = 'auto';
24766
- return opts;
24767
- }
24768
-
24769
24763
  function collectArcIds(shapes) {
24770
24764
  var ids = {};
24771
24765
  traversePaths(shapes, function(o) {
@@ -40329,12 +40323,13 @@ ${svg}
40329
40323
 
40330
40324
  parser.command('filter-slivers')
40331
40325
  .describe('remove small polygon rings')
40326
+ .option('gap-width', gapWidthOpt)
40332
40327
  .option('min-area', {
40333
40328
  type: 'area',
40334
- describe: 'area threshold (e.g. 2sqkm)'
40329
+ describe: '(deprecated) use gap-width= instead'
40335
40330
  })
40336
40331
  .option('sliver-control', {
40337
- describe: 'boost area threshold of slivers (0-1, default is 1)',
40332
+ describe: '(deprecated) use gap-width= instead',
40338
40333
  type: 'number'
40339
40334
  })
40340
40335
  .option('weighted', {
@@ -40383,9 +40378,9 @@ ${svg}
40383
40378
  .option('no-replace', noReplaceOpt);
40384
40379
 
40385
40380
  parser.command('grid')
40386
- .describe('create a grid of square, hexagonal, rhombus or triangle polygons')
40381
+ .describe('create a grid of square, hexagonal, rhombus, triangle or cairo polygons')
40387
40382
  .option('type', {
40388
- describe: 'square, square2, hex, hex2, rhombus, rhombus2, triangle or triangle2 (default is square)'
40383
+ describe: 'square, square2, hex, hex2, rhombus, rhombus2, triangle, triangle2 or cairo (default is square)'
40389
40384
  })
40390
40385
  .option('interval', {
40391
40386
  describe: 'side length (e.g. 500m, 12km)',
@@ -40407,6 +40402,10 @@ ${svg}
40407
40402
  describe: 'scale factor for cells, between 0 and 2',
40408
40403
  type: 'number'
40409
40404
  })
40405
+ .option('rotate', {
40406
+ describe: 'rotation angle in degrees (default is 0)',
40407
+ type: 'number'
40408
+ })
40410
40409
  // .option('bbox', {
40411
40410
  // type: 'bbox',
40412
40411
  // describe: 'xmin,ymin,xmax,ymax (default is bbox of data)'
@@ -56569,7 +56568,13 @@ ${svg}
56569
56568
  };
56570
56569
 
56571
56570
  function filterSlivers(lyr, dataset, optsArg) {
56572
- var opts = utils.extend({sliver_control: 1}, optsArg);
56571
+ var opts = applyDefaultGapWidthOpts(optsArg);
56572
+ // Legacy area path only: keep the historical sliver-control=1 default.
56573
+ // Do not inject it before applyDefaultGapWidthOpts, or the implicit default
56574
+ // would lock the command onto the area path.
56575
+ if (opts.gap_width == null && opts.sliver_control == null) {
56576
+ opts.sliver_control = 1;
56577
+ }
56573
56578
  var filterData = getSliverFilter(lyr, dataset, opts);
56574
56579
  var ringTest = filterData.filter;
56575
56580
  var removed = 0;
@@ -74555,7 +74560,7 @@ ${svg}
74555
74560
  cmd.polygonGrid = function(targetLayers, targetDataset, opts) {
74556
74561
  requireProjectedDataset(targetDataset);
74557
74562
  var params = getGridParams(targetLayers, targetDataset, opts);
74558
- var gridDataset = makeGridDataset(params); // grid is a new dataset
74563
+ var gridDataset = makeGridDataset(params, opts); // grid is a new dataset
74559
74564
  gridDataset.info = copyDatasetInfo(targetDataset.info);
74560
74565
  setOutputLayerName(gridDataset.layers[0], null, 'grid', opts);
74561
74566
  if (opts.debug) gridDataset.layers.push(cmd.pointGrid2(targetLayers, targetDataset, opts));
@@ -74619,35 +74624,67 @@ ${svg}
74619
74624
 
74620
74625
  function makeGridDataset(params, opts) {
74621
74626
  var geojson, dataset;
74627
+ var rotation = opts.rotate || 0;
74628
+ var genParams = rotation ? getRotatedGridParams(params, rotation) : params;
74622
74629
  if (params.type == 'square') {
74623
- geojson = getSquareGridGeoJSON(getSquareGridCoordinates(params));
74630
+ geojson = getSquareGridGeoJSON(getSquareGridCoordinates(genParams));
74624
74631
  } else if (params.type == 'square2') {
74625
- geojson = getRotatedSquareGridGeoJSON(params);
74632
+ geojson = getRotatedSquareGridGeoJSON(genParams);
74626
74633
  } else if (params.type == 'hex') {
74627
- geojson = getHexGridGeoJSON(getHexGridCoordinates(params));
74634
+ geojson = getHexGridGeoJSON(getHexGridCoordinates(genParams));
74628
74635
  } else if (params.type == 'hex2') {
74629
74636
  // use rotated grid
74630
- geojson = getHexGridGeoJSON(getHexGridCoordinates(swapGridParams(params)));
74637
+ geojson = getHexGridGeoJSON(getHexGridCoordinates(swapGridParams(genParams)));
74631
74638
  swapPolygonCoords(geojson);
74632
74639
  } else if (params.type == 'rhombus') {
74633
- geojson = getRhombusGridGeoJSON(params, false);
74640
+ geojson = getRhombusGridGeoJSON(genParams, false);
74634
74641
  } else if (params.type == 'rhombus2') {
74635
- geojson = getRhombusGridGeoJSON(params, true);
74642
+ geojson = getRhombusGridGeoJSON(genParams, true);
74636
74643
  } else if (params.type == 'triangle') {
74637
- geojson = getTriangleGridGeoJSON(params, false);
74644
+ geojson = getTriangleGridGeoJSON(genParams, false);
74638
74645
  } else if (params.type == 'triangle2') {
74639
- geojson = getTriangleGridGeoJSON(params, true);
74646
+ geojson = getTriangleGridGeoJSON(genParams, true);
74647
+ } else if (params.type == 'cairo') {
74648
+ geojson = getCairoGridGeoJSON(genParams);
74640
74649
  } else {
74641
74650
  stop$1('Unsupported grid type');
74642
74651
  }
74643
74652
  scaleGridCells(geojson, params.cellScale);
74644
74653
  alignGridToBounds(geojson, params.bbox);
74654
+ if (rotation) {
74655
+ rotateGridCells(geojson, rotation, params.bbox);
74656
+ }
74645
74657
  cullGridCells(geojson, params.bbox);
74646
74658
  dataset = importGeoJSON(geojson, {});
74647
74659
  buildTopology(dataset);
74648
74660
  return dataset;
74649
74661
  }
74650
74662
 
74663
+ // Expand the generation rectangle so that, after rotation around the shared
74664
+ // center, the grid still covers the original target bbox.
74665
+ function getRotatedGridParams(params, rotation) {
74666
+ var angle = rotation * Math.PI / 180;
74667
+ var cos = Math.abs(Math.cos(angle));
74668
+ var sin = Math.abs(Math.sin(angle));
74669
+ return utils.defaults({
74670
+ width: params.width * cos + params.height * sin,
74671
+ height: params.width * sin + params.height * cos
74672
+ }, params);
74673
+ }
74674
+
74675
+ function rotateGridCells(geojson, rotation, bbox) {
74676
+ var cx = (bbox[0] + bbox[2]) / 2;
74677
+ var cy = (bbox[1] + bbox[3]) / 2;
74678
+ var transform = getAffineTransform(rotation, 1, [0, 0], [cx, cy]);
74679
+ geojson.geometries.forEach(function(geom) {
74680
+ if (geom.type == 'Polygon') {
74681
+ geom.coordinates[0] = geom.coordinates[0].map(function(xy) {
74682
+ return transform(xy[0], xy[1]);
74683
+ });
74684
+ }
74685
+ });
74686
+ }
74687
+
74651
74688
  function swapGridParams(params) {
74652
74689
  var bbox = params.bbox;
74653
74690
  return utils.defaults({
@@ -74729,6 +74766,7 @@ ${svg}
74729
74766
  if (type == 'hex' || type == 'hex2') return 3 * Math.sqrt(3) / 2;
74730
74767
  if (type == 'rhombus' || type == 'rhombus2') return Math.sqrt(3) / 2;
74731
74768
  if (type == 'triangle' || type == 'triangle2') return Math.sqrt(3) / 4;
74769
+ if (type == 'cairo') return (4 + Math.sqrt(7)) / 4;
74732
74770
  stop$1('Unsupported grid type');
74733
74771
  }
74734
74772
 
@@ -74882,6 +74920,95 @@ ${svg}
74882
74920
  return geojson;
74883
74921
  }
74884
74922
 
74923
+ // Equilateral type-4 Cairo tessellation. Four-valent (90°) vertices sit on a
74924
+ // square lattice of spacing S = interval * (√7+1)/2; each lattice square
74925
+ // contains a central bar of length `interval`, alternating horizontal and
74926
+ // vertical. Two pentagons share each bar.
74927
+ //
74928
+ // Vertices are computed once and reused. Algebraically equal expressions
74929
+ // such as i*S and (i-1)*S + S are not bit-identical, and independently
74930
+ // computed copies of the same 4-way junction become T-intersections.
74931
+ function getCairoGridGeoJSON(params) {
74932
+ var side = params.interval;
74933
+ var spacing = side * (Math.sqrt(7) + 1) / 2;
74934
+ var halfBar = side / 2;
74935
+ var iMin = -1;
74936
+ var jMin = -1;
74937
+ var iMax = Math.ceil(params.width / spacing) + 1;
74938
+ var jMax = Math.ceil(params.height / spacing) + 1;
74939
+ var lattice = [];
74940
+ var bars = [];
74941
+ var geometries = [];
74942
+ var i, j, y, cx, cy;
74943
+
74944
+ for (j = jMin - 1; j <= jMax + 1; j++) {
74945
+ y = j * spacing;
74946
+ lattice[j] = [];
74947
+ for (i = iMin - 1; i <= iMax + 1; i++) {
74948
+ lattice[j][i] = [i * spacing, y];
74949
+ }
74950
+ }
74951
+
74952
+ for (j = jMin - 1; j < jMax + 1; j++) {
74953
+ bars[j] = [];
74954
+ for (i = iMin - 1; i < iMax + 1; i++) {
74955
+ cx = (i + 0.5) * spacing;
74956
+ cy = (j + 0.5) * spacing;
74957
+ bars[j][i] = (i + j) % 2 === 0 ? {
74958
+ a: [cx - halfBar, cy],
74959
+ b: [cx + halfBar, cy]
74960
+ } : {
74961
+ a: [cx, cy - halfBar],
74962
+ b: [cx, cy + halfBar]
74963
+ };
74964
+ }
74965
+ }
74966
+
74967
+ for (j = jMin; j < jMax; j++) {
74968
+ for (i = iMin; i < iMax; i++) {
74969
+ if ((i + j) % 2 === 0) {
74970
+ appendCairoPentagon(geometries, [
74971
+ lattice[j][i],
74972
+ bars[j - 1][i].b,
74973
+ lattice[j][i + 1],
74974
+ bars[j][i].b,
74975
+ bars[j][i].a
74976
+ ]);
74977
+ appendCairoPentagon(geometries, [
74978
+ lattice[j + 1][i],
74979
+ bars[j][i].a,
74980
+ bars[j][i].b,
74981
+ lattice[j + 1][i + 1],
74982
+ bars[j + 1][i].a
74983
+ ]);
74984
+ } else {
74985
+ appendCairoPentagon(geometries, [
74986
+ lattice[j][i],
74987
+ bars[j][i].a,
74988
+ bars[j][i].b,
74989
+ lattice[j + 1][i],
74990
+ bars[j][i - 1].b
74991
+ ]);
74992
+ appendCairoPentagon(geometries, [
74993
+ lattice[j][i + 1],
74994
+ bars[j][i + 1].a,
74995
+ lattice[j + 1][i + 1],
74996
+ bars[j][i].b,
74997
+ bars[j][i].a
74998
+ ]);
74999
+ }
75000
+ }
75001
+ }
75002
+ return {type: 'GeometryCollection', geometries: geometries};
75003
+ }
75004
+
75005
+ function appendCairoPentagon(geometries, verts) {
75006
+ geometries.push({
75007
+ type: 'Polygon',
75008
+ coordinates: [verts.concat([verts[0]])]
75009
+ });
75010
+ }
75011
+
74885
75012
  function subdivideHexagon(coords) {
74886
75013
  var center = getPolygonRingCenter(coords);
74887
75014
  return [
@@ -80084,7 +80211,7 @@ ${svg}
80084
80211
  return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
80085
80212
  }
80086
80213
 
80087
- var version = "0.7.58";
80214
+ var version = "0.7.60";
80088
80215
 
80089
80216
  // Parse command line args into commands and run them
80090
80217
  // Function takes an optional Node-style callback. A Promise is returned if no callback is given.