mapshaper 0.6.53 → 0.6.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/mapshaper.js CHANGED
@@ -1,6 +1,6 @@
1
1
  (function () {
2
2
 
3
- var VERSION = "0.6.53";
3
+ var VERSION = "0.6.55";
4
4
 
5
5
 
6
6
  var utils = /*#__PURE__*/Object.freeze({
@@ -11075,7 +11075,7 @@
11075
11075
  }
11076
11076
 
11077
11077
  function isGzipFile(file) {
11078
- return /\.gz/i.test(file);
11078
+ return /\.gz$/i.test(file);
11079
11079
  }
11080
11080
 
11081
11081
  function isSupportedOutputFormat(fmt) {
@@ -25338,7 +25338,7 @@ ${svg}
25338
25338
  .describe('create commands on-the-fly and run them')
25339
25339
  .option('expression', {
25340
25340
  DEFAULT: true,
25341
- describe: 'JS expression to generate command(s)'
25341
+ describe: 'JS expression or template to generate command(s)'
25342
25342
  })
25343
25343
  // deprecated
25344
25344
  .option('commands', {alias_to: 'expression'})
@@ -41842,6 +41842,185 @@ ${svg}
41842
41842
  }, {});
41843
41843
  }
41844
41844
 
41845
+ // import { importGeoJSON } from '../geojson/geojson-import';
41846
+
41847
+ function getTargetProxy(target) {
41848
+ var lyr = target.layers[0];
41849
+ var data = getLayerInfo(lyr, target.dataset); // layer_name, feature_count etc
41850
+ data.layer = lyr;
41851
+ data.dataset = target.dataset;
41852
+ addGetters(data, {
41853
+ // export as an object, not a string or buffer
41854
+ geojson: getGeoJSON
41855
+ });
41856
+
41857
+ function getGeoJSON() {
41858
+ var features = exportLayerAsGeoJSON(lyr, target.dataset, {rfc7946: true}, true);
41859
+ return {
41860
+ type: 'FeatureCollection',
41861
+ features: features
41862
+ };
41863
+ }
41864
+
41865
+ return data;
41866
+ }
41867
+
41868
+ // Support for evaluating expressions embedded in curly-brace templates
41869
+
41870
+ // Returns: a string (e.g. a command string used by the -run command)
41871
+ async function evalTemplateExpression(expression, targets, ctx) {
41872
+ ctx = ctx || getBaseContext();
41873
+ // TODO: throw an error if target is used when there are multiple targets
41874
+ if (targets && targets.length == 1) {
41875
+ Object.defineProperty(ctx, 'target', {value: getTargetProxy(targets[0])});
41876
+ }
41877
+ // Add global functions and data to the expression context
41878
+ // (e.g. functions imported via the -require command)
41879
+ var globals = getStashedVar('defs') || {};
41880
+ ctx.global = globals;
41881
+ utils.extend(ctx, ctx.global);
41882
+
41883
+ var output = await compileTemplate(expression, ctx);
41884
+ if (hasFunctionCall(output, ctx)) {
41885
+ // also evaluate function calls that are not enclosed in curly braces
41886
+ // (convenience syntax)
41887
+ output = await evalExpression(output, ctx);
41888
+ }
41889
+ return output;
41890
+ }
41891
+
41892
+ async function compileTemplate(template, ctx) {
41893
+ var subExpressions = parseTemplate(template);
41894
+ var promises = subExpressions.map(expr => evalExpression(expr, ctx));
41895
+ var replacements = await Promise.all(promises);
41896
+ return applyReplacements(template, replacements);
41897
+ }
41898
+
41899
+ async function evalExpression(expression, ctx) {
41900
+ var output;
41901
+ try {
41902
+ output = Function('ctx', 'with(ctx) {return (' + expression + ');}').call({}, ctx);
41903
+ } catch(e) {
41904
+ stop(e.name, 'in JS source:', e.message);
41905
+ }
41906
+ return output;
41907
+ }
41908
+
41909
+ // Returns array of 0 or more embedded curly-brace expressions
41910
+ function parseTemplate(str) {
41911
+ var arr = [];
41912
+ parseTemplateParts(str).forEach(function(s, i) {
41913
+ if (i % 2 == 1) {
41914
+ arr.push(s.substring(1, s.length-1)); // remove braces
41915
+ }
41916
+ });
41917
+ return arr;
41918
+ }
41919
+
41920
+ // template: template string
41921
+ // replacements: array of strings or values that can be coerced to strings
41922
+ function applyReplacements(template, replacements) {
41923
+ var parts = parseTemplateParts(template);
41924
+ return parts.reduce(function(memo, s, i) {
41925
+ return i % 2 == 1 ? memo + (replacements.shift() || '') : memo + s;
41926
+ }, '');
41927
+ }
41928
+
41929
+ // Divides a string into substrings; even-index strings contain literal strings,
41930
+ // Odd-indexed strings contain curly-brace-delimited template expressions.
41931
+ // JSON objects are treated as literal strings; other top-level curly braces are
41932
+ // assumed to be embedded expressions.
41933
+ // For example: parseTemplateParts('{"hello"}, world!') => ['', '{"hello"}', ', world!']
41934
+ //
41935
+ function parseTemplateParts(str) {
41936
+ // TODO: consider adding \ escapes
41937
+ var depth=0;
41938
+ var parts = [];
41939
+ var part = '';
41940
+ var c;
41941
+
41942
+ for (var i=0, n=str.length; i<n; i++) {
41943
+ c = str.charAt(i);
41944
+ if (c == '{') {
41945
+ if (depth == 0) {
41946
+ parts.push(part);
41947
+ part = '';
41948
+ }
41949
+ depth++;
41950
+ }
41951
+ part += c;
41952
+ if (c == '}') {
41953
+ depth--;
41954
+ if (depth < 0) {
41955
+ // unexpected... throw an error?
41956
+ depth++;
41957
+ } else if (depth == 0 && isValidJSON(part)) {
41958
+ // embedded JSON objects are not template parts -- undo
41959
+ part = parts.pop() + part;
41960
+ } else if (depth == 0) {
41961
+ parts.push(part);
41962
+ part = '';
41963
+ }
41964
+ }
41965
+ }
41966
+ parts.push(part);
41967
+ return parts;
41968
+ }
41969
+
41970
+ // Tests if an expression string contains a call to an indexed function
41971
+ // defs: Object containing functions indexed by function name
41972
+ function hasFunctionCall(str, defs) {
41973
+ var rxp = /([$_a-z][$_a-z0-9]*)\(/ig;
41974
+ return Array.from(str.matchAll(rxp)).some(match => match[1] in defs);
41975
+ }
41976
+
41977
+ function isValidJSON(str) {
41978
+ try {
41979
+ JSON.parse(str);
41980
+ } catch(e) {
41981
+ return false;
41982
+ }
41983
+ return true;
41984
+ }
41985
+
41986
+ cmd.require = async function(targets, opts) {
41987
+ var defs = getStashedVar('defs');
41988
+ var moduleFile, moduleName, mod;
41989
+ if (!opts.module) {
41990
+ stop("Missing module name or path to module");
41991
+ }
41992
+ if (cli.isFile(opts.module)) {
41993
+ moduleFile = opts.module;
41994
+ } else if (cli.isFile(opts.module + '.js')) {
41995
+ moduleFile = opts.module + '.js';
41996
+ } else {
41997
+ moduleName = opts.module;
41998
+ }
41999
+ if (moduleFile && !require$1('path').isAbsolute(moduleFile)) {
42000
+ moduleFile = require$1('path').join(process.cwd(), moduleFile);
42001
+ }
42002
+ try {
42003
+ mod = require$1(moduleFile || moduleName);
42004
+ if (typeof mod == 'function') {
42005
+ // -require now includes the functionality of the old -external command
42006
+ var retn = mod(api);
42007
+ if (retn && isValidExternalCommand(retn)) {
42008
+ cmd.registerCommand(retn.name, retn);
42009
+ }
42010
+ }
42011
+ } catch(e) {
42012
+ stop('Unable to load external module:', e.message, getErrorDetail(e));
42013
+ }
42014
+ if (moduleName || opts.alias) {
42015
+ defs[opts.alias || moduleName] = mod;
42016
+ } else {
42017
+ Object.assign(defs, mod);
42018
+ }
42019
+ if (opts.init) {
42020
+ await evalTemplateExpression(opts.init, targets);
42021
+ }
42022
+ };
42023
+
41845
42024
  // Parse an array or a string of command line tokens into an array of
41846
42025
  // command objects.
41847
42026
  function parseCommands(tokens) {
@@ -41894,29 +42073,6 @@ ${svg}
41894
42073
  parseConsoleCommands: parseConsoleCommands
41895
42074
  });
41896
42075
 
41897
- // import { importGeoJSON } from '../geojson/geojson-import';
41898
-
41899
- function getTargetProxy(target) {
41900
- var lyr = target.layers[0];
41901
- var data = getLayerInfo(lyr, target.dataset); // layer_name, feature_count etc
41902
- data.layer = lyr;
41903
- data.dataset = target.dataset;
41904
- addGetters(data, {
41905
- // export as an object, not a string or buffer
41906
- geojson: getGeoJSON
41907
- });
41908
-
41909
- function getGeoJSON() {
41910
- var features = exportLayerAsGeoJSON(lyr, target.dataset, {rfc7946: true}, true);
41911
- return {
41912
- type: 'FeatureCollection',
41913
- features: features
41914
- };
41915
- }
41916
-
41917
- return data;
41918
- }
41919
-
41920
42076
  function getIOProxy(job) {
41921
42077
  var obj = {
41922
42078
  _cache: {}
@@ -41955,20 +42111,14 @@ ${svg}
41955
42111
  // }
41956
42112
 
41957
42113
  cmd.run = async function(job, targets, opts) {
41958
- var tmp, commands;
42114
+ var tmp, commands, ctx;
41959
42115
  if (!opts.expression) {
41960
42116
  stop("Missing expression parameter");
41961
42117
  }
41962
-
42118
+ ctx = getBaseContext();
41963
42119
  // io proxy adds ability to add datasets dynamically in a required function
41964
- var ctx = getBaseContext();
41965
42120
  ctx.io = getIOProxy();
41966
- tmp = runGlobalExpression(opts.expression, targets, ctx);
41967
-
41968
- // Support async functions as expressions
41969
- if (utils.isPromise(tmp)) {
41970
- tmp = await tmp;
41971
- }
42121
+ tmp = await evalTemplateExpression(opts.expression, targets, ctx);
41972
42122
  if (tmp && !utils.isString(tmp)) {
41973
42123
  stop('Expected a string containing mapshaper commands; received:', tmp);
41974
42124
  }
@@ -41987,63 +42137,6 @@ ${svg}
41987
42137
  }
41988
42138
  };
41989
42139
 
41990
- // This could return a Promise or a value or nothing
41991
- function runGlobalExpression(expression, targets, ctx) {
41992
- ctx = ctx || getBaseContext();
41993
- var output;
41994
- // TODO: throw an informative error if target is used when there are multiple targets
41995
- if (targets && targets.length == 1) {
41996
- Object.defineProperty(ctx, 'target', {value: getTargetProxy(targets[0])});
41997
- }
41998
- // Add defined functions and data to the expression context
41999
- // (Such as functions imported via the -require command)
42000
- utils.extend(ctx, getStashedVar('defs'));
42001
- try {
42002
- output = Function('ctx', 'with(ctx) {return (' + expression + ');}').call({}, ctx);
42003
- } catch(e) {
42004
- stop(e.name, 'in JS source:', e.message);
42005
- }
42006
- return output;
42007
- }
42008
-
42009
- cmd.require = function(targets, opts) {
42010
- var defs = getStashedVar('defs');
42011
- var moduleFile, moduleName, mod;
42012
- if (!opts.module) {
42013
- stop("Missing module name or path to module");
42014
- }
42015
- if (cli.isFile(opts.module)) {
42016
- moduleFile = opts.module;
42017
- } else if (cli.isFile(opts.module + '.js')) {
42018
- moduleFile = opts.module + '.js';
42019
- } else {
42020
- moduleName = opts.module;
42021
- }
42022
- if (moduleFile && !require$1('path').isAbsolute(moduleFile)) {
42023
- moduleFile = require$1('path').join(process.cwd(), moduleFile);
42024
- }
42025
- try {
42026
- mod = require$1(moduleFile || moduleName);
42027
- if (typeof mod == 'function') {
42028
- // -require now includes the functionality of the old -external command
42029
- var retn = mod(api);
42030
- if (retn && isValidExternalCommand(retn)) {
42031
- cmd.registerCommand(retn.name, retn);
42032
- }
42033
- }
42034
- } catch(e) {
42035
- stop('Unable to load external module:', e.message, getErrorDetail(e));
42036
- }
42037
- if (moduleName || opts.alias) {
42038
- defs[opts.alias || moduleName] = mod;
42039
- } else {
42040
- Object.assign(defs, mod);
42041
- }
42042
- if (opts.init) {
42043
- runGlobalExpression(opts.init, targets);
42044
- }
42045
- };
42046
-
42047
42140
  cmd.shape = function(targetDataset, opts) {
42048
42141
  var geojson, dataset;
42049
42142
  if (opts.coordinates) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mapshaper",
3
- "version": "0.6.53",
3
+ "version": "0.6.55",
4
4
  "description": "A tool for editing vector datasets for mapping and GIS.",
5
5
  "keywords": [
6
6
  "shapefile",
package/www/index.html CHANGED
@@ -276,27 +276,11 @@ a smoother appearance.</div></div></div></div>
276
276
 
277
277
  <div class="option-menu">
278
278
 
279
- <div id="path-import-options">
280
- <h4>Options</h4>
281
-
282
- <div><label for="repair-intersections-opt"><input type="checkbox" checked class="checkbox" id="repair-intersections-opt"/>detect line intersections</label>
283
- <div class="tip-button">?<div class="tip-anchor">
284
-
285
- <div class="tip">Detect line intersections, including
286
- self-intersections, to help identify
287
- topological errors in a dataset.</div></div></div></div>
288
-
289
- <!-- <div><label for="snap-points-opt"><input type="checkbox" class="checkbox" id="snap-points-opt" />snap vertices</label>
290
- <div class="tip-button">?<div class="tip-anchor">
291
- <div class="tip">Fix topology errors by snapping
292
- together points with nearly identical
293
- coordinates. This option does not
294
- apply to TopoJSON files.</div></div></div></div> -->
295
- <div style="height:5px"></div>
296
-
279
+ <div id="path-import-options">
280
+ <h4>Options</h4>
297
281
  </div>
298
282
 
299
- <div><input type="text" class="text-input advanced-options" placeholder="import options" />
283
+ <div><input type="text" class="text-input advanced-options" placeholder="command line import options" />
300
284
  <a href="https://github.com/mbloch/mapshaper/wiki/Command-Reference#-i-input" target="_mapshaper_import_docs">
301
285
  <div class="tip-button">?<div class="tip-anchor">
302
286
  <div class="tip">Enter options from the command line
@@ -324,8 +308,10 @@ encoding=big5</span>. Click to see all options.</div></div></div></div>
324
308
  <div class="mshp-main-map main-area map-area">
325
309
  <div class="coordinate-info colored-text selectable"></div>
326
310
  <div class="intersection-display">
327
- <div class="intersection-count">0 line intersections</div>
311
+ <span class="intersection-check text-btn colored-text">Check line intersections</span><span class="intersection-count">0 line intersections</span>
312
+ <img class="close-btn" src="images/close.png">
328
313
  <div class="repair-btn text-btn colored-text">Repair</div>
314
+
329
315
  </div>
330
316
  <div class="map-layers"></div>
331
317
  <div class="basemap-container"><div class="basemap"></div></div>
@@ -654,8 +654,9 @@
654
654
  },
655
655
 
656
656
  show: function(css) {
657
+ var tag = this.el && this.el.tagName;
657
658
  if (!this.visible()) {
658
- this.css('display:block;');
659
+ this.css('display', tag == 'SPAN' ? 'inline-block' : 'block');
659
660
  this._hidden = false;
660
661
  }
661
662
  return this;
@@ -2101,8 +2102,6 @@
2101
2102
  } else {
2102
2103
  var freeform = El('#import-options .advanced-options').node().value;
2103
2104
  importOpts = GUI.parseFreeformOptions(freeform, 'i');
2104
- importOpts.no_repair = !El("#repair-intersections-opt").node().checked;
2105
- // importOpts.snap = !!El("#snap-points-opt").node().checked;
2106
2105
  }
2107
2106
  return importOpts;
2108
2107
  }
@@ -3495,117 +3494,214 @@
3495
3494
 
3496
3495
  }
3497
3496
 
3497
+ // Test if map should be re-framed to show updated layer
3498
+ function mapNeedsReset(newBounds, prevBounds, viewportBounds, flags) {
3499
+ var viewportPct = getIntersectionPct(newBounds, viewportBounds);
3500
+ var contentPct = getIntersectionPct(viewportBounds, newBounds);
3501
+ var boundsChanged = !prevBounds.equals(newBounds);
3502
+ var inView = newBounds.intersects(viewportBounds);
3503
+ var areaChg = newBounds.area() / prevBounds.area();
3504
+ var chgThreshold = flags.proj ? 1e3 : 1e8;
3505
+ // don't reset if layer extent hasn't changed
3506
+ if (!boundsChanged) return false;
3507
+ // reset if layer is out-of-view
3508
+ if (!inView) return true;
3509
+ // reset if content is mostly offscreen
3510
+ if (viewportPct < 0.3 && contentPct < 0.9) return true;
3511
+ // reset if content bounds have changed a lot (e.g. after projection)
3512
+ if (areaChg > chgThreshold || areaChg < 1/chgThreshold) return true;
3513
+ return false;
3514
+ }
3515
+
3516
+ // Test if an update may have affected the visible shape of arcs
3517
+ // @flags Flags from update event
3518
+ function arcsMayHaveChanged(flags) {
3519
+ return flags.simplify_method || flags.simplify || flags.proj ||
3520
+ flags.arc_count || flags.repair || flags.clip || flags.erase ||
3521
+ flags.slice || flags.affine || flags.rectangle || flags.buffer ||
3522
+ flags.union || flags.mosaic || flags.snap || flags.clean || flags.drop || false;
3523
+ }
3524
+
3525
+ // check for operations that may change the number of self intersections in the
3526
+ // target layer.
3527
+ function intersectionsMayHaveChanged(flags) {
3528
+ return arcsMayHaveChanged(flags) || flags.select || flags['merge-layers'] ||
3529
+ flags.filter || flags.dissolve || flags.dissolve2;
3530
+ }
3531
+
3532
+ // Test if an update allows hover popup to stay open
3533
+ function popupCanStayOpen(flags) {
3534
+ // keeping popup open after -drop geometry causes problems...
3535
+ // // if (arcsMayHaveChanged(flags)) return false;
3536
+ if (arcsMayHaveChanged(flags)) return false;
3537
+ if (flags.points || flags.proj) return false;
3538
+ if (!flags.same_table) return false;
3539
+ return true;
3540
+ }
3541
+
3542
+ // Returns proportion of bb2 occupied by bb1
3543
+ function getIntersectionPct(bb1, bb2) {
3544
+ return getBoundsIntersection(bb1, bb2).area() / bb2.area() || 0;
3545
+ }
3546
+
3547
+ function getBoundsIntersection(a, b) {
3548
+ var c = new Bounds();
3549
+ if (a.intersects(b)) {
3550
+ c.setBounds(Math.max(a.xmin, b.xmin), Math.max(a.ymin, b.ymin),
3551
+ Math.min(a.xmax, b.xmax), Math.min(a.ymax, b.ymax));
3552
+ }
3553
+ return c;
3554
+ }
3555
+
3498
3556
  function RepairControl(gui) {
3499
3557
  var map = gui.map,
3500
3558
  model = gui.model,
3501
3559
  el = gui.container.findChild(".intersection-display"),
3502
3560
  readout = el.findChild(".intersection-count"),
3561
+ checkBtn = el.findChild(".intersection-check"),
3503
3562
  repairBtn = el.findChild(".repair-btn"),
3504
- // keeping a reference to current arcs and intersections, so intersections
3505
- // don't need to be recalculated when 'repair' button is pressed.
3506
- _currArcs,
3507
- _currLayer,
3508
- _currXX;
3563
+ _simplifiedXX, // saved simplified intersections, for repair
3564
+ _unsimplifiedXX, // saved unsimplified intersection data, for performance
3565
+ _disabled = false,
3566
+ _on = false;
3509
3567
 
3510
- gui.on('simplify_drag_start', hide);
3511
- gui.on('simplify_drag_end', updateAsync);
3568
+ el.findChild('.close-btn').on('click', dismissForever);
3512
3569
 
3513
- model.on('update', function(e) {
3514
- var flags = e.flags;
3515
- var intersectionsMayHaveChanged = flags.simplify || flags.proj ||
3516
- flags.arc_count || flags.snap || flags.affine || flags.points || flags['merge-layers'];
3517
- if (intersectionsMayHaveChanged) {
3518
- // delete any cached intersection data, to trigger re-calculation
3519
- e.dataset._intersections = null;
3520
- updateAsync();
3521
- } else if (flags.select) {
3522
- // new active layer, but no editing commands were run -- use cached intersections (if available)
3523
- updateAsync();
3570
+ gui.on('simplify_drag_start', function() {
3571
+ if (intersectionsAreOn()) {
3572
+ hide();
3524
3573
  }
3525
3574
  });
3526
3575
 
3576
+ gui.on('simplify_drag_end', function() {
3577
+ updateSync('simplify_drag_end');
3578
+ });
3579
+
3580
+ checkBtn.on('click', function() {
3581
+ checkBtn.hide();
3582
+ _on = true;
3583
+ updateSync();
3584
+ });
3585
+
3527
3586
  repairBtn.on('click', function() {
3528
- _currXX = internal.repairIntersections(_currArcs, _currXX);
3529
- showIntersections();
3530
- repairBtn.addClass('disabled');
3587
+ var e = model.getActiveLayer();
3588
+ if (!_simplifiedXX || !e.dataset.arcs) return;
3589
+ _simplifiedXX = internal.repairIntersections(e.dataset.arcs, _simplifiedXX);
3590
+ showIntersections(_simplifiedXX, e.layer, e.dataset.arcs);
3591
+ repairBtn.hide();
3531
3592
  model.updated({repair: true});
3532
3593
  gui.session.simplificationRepair();
3533
3594
  });
3534
3595
 
3535
- function hide() {
3536
- el.hide();
3537
- map.setIntersectionLayer(null);
3596
+ model.on('update', function(e) {
3597
+ if (!intersectionsAreOn()) {
3598
+ reset(); // need this?
3599
+ return;
3600
+ }
3601
+ var needRefresh = e.flags.simplify_method || e.flags.simplify ||
3602
+ e.flags.repair || e.flags.clean;
3603
+ if (needRefresh) {
3604
+ updateAsync();
3605
+ } else if (e.flags.simplify_amount) {
3606
+ // slider is being dragged - hide readout and dots, retain data
3607
+ hide();
3608
+ } else if (intersectionsMayHaveChanged(e.flags)) {
3609
+ // intersections may have changed -- reset the display
3610
+ reset();
3611
+ } else {
3612
+ // keep displaying the current intersections
3613
+ }
3614
+ });
3615
+
3616
+ function intersectionsAreOn() {
3617
+ return _on && !_disabled;
3538
3618
  }
3539
3619
 
3540
- function enabledForDataset(dataset) {
3541
- var info = dataset.info || {};
3542
- var opts = info.import_options || {};
3543
- return !opts.no_repair && !info.no_intersections;
3620
+ function turnOff() {
3621
+ hide();
3622
+ _on = false;
3623
+ _simplifiedXX = null;
3624
+ _unsimplifiedXX = null;
3544
3625
  }
3545
3626
 
3546
- // Delay intersection calculation, so map can redraw after previous
3547
- // operation (e.g. layer load, simplification change)
3627
+ function reset() {
3628
+ turnOff();
3629
+ if (_disabled) {
3630
+ return;
3631
+ }
3632
+ var e = model.getActiveLayer();
3633
+ if (internal.layerHasPaths(e.layer)) {
3634
+ el.show();
3635
+ checkBtn.show();
3636
+ readout.hide();
3637
+ repairBtn.hide();
3638
+ }
3639
+ }
3640
+
3641
+ function dismissForever() {
3642
+ _disabled = true;
3643
+ turnOff();
3644
+ }
3645
+
3646
+ function hide() {
3647
+ map.setIntersectionLayer(null);
3648
+ el.hide();
3649
+ }
3650
+
3651
+ // Update intersection display, after a short delay so map can redraw after previous
3652
+ // operation (e.g. simplification change)
3548
3653
  function updateAsync() {
3549
- reset();
3550
3654
  setTimeout(updateSync, 10);
3551
3655
  }
3552
3656
 
3553
- function updateSync() {
3657
+ function updateSync(action) {
3658
+ if (!intersectionsAreOn()) return;
3554
3659
  var e = model.getActiveLayer();
3555
- var dataset = e.dataset;
3556
- var arcs = dataset && dataset.arcs;
3557
- var XX, showBtn;
3558
- var opts = {
3660
+ var arcs = e.dataset && e.dataset.arcs;
3661
+ var intersectionOpts = {
3559
3662
  unique: true,
3560
3663
  tolerance: 0
3561
3664
  };
3562
- if (!arcs || !internal.layerHasPaths(e.layer) || !enabledForDataset(dataset)) return;
3665
+ if (!arcs || !internal.layerHasPaths(e.layer)) {
3666
+ return;
3667
+ }
3563
3668
  if (arcs.getRetainedInterval() > 0) {
3564
- // TODO: cache these intersections
3565
- XX = internal.findSegmentIntersections(arcs, opts);
3566
- showBtn = XX.length > 0;
3567
- } else { // no simplification
3568
- XX = dataset._intersections;
3569
- if (!XX) {
3570
- // cache intersections at 0 simplification, to avoid recalculating
3571
- // every time the simplification slider is set to 100% or the layer is selected at 100%
3572
- XX = dataset._intersections = internal.findSegmentIntersections(arcs, opts);
3573
- }
3574
- showBtn = false;
3669
+ // simplification
3670
+ _simplifiedXX = internal.findSegmentIntersections(arcs, intersectionOpts);
3671
+ } else {
3672
+ // no simplification
3673
+ _simplifiedXX = null; // clear any old simplified XX
3674
+ if (_unsimplifiedXX && action == 'simplify_drag_end') {
3675
+ // re-use previously generated intersection data (optimization)
3676
+ } else {
3677
+ _unsimplifiedXX = internal.findSegmentIntersections(arcs, intersectionOpts);
3678
+ }
3575
3679
  }
3576
- el.show();
3577
- _currLayer = e.layer;
3578
- _currArcs = arcs;
3579
- _currXX = XX;
3580
- showIntersections();
3581
- repairBtn.classed('disabled', !showBtn);
3680
+ showIntersections(_simplifiedXX || _unsimplifiedXX, e.layer, arcs);
3582
3681
  }
3583
3682
 
3584
- function reset() {
3585
- _currArcs = null;
3586
- _currXX = null;
3587
- _currLayer = null;
3588
- hide();
3589
- }
3590
-
3591
- function dismiss() {
3592
- var dataset = model.getActiveLayer().dataset;
3593
- dataset._intersections = null;
3594
- dataset.info.no_intersections = true;
3595
- reset();
3596
- }
3597
-
3598
- function showIntersections() {
3599
- var n = _currXX.length, pointLyr;
3600
- if (n > 0) {
3601
- // console.log("first intersection:", internal.getIntersectionDebugData(XX[0], arcs));
3602
- pointLyr = internal.getIntersectionLayer(_currXX, _currLayer, _currArcs);
3683
+ function showIntersections(xx, lyr, arcs) {
3684
+ var pointLyr, count = 0, html;
3685
+ el.show();
3686
+ readout.show();
3687
+ checkBtn.hide();
3688
+ if (xx.length > 0) {
3689
+ pointLyr = internal.getIntersectionLayer(xx, lyr, arcs);
3690
+ count = internal.countPointsInLayer(pointLyr);
3691
+ }
3692
+ if (count == 0) {
3693
+ map.setIntersectionLayer(null);
3694
+ html = '<span class="icon black"></span>No self-intersections';
3695
+ } else {
3603
3696
  map.setIntersectionLayer(pointLyr, {layers:[pointLyr]});
3604
- readout.html(utils$1.format('<span class="icon"></span>%s line intersection%s <img class="close-btn" src="images/close.png">', n, utils$1.pluralSuffix(n)));
3605
- readout.findChild('.close-btn').on('click', dismiss);
3697
+ html = utils$1.format('<span class="icon"></span>%s line intersection%s', count, utils$1.pluralSuffix(count));
3698
+ }
3699
+ readout.html(html);
3700
+
3701
+ if (_simplifiedXX && count > 0) {
3702
+ repairBtn.show();
3606
3703
  } else {
3607
- map.setIntersectionLayer(null);
3608
- readout.html('');
3704
+ repairBtn.hide();
3609
3705
  }
3610
3706
  }
3611
3707
  }
@@ -8472,39 +8568,6 @@
8472
8568
  return _self;
8473
8569
  }
8474
8570
 
8475
- // Test if map should be re-framed to show updated layer
8476
- function mapNeedsReset(newBounds, prevBounds, viewportBounds, flags) {
8477
- var viewportPct = getIntersectionPct(newBounds, viewportBounds);
8478
- var contentPct = getIntersectionPct(viewportBounds, newBounds);
8479
- var boundsChanged = !prevBounds.equals(newBounds);
8480
- var inView = newBounds.intersects(viewportBounds);
8481
- var areaChg = newBounds.area() / prevBounds.area();
8482
- var chgThreshold = flags.proj ? 1e3 : 1e8;
8483
- // don't reset if layer extent hasn't changed
8484
- if (!boundsChanged) return false;
8485
- // reset if layer is out-of-view
8486
- if (!inView) return true;
8487
- // reset if content is mostly offscreen
8488
- if (viewportPct < 0.3 && contentPct < 0.9) return true;
8489
- // reset if content bounds have changed a lot (e.g. after projection)
8490
- if (areaChg > chgThreshold || areaChg < 1/chgThreshold) return true;
8491
- return false;
8492
- }
8493
-
8494
- // Returns proportion of bb2 occupied by bb1
8495
- function getIntersectionPct(bb1, bb2) {
8496
- return getBoundsIntersection(bb1, bb2).area() / bb2.area() || 0;
8497
- }
8498
-
8499
- function getBoundsIntersection(a, b) {
8500
- var c = new Bounds();
8501
- if (a.intersects(b)) {
8502
- c.setBounds(Math.max(a.xmin, b.xmin), Math.max(a.ymin, b.ymin),
8503
- Math.min(a.xmax, b.xmax), Math.min(a.ymax, b.ymax));
8504
- }
8505
- return c;
8506
- }
8507
-
8508
8571
  function isMultilineLabel(textNode) {
8509
8572
  return textNode.childNodes.length > 1;
8510
8573
  }
@@ -11297,25 +11360,6 @@
11297
11360
  };
11298
11361
  }
11299
11362
 
11300
- // Test if an update may have affected the visible shape of arcs
11301
- // @flags Flags from update event
11302
- function arcsMayHaveChanged(flags) {
11303
- return flags.simplify_method || flags.simplify || flags.proj ||
11304
- flags.arc_count || flags.repair || flags.clip || flags.erase ||
11305
- flags.slice || flags.affine || flags.rectangle || flags.buffer ||
11306
- flags.union || flags.mosaic || flags.snap || flags.clean || flags.drop || false;
11307
- }
11308
-
11309
- // Test if an update allows hover popup to stay open
11310
- function popupCanStayOpen(flags) {
11311
- // keeping popup open after -drop geometry causes problems...
11312
- // // if (arcsMayHaveChanged(flags)) return false;
11313
- if (arcsMayHaveChanged(flags)) return false;
11314
- if (flags.points || flags.proj) return false;
11315
- if (!flags.same_table) return false;
11316
- return true;
11317
- }
11318
-
11319
11363
 
11320
11364
  // Update map frame after user navigates the map in frame edit mode
11321
11365
  function updateFrameExtent() {
package/www/mapshaper.js CHANGED
@@ -1,6 +1,6 @@
1
1
  (function () {
2
2
 
3
- var VERSION = "0.6.53";
3
+ var VERSION = "0.6.55";
4
4
 
5
5
 
6
6
  var utils = /*#__PURE__*/Object.freeze({
@@ -11075,7 +11075,7 @@
11075
11075
  }
11076
11076
 
11077
11077
  function isGzipFile(file) {
11078
- return /\.gz/i.test(file);
11078
+ return /\.gz$/i.test(file);
11079
11079
  }
11080
11080
 
11081
11081
  function isSupportedOutputFormat(fmt) {
@@ -25338,7 +25338,7 @@ ${svg}
25338
25338
  .describe('create commands on-the-fly and run them')
25339
25339
  .option('expression', {
25340
25340
  DEFAULT: true,
25341
- describe: 'JS expression to generate command(s)'
25341
+ describe: 'JS expression or template to generate command(s)'
25342
25342
  })
25343
25343
  // deprecated
25344
25344
  .option('commands', {alias_to: 'expression'})
@@ -41842,6 +41842,185 @@ ${svg}
41842
41842
  }, {});
41843
41843
  }
41844
41844
 
41845
+ // import { importGeoJSON } from '../geojson/geojson-import';
41846
+
41847
+ function getTargetProxy(target) {
41848
+ var lyr = target.layers[0];
41849
+ var data = getLayerInfo(lyr, target.dataset); // layer_name, feature_count etc
41850
+ data.layer = lyr;
41851
+ data.dataset = target.dataset;
41852
+ addGetters(data, {
41853
+ // export as an object, not a string or buffer
41854
+ geojson: getGeoJSON
41855
+ });
41856
+
41857
+ function getGeoJSON() {
41858
+ var features = exportLayerAsGeoJSON(lyr, target.dataset, {rfc7946: true}, true);
41859
+ return {
41860
+ type: 'FeatureCollection',
41861
+ features: features
41862
+ };
41863
+ }
41864
+
41865
+ return data;
41866
+ }
41867
+
41868
+ // Support for evaluating expressions embedded in curly-brace templates
41869
+
41870
+ // Returns: a string (e.g. a command string used by the -run command)
41871
+ async function evalTemplateExpression(expression, targets, ctx) {
41872
+ ctx = ctx || getBaseContext();
41873
+ // TODO: throw an error if target is used when there are multiple targets
41874
+ if (targets && targets.length == 1) {
41875
+ Object.defineProperty(ctx, 'target', {value: getTargetProxy(targets[0])});
41876
+ }
41877
+ // Add global functions and data to the expression context
41878
+ // (e.g. functions imported via the -require command)
41879
+ var globals = getStashedVar('defs') || {};
41880
+ ctx.global = globals;
41881
+ utils.extend(ctx, ctx.global);
41882
+
41883
+ var output = await compileTemplate(expression, ctx);
41884
+ if (hasFunctionCall(output, ctx)) {
41885
+ // also evaluate function calls that are not enclosed in curly braces
41886
+ // (convenience syntax)
41887
+ output = await evalExpression(output, ctx);
41888
+ }
41889
+ return output;
41890
+ }
41891
+
41892
+ async function compileTemplate(template, ctx) {
41893
+ var subExpressions = parseTemplate(template);
41894
+ var promises = subExpressions.map(expr => evalExpression(expr, ctx));
41895
+ var replacements = await Promise.all(promises);
41896
+ return applyReplacements(template, replacements);
41897
+ }
41898
+
41899
+ async function evalExpression(expression, ctx) {
41900
+ var output;
41901
+ try {
41902
+ output = Function('ctx', 'with(ctx) {return (' + expression + ');}').call({}, ctx);
41903
+ } catch(e) {
41904
+ stop(e.name, 'in JS source:', e.message);
41905
+ }
41906
+ return output;
41907
+ }
41908
+
41909
+ // Returns array of 0 or more embedded curly-brace expressions
41910
+ function parseTemplate(str) {
41911
+ var arr = [];
41912
+ parseTemplateParts(str).forEach(function(s, i) {
41913
+ if (i % 2 == 1) {
41914
+ arr.push(s.substring(1, s.length-1)); // remove braces
41915
+ }
41916
+ });
41917
+ return arr;
41918
+ }
41919
+
41920
+ // template: template string
41921
+ // replacements: array of strings or values that can be coerced to strings
41922
+ function applyReplacements(template, replacements) {
41923
+ var parts = parseTemplateParts(template);
41924
+ return parts.reduce(function(memo, s, i) {
41925
+ return i % 2 == 1 ? memo + (replacements.shift() || '') : memo + s;
41926
+ }, '');
41927
+ }
41928
+
41929
+ // Divides a string into substrings; even-index strings contain literal strings,
41930
+ // Odd-indexed strings contain curly-brace-delimited template expressions.
41931
+ // JSON objects are treated as literal strings; other top-level curly braces are
41932
+ // assumed to be embedded expressions.
41933
+ // For example: parseTemplateParts('{"hello"}, world!') => ['', '{"hello"}', ', world!']
41934
+ //
41935
+ function parseTemplateParts(str) {
41936
+ // TODO: consider adding \ escapes
41937
+ var depth=0;
41938
+ var parts = [];
41939
+ var part = '';
41940
+ var c;
41941
+
41942
+ for (var i=0, n=str.length; i<n; i++) {
41943
+ c = str.charAt(i);
41944
+ if (c == '{') {
41945
+ if (depth == 0) {
41946
+ parts.push(part);
41947
+ part = '';
41948
+ }
41949
+ depth++;
41950
+ }
41951
+ part += c;
41952
+ if (c == '}') {
41953
+ depth--;
41954
+ if (depth < 0) {
41955
+ // unexpected... throw an error?
41956
+ depth++;
41957
+ } else if (depth == 0 && isValidJSON(part)) {
41958
+ // embedded JSON objects are not template parts -- undo
41959
+ part = parts.pop() + part;
41960
+ } else if (depth == 0) {
41961
+ parts.push(part);
41962
+ part = '';
41963
+ }
41964
+ }
41965
+ }
41966
+ parts.push(part);
41967
+ return parts;
41968
+ }
41969
+
41970
+ // Tests if an expression string contains a call to an indexed function
41971
+ // defs: Object containing functions indexed by function name
41972
+ function hasFunctionCall(str, defs) {
41973
+ var rxp = /([$_a-z][$_a-z0-9]*)\(/ig;
41974
+ return Array.from(str.matchAll(rxp)).some(match => match[1] in defs);
41975
+ }
41976
+
41977
+ function isValidJSON(str) {
41978
+ try {
41979
+ JSON.parse(str);
41980
+ } catch(e) {
41981
+ return false;
41982
+ }
41983
+ return true;
41984
+ }
41985
+
41986
+ cmd.require = async function(targets, opts) {
41987
+ var defs = getStashedVar('defs');
41988
+ var moduleFile, moduleName, mod;
41989
+ if (!opts.module) {
41990
+ stop("Missing module name or path to module");
41991
+ }
41992
+ if (cli.isFile(opts.module)) {
41993
+ moduleFile = opts.module;
41994
+ } else if (cli.isFile(opts.module + '.js')) {
41995
+ moduleFile = opts.module + '.js';
41996
+ } else {
41997
+ moduleName = opts.module;
41998
+ }
41999
+ if (moduleFile && !require$1('path').isAbsolute(moduleFile)) {
42000
+ moduleFile = require$1('path').join(process.cwd(), moduleFile);
42001
+ }
42002
+ try {
42003
+ mod = require$1(moduleFile || moduleName);
42004
+ if (typeof mod == 'function') {
42005
+ // -require now includes the functionality of the old -external command
42006
+ var retn = mod(api);
42007
+ if (retn && isValidExternalCommand(retn)) {
42008
+ cmd.registerCommand(retn.name, retn);
42009
+ }
42010
+ }
42011
+ } catch(e) {
42012
+ stop('Unable to load external module:', e.message, getErrorDetail(e));
42013
+ }
42014
+ if (moduleName || opts.alias) {
42015
+ defs[opts.alias || moduleName] = mod;
42016
+ } else {
42017
+ Object.assign(defs, mod);
42018
+ }
42019
+ if (opts.init) {
42020
+ await evalTemplateExpression(opts.init, targets);
42021
+ }
42022
+ };
42023
+
41845
42024
  // Parse an array or a string of command line tokens into an array of
41846
42025
  // command objects.
41847
42026
  function parseCommands(tokens) {
@@ -41894,29 +42073,6 @@ ${svg}
41894
42073
  parseConsoleCommands: parseConsoleCommands
41895
42074
  });
41896
42075
 
41897
- // import { importGeoJSON } from '../geojson/geojson-import';
41898
-
41899
- function getTargetProxy(target) {
41900
- var lyr = target.layers[0];
41901
- var data = getLayerInfo(lyr, target.dataset); // layer_name, feature_count etc
41902
- data.layer = lyr;
41903
- data.dataset = target.dataset;
41904
- addGetters(data, {
41905
- // export as an object, not a string or buffer
41906
- geojson: getGeoJSON
41907
- });
41908
-
41909
- function getGeoJSON() {
41910
- var features = exportLayerAsGeoJSON(lyr, target.dataset, {rfc7946: true}, true);
41911
- return {
41912
- type: 'FeatureCollection',
41913
- features: features
41914
- };
41915
- }
41916
-
41917
- return data;
41918
- }
41919
-
41920
42076
  function getIOProxy(job) {
41921
42077
  var obj = {
41922
42078
  _cache: {}
@@ -41955,20 +42111,14 @@ ${svg}
41955
42111
  // }
41956
42112
 
41957
42113
  cmd.run = async function(job, targets, opts) {
41958
- var tmp, commands;
42114
+ var tmp, commands, ctx;
41959
42115
  if (!opts.expression) {
41960
42116
  stop("Missing expression parameter");
41961
42117
  }
41962
-
42118
+ ctx = getBaseContext();
41963
42119
  // io proxy adds ability to add datasets dynamically in a required function
41964
- var ctx = getBaseContext();
41965
42120
  ctx.io = getIOProxy();
41966
- tmp = runGlobalExpression(opts.expression, targets, ctx);
41967
-
41968
- // Support async functions as expressions
41969
- if (utils.isPromise(tmp)) {
41970
- tmp = await tmp;
41971
- }
42121
+ tmp = await evalTemplateExpression(opts.expression, targets, ctx);
41972
42122
  if (tmp && !utils.isString(tmp)) {
41973
42123
  stop('Expected a string containing mapshaper commands; received:', tmp);
41974
42124
  }
@@ -41987,63 +42137,6 @@ ${svg}
41987
42137
  }
41988
42138
  };
41989
42139
 
41990
- // This could return a Promise or a value or nothing
41991
- function runGlobalExpression(expression, targets, ctx) {
41992
- ctx = ctx || getBaseContext();
41993
- var output;
41994
- // TODO: throw an informative error if target is used when there are multiple targets
41995
- if (targets && targets.length == 1) {
41996
- Object.defineProperty(ctx, 'target', {value: getTargetProxy(targets[0])});
41997
- }
41998
- // Add defined functions and data to the expression context
41999
- // (Such as functions imported via the -require command)
42000
- utils.extend(ctx, getStashedVar('defs'));
42001
- try {
42002
- output = Function('ctx', 'with(ctx) {return (' + expression + ');}').call({}, ctx);
42003
- } catch(e) {
42004
- stop(e.name, 'in JS source:', e.message);
42005
- }
42006
- return output;
42007
- }
42008
-
42009
- cmd.require = function(targets, opts) {
42010
- var defs = getStashedVar('defs');
42011
- var moduleFile, moduleName, mod;
42012
- if (!opts.module) {
42013
- stop("Missing module name or path to module");
42014
- }
42015
- if (cli.isFile(opts.module)) {
42016
- moduleFile = opts.module;
42017
- } else if (cli.isFile(opts.module + '.js')) {
42018
- moduleFile = opts.module + '.js';
42019
- } else {
42020
- moduleName = opts.module;
42021
- }
42022
- if (moduleFile && !require$1('path').isAbsolute(moduleFile)) {
42023
- moduleFile = require$1('path').join(process.cwd(), moduleFile);
42024
- }
42025
- try {
42026
- mod = require$1(moduleFile || moduleName);
42027
- if (typeof mod == 'function') {
42028
- // -require now includes the functionality of the old -external command
42029
- var retn = mod(api);
42030
- if (retn && isValidExternalCommand(retn)) {
42031
- cmd.registerCommand(retn.name, retn);
42032
- }
42033
- }
42034
- } catch(e) {
42035
- stop('Unable to load external module:', e.message, getErrorDetail(e));
42036
- }
42037
- if (moduleName || opts.alias) {
42038
- defs[opts.alias || moduleName] = mod;
42039
- } else {
42040
- Object.assign(defs, mod);
42041
- }
42042
- if (opts.init) {
42043
- runGlobalExpression(opts.init, targets);
42044
- }
42045
- };
42046
-
42047
42140
  cmd.shape = function(targetDataset, opts) {
42048
42141
  var geojson, dataset;
42049
42142
  if (opts.coordinates) {
package/www/page.css CHANGED
@@ -468,6 +468,11 @@ body.dragover #import-options-drop-area .drop-area {
468
468
  margin: 0 0 5px 0;
469
469
  }
470
470
 
471
+ ::placeholder {
472
+ color: #aaa;
473
+ opacity: 1;
474
+ }
475
+
471
476
 
472
477
  #mshp-not-supported {
473
478
  display: none;
@@ -954,8 +959,8 @@ img.close-btn:hover,
954
959
  left: 13px;
955
960
  }
956
961
 
957
- .intersection-count {
958
- vertical-align: middle;
962
+ .intersection-check {
963
+ display: none;
959
964
  }
960
965
 
961
966
  .intersection-count .icon {
@@ -966,18 +971,22 @@ img.close-btn:hover,
966
971
  margin: 0 5px 2px 0;
967
972
  }
968
973
 
969
- .intersection-count .close-btn {
974
+ .intersection-count .icon.black {
975
+ background-color: black;
976
+ }
977
+
978
+ .intersection-display .close-btn {
970
979
  width: 16px;
971
980
  height: 16px;
972
981
  cursor: pointer;
973
982
  position: relative;
974
983
  top: 4px;
975
- margin-left: 2px;
984
+ margin-left: 1px;
976
985
  }
977
986
 
978
- .intersection-display .text-btn.disabled {
987
+ /*.intersection-display .text-btn.disabled {
979
988
  visibility: hidden;
980
- }
989
+ }*/
981
990
 
982
991
  /* --- Popup -------------------- */
983
992