mapshaper 0.7.67 → 0.7.68

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.
@@ -9531,7 +9531,7 @@
9531
9531
 
9532
9532
  function Console(gui) {
9533
9533
  var model = gui.model;
9534
- var CURSOR = '$ ';
9534
+ var CURSOR = '> ';
9535
9535
  var PROMPT = 'Enter mapshaper commands or type "tips" for console help.';
9536
9536
  var el = gui.container.findChild('.console').hide();
9537
9537
  var content = el.findChild('.console-buffer');
@@ -9548,15 +9548,6 @@
9548
9548
  var history = [];
9549
9549
  var historyId = 0;
9550
9550
  var _isOpen = false;
9551
- var btn = gui.container.findChild('.console-btn')
9552
- .on('click', toggle)
9553
- .on('keydown', function(e) {
9554
- if (e.key == 'Enter' || e.key == ' ') {
9555
- e.preventDefault();
9556
- e.stopPropagation();
9557
- toggle();
9558
- }
9559
- });
9560
9551
  var globals = {}; // share user-defined globals (job.defs) between runs
9561
9552
  var sharedVars = {}; // share -vars / -defaults templating scope between runs
9562
9553
 
@@ -9588,10 +9579,6 @@
9588
9579
  }
9589
9580
  });
9590
9581
 
9591
- function toggle() {
9592
- gui.toggleSidebarPanel('console');
9593
- }
9594
-
9595
9582
  gui.on('sidebar', function(e) {
9596
9583
  if (e.panels.includes('console')) {
9597
9584
  turnOn();
@@ -9642,7 +9629,6 @@
9642
9629
  function turnOn() {
9643
9630
  // if (!_isOpen && !model.isEmpty()) {
9644
9631
  if (!_isOpen) {
9645
- btn.addClass('active').attr('aria-expanded', 'true');
9646
9632
  _isOpen = true;
9647
9633
  // Route logging output to the in-app console while it's open, so a
9648
9634
  // user typing CLI commands here sees the results inline -- the same
@@ -9663,7 +9649,6 @@
9663
9649
 
9664
9650
  function turnOff() {
9665
9651
  if (_isOpen) {
9666
- btn.removeClass('active').attr('aria-expanded', 'false');
9667
9652
  _isOpen = false;
9668
9653
  if (GUI.isActiveInstance(gui)) {
9669
9654
  setLoggingForGUI(gui); // reset stop, message and error functions
@@ -10237,14 +10222,14 @@
10237
10222
  }
10238
10223
 
10239
10224
  function printExamples() {
10240
- printExample("See a list of all console commands", "$ help");
10241
- printExample("Get help using a single command", "$ help innerlines");
10242
- printExample("Get information about imported datasets", "$ info");
10243
- printExample("Print bot/debug runtime context as JSON", "$ context");
10244
- printExample("Display browser session as shell commands", "$ history");
10245
- printExample("Delete one state from a national dataset","$ filter 'STATE != \"Alaska\"'");
10246
- printExample("Aggregate counties to states by dissolving shared edges" ,"$ dissolve 'STATE'");
10247
- printExample("Clear the console", "$ clear");
10225
+ printExample("See a list of all console commands", CURSOR + "help");
10226
+ printExample("Get help using a single command", CURSOR + "help innerlines");
10227
+ printExample("Get information about imported datasets", CURSOR + "info");
10228
+ printExample("Print bot/debug runtime context as JSON", CURSOR + "context");
10229
+ printExample("Display browser session as shell commands", CURSOR + "history");
10230
+ printExample("Delete one state from a national dataset", CURSOR + "filter 'STATE != \"Alaska\"'");
10231
+ printExample("Aggregate counties to states by dissolving shared edges" , CURSOR + "dissolve 'STATE'");
10232
+ printExample("Clear the console", CURSOR + "clear");
10248
10233
  }
10249
10234
 
10250
10235
  function renderStructuredConsoleMessage(msg) {
@@ -11219,6 +11204,92 @@
11219
11204
  };
11220
11205
  }
11221
11206
 
11207
+ // Short descriptions of what a layer holds, for the contents column of the
11208
+ // layer panel: "6 polygons", "1 data record", "12 labels".
11209
+ //
11210
+ // A point layer is described by how its features are drawn, in the terms of
11211
+ // the point style panel: labels, symbols, circles and plain points. A layer
11212
+ // that mixes them lists each kind it has, e.g. "12 labels, 30 points".
11213
+ //
11214
+ // Features with null geometry are left out of the counts, which say what is on
11215
+ // the map. The column is too narrow to list them as well, so they go in a
11216
+ // tooltip with the full description.
11217
+
11218
+ var POINT_KINDS = ['label', 'symbol', 'circle', 'point'];
11219
+
11220
+ // How a point feature is drawn, given its data record.
11221
+ // A label wins over any symbol or circle drawn with it.
11222
+ function getPointKind(d) {
11223
+ if (!d) return 'point';
11224
+ if ('label-text' in d) return 'label';
11225
+ if (d['svg-symbol'] || d.icon) return 'symbol';
11226
+ if (d.r > 0) return 'circle';
11227
+ return 'point';
11228
+ }
11229
+
11230
+ // The kind of feature a point layer is set up for, from its field names:
11231
+ // names the features of an empty layer, e.g. "0 labels".
11232
+ function getPointLayerKind(fields) {
11233
+ if (fields.includes('label-text')) return 'label';
11234
+ if (fields.includes('svg-symbol') || fields.includes('icon')) return 'symbol';
11235
+ if (fields.includes('r')) return 'circle';
11236
+ return 'point';
11237
+ }
11238
+
11239
+ // Same test as the -info command's null shape count
11240
+ function isNullShape(shp) {
11241
+ return !shp || shp.length === 0;
11242
+ }
11243
+
11244
+ // @geometryType: 'point', 'polygon' or 'polyline'
11245
+ // @shapes: the layer's shapes
11246
+ // @records: data records, or null for a layer without attribute data
11247
+ // @fields: names of the layer's data fields
11248
+ // Returns {text, nulls, title}; title is the tooltip, or null if there are no nulls.
11249
+ function describeFeatureContents(geometryType, shapes, records, fields) {
11250
+ var nulls = shapes.length - countNonNull(shapes);
11251
+ var text = geometryType == 'point' ?
11252
+ describePoints(shapes, records, fields || []) :
11253
+ formatCount(shapes.length - nulls, geometryType);
11254
+ return {
11255
+ text: text,
11256
+ nulls: nulls,
11257
+ title: nulls > 0 ? text + ', ' + formatInteger(nulls) + ' without geometry' : null
11258
+ };
11259
+ }
11260
+
11261
+ function describePoints(shapes, records, fields) {
11262
+ var counts = {label: 0, symbol: 0, circle: 0, point: 0};
11263
+ var parts, i;
11264
+ for (i=0; i<shapes.length; i++) {
11265
+ if (isNullShape(shapes[i])) continue;
11266
+ counts[getPointKind(records ? records[i] : null)]++;
11267
+ }
11268
+ parts = POINT_KINDS.filter(function(kind) {
11269
+ return counts[kind] > 0;
11270
+ }).map(function(kind) {
11271
+ return formatCount(counts[kind], kind);
11272
+ });
11273
+ return parts.length > 0 ? parts.join(', ') :
11274
+ formatCount(0, getPointLayerKind(fields));
11275
+ }
11276
+
11277
+ function countNonNull(shapes) {
11278
+ var n = 0;
11279
+ for (var i=0; i<shapes.length; i++) {
11280
+ if (!isNullShape(shapes[i])) n++;
11281
+ }
11282
+ return n;
11283
+ }
11284
+
11285
+ function formatCount(n, noun) {
11286
+ return formatInteger(n) + ' ' + noun + (n == 1 ? '' : 's');
11287
+ }
11288
+
11289
+ function formatInteger(n) {
11290
+ return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
11291
+ }
11292
+
11222
11293
  var openMenu$1;
11223
11294
  var openMenuId;
11224
11295
  // One menu element per parent element, created on demand.
@@ -11612,7 +11683,6 @@
11612
11683
  var el = gui.container.findChild(".layer-control").hide();
11613
11684
  var btn = gui.container.findChild('.layer-control-btn');
11614
11685
  var headerBtn = btn.findChild('.active-layer-label');
11615
- var tab = gui.container.findChild('.layer-tab');
11616
11686
  var isOpen = false;
11617
11687
  var cache = new DomCache();
11618
11688
  var pinAll = el.findChild('.pin-all'); // button for toggling layer visibility
@@ -11647,17 +11717,6 @@
11647
11717
  gui.toggleSidebarPanel('console');
11648
11718
  }
11649
11719
  });
11650
- tab.on('click', toggle).on('keydown', function(e) {
11651
- if (e.key == 'Enter') {
11652
- e.preventDefault();
11653
- e.stopPropagation();
11654
- toggle();
11655
- } else if (e.key == ' ') {
11656
- e.preventDefault();
11657
- e.stopPropagation();
11658
- gui.toggleSidebarPanel('console');
11659
- }
11660
- });
11661
11720
 
11662
11721
  function toggle() {
11663
11722
  gui.toggleSidebarPanel('layers');
@@ -11763,7 +11822,6 @@
11763
11822
  function turnOn() {
11764
11823
  if (isOpen) return;
11765
11824
  isOpen = true;
11766
- tab.addClass('active').attr('aria-expanded', 'true');
11767
11825
  render();
11768
11826
  el.addClass('open');
11769
11827
  el.show();
@@ -11773,7 +11831,6 @@
11773
11831
  if (!isOpen) return;
11774
11832
  stopDragging();
11775
11833
  isOpen = false;
11776
- tab.removeClass('active').attr('aria-expanded', 'false');
11777
11834
  el.removeClass('open');
11778
11835
  el.hide();
11779
11836
  }
@@ -11854,8 +11911,7 @@
11854
11911
 
11855
11912
  function renderLayer(lyr, dataset, opts) {
11856
11913
  var classes = 'layer-item';
11857
- var isFrame = internal.isFrameLayer(lyr, dataset.arcs);
11858
- var entry, html;
11914
+ var html;
11859
11915
 
11860
11916
  if (opts.pinnable) classes += ' pinnable';
11861
11917
  if (map.isActiveLayer(lyr)) classes += ' active';
@@ -11863,8 +11919,9 @@
11863
11919
  if (lyr.pinned) classes += ' pinned';
11864
11920
 
11865
11921
  html = '<!-- ' + lyr.menu_id + '--><div class="' + classes + '">';
11866
- html += rowHTML('name', '<span class="layer-name colored-text dot-underline">' + formatLayerNameForDisplay(lyr.name) + '</span>', 'row1');
11867
- html += rowHTML(isFrame ? 'size' : 'contents', describeLyr(lyr, dataset));
11922
+ html += '<div class="layer-row"><div class="layer-name-col">' +
11923
+ '<span class="layer-name colored-text dot-underline">' + formatLayerNameForDisplay(lyr.name) + '</span></div>' +
11924
+ renderContents(lyr, dataset) + '</div>';
11868
11925
  html += '<span class="more-btn layer-btn" role="button" aria-label="More layer options"></span>';
11869
11926
  if (opts.pinnable) {
11870
11927
  html += '<img class="eye-btn black-eye layer-btn" draggable="false" src="images/eye.png">';
@@ -12114,29 +12171,34 @@
12114
12171
 
12115
12172
  }
12116
12173
 
12174
+ // Returns {text, title}; title is a tooltip, or null
12117
12175
  function describeLyr(lyr, dataset) {
12118
- var n = internal.getFeatureCount(lyr),
12119
- isFrame = internal.isFrameLayer(lyr, dataset.arcs),
12120
- str, type;
12121
- if (lyr.data && !lyr.shapes) {
12122
- type = 'data record';
12123
- } else if (lyr.geometry_type) {
12124
- type = lyr.geometry_type + ' feature';
12125
- } else if (internal.layerHasRaster(lyr)) {
12126
- type = 'raster layer';
12127
- }
12128
- if (isFrame) {
12129
- str = internal.formatFrameSizeForDisplay(
12176
+ if (internal.isFrameLayer(lyr, dataset.arcs)) {
12177
+ return {text: internal.formatFrameSizeForDisplay(
12130
12178
  internal.getFrameLayerData(lyr, dataset.arcs)
12131
- );
12132
- } else if (internal.layerHasRaster(lyr)) {
12133
- str = utils$1.format('%,d x %,d %s', internal.getRasterWidth(lyr.raster), internal.getRasterHeight(lyr.raster), type);
12134
- } else if (type) {
12135
- str = utils$1.format('%,d %s%s', n, type, utils$1.pluralSuffix(n));
12136
- } else {
12137
- str = "[empty]";
12179
+ ), title: null};
12138
12180
  }
12139
- return str;
12181
+ if (internal.layerHasRaster(lyr)) {
12182
+ return {text: utils$1.format('%,d x %,d raster', internal.getRasterWidth(lyr.raster),
12183
+ internal.getRasterHeight(lyr.raster)), title: null};
12184
+ }
12185
+ if (lyr.data && !lyr.shapes) {
12186
+ return {text: formatCount(internal.getFeatureCount(lyr), 'data record'), title: null};
12187
+ }
12188
+ if (lyr.geometry_type) {
12189
+ return describeFeatureContents(lyr.geometry_type, lyr.shapes,
12190
+ lyr.data ? lyr.data.getRecords() : null, lyr.data ? lyr.data.getFields() : []);
12191
+ }
12192
+ return {text: '[empty]', title: null};
12193
+ }
12194
+
12195
+ function renderContents(lyr, dataset) {
12196
+ var o = describeLyr(lyr, dataset);
12197
+ if (!o.title) {
12198
+ return '<div class="layer-contents">' + o.text + '</div>';
12199
+ }
12200
+ return '<div class="layer-contents has-nulls"><span title="' + o.title + '">' +
12201
+ o.text + '</span></div>';
12140
12202
  }
12141
12203
 
12142
12204
  function renderLayerInfo(info) {
@@ -12265,9 +12327,48 @@
12265
12327
  return !!(lyr && (lyr.geometry_type == 'point' || lyr.geometry_type == 'polyline' || lyr.geometry_type == 'polygon'));
12266
12328
  }
12267
12329
 
12268
- function rowHTML(c1, c2, cname) {
12269
- return utils$1.format('<div class="row%s"><div class="col1">%s</div>' +
12270
- '<div class="col2">%s</div></div>', cname ? ' ' + cname : '', c1, c2);
12330
+ }
12331
+
12332
+ // The buttons that open and close the sidebar panels: an icon strip on the
12333
+ // map while the sidebar is closed, and a strip of toggles at the top of the
12334
+ // sidebar while it is open. Each button toggles its own panel, so both panels
12335
+ // can be open at once.
12336
+ function SidebarTabs(gui) {
12337
+ var buttons = gui.container.findChildren('.sidebar-tab, .sidebar-strip-btn');
12338
+ var hideBtn = gui.container.findChild('.sidebar-hide-btn');
12339
+
12340
+ buttons.forEach(function(btn) {
12341
+ var panel = btn.attr('data-panel');
12342
+ btn.on('click', function() {
12343
+ gui.toggleSidebarPanel(panel);
12344
+ });
12345
+ onKeyboardActivate(btn, function() {
12346
+ gui.toggleSidebarPanel(panel);
12347
+ });
12348
+ });
12349
+
12350
+ hideBtn.on('click', hideSidebar);
12351
+ onKeyboardActivate(hideBtn, hideSidebar);
12352
+
12353
+ gui.on('sidebar', function(e) {
12354
+ buttons.forEach(function(btn) {
12355
+ var open = e.panels.includes(btn.attr('data-panel'));
12356
+ btn.classed('active', open).attr('aria-expanded', String(open));
12357
+ });
12358
+ });
12359
+
12360
+ function hideSidebar() {
12361
+ gui.setSidebarPanels(null);
12362
+ }
12363
+
12364
+ function onKeyboardActivate(btn, action) {
12365
+ btn.on('keydown', function(e) {
12366
+ if (e.key == 'Enter' || e.key == ' ') {
12367
+ e.preventDefault();
12368
+ e.stopPropagation();
12369
+ action();
12370
+ }
12371
+ });
12271
12372
  }
12272
12373
  }
12273
12374
 
@@ -12400,7 +12501,9 @@
12400
12501
  if (!container.node()) return;
12401
12502
  DRAW_KINDS.forEach(function(kind, i) {
12402
12503
  if (i > 0) {
12403
- El('span').addClass('layer-menu-link-separator').html('&nbsp;·&nbsp;')
12504
+ // A breaking space after the dot, so that a sidebar too narrow for the
12505
+ // whole list wraps it between links, and never inside one.
12506
+ El('span').addClass('layer-menu-link-separator').html('&nbsp;· ')
12404
12507
  .appendTo(container);
12405
12508
  }
12406
12509
  El('span').addClass('layer-menu-link').attr('data-kind', kind.kind)
@@ -14234,25 +14337,25 @@
14234
14337
  // labelModeIsAvailable().
14235
14338
  var menus = {
14236
14339
  standard: ['info', 'selection', 'box', 'ruler'],
14237
- empty: ['edit_polygons', 'edit_lines', 'edit_points', 'box', 'ruler'],
14238
- polygons: ['info', 'selection', 'box', 'polygon_style', 'edit_polygons', 'ruler'],
14239
- rectangles: ['info', 'selection', 'box', 'polygon_style', 'rectangles', 'edit_polygons', 'ruler'],
14240
- lines: ['info', 'selection', 'box', 'line_style', 'edit_lines', 'snip_lines', 'ruler'],
14340
+ empty: [ 'label','edit_points', 'edit_lines', 'edit_polygons','box', 'ruler'],
14341
+ polygons: ['info', 'selection', 'polygon_style', 'edit_polygons', 'box', 'ruler'],
14342
+ rectangles: ['info', 'selection', 'polygon_style', 'edit_polygons', 'rectangles', 'box', 'ruler'],
14343
+ lines: ['info', 'selection', 'line_style', 'edit_lines', 'snip_lines', 'box', 'ruler'],
14241
14344
  table: ['info', 'selection'],
14242
- raster: ['ruler', 'box'],
14243
- // A label layer has no entry for styling labels, for adding and dragging
14244
- // points, or for positioning labels: the label tool does all three, along
14245
- // with creating and retyping labels, so each of them offered a subset of
14246
- // what sat next to it in the menu.
14247
- labels: ['info', 'selection', 'box', 'label', 'ruler'],
14248
- points: ['info', 'selection', 'box', 'point_style', 'edit_points', 'ruler'], // , 'add-points'
14345
+ raster: ['box', 'ruler'],
14346
+ labels: ['info', 'selection', 'label', 'box', 'ruler'],
14347
+ points: ['info', 'selection', 'edit_points', 'point_style', 'box', 'ruler'], // , 'add-points'
14249
14348
  // An empty point layer is the layer the "Draw: labels" link creates,
14250
14349
  // and a label goes into it rather than beside it (see labelWouldJoin), so
14251
14350
  // the label tool belongs in its menu as well as the point tools: it is the
14252
14351
  // way back into a labels layer that has no label in it yet.
14253
- emptyPoints: ['info', 'selection', 'box', 'point_style', 'label', 'edit_points', 'ruler']
14352
+ emptyPoints: ['info', 'selection', 'label', 'point_style', 'edit_points', 'box', 'ruler']
14254
14353
  };
14255
14354
 
14355
+ // Tools that work the same whatever the active layer is. They go below a
14356
+ // divider, so the layer-name heading visibly covers only the tools above it.
14357
+ var layerIndependentModes = ['box', 'ruler'];
14358
+
14256
14359
  var prompts = {
14257
14360
  box: 'Shift-drag to draw a box',
14258
14361
  data: 'Click-select features to edit their attributes',
@@ -14348,7 +14451,7 @@
14348
14451
  };
14349
14452
 
14350
14453
  this.modeUsesPopup = function(mode) {
14351
- return ['info', 'selection', 'data', 'box', 'edit_points', 'rectangles'].includes(mode);
14454
+ return ['info', 'selection', 'data', 'box', 'edit_points'].includes(mode);
14352
14455
  };
14353
14456
 
14354
14457
  this.modeSupportsUndo = function(mode) {
@@ -14419,27 +14522,54 @@
14419
14522
 
14420
14523
  function renderMenu() {
14421
14524
  if (!menu) return;
14422
- var modes = getAvailableModes();
14423
- menu.empty();
14424
- modes.forEach(function(mode) {
14525
+ var modes = getAvailableModes().filter(function(mode) {
14425
14526
  // don't show "turn off" link if not currently editing
14426
- if (_editMode == 'off' && mode == 'off') return;
14427
- var link = El('div').addClass('nav-menu-item').attr('data-name', mode).text(getModeLabel(mode)).appendTo(menu);
14428
- link.on('click', function(e) {
14429
- if (_editMode == mode) {
14430
- // closeMenu();
14431
- setMode('off');
14432
- } else if (_editMode != mode) {
14433
- setMode(mode);
14434
- if (mode == 'off') closeMenu(120); // only close if turning off
14435
- // closeMenu(mode == 'off' ? 120 : 400); // close after selecting
14436
- }
14437
- e.stopPropagation();
14438
- });
14527
+ return !(_editMode == 'off' && mode == 'off');
14528
+ });
14529
+ var layerModes = modes.filter(function(mode) {
14530
+ return !layerIndependentModes.includes(mode);
14531
+ });
14532
+ var otherModes = modes.filter(function(mode) {
14533
+ return layerIndependentModes.includes(mode);
14439
14534
  });
14535
+ var lyr = gui.model.getActiveLayer()?.layer;
14536
+ menu.empty();
14537
+ if (lyr && layerModes.length > 0) {
14538
+ renderLayerHeading(lyr);
14539
+ }
14540
+ layerModes.forEach(renderMenuItem);
14541
+ if (layerModes.length > 0 && otherModes.length > 0) {
14542
+ El('div').addClass('nav-menu-divider').appendTo(menu);
14543
+ }
14544
+ otherModes.forEach(renderMenuItem);
14440
14545
  updateSelectionHighlight();
14441
14546
  }
14442
14547
 
14548
+ // Names the layer the tools above the divider act on.
14549
+ function renderLayerHeading(lyr) {
14550
+ var name = formatLayerNameForDisplay(lyr.name);
14551
+ var heading = El('div').addClass('nav-menu-heading').attr('title', name).text(name).appendTo(menu);
14552
+ // A click on the heading would reach the button, which picks a tool.
14553
+ heading.on('click', function(e) {
14554
+ e.stopPropagation();
14555
+ });
14556
+ }
14557
+
14558
+ function renderMenuItem(mode) {
14559
+ var link = El('div').addClass('nav-menu-item').attr('data-name', mode).text(getModeLabel(mode)).appendTo(menu);
14560
+ link.on('click', function(e) {
14561
+ if (_editMode == mode) {
14562
+ // closeMenu();
14563
+ setMode('off');
14564
+ } else if (_editMode != mode) {
14565
+ setMode(mode);
14566
+ if (mode == 'off') closeMenu(120); // only close if turning off
14567
+ // closeMenu(mode == 'off' ? 120 : 400); // close after selecting
14568
+ }
14569
+ e.stopPropagation();
14570
+ });
14571
+ }
14572
+
14443
14573
  function getModeLabel(mode) {
14444
14574
  return labels[mode];
14445
14575
  }
@@ -16565,23 +16695,38 @@
16565
16695
  function makeColorField(parent, chit, input) {
16566
16696
  var box = El('div').addClass('label-color-field').appendTo(parent);
16567
16697
  chit.appendTo(box);
16568
- input.appendTo(box);
16698
+ input.addClass('label-color-input').appendTo(box);
16569
16699
  return box;
16570
16700
  }
16571
16701
 
16572
- // A colour and its opacity on one line: the swatch inside the field with the
16573
- // hex value, and the opacity in the narrow column beside it. Fill and Stroke
16574
- // are each one of these, and so are a label's text and icon colours.
16702
+ // A colour field with its opacity at the right-hand end, behind a divider:
16703
+ // opacity qualifies the colour, and one border says so where two fields side
16704
+ // by side said they were separate settings. It also leaves the narrow column
16705
+ // of the row free for a field that needs it. The opacity has no caption; its
16706
+ // percent sign and its tooltip say what it is.
16707
+ //
16708
+ // Returns the field's box; the opacity input is made by makeOpacityInput().
16709
+ function makeColorOpacityField(parent, chit, input, opacityOpts) {
16710
+ var box = makeColorField(parent, chit, input).addClass('label-color-opacity-field');
16711
+ var opacity = makeOpacityInput(box, opacityOpts);
16712
+ return {box: box, opacity: opacity};
16713
+ }
16714
+
16715
+ // A colour and its opacity in one field, captioned, in the wide column of a
16716
+ // split row. Fill and Stroke are each one of these.
16575
16717
  //
16576
- // opts.label the caption over the colour field
16718
+ // opts.label the caption over the field
16577
16719
  // opts.onColor(hex) a colour was typed, picked or previewed to a finish
16578
16720
  // opts.onOpacity(frac) a usable percentage was typed
16579
16721
  // opts.revert() one that was not, so put the row back as it was
16722
+ //
16723
+ // control.aside is the row's narrow column, empty, for a field that belongs
16724
+ // beside the colour -- a stroke's width, say.
16580
16725
  function makeColorRow(parent, opts) {
16581
16726
  var row = El('div').addClass('label-style-row label-split-row').appendTo(parent);
16582
16727
  var colorCell = El('div').addClass('label-split-cell label-color-row').appendTo(row);
16583
- var opacityCell = El('div').addClass('label-split-cell').appendTo(row);
16584
- var control = {row: row, chit: null, input: null, opacity: null, picker: null};
16728
+ var aside = El('div').addClass('label-split-cell').appendTo(row);
16729
+ var control = {row: row, aside: aside, chit: null, input: null, opacity: null, picker: null};
16585
16730
 
16586
16731
  control.setColor = function(color) {
16587
16732
  control.input.node().value = color || '';
@@ -16611,7 +16756,10 @@
16611
16756
  if (isHexColor(color)) control.picker.setColor(color);
16612
16757
  opts.onColor(color);
16613
16758
  });
16614
- makeColorField(colorCell, control.chit, control.input);
16759
+ control.opacity = makeColorOpacityField(colorCell, control.chit, control.input, {
16760
+ onSet: opts.onOpacity,
16761
+ revert: opts.revert
16762
+ }).opacity;
16615
16763
  control.picker = new ColorPicker(colorCell, {
16616
16764
  presetRows: layerColorPresetRows,
16617
16765
  // A drag in the picker shows on the map's own terms -- the swatch and the
@@ -16622,12 +16770,6 @@
16622
16770
  opts.onColor(hex);
16623
16771
  }
16624
16772
  });
16625
-
16626
- El('span').appendTo(opacityCell).text('Opacity');
16627
- control.opacity = makeOpacityInput(opacityCell, {
16628
- onSet: opts.onOpacity,
16629
- revert: opts.revert
16630
- });
16631
16773
  return control;
16632
16774
  }
16633
16775
 
@@ -17174,11 +17316,11 @@
17174
17316
  // stroke of twice this.
17175
17317
  var defaultHaloWidth = 2;
17176
17318
  var defaultHaloColor = internal.svg.DEFAULT_HALO_COLOR;
17177
- // The line height field shows this rather than renderLabel()'s 1.1em default,
17178
- // and shows it as a placeholder rather than a value, so that a label carries
17179
- // no line-height until one is chosen. "auto" is the honest description of a
17180
- // blank field: something else decides.
17181
- var lineHeightPlaceholder = 'auto';
17319
+ // The line height a label without one is drawn with, shown as a placeholder
17320
+ // rather than a value so that a label carries no line-height until one is
17321
+ // chosen. Shown as a bare multiple, which also says what a bare number typed
17322
+ // here means: a multiple of the font size, as in CSS, not px.
17323
+ var lineHeightPlaceholder = String(internal.svg.DEFAULT_LINE_HEIGHT);
17182
17324
  // What a control shows for a selection whose labels disagree about it. One
17183
17325
  // word, used in every field and menu that can be in that state: the panel
17184
17326
  // otherwise says it by showing nothing, which is also what an unset property
@@ -17451,15 +17593,16 @@
17451
17593
  clearLink = El('span').addClass('label-editing-clear colored-text').appendTo(selectRow).text('deselect').on('click', clearSelection);
17452
17594
 
17453
17595
  // Three sections rather than one flat stack: the text, the symbol beside
17454
- // it, and where the two sit relative to each other. Text and Icon are
17455
- // deliberately the same shape -- a colour and its opacity on one line, a
17456
- // size on the line above -- so that the second reads as a variation on the
17457
- // first rather than as a different kind of control.
17596
+ // it, and where the two sit relative to each other. Every section's colour
17597
+ // row is the same shape -- the colour with its opacity in one field, and
17598
+ // the value that qualifies it most closely beside it -- so that each reads
17599
+ // as a variation on the first rather than as a different kind of control.
17458
17600
  var textSection = addSection('Text');
17459
17601
 
17460
- // The controls whose own contents say what they are -- a font name, a hex
17461
- // colour, a percentage, a size beside a font style -- carry no label. The
17462
- // ones that would be a bare number otherwise keep theirs.
17602
+ // The controls whose own contents say what they are -- a font name, a
17603
+ // size beside a font style -- carry no label. The ones that would be a bare
17604
+ // number otherwise keep theirs, and so does a colour field, because the
17605
+ // field beside it has one and "Color" says the percentage is its opacity.
17463
17606
  var fontRow = El('div').addClass('label-style-row').appendTo(textSection);
17464
17607
  fontSelect = El('select').attr('title', 'Font').appendTo(fontRow).on('change', function() {
17465
17608
  if (fontSelect.node().value) {
@@ -17485,11 +17628,20 @@
17485
17628
  onDone: releaseFocus
17486
17629
  });
17487
17630
 
17631
+ // Letter spacing beside the colour, above line height: the two spacing
17632
+ // values read as a pair in the narrow column, and the left of the row
17633
+ // below is where the alignment buttons go.
17634
+ //
17635
+ // A text block's width is not here: it is set by dragging the block's
17636
+ // handle, and a field for it would be clutter on every other label.
17488
17637
  var colorRow = El('div').addClass('label-style-row label-split-row').appendTo(textSection);
17489
- var textColorCell = El('div').addClass('label-split-cell label-color-row').appendTo(colorRow);
17638
+ var textColorCell = El('div').addClass('label-split-cell label-color-row label-text-color-row').appendTo(colorRow);
17639
+ El('span').appendTo(textColorCell).text('Color');
17490
17640
  colorChit = El('div').addClass('label-color-chit').attr('role', 'button');
17491
17641
  colorInput = El('input').attr('type', 'text').attr('title', 'Text color');
17492
- colorFieldBox = makeColorField(textColorCell, colorChit, colorInput);
17642
+ var textColorField = addColorOpacityField(textColorCell, colorChit, colorInput, applyLabelOpacity);
17643
+ colorFieldBox = textColorField.box;
17644
+ opacityInput = textColorField.opacity;
17493
17645
  colorChit.on('click', function() {
17494
17646
  if (this.classList.contains('disabled')) return;
17495
17647
  toggleColorPicker();
@@ -17505,18 +17657,7 @@
17505
17657
  });
17506
17658
  colorPicker = initColorPicker(textColorCell, colorChit, colorInput, applyLabelColor);
17507
17659
 
17508
- var opacityCell = El('div').addClass('label-split-cell label-opacity-row label-text-opacity-row').appendTo(colorRow);
17509
- opacityInput = addOpacityInput(opacityCell, applyLabelOpacity);
17510
-
17511
- // Letter spacing takes the right-hand column on its own, above line
17512
- // height: the two spacing values read as a pair there, and the left of the
17513
- // row is where the alignment buttons go.
17514
- //
17515
- // A text block's width is not here: it is set by dragging the block's
17516
- // handle, and a field for it would be clutter on every other label.
17517
- var letterRow = El('div').addClass('label-style-row label-split-row').appendTo(textSection);
17518
- El('div').addClass('label-split-cell').appendTo(letterRow);
17519
- var letterCell = El('div').addClass('label-split-cell label-spacing-row').appendTo(letterRow);
17660
+ var letterCell = El('div').addClass('label-split-cell label-spacing-row').appendTo(colorRow);
17520
17661
  El('span').appendTo(letterCell).text('Letter spacing');
17521
17662
  letterSpacingInput = makeMeasureInput(letterCell, letterSpacingField, '0');
17522
17663
 
@@ -17554,8 +17695,8 @@
17554
17695
 
17555
17696
  // A halo is a yes/no that its three values then qualify, as a symbol is, so
17556
17697
  // it is switched the same way and its controls are inert while it is off.
17557
- // Its rows are ones the Text section already has: a colour and its opacity
17558
- // on one line, and a width in the narrow column beneath, where letter
17698
+ // Its row is the shape of the Text section's colour row: a colour and its
17699
+ // opacity, and a width in the narrow column beside them, where letter
17559
17700
  // spacing sits above.
17560
17701
  haloSection = addSection('Halo');
17561
17702
  var haloTitle = haloSection.findChild('.label-style-section-title');
@@ -17566,10 +17707,13 @@
17566
17707
  });
17567
17708
 
17568
17709
  var haloColorRow = El('div').addClass('label-style-row label-split-row').appendTo(haloSection);
17569
- var haloColorCell = El('div').addClass('label-split-cell label-color-row').appendTo(haloColorRow);
17710
+ var haloColorCell = El('div').addClass('label-split-cell label-color-row label-halo-color-row').appendTo(haloColorRow);
17711
+ El('span').appendTo(haloColorCell).text('Color');
17570
17712
  haloColorChit = El('div').addClass('label-color-chit').attr('role', 'button');
17571
17713
  haloColorInput = El('input').attr('type', 'text').attr('title', 'Halo color');
17572
- haloColorFieldBox = makeColorField(haloColorCell, haloColorChit, haloColorInput);
17714
+ var haloColorField = addColorOpacityField(haloColorCell, haloColorChit, haloColorInput, applyHaloOpacity);
17715
+ haloColorFieldBox = haloColorField.box;
17716
+ haloOpacityInput = haloColorField.opacity;
17573
17717
  haloColorChit.on('click', function() {
17574
17718
  if (this.classList.contains('disabled')) return;
17575
17719
  haloColorPicker.toggle();
@@ -17585,12 +17729,7 @@
17585
17729
  });
17586
17730
  haloColorPicker = initColorPicker(haloColorCell, haloColorChit, haloColorInput, applyHaloColor);
17587
17731
 
17588
- var haloOpacityCell = El('div').addClass('label-split-cell label-opacity-row').appendTo(haloColorRow);
17589
- haloOpacityInput = addOpacityInput(haloOpacityCell, applyHaloOpacity);
17590
-
17591
- var haloWidthRow = El('div').addClass('label-style-row label-split-row').appendTo(haloSection);
17592
- El('div').addClass('label-split-cell').appendTo(haloWidthRow);
17593
- var haloWidthCell = El('div').addClass('label-split-cell label-spacing-row').appendTo(haloWidthRow);
17732
+ var haloWidthCell = El('div').addClass('label-split-cell label-spacing-row').appendTo(haloColorRow);
17594
17733
  El('span').appendTo(haloWidthCell).text('Width');
17595
17734
  // Halves as well as whole pixels: a halo is usually 1 to 2px, and the
17596
17735
  // step between those two is most of the range anyone uses.
@@ -17617,14 +17756,9 @@
17617
17756
  className: 'label-icon-toggle',
17618
17757
  onChange: setIconOn
17619
17758
  });
17620
- // The size's caption sits on the heading line, over its own column. It
17621
- // belongs to the field in the row below, but the shapes beside that field
17622
- // have no caption of their own, and a caption over one control of a pair
17623
- // pushes it out of line with the other.
17624
- El('div').addClass('label-icon-size-caption').appendTo(iconTitle).text('Size');
17625
-
17626
- var iconSizeRow = El('div').addClass('label-style-row label-split-row label-icon-shapes-row').appendTo(iconSection);
17627
- var iconRow = El('div').addClass('label-split-cell').appendTo(iconSizeRow);
17759
+ // The shapes have the whole row, so there is room for more of them, and
17760
+ // the size sits beside the colour below, as the halo's width does.
17761
+ var iconRow = El('div').addClass('label-style-row label-icon-shapes-row').appendTo(iconSection);
17628
17762
  var iconGroup = iconGroupEl = El('div').addClass('label-btn-group label-icon-buttons').appendTo(iconRow);
17629
17763
  iconBtns = {};
17630
17764
  iconTypes.forEach(function(icon) {
@@ -17637,21 +17771,14 @@
17637
17771
  appendIconButtonSymbol(btn, icon.name);
17638
17772
  });
17639
17773
 
17640
- var sizeRow = El('div').addClass('label-split-cell label-icon-size-row').appendTo(iconSizeRow);
17641
- iconSizeInput = new SizeField(sizeRow, {
17642
- title: 'Symbol size in px',
17643
- onSet: function(value) {
17644
- applyIconSize(value);
17645
- },
17646
- onStep: nudgeIconSize,
17647
- onDone: releaseFocus
17648
- });
17649
-
17650
17774
  var iconColorRow = El('div').addClass('label-style-row label-split-row').appendTo(iconSection);
17651
17775
  var iconColorCell = El('div').addClass('label-split-cell label-color-row label-icon-color-row').appendTo(iconColorRow);
17776
+ El('span').appendTo(iconColorCell).text('Color');
17652
17777
  iconColorChit = El('div').addClass('label-color-chit').attr('role', 'button');
17653
17778
  iconColorInput = El('input').attr('type', 'text').attr('title', 'Symbol color');
17654
- iconColorFieldBox = makeColorField(iconColorCell, iconColorChit, iconColorInput);
17779
+ var iconColorField = addColorOpacityField(iconColorCell, iconColorChit, iconColorInput, applyIconOpacity);
17780
+ iconColorFieldBox = iconColorField.box;
17781
+ iconOpacityInput = iconColorField.opacity;
17655
17782
  iconColorChit.on('click', function() {
17656
17783
  if (this.classList.contains('disabled')) return;
17657
17784
  iconColorPicker.toggle();
@@ -17667,8 +17794,16 @@
17667
17794
  });
17668
17795
  iconColorPicker = initColorPicker(iconColorCell, iconColorChit, iconColorInput, applyIconColor);
17669
17796
 
17670
- var iconOpacityCell = El('div').addClass('label-split-cell label-opacity-row label-icon-opacity-row').appendTo(iconColorRow);
17671
- iconOpacityInput = addOpacityInput(iconOpacityCell, applyIconOpacity);
17797
+ var sizeRow = El('div').addClass('label-split-cell label-icon-size-row').appendTo(iconColorRow);
17798
+ El('span').appendTo(sizeRow).text('Size');
17799
+ iconSizeInput = new SizeField(sizeRow, {
17800
+ title: 'Symbol size in px',
17801
+ onSet: function(value) {
17802
+ applyIconSize(value);
17803
+ },
17804
+ onStep: nudgeIconSize,
17805
+ onDone: releaseFocus
17806
+ });
17672
17807
 
17673
17808
  initCalloutSection();
17674
17809
 
@@ -17780,10 +17915,13 @@
17780
17915
  });
17781
17916
 
17782
17917
  var colorRow = El('div').addClass('label-style-row label-split-row').appendTo(calloutSection);
17783
- var colorCell = El('div').addClass('label-split-cell label-color-row').appendTo(colorRow);
17918
+ var colorCell = El('div').addClass('label-split-cell label-color-row label-callout-color-row').appendTo(colorRow);
17919
+ El('span').appendTo(colorCell).text('Color');
17784
17920
  calloutColorChit = El('div').addClass('label-color-chit').attr('role', 'button');
17785
17921
  calloutColorInput = El('input').attr('type', 'text').attr('title', 'Callout color');
17786
- calloutColorFieldBox = makeColorField(colorCell, calloutColorChit, calloutColorInput);
17922
+ var calloutColorField = addColorOpacityField(colorCell, calloutColorChit, calloutColorInput, applyCalloutOpacity);
17923
+ calloutColorFieldBox = calloutColorField.box;
17924
+ calloutOpacityInput = calloutColorField.opacity;
17787
17925
  calloutColorChit.on('click', function() {
17788
17926
  if (this.classList.contains('disabled')) return;
17789
17927
  calloutColorPicker.toggle();
@@ -17798,14 +17936,10 @@
17798
17936
  }
17799
17937
  });
17800
17938
  calloutColorPicker = initColorPicker(colorCell, calloutColorChit, calloutColorInput, applyCalloutColor);
17801
- var opacityCell = El('div').addClass('label-split-cell label-opacity-row').appendTo(colorRow);
17802
- calloutOpacityInput = addOpacityInput(opacityCell, applyCalloutOpacity);
17803
17939
 
17804
17940
  // The gap in the narrow column, under the sizes: it has a blank state,
17805
17941
  // "auto", which clears the anchor's symbol, so it is not a size field.
17806
- var gapRow = El('div').addClass('label-style-row label-split-row').appendTo(calloutSection);
17807
- El('div').addClass('label-split-cell').appendTo(gapRow);
17808
- var gapCell = El('div').addClass('label-split-cell label-spacing-row label-callout-gap-row').appendTo(gapRow);
17942
+ var gapCell = El('div').addClass('label-split-cell label-spacing-row label-callout-gap-row').appendTo(colorRow);
17809
17943
  El('span').appendTo(gapCell).text('Gap');
17810
17944
  calloutGapInput = El('input').attr('type', 'text').addClass('label-measure-input')
17811
17945
  .attr('title', 'Space between the callout and the anchor, in px')
@@ -17887,9 +18021,9 @@
17887
18021
  };
17888
18022
  }
17889
18023
 
17890
- function addOpacityInput(parent, action) {
17891
- return makeOpacityInput(parent, {
17892
- onSet: action,
18024
+ function addColorOpacityField(parent, chit, input, onOpacity) {
18025
+ return makeColorOpacityField(parent, chit, input, {
18026
+ onSet: onOpacity,
17893
18027
  revert: updateControls
17894
18028
  });
17895
18029
  }
@@ -19511,7 +19645,7 @@
19511
19645
 
19512
19646
  fillControl = addColorControl(panel, 'Fill', 'fill', '');
19513
19647
  strokeControl = addColorControl(panel, 'Stroke', 'stroke', '#000000');
19514
- strokeWidthField = addStrokeWidthControl(panel);
19648
+ strokeWidthField = addStrokeWidthControl(strokeControl.aside);
19515
19649
 
19516
19650
  var buttonRow = El('div').addClass('label-style-row label-panel-button-row').appendTo(panel);
19517
19651
  randomFillBtn = makePanelActionButton(buttonRow, 'Random fills', applyRandomFillColors);
@@ -19553,14 +19687,11 @@
19553
19687
  return control;
19554
19688
  }
19555
19689
 
19556
- // In the narrow column, under the stroke's opacity. Stepping runs up a ladder
19690
+ // In the narrow column beside the stroke's colour. Stepping runs up a ladder
19557
19691
  // of widths rather than by a fixed amount, because the useful ones are close
19558
19692
  // together at the hairline end and far apart above 2px.
19559
- function addStrokeWidthControl(parent) {
19560
- var row = El('div').addClass('label-style-row label-split-row').appendTo(parent);
19561
- El('div').addClass('label-split-cell').appendTo(row);
19562
- var cell = El('div').addClass('label-split-cell').appendTo(row);
19563
- El('span').appendTo(cell).text('Stroke width');
19693
+ function addStrokeWidthControl(cell) {
19694
+ El('span').appendTo(cell).text('Width');
19564
19695
  return new SizeField(cell, {
19565
19696
  min: 0,
19566
19697
  // Quarter-pixel widths are the useful hairlines, and the ladder below
@@ -20007,14 +20138,11 @@
20007
20138
  circleStrokeControl = addCircleColorControl('Stroke', 'stroke');
20008
20139
  circleControlRows.push(circleFillControl.row, circleStrokeControl.row);
20009
20140
 
20010
- // The two sizes of a circle side by side, in the shape the panel uses for
20011
- // every other pair.
20012
- var sizeRow = El('div').addClass('label-style-row label-split-row').appendTo(circlesSection);
20013
- // Radius in the wide column, stroke width in the narrow one under the
20014
- // stroke's opacity, matching the line and polygon panels.
20015
- var radiusCell = El('div').addClass('label-split-cell').appendTo(sizeRow);
20016
- var widthCell = El('div').addClass('label-split-cell').appendTo(sizeRow);
20017
- El('span').appendTo(widthCell).text('Stroke width');
20141
+ // Each size beside the colour it goes with: the radius beside the fill,
20142
+ // the stroke's width beside its colour, as in the line and polygon panels.
20143
+ var radiusCell = circleFillControl.aside;
20144
+ var widthCell = circleStrokeControl.aside;
20145
+ El('span').appendTo(widthCell).text('Width');
20018
20146
  circleStrokeWidthField = new SizeField(widthCell, {
20019
20147
  min: 0,
20020
20148
  decimals: 2, // quarter-pixel hairlines, which the ladder below steps through
@@ -20045,7 +20173,6 @@
20045
20173
  },
20046
20174
  onDone: releaseFocus
20047
20175
  });
20048
- circleControlRows.push(sizeRow);
20049
20176
  }
20050
20177
 
20051
20178
  function initSymbolsSection() {
@@ -23102,8 +23229,11 @@
23102
23229
  gui.on('interaction_mode_change', function(e) {
23103
23230
  if (e.mode === 'selection') {
23104
23231
  gui.enterMode('selection_tool');
23105
- } else if (_on) {
23106
- turnOff();
23232
+ } else if (gui.getMode() == 'selection_tool') {
23233
+ // Leave the gui mode rather than just calling turnOff(): a mode left
23234
+ // behind makes the next enterMode('selection_tool') a no-op, so the tool
23235
+ // would never turn back on.
23236
+ gui.clearMode();
23107
23237
  }
23108
23238
  });
23109
23239
 
@@ -26361,7 +26491,7 @@
26361
26491
 
26362
26492
  function getLineHeight(o) {
26363
26493
  var rec = o.pending ? o.pending.getStyle() : getRecord(o.target, o.id);
26364
- return rec && rec['line-height'] || '1.1em';
26494
+ return internal.svg.getLineHeightDy(rec && rec['line-height']);
26365
26495
  }
26366
26496
 
26367
26497
  function getProvider(nodes) {
@@ -31489,50 +31619,73 @@
31489
31619
  gui.on('interaction_mode_change', function(e) {
31490
31620
  if (e.mode === 'rectangles') {
31491
31621
  gui.enterMode('rectangle_tool');
31492
- } else if (_on) {
31493
- turnOff();
31622
+ } else if (gui.getMode() == 'rectangle_tool') {
31623
+ // Leave the gui mode rather than just calling turnOff(): a mode left
31624
+ // behind makes the next enterMode('rectangle_tool') a no-op, so the tool
31625
+ // would never turn back on.
31626
+ gui.clearMode();
31494
31627
  }
31495
31628
  });
31496
31629
 
31497
31630
  hit.on('change', function(e) {
31498
31631
  if (!_on) return;
31499
31632
  // TODO: handle multiple hits (see gui-inspection-control)
31500
- var id = e.id;
31501
31633
  if (e.id > -1 && e.pinned) {
31502
- var target = hit.getHitTarget();
31503
- var path = target.shapes[e.id][0];
31504
- var bbox = target.gui.displayArcs.getSimpleShapeBounds(path).toArray();
31505
- box.setDataCoords(bbox);
31506
- dragInfo = {
31507
- id: e.id,
31508
- target: target,
31509
- ids: [],
31510
- points: []
31511
- };
31512
- var iter = target.gui.displayArcs.getShapeIter(path);
31513
- while (iter.hasNext()) {
31514
- dragInfo.points.push([iter.x, iter.y]);
31515
- dragInfo.ids.push(iter._arc.i);
31634
+ if (!dragInfo || dragInfo.id != e.id) {
31635
+ selectRectangle(e.id);
31516
31636
  }
31517
- gui.container.findChild('.map-layers').classed('dragging', true);
31518
-
31519
- } else if (dragInfo) {
31520
- gui.dispatchEvent('rectangle_dragend', dragInfo); // save undo state
31521
- gui.container.findChild('.map-layers').classed('dragging', false);
31522
- reset();
31523
31637
  } else {
31524
- box.hide();
31638
+ reset();
31525
31639
  }
31526
-
31527
31640
  });
31528
31641
 
31529
31642
  box.on('handle_drag', function(e) {
31530
31643
  if (!_on || !dragInfo) return;
31531
31644
  var coords = internal.bboxToCoords(box.getDataCoords());
31532
31645
  setRectangleCoords(dragInfo.target, dragInfo.ids, coords);
31646
+ dragInfo.moved = true;
31533
31647
  gui.dispatchEvent('map-needs-refresh');
31534
31648
  });
31535
31649
 
31650
+ // Each handle drag is one undo step, saved when it ends -- waiting until the
31651
+ // rectangle is deselected would lose the edit if the mode changes first.
31652
+ box.on('handle_up', function() {
31653
+ if (!_on || !dragInfo || !dragInfo.moved) return;
31654
+ gui.dispatchEvent('rectangle_dragend', {
31655
+ target: dragInfo.target,
31656
+ ids: dragInfo.ids,
31657
+ points: dragInfo.points
31658
+ });
31659
+ dragInfo.points = getDataPoints(dragInfo.target, dragInfo.ids);
31660
+ dragInfo.moved = false;
31661
+ });
31662
+
31663
+ function selectRectangle(id) {
31664
+ var target = hit.getHitTarget();
31665
+ var path = target.shapes[id][0];
31666
+ var bbox = target.gui.displayArcs.getSimpleShapeBounds(path).toArray();
31667
+ var ids = [];
31668
+ var iter = target.gui.displayArcs.getShapeIter(path);
31669
+ while (iter.hasNext()) {
31670
+ ids.push(iter._arc.i);
31671
+ }
31672
+ box.setDataCoords(bbox);
31673
+ dragInfo = {
31674
+ id: id,
31675
+ target: target,
31676
+ ids: ids,
31677
+ points: getDataPoints(target, ids), // for undo
31678
+ moved: false
31679
+ };
31680
+ gui.container.findChild('.map-layers').classed('dragging', true);
31681
+ }
31682
+
31683
+ function getDataPoints(target, ids) {
31684
+ return ids.map(function(id) {
31685
+ return getVertexCoords(target, id);
31686
+ });
31687
+ }
31688
+
31536
31689
  function turnOn() {
31537
31690
  box.turnOn();
31538
31691
  _on = true;
@@ -31550,6 +31703,9 @@
31550
31703
 
31551
31704
  function reset() {
31552
31705
  box.hide();
31706
+ if (dragInfo) {
31707
+ gui.container.findChild('.map-layers').classed('dragging', false);
31708
+ }
31553
31709
  dragInfo = null;
31554
31710
  }
31555
31711
  }
@@ -33613,22 +33769,20 @@
33613
33769
 
33614
33770
  // Fitting obeys the same mode as a handle drag: holding the scale grows the
33615
33771
  // output to cover the new extent, holding the output rescales onto it. The
33616
- // command works out both the padding and the held-scale width, so the
33617
- // margin's unit handling lives in one place.
33772
+ // command works out the extent, which has to leave room for symbols and
33773
+ // labels at the scale it ends up at, along with the padding and the
33774
+ // held-scale width, so the margin's unit handling lives in one place.
33618
33775
  function fitVisibleLayers() {
33619
33776
  var target = getFrameTarget();
33620
33777
  var entries = getCompositionEntries();
33621
33778
  var margin = getMargin();
33622
- var parts, bounds;
33779
+ var parts;
33623
33780
  if (!target || !entries.length) return;
33624
- bounds = entries.reduce(function(memo, o) {
33625
- return memo.mergeBounds(
33626
- internal.getLayerBounds(o.layer, o.dataset.arcs)
33627
- );
33628
- }, new internal.Bounds());
33629
33781
  parts = [
33630
33782
  '-update-frame',
33631
- 'bbox=' + quoteCommandValue(bounds.toArray().join(','))
33783
+ 'fit=' + internal.formatOptionValue(entries.map(function(o) {
33784
+ return internal.getLayerTargetId(gui.model, o.layer);
33785
+ }).join(','))
33632
33786
  ];
33633
33787
  if (margin) parts.push('offset=' + quoteCommandValue(margin));
33634
33788
  if (!lockSize) parts.push('fix-scale');
@@ -33975,9 +34129,9 @@
33975
34129
  revert: updateControls
33976
34130
  });
33977
34131
  // The width belongs to the same property as the colour and opacity beside
33978
- // it, and all three fit on the row. The background row leaves the column
33979
- // empty rather than putting the width on a line of its own.
33980
- neatlineWidthInput = makeRowField(neatlineControl.row, 'Width')
34132
+ // it. The background row leaves the column empty rather than putting the
34133
+ // width on a line of its own.
34134
+ neatlineWidthInput = makeCellField(neatlineControl.aside, 'Width')
33981
34135
  .addClass('frame-neatline-width')
33982
34136
  .on('change', updateNeatlineWidth);
33983
34137
  var clearRow = El('div')
@@ -34002,9 +34156,9 @@
34002
34156
  unitsSelect.node().value = units;
34003
34157
  aspectValue.text(getAspectText(frame));
34004
34158
  backgroundControl.showColor(rec.fill || '');
34005
- backgroundControl.opacity.node().value = formatOpacity(rec['fill-opacity']);
34159
+ backgroundControl.opacity.node().value = formatOpacity(rec['fill-opacity'], rec.fill);
34006
34160
  neatlineControl.showColor(rec.stroke || '');
34007
- neatlineControl.opacity.node().value = formatOpacity(rec['stroke-opacity']);
34161
+ neatlineControl.opacity.node().value = formatOpacity(rec['stroke-opacity'], rec.stroke);
34008
34162
  neatlineWidthInput.node().value =
34009
34163
  rec['stroke-width'] === undefined ? '' : rec['stroke-width'];
34010
34164
  boundsValue.text(frame.bbox.map(formatCoordinate).join(', '));
@@ -34092,8 +34246,7 @@
34092
34246
  }
34093
34247
 
34094
34248
  // An extra captioned cell on an existing colour row.
34095
- function makeRowField(row, label) {
34096
- var cell = El('div').addClass('label-split-cell').appendTo(row);
34249
+ function makeCellField(cell, label) {
34097
34250
  El('span').appendTo(cell).text(label);
34098
34251
  return makeTextInput(cell);
34099
34252
  }
@@ -34131,9 +34284,13 @@
34131
34284
  return String(Number(value.toPrecision(12)));
34132
34285
  }
34133
34286
 
34134
- function formatOpacity(value) {
34135
- return value === undefined ? '' :
34136
- String(Math.round(Number(value) * 100)) + '%';
34287
+ // A colour with no opacity of its own is drawn opaque, and says so; with no
34288
+ // colour either, there is nothing for an opacity to apply to.
34289
+ function formatOpacity(value, color) {
34290
+ if (value === undefined || value === null || value === '') {
34291
+ return color ? '100%' : '';
34292
+ }
34293
+ return String(Math.round(Number(value) * 100)) + '%';
34137
34294
  }
34138
34295
 
34139
34296
  // A fixed ratio holds when the frame is rescaled; one taken from the extent
@@ -35151,7 +35308,8 @@
35151
35308
  }
35152
35309
 
35153
35310
  function clampSidebarPanelsSeparatorPosition(pageY) {
35154
- var pct = 100 * (pageY - 29) / (window.innerHeight - 29);
35311
+ var rect = gui.container.findChild('.sidebar-panels').node().getBoundingClientRect();
35312
+ var pct = 100 * (pageY - rect.top) / rect.height;
35155
35313
  return Math.max(15, Math.min(85, pct));
35156
35314
  }
35157
35315
  }
@@ -35582,6 +35740,7 @@
35582
35740
  new ImportControl(gui, importOpts);
35583
35741
  new ExportControl(gui);
35584
35742
  new LayerControl(gui);
35743
+ new SidebarTabs(gui);
35585
35744
  new AddLayerLinks(gui);
35586
35745
  HeaderMenu();
35587
35746
  gui.console = new Console(gui);
@@ -35644,6 +35803,24 @@
35644
35803
  El('body').addClass('map-view');
35645
35804
  gui.console.runInitialCommands(getInitialConsoleCommands());
35646
35805
  });
35806
+
35807
+ // The layer list, opened beside the first data so that the user sees what
35808
+ // was imported and where to go next -- once, and only for data: dismissing
35809
+ // the splash popup without importing anything leaves the sidebar closed.
35810
+ // Waits for any menu mode (the import dialog, while files are still loading)
35811
+ // to close, since opening a panel would close it.
35812
+ var layersShownForData = false;
35813
+ gui.model.on('update', showLayersForFirstData);
35814
+ gui.on('mode', showLayersForFirstData);
35815
+ function showLayersForFirstData() {
35816
+ var panels;
35817
+ if (layersShownForData || gui.model.isEmpty() || gui.getMode()) return;
35818
+ layersShownForData = true;
35819
+ panels = gui.getSidebarPanels();
35820
+ if (!panels.includes('layers')) {
35821
+ gui.setSidebarPanels(panels.concat('layers').sort());
35822
+ }
35823
+ }
35647
35824
  };
35648
35825
 
35649
35826
  })();