mapshaper 0.7.71 → 0.7.72

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.
@@ -3021,7 +3021,7 @@
3021
3021
  return str + repeatString(pad, size - str.length);
3022
3022
  }
3023
3023
 
3024
- function trim(str) {
3024
+ function trim$1(str) {
3025
3025
  return ltrim(rtrim(str));
3026
3026
  }
3027
3027
 
@@ -3625,7 +3625,7 @@
3625
3625
  range, regexEscape, reorderArray: reorderArray$1, repeat, repeatString,
3626
3626
  replaceArray, rpad, rtrim,
3627
3627
  shuffle, some, sortArrayIndex, sortOn, splitLines, sum,
3628
- toArray, toBuffer, trim, trimQuotes,
3628
+ toArray, toBuffer, trim: trim$1, trimQuotes,
3629
3629
  uniq, uniqifyNames,
3630
3630
  wildcardToRegExp
3631
3631
  };
@@ -15892,6 +15892,11 @@
15892
15892
  var colorPicker = El('div').addClass('label-color-picker').appendTo(parent).hide();
15893
15893
  var sbCanvas, hueCanvas, sbMarker, hueMarker, pickerHsbInputs;
15894
15894
  var pickerColor = {h: 0, s: 0, b: 0};
15895
+ // The hex value the picker was set to, until it is moved off it. HSB is
15896
+ // held in bytes, which cannot represent every RGB colour, so a colour that
15897
+ // was typed or picked from a preset would otherwise come back as a
15898
+ // neighbour of itself (#ff8800 as #ff8a00).
15899
+ var pickerHex = null;
15895
15900
  var presetRows = opts.presetRows || [grayscaleColorPresets];
15896
15901
 
15897
15902
  init();
@@ -15920,13 +15925,15 @@
15920
15925
 
15921
15926
  this.setColor = function(color) {
15922
15927
  if (isHexColor(color)) {
15923
- setPickerColor(hexToHsb(color));
15928
+ setPickerColor(hexToHsb(color), color);
15924
15929
  }
15925
15930
  };
15926
15931
 
15927
- this.getColor = function() {
15928
- return hsbToHex(pickerColor);
15929
- };
15932
+ this.getColor = getPickerHex;
15933
+
15934
+ function getPickerHex() {
15935
+ return pickerHex || hsbToHex(pickerColor);
15936
+ }
15930
15937
 
15931
15938
  // The picker is placed in viewport coordinates (see .label-color-picker), so
15932
15939
  // it has to be put against the field it belongs to each time it opens. The
@@ -15991,7 +15998,7 @@
15991
15998
  }
15992
15999
 
15993
16000
  function applyPreset(color) {
15994
- setPickerColor(hexToHsb(color));
16001
+ setPickerColor(hexToHsb(color), color);
15995
16002
  commitPickerColor();
15996
16003
  }
15997
16004
 
@@ -16061,15 +16068,17 @@
16061
16068
  };
16062
16069
  }
16063
16070
 
16064
- function setPickerColor(hsb) {
16071
+ // @hex: the exact colour, when @hsb was made from one
16072
+ function setPickerColor(hsb, hex) {
16065
16073
  pickerColor = {
16066
16074
  h: clamp$4(Math.round(hsb.h), 0, 255),
16067
16075
  s: clamp$4(Math.round(hsb.s), 0, 255),
16068
16076
  b: clamp$4(Math.round(hsb.b), 0, 255)
16069
16077
  };
16078
+ pickerHex = hex || null;
16070
16079
  drawColorPicker();
16071
16080
  updatePickerFields();
16072
- if (opts.onPreview) opts.onPreview(hsbToHex(pickerColor));
16081
+ if (opts.onPreview) opts.onPreview(getPickerHex());
16073
16082
  }
16074
16083
 
16075
16084
  function updatePickerFields() {
@@ -16125,7 +16134,7 @@
16125
16134
  }
16126
16135
 
16127
16136
  function commitPickerColor() {
16128
- if (opts.onChange) opts.onChange(hsbToHex(pickerColor));
16137
+ if (opts.onChange) opts.onChange(getPickerHex());
16129
16138
  }
16130
16139
 
16131
16140
  function getMarkerColor(rgb) {
@@ -16894,7 +16903,7 @@
16894
16903
  });
16895
16904
  }
16896
16905
 
16897
- // A button with a word on it rather than a glyph: Clear style, Random fills,
16906
+ // A button with a word on it rather than a glyph: Clear style, Random fill,
16898
16907
  // Create. Shaped like the panel's fields rather than like its 19px icon
16899
16908
  // buttons, because what it does is named rather than drawn, and it is as wide
16900
16909
  // as the name.
@@ -16952,7 +16961,10 @@
16952
16961
  // split row. Fill and Stroke are each one of these.
16953
16962
  //
16954
16963
  // opts.label the caption over the field
16955
- // opts.onColor(hex) a colour was typed, picked or previewed to a finish
16964
+ // opts.onColor(hex) a colour was typed, picked or previewed to a finish;
16965
+ // '' when the field was emptied, which unsets a colour
16966
+ // that can be unset and restores the default of one
16967
+ // that cannot
16956
16968
  // opts.onOpacity(frac) a usable percentage was typed
16957
16969
  // opts.revert() one that was not, so put the row back as it was
16958
16970
  // opts.noOpacity a colour with no opacity of its own, like a pattern's,
@@ -17220,6 +17232,36 @@
17220
17232
  return gui.state.label_text_session || null;
17221
17233
  }
17222
17234
 
17235
+ // The label tool's armed placement tool: 'anchor', 'block', 'path' or null.
17236
+ // The tool owns it; the panel reads it, because an armed tool with nothing
17237
+ // selected is what points the panel at new labels rather than at all of them.
17238
+ function setLabelToolArmed(gui, mode) {
17239
+ var val = mode || null;
17240
+ if ((gui.state.label_tool_armed || null) === val) return;
17241
+ gui.state.label_tool_armed = val;
17242
+ gui.dispatchEvent('label_tool_armed_change');
17243
+ }
17244
+
17245
+ function getLabelToolArmed(gui) {
17246
+ return gui.state.label_tool_armed || null;
17247
+ }
17248
+
17249
+ // The "Editing:" line at the top of the style panels, which says what a
17250
+ // change to the panel's controls will act on. One wording for every panel.
17251
+ //
17252
+ // o.selected how many features are selected
17253
+ // o.total how many features the layer has
17254
+ // o.newLabels (label panel) a placement tool is armed with nothing
17255
+ // selected, so the controls set the style of the next label
17256
+ // o.editingText (label panel) a label is open for typing
17257
+ function formatEditingStatus(o) {
17258
+ var what = o.editingText ? 'this label' :
17259
+ o.selected > 0 ? o.selected + ' selected' :
17260
+ o.newLabels ? 'new labels' :
17261
+ o.total > 0 ? 'all' : 'none (empty layer)';
17262
+ return 'Editing: ' + what;
17263
+ }
17264
+
17223
17265
  // Dragging an anchored label's text away from its anchor: what the drag
17224
17266
  // writes, and which of the nine positions it ends up nearest.
17225
17267
  //
@@ -17792,6 +17834,9 @@
17792
17834
  }, {
17793
17835
  name: 'open-arrow',
17794
17836
  title: 'open arrowhead at the anchor'
17837
+ }, {
17838
+ name: 'ring',
17839
+ title: 'ring around the anchor'
17795
17840
  }];
17796
17841
  // The anchor is at the lower left of the shapes and at the left of the ends,
17797
17842
  // which is where the markers go. The arrowheads are drawn to the renderer's
@@ -17803,7 +17848,8 @@
17803
17848
  curve: '<path d="M3.5 12.5Q4.5 4.5 12.5 4"></path>',
17804
17849
  none: '<path d="M3 8H13"></path>',
17805
17850
  arrow: '<path d="M6.84 8H13"></path><path class="fill" d="M2.5 8L8.7 6.05V9.95Z"></path>',
17806
- 'open-arrow': '<path d="M3.2 8H13"></path><path d="M7.38 5.07L3.2 8L7.38 10.93"></path>'
17851
+ 'open-arrow': '<path d="M3.2 8H13"></path><path d="M7.38 5.07L3.2 8L7.38 10.93"></path>',
17852
+ ring: '<path d="M8.4 8H13"></path><circle cx="5.6" cy="8" r="2.8"></circle>'
17807
17853
  };
17808
17854
  var iconButtonSymbols = {
17809
17855
  circle: '<circle cx="8" cy="8" r="4.25"></circle>',
@@ -17854,6 +17900,11 @@
17854
17900
  // a label opened for typing becomes what the controls act on
17855
17901
  if (panel.visible()) updateControls();
17856
17902
  });
17903
+ gui.on('label_tool_armed_change', function() {
17904
+ // arming a tool points an empty selection at new labels, and disarming
17905
+ // points it back at the whole layer
17906
+ if (panel.visible()) updateControls();
17907
+ });
17857
17908
  gui.on('label_position_mode_change', function() {
17858
17909
  // The tool sets the mode back to Fixed when it opens, and the toggle is
17859
17910
  // the only thing on screen that says which of the two is on.
@@ -18003,12 +18054,9 @@
18003
18054
  });
18004
18055
  colorInput.on('change', function() {
18005
18056
  var color = colorInput.node().value.trim();
18006
- if (color) {
18007
- if (isHexColor(color)) {
18008
- colorPicker.setColor(color);
18009
- }
18010
- applyLabelColor(color);
18011
- }
18057
+ // An emptied field unsets the colour.
18058
+ if (isHexColor(color)) colorPicker.setColor(color);
18059
+ applyLabelColor(color);
18012
18060
  });
18013
18061
  colorPicker = initColorPicker(textColorCell, colorChit, colorInput, applyLabelColor);
18014
18062
 
@@ -18075,12 +18123,8 @@
18075
18123
  });
18076
18124
  haloColorInput.on('change', function() {
18077
18125
  var color = haloColorInput.node().value.trim();
18078
- if (color) {
18079
- if (isHexColor(color)) {
18080
- haloColorPicker.setColor(color);
18081
- }
18082
- applyHaloColor(color);
18083
- }
18126
+ if (isHexColor(color)) haloColorPicker.setColor(color);
18127
+ applyHaloColor(color);
18084
18128
  });
18085
18129
  haloColorPicker = initColorPicker(haloColorCell, haloColorChit, haloColorInput, applyHaloColor);
18086
18130
 
@@ -18140,12 +18184,8 @@
18140
18184
  });
18141
18185
  iconColorInput.on('change', function() {
18142
18186
  var color = iconColorInput.node().value.trim();
18143
- if (color) {
18144
- if (isHexColor(color)) {
18145
- iconColorPicker.setColor(color);
18146
- }
18147
- applyIconColor(color);
18148
- }
18187
+ if (isHexColor(color)) iconColorPicker.setColor(color);
18188
+ applyIconColor(color);
18149
18189
  });
18150
18190
  iconColorPicker = initColorPicker(iconColorCell, iconColorChit, iconColorInput, applyIconColor);
18151
18191
 
@@ -18259,7 +18299,7 @@
18259
18299
  var endSizeCell = El('div').addClass('label-split-cell label-spacing-row label-callout-end-size-row').appendTo(endRow);
18260
18300
  El('span').appendTo(endSizeCell).text('Size');
18261
18301
  calloutEndSizeInput = new SizeField(endSizeCell, {
18262
- title: 'Arrowhead size in px, the length of its sides',
18302
+ title: 'Marker size in px: an arrowhead\'s sides, or a ring\'s diameter',
18263
18303
  min: 1,
18264
18304
  max: 60,
18265
18305
  step: 1,
@@ -18283,12 +18323,8 @@
18283
18323
  });
18284
18324
  calloutColorInput.on('change', function() {
18285
18325
  var color = calloutColorInput.node().value.trim();
18286
- if (color) {
18287
- if (isHexColor(color)) {
18288
- calloutColorPicker.setColor(color);
18289
- }
18290
- applyCalloutColor(color);
18291
- }
18326
+ if (isHexColor(color)) calloutColorPicker.setColor(color);
18327
+ applyCalloutColor(color);
18292
18328
  });
18293
18329
  calloutColorPicker = initColorPicker(colorCell, calloutColorChit, calloutColorInput, applyCalloutColor);
18294
18330
 
@@ -18410,6 +18446,16 @@
18410
18446
  return !!(gui.interaction && gui.interaction.getMode() == labelMode);
18411
18447
  }
18412
18448
 
18449
+ // Whether an empty selection means the next label rather than every label:
18450
+ // only while a placement tool is armed, or a label not yet made is being
18451
+ // typed. With no tool armed the panel styles the whole layer, as the line
18452
+ // and polygon panels do.
18453
+ function editingNewLabels() {
18454
+ var session = getLabelTextSession(gui);
18455
+ if (!labelModeIsOn()) return false;
18456
+ return !!getLabelToolArmed(gui) || !!(session && session.id == -1);
18457
+ }
18458
+
18413
18459
  function updateVisibility() {
18414
18460
  var enabled = activeLayerHasLabels();
18415
18461
  textBtn.classed('disabled', !enabled);
@@ -18493,13 +18539,14 @@
18493
18539
  if (session) return [];
18494
18540
  ids = getSelectionIds();
18495
18541
  if (ids.length > 0) return ids;
18496
- return labelModeIsOn() ? [] : getAllLabelIds();
18542
+ return editingNewLabels() ? [] : getAllLabelIds();
18497
18543
  }
18498
18544
 
18499
- // With nothing selected the controls edit the style that new labels are given,
18500
- // so they stay live even when there is no label, and no layer, yet.
18545
+ // With a tool armed and nothing selected the controls edit the style that
18546
+ // new labels are given, so they stay live even when there is no label, and
18547
+ // no layer, yet. With no tool armed and no labels, there is nothing to edit.
18501
18548
  function controlsEnabled() {
18502
- return getTargetIds().length > 0 || labelModeIsOn();
18549
+ return getTargetIds().length > 0 || editingNewLabels();
18503
18550
  }
18504
18551
 
18505
18552
  function getAllLabelIds() {
@@ -18729,22 +18776,21 @@
18729
18776
  });
18730
18777
  }
18731
18778
 
18779
+ // What the controls will act on. With a tool armed an empty selection is the
18780
+ // next label rather than the whole layer, and saying "all" there is what
18781
+ // would make the panel untrustworthy.
18732
18782
  function updateEditingStatus(count, editingText) {
18733
- editingStatus.text('Editing: ' + describeTarget(count, editingText));
18783
+ editingStatus.text(formatEditingStatus({
18784
+ selected: count,
18785
+ total: getActiveLayer() ? internal.getFeatureCount(getActiveLayer()) : 0,
18786
+ newLabels: editingNewLabels(),
18787
+ editingText: editingText
18788
+ }));
18734
18789
  // "deselect" is for a selection the user made; a label being typed into is
18735
18790
  // left by clicking away or pressing Escape, not from here.
18736
18791
  clearLink.classed('hidden', count === 0 || editingText);
18737
18792
  }
18738
18793
 
18739
- // What the controls will act on. In label mode an empty selection is the next
18740
- // label rather than the whole layer, and saying "all" there is what would
18741
- // make the panel untrustworthy.
18742
- function describeTarget(count, editingText) {
18743
- if (editingText) return 'this label';
18744
- if (count > 0) return count + ' selected';
18745
- return labelModeIsOn() ? 'new labels' : 'all';
18746
- }
18747
-
18748
18794
  function updateFontSizeControls(shown) {
18749
18795
  fontSizeInput.setValue(shown.value || '');
18750
18796
  fontSizeInput.setPlaceholder(shown.mixed ? MIXED_TEXT : '');
@@ -18953,7 +18999,7 @@
18953
18999
  var ids = getTargetIds();
18954
19000
  var table = getActiveTable();
18955
19001
  if (ids.length === 0) {
18956
- return internal.svg.labelHasHalo(labelModeIsOn() ? getNewLabelStyle(gui) : null) ? 'on' : 'off';
19002
+ return internal.svg.labelHasHalo(editingNewLabels() ? getNewLabelStyle(gui) : null) ? 'on' : 'off';
18957
19003
  }
18958
19004
  return getToggleState(ids.map(function(id) {
18959
19005
  return internal.svg.labelHasHalo(table && table.getRecordAt(id));
@@ -19013,7 +19059,7 @@
19013
19059
  var ids = getTargetIds();
19014
19060
  var table = getActiveTable();
19015
19061
  if (ids.length === 0) {
19016
- return internal.svg.labelHasCallout(labelModeIsOn() ? getNewLabelStyle(gui) : null) ? 'on' : 'off';
19062
+ return internal.svg.labelHasCallout(editingNewLabels() ? getNewLabelStyle(gui) : null) ? 'on' : 'off';
19017
19063
  }
19018
19064
  return getToggleState(ids.map(function(id) {
19019
19065
  return internal.svg.labelHasCallout(table && table.getRecordAt(id));
@@ -19155,7 +19201,7 @@
19155
19201
  function updateCalloutEndSizeControl(off) {
19156
19202
  var ids = getTargetIds().length > 0 ? getCalloutEndTargetIds() : [];
19157
19203
  var noMarker = getTargetIds().length > 0 ? ids.length === 0 :
19158
- getCalloutEnd(labelModeIsOn() ? getNewLabelStyle(gui) : null) == 'none';
19204
+ getCalloutEnd(editingNewLabels() ? getNewLabelStyle(gui) : null) == 'none';
19159
19205
  var shown = off || noMarker ? {value: '', mixed: false} : getCalloutEndSizeShown(ids);
19160
19206
  calloutEndSizeInput.setValue(shown.value);
19161
19207
  calloutEndSizeInput.setPlaceholder(shown.mixed ? MIXED_TEXT : '');
@@ -19194,7 +19240,7 @@
19194
19240
  var vals;
19195
19241
  if (!controlsEnabled()) return {value: '', mixed: false};
19196
19242
  if (ids.length === 0) {
19197
- return {value: fn(labelModeIsOn() ? getNewLabelStyle(gui) : null), mixed: false};
19243
+ return {value: fn(editingNewLabels() ? getNewLabelStyle(gui) : null), mixed: false};
19198
19244
  }
19199
19245
  vals = ids.map(function(id) {
19200
19246
  return fn(table && table.getRecordAt(id));
@@ -19270,7 +19316,7 @@
19270
19316
  var records = getActiveTable() && getActiveTable().getRecords();
19271
19317
  var value, val;
19272
19318
  if (ids.length === 0) {
19273
- return resolveAlignment(labelModeIsOn() ? getNewLabelStyle(gui) : null);
19319
+ return resolveAlignment(editingNewLabels() ? getNewLabelStyle(gui) : null);
19274
19320
  }
19275
19321
  for (var i = 0; i < ids.length; i++) {
19276
19322
  val = resolveAlignment(records && records[ids[i]]);
@@ -19290,7 +19336,7 @@
19290
19336
  // label will be created with, falling back to the same default an existing
19291
19337
  // label would show.
19292
19338
  function getNewLabelValue(field, opts) {
19293
- var style = labelModeIsOn() ? getNewLabelStyle(gui) : null;
19339
+ var style = editingNewLabels() ? getNewLabelStyle(gui) : null;
19294
19340
  var val = style ? style[field] : '';
19295
19341
  if (val || val === 0) return val;
19296
19342
  return opts && opts.useDefault ? opts.defaultValue : '';
@@ -19381,7 +19427,7 @@
19381
19427
  var records = getActiveTable() && getActiveTable().getRecords();
19382
19428
  var ids = getTargetIds();
19383
19429
  if (ids.length === 0) {
19384
- return !!(labelModeIsOn() && getNewLabelStyle(gui)[textAnchorField]);
19430
+ return !!(editingNewLabels() && getNewLabelStyle(gui)[textAnchorField]);
19385
19431
  }
19386
19432
  return ids.some(function(id) {
19387
19433
  var rec = records && records[id];
@@ -19921,13 +19967,13 @@
19921
19967
  // no fill gets a clear one, so that the pattern is drawn over whatever is
19922
19968
  // underneath.
19923
19969
  function getPatternBackground(fill) {
19924
- return isBlank$1(fill) ? 'none' : String(fill).trim();
19970
+ return isBlank$2(fill) ? 'none' : String(fill).trim();
19925
19971
  }
19926
19972
 
19927
19973
  // Whether the settings describe a pattern the renderer will draw. A hatch with
19928
19974
  // no gap would be a solid fill, and the parser refuses a stripe of no width.
19929
19975
  function isValidPatternControls(o) {
19930
- if (!o || patternTypes.indexOf(o.type) == -1 || isBlank$1(o.color)) return false;
19976
+ if (!o || patternTypes.indexOf(o.type) == -1 || isBlank$2(o.color)) return false;
19931
19977
  if (!(o.size > 0) || !isFinite(o.angle)) return false;
19932
19978
  return o.type == 'hatches' ? o.gap > 0 : o.gap >= 0;
19933
19979
  }
@@ -20021,7 +20067,7 @@
20021
20067
  return str == 'transparent' ? 'none' : str;
20022
20068
  }
20023
20069
 
20024
- function isBlank$1(val) {
20070
+ function isBlank$2(val) {
20025
20071
  return val === undefined || val === null || String(val).trim() === '';
20026
20072
  }
20027
20073
 
@@ -20038,7 +20084,7 @@
20038
20084
  // opts.revert() put the panel back as the data has it
20039
20085
  // opts.releaseFocus()
20040
20086
  function PatternFillControl(parent, opts) {
20041
- var section = makePanelSection(parent, 'Pattern');
20087
+ var section = makePanelSection(parent, 'Patterns');
20042
20088
  var shown = {type: 'none'};
20043
20089
  // What switching the pattern on applies when the features have none: the
20044
20090
  // last pattern the section showed, so that off and on again is a round trip.
@@ -20083,7 +20129,7 @@
20083
20129
  var code = rec && rec['fill-pattern'];
20084
20130
  var edit = {id: id, styles: styles};
20085
20131
  var refilled;
20086
- if (refill && !isBlank(code)) {
20132
+ if (refill && !isBlank$1(code)) {
20087
20133
  refilled = refillPattern(internal.parsePattern(code), rec.fill, fill);
20088
20134
  if (refilled) edit.styles = styles.concat([['fill-pattern', refilled]]);
20089
20135
  }
@@ -20100,7 +20146,7 @@
20100
20146
  ids.forEach(function(id) {
20101
20147
  var rec = records[id];
20102
20148
  var code = rec && rec['fill-pattern'];
20103
- var o = isBlank(code) ? null : getPatternControls(internal.parsePattern(code), rec.fill);
20149
+ var o = isBlank$1(code) ? null : getPatternControls(internal.parsePattern(code), rec.fill);
20104
20150
  if (o) edits.push({id: id, styles: [['fill-pattern', formatFillPatternExpression(o)]]});
20105
20151
  });
20106
20152
  return edits;
@@ -20130,7 +20176,12 @@
20130
20176
  label: 'Color',
20131
20177
  noOpacity: true,
20132
20178
  onColor: function(color) {
20133
- if (color) applyChange({color: color});
20179
+ // A pattern has to have a colour, so an emptied field is its default.
20180
+ if (!color) {
20181
+ color = defaultPatternColor;
20182
+ colorControl.showColor(color);
20183
+ }
20184
+ applyChange({color: color});
20134
20185
  },
20135
20186
  revert: opts.revert
20136
20187
  });
@@ -20258,7 +20309,7 @@
20258
20309
 
20259
20310
  function hasPattern(id) {
20260
20311
  var rec = opts.getRecords()[id];
20261
- return !isBlank(rec && rec['fill-pattern']);
20312
+ return !isBlank$1(rec && rec['fill-pattern']);
20262
20313
  }
20263
20314
 
20264
20315
  function applyChange(change) {
@@ -20330,7 +20381,7 @@
20330
20381
 
20331
20382
  function readPatternState(rec) {
20332
20383
  var code = rec && rec['fill-pattern'];
20333
- if (isBlank(code)) return {type: 'none'};
20384
+ if (isBlank$1(code)) return {type: 'none'};
20334
20385
  return getPatternControls(internal.parsePattern(code), rec.fill) ||
20335
20386
  {type: 'custom', code: String(code)};
20336
20387
  }
@@ -20346,12 +20397,292 @@
20346
20397
  return undefined;
20347
20398
  }
20348
20399
 
20400
+ function isBlank$1(val) {
20401
+ return val === undefined || val === null || String(val).trim() === '';
20402
+ }
20403
+
20404
+ // The polygon panel's "Effects" section: an outer and an inner glow, each a
20405
+ // color, an opacity and a width. See svg-glow.mjs for how they are drawn.
20406
+ //
20407
+ // A glow is there when its color is set, so each glow is a color row with no
20408
+ // switch of its own: picking a color adds the glow, and emptying the color
20409
+ // field removes it. The section's switch opens the section, and switching it
20410
+ // off removes both glows and their settings.
20411
+ //
20412
+ // A width or opacity typed while no feature has the glow is kept, and goes
20413
+ // with the color when one is picked.
20414
+ //
20415
+ // opts.getRecords() the target layer's records
20416
+ // opts.getTargetIds() the features being styled
20417
+ // opts.applyEdits(edits, title) run per-feature edits: [{id, styles}]
20418
+ // opts.revert() put the panel back as the data has it
20419
+ // opts.releaseFocus()
20420
+
20421
+ var glowTypes = ['outer', 'inner'];
20422
+ var glowTitles = {outer: 'Outer glow', inner: 'Inner glow'};
20423
+
20424
+ function GlowEffectsControl(parent, opts) {
20425
+ var section = makePanelSection(parent, 'Effects');
20426
+ var rows = {};
20427
+ // Switched on, with no glow to show yet.
20428
+ var opened = false;
20429
+ var pending = {outer: {}, inner: {}};
20430
+ var toggle = makePanelToggle(section.findChild('.label-style-section-title'), {
20431
+ title: 'Add glow effects',
20432
+ className: 'layer-effects-toggle',
20433
+ onChange: function(on) {
20434
+ var edits;
20435
+ opened = on;
20436
+ pending = {outer: {}, inner: {}};
20437
+ edits = on ? [] : getEffectsOffEdits(opts.getRecords(), opts.getTargetIds());
20438
+ if (edits.length > 0) {
20439
+ opts.applyEdits(edits, 'Remove glow effects');
20440
+ } else {
20441
+ update();
20442
+ }
20443
+ }
20444
+ });
20445
+ glowTypes.forEach(function(type) {
20446
+ rows[type] = addGlowRow(type);
20447
+ });
20448
+
20449
+ this.section = section;
20450
+ this.update = update;
20451
+ this.hidePickers = hidePickers;
20452
+
20453
+ this.reset = function() {
20454
+ opened = false;
20455
+ pending = {outer: {}, inner: {}};
20456
+ hidePickers();
20457
+ };
20458
+
20459
+ // The glows every target has, for a saved style.
20460
+ this.addStyleValues = function(style) {
20461
+ var records = opts.getRecords();
20462
+ var ids = opts.getTargetIds();
20463
+ glowTypes.forEach(function(type) {
20464
+ if (getGlowState(records, ids, type) != 'on') return;
20465
+ ['color', 'width', 'opacity'].forEach(function(name) {
20466
+ var field = getGlowField(type, name);
20467
+ var val = getCommonValue(records, ids, field);
20468
+ if (val || val === 0) style[field] = val;
20469
+ });
20470
+ });
20471
+ };
20472
+
20473
+ function hidePickers() {
20474
+ rows.outer.picker.hide();
20475
+ rows.inner.picker.hide();
20476
+ }
20477
+
20478
+ function addGlowRow(type) {
20479
+ var o = makeColorRow(section, {
20480
+ label: glowTitles[type],
20481
+ onColor: function(color) {
20482
+ applyGlowColor(type, color);
20483
+ },
20484
+ onOpacity: function(value) {
20485
+ applyGlowValue(type, 'opacity', value);
20486
+ },
20487
+ revert: opts.revert
20488
+ });
20489
+ o.row.addClass('layer-glow-row layer-' + type + '-glow-row');
20490
+ El('span').appendTo(o.aside).text('Width');
20491
+ o.width = new SizeField(o.aside, {
20492
+ min: 0.5,
20493
+ max: 200,
20494
+ step: 1,
20495
+ bigStep: 5,
20496
+ decimals: 1,
20497
+ title: 'How far the glow reaches from the edge, in px',
20498
+ onSet: function(value) {
20499
+ applyGlowValue(type, 'width', value);
20500
+ },
20501
+ onStep: function(delta) {
20502
+ var shown = Number(o.width.getValue());
20503
+ if (!(shown > 0)) shown = internal.svg.DEFAULT_GLOW_WIDTH;
20504
+ applyGlowValue(type, 'width', Math.max(0.5, Math.round(shown + delta)));
20505
+ },
20506
+ onDone: opts.releaseFocus
20507
+ });
20508
+ return o;
20509
+ }
20510
+
20511
+ function applyGlowColor(type, color) {
20512
+ var edits = getGlowColorEdits(opts.getRecords(), opts.getTargetIds(), type, color, pending[type]);
20513
+ if (color) pending[type] = {};
20514
+ if (edits.length > 0) {
20515
+ opts.applyEdits(edits, color ? glowTitles[type] : 'Remove ' + glowTitles[type].toLowerCase());
20516
+ } else {
20517
+ update();
20518
+ }
20519
+ }
20520
+
20521
+ // A width or opacity goes to the features that have the glow, as the label
20522
+ // panel's halo settings go only to the labels that have a halo.
20523
+ function applyGlowValue(type, name, value) {
20524
+ var records = opts.getRecords();
20525
+ var ids = opts.getTargetIds().filter(function(id) {
20526
+ return hasGlow(records[id], type);
20527
+ });
20528
+ if (ids.length === 0) {
20529
+ pending[type][name] = value;
20530
+ update();
20531
+ return;
20532
+ }
20533
+ opts.applyEdits(ids.map(function(id) {
20534
+ return {id: id, styles: [[getGlowField(type, name), formatGlowValue(name, value)]]};
20535
+ }));
20536
+ }
20537
+
20538
+ function update() {
20539
+ var records = opts.getRecords();
20540
+ var ids = opts.getTargetIds();
20541
+ var state = getEffectsState(records, ids);
20542
+ if (state != 'off') opened = true;
20543
+ toggle.setState(state == 'off' && opened ? 'on' : state);
20544
+ section.classed('collapsed', state == 'off' && !opened);
20545
+ if (state == 'off' && !opened) hidePickers();
20546
+ glowTypes.forEach(function(type) {
20547
+ updateGlowRow(type, records, ids);
20548
+ });
20549
+ }
20550
+
20551
+ function updateGlowRow(type, records, ids) {
20552
+ var o = rows[type];
20553
+ var shown = getShownGlow(records, ids, type, pending[type]);
20554
+ o.showColor(shown.color);
20555
+ o.input.attr('placeholder', shown.mixed ? 'mixed' : '');
20556
+ o.chit.classed('mixed', shown.mixed);
20557
+ o.opacity.node().value = shown.opacity === '' ? '' : formatOpacityPct(shown.opacity);
20558
+ o.width.setValue(shown.width === '' ? '' : String(shown.width));
20559
+ o.width.setPlaceholder(shown.width === '' ? 'mixed' : '');
20560
+ }
20561
+ }
20562
+
20563
+ function getGlowField(type, name) {
20564
+ return type + '-glow-' + name;
20565
+ }
20566
+
20567
+ // In the panel, a feature has a glow when the glow's color is set.
20568
+ function hasGlow(rec, type) {
20569
+ return !!rec && !isBlank(rec[getGlowField(type, 'color')]);
20570
+ }
20571
+
20572
+ // 'on', 'off' or 'mixed', over the features @ids
20573
+ function getGlowState(records, ids, type) {
20574
+ var n = ids.filter(function(id) { return hasGlow(records[id], type); }).length;
20575
+ return n === 0 ? 'off' : n < ids.length ? 'mixed' : 'on';
20576
+ }
20577
+
20578
+ function getEffectsState(records, ids) {
20579
+ var n = ids.filter(function(id) {
20580
+ return hasGlow(records[id], 'outer') || hasGlow(records[id], 'inner');
20581
+ }).length;
20582
+ return n === 0 ? 'off' : n < ids.length ? 'mixed' : 'on';
20583
+ }
20584
+
20585
+ // Per-feature edits for a color typed or picked for one glow. A color adds the
20586
+ // glow to every feature, with the @pending width and opacity where a feature
20587
+ // has none of its own; an empty color removes the glow's color.
20588
+ function getGlowColorEdits(records, ids, type, color, pending) {
20589
+ var colorField = getGlowField(type, 'color');
20590
+ var edits = [];
20591
+ ids.forEach(function(id) {
20592
+ var rec = records[id] || {};
20593
+ var styles = [];
20594
+ if (!color) {
20595
+ if (!isBlank(rec[colorField])) styles.push([colorField, '']);
20596
+ } else {
20597
+ if (String(rec[colorField]).trim() != color) styles.push([colorField, color]);
20598
+ ['width', 'opacity'].forEach(function(name) {
20599
+ var field = getGlowField(type, name);
20600
+ if (pending && name in pending && isBlank(rec[field])) {
20601
+ var val = formatGlowValue(name, pending[name]);
20602
+ if (val !== '') styles.push([field, val]);
20603
+ }
20604
+ });
20605
+ }
20606
+ if (styles.length > 0) edits.push({id: id, styles: styles});
20607
+ });
20608
+ return edits;
20609
+ }
20610
+
20611
+ // Switching Effects off removes every glow setting, which mean nothing
20612
+ // without a color.
20613
+ function getEffectsOffEdits(records, ids) {
20614
+ var edits = [];
20615
+ ids.forEach(function(id) {
20616
+ var rec = records[id] || {};
20617
+ var styles = internal.svg.glowFields.filter(function(field) {
20618
+ return !isBlank(rec[field]);
20619
+ }).map(function(field) {
20620
+ return [field, ''];
20621
+ });
20622
+ if (styles.length > 0) edits.push({id: id, styles: styles});
20623
+ });
20624
+ return edits;
20625
+ }
20626
+
20627
+ // What a glow's row shows. The color is blank where no feature has the glow,
20628
+ // and blank and mixed where only some do or their colors differ. The width
20629
+ // and opacity are those the glowing features agree on, '' where they differ;
20630
+ // with no glowing features, they are the @pending ones, the default width and
20631
+ // no opacity.
20632
+ function getShownGlow(records, ids, type, pending) {
20633
+ var glowIds = ids.filter(function(id) { return hasGlow(records[id], type); });
20634
+ var defaultWidth = internal.svg.DEFAULT_GLOW_WIDTH;
20635
+ var color, mixed;
20636
+ pending = pending || {};
20637
+ if (glowIds.length === 0) {
20638
+ return {
20639
+ color: '',
20640
+ mixed: false,
20641
+ width: 'width' in pending ? pending.width : defaultWidth,
20642
+ opacity: 'opacity' in pending ? pending.opacity : ''
20643
+ };
20644
+ }
20645
+ color = getCommonValue(records, glowIds, getGlowField(type, 'color'), undefined, trim);
20646
+ mixed = glowIds.length < ids.length || color === '';
20647
+ return {
20648
+ color: mixed ? '' : color,
20649
+ mixed: mixed,
20650
+ width: getCommonValue(records, glowIds, getGlowField(type, 'width'), defaultWidth, Number),
20651
+ opacity: getCommonValue(records, glowIds, getGlowField(type, 'opacity'), 1, Number)
20652
+ };
20653
+ }
20654
+
20655
+ // Full opacity is the default, and stored as no opacity at all.
20656
+ function formatGlowValue(name, value) {
20657
+ return name == 'opacity' && value >= 1 ? '' : value;
20658
+ }
20659
+
20660
+ // The value of @field that the features @ids share, or '' if they differ.
20661
+ function getCommonValue(records, ids, field, defaultValue, convert) {
20662
+ var value, val;
20663
+ for (var i=0; i<ids.length; i++) {
20664
+ val = records[ids[i]] && records[ids[i]][field];
20665
+ if (isBlank(val)) val = defaultValue === undefined ? '' : defaultValue;
20666
+ if (convert && val !== '' && val !== null) val = convert(val);
20667
+ if (i === 0) {
20668
+ value = val;
20669
+ } else if (val !== value) {
20670
+ return '';
20671
+ }
20672
+ }
20673
+ return ids.length > 0 ? value : '';
20674
+ }
20675
+
20676
+ function trim(val) {
20677
+ return String(val).trim();
20678
+ }
20679
+
20349
20680
  function isBlank(val) {
20350
20681
  return val === undefined || val === null || String(val).trim() === '';
20351
20682
  }
20352
20683
 
20353
20684
  var savedStylesKey = 'layer_style_presets';
20354
- var styleFields = ['stroke', 'stroke-width', 'stroke-opacity', 'stroke-dasharray', 'line-start', 'line-end', 'line-end-size', 'fill', 'fill-opacity', 'fill-pattern'];
20685
+ var styleFields = ['stroke', 'stroke-width', 'stroke-opacity', 'stroke-dasharray', 'line-start', 'line-end', 'line-end-size', 'fill', 'fill-opacity', 'fill-pattern'].concat(internal.svg.glowFields);
20355
20686
  var arrowShapes = [{
20356
20687
  name: 'arrow',
20357
20688
  title: 'solid arrowheads'
@@ -20386,7 +20717,7 @@
20386
20717
  function LayerStyleTool(gui) {
20387
20718
  var parent = gui.container.findChild('.mshp-main-map');
20388
20719
  var panel = El('div').addClass('label-style-panel layer-style-panel rollover').appendTo(parent).hide();
20389
- var title, editingStatus, clearLink, strokeControl, fillControl, strokeWidthField, dashControl, arrowControl, randomFillBtn, presetControl, patternControl, hit;
20720
+ var title, editingStatus, clearLink, strokeControl, fillControl, strokeWidthField, dashControl, arrowControl, randomFillBtn, presetControl, patternControl, glowControl, hit;
20390
20721
  var targetLayer = null;
20391
20722
  // What the arrowhead switch turns on, for lines that have no heads
20392
20723
  var lastArrow = {shape: 'arrow', position: 'end'};
@@ -20445,6 +20776,7 @@
20445
20776
  fillControl.picker.hide();
20446
20777
  patternControl.hidePicker();
20447
20778
  patternControl.reset();
20779
+ glowControl.reset();
20448
20780
  targetLayer = null;
20449
20781
  }
20450
20782
 
@@ -20474,14 +20806,15 @@
20474
20806
  editingStatus = El('span').addClass('label-editing-status').appendTo(editRow);
20475
20807
  clearLink = El('span').addClass('label-editing-clear colored-text').appendTo(editRow).text('deselect').on('click', clearSelection);
20476
20808
 
20477
- fillControl = addColorControl(panel, 'Fill', 'fill', '');
20478
20809
  strokeControl = addColorControl(panel, 'Stroke', 'stroke', '#000000');
20479
20810
  strokeWidthField = addStrokeWidthControl(strokeControl.aside);
20811
+ fillControl = addColorControl(panel, 'Fill', 'fill', '');
20812
+ randomFillBtn = makePanelActionButton(fillControl.aside, 'Random fill', applyRandomFillColors)
20813
+ .addClass('layer-random-fill-btn');
20480
20814
  dashControl = addDashArrayControl(panel);
20481
20815
  arrowControl = addArrowControl(panel);
20482
20816
 
20483
20817
  var buttonRow = El('div').addClass('label-style-row label-panel-button-row').appendTo(panel);
20484
- randomFillBtn = makePanelActionButton(buttonRow, 'Random fills', applyRandomFillColors);
20485
20818
  makePanelActionButton(buttonRow, 'Clear style', clearLayerStyle);
20486
20819
 
20487
20820
  patternControl = new PatternFillControl(panel, {
@@ -20494,6 +20827,14 @@
20494
20827
  releaseFocus: releaseFocus
20495
20828
  });
20496
20829
 
20830
+ glowControl = new GlowEffectsControl(panel, {
20831
+ getRecords: getTargetRecords,
20832
+ getTargetIds: getTargetIds,
20833
+ applyEdits: runStyleEdits,
20834
+ revert: updateControls,
20835
+ releaseFocus: releaseFocus
20836
+ });
20837
+
20497
20838
  presetControl = new StylePresetControl(panel, {
20498
20839
  storageKey: savedStylesKey,
20499
20840
  type: getStyleType,
@@ -20515,10 +20856,9 @@
20515
20856
  var control = makeColorRow(parent, {
20516
20857
  label: label,
20517
20858
  onColor: function(color) {
20518
- // Blanking the field is not a way to unset a colour: -style reads an
20519
- // empty value as "remove this", and a field left empty by a mistyped
20520
- // hex would then clear the layer rather than say nothing.
20521
- if (color) applyColorControlStyle(control, color);
20859
+ // An emptied field unsets the colour: -style reads an empty value as
20860
+ // "remove this".
20861
+ applyColorControlStyle(control, color);
20522
20862
  },
20523
20863
  onOpacity: function(value) {
20524
20864
  applyLayerStyle(field + '-opacity', value);
@@ -20786,6 +21126,8 @@
20786
21126
  randomFillBtn.classed('hidden', geom != 'polygon');
20787
21127
  patternControl.section.classed('hidden', geom != 'polygon');
20788
21128
  if (geom == 'polygon') patternControl.update();
21129
+ glowControl.section.classed('hidden', geom != 'polygon');
21130
+ if (geom == 'polygon') glowControl.update();
20789
21131
  presetControl.render();
20790
21132
  updateSavedStyleControls();
20791
21133
  }
@@ -20846,7 +21188,7 @@
20846
21188
 
20847
21189
  function applyColorControlStyle(control, color) {
20848
21190
  var styles = [[control.field, color]];
20849
- if (control.field == 'stroke' && strokeWidthIsUnsetForTargets()) {
21191
+ if (color && control.field == 'stroke' && strokeWidthIsUnsetForTargets()) {
20850
21192
  styles.push(['stroke-width', 1]);
20851
21193
  }
20852
21194
  if (control.field == 'fill') {
@@ -20968,6 +21310,7 @@
20968
21310
  addStyleValue(style, 'fill', getControlValue(fillControl.input));
20969
21311
  addStyleValue(style, 'fill-opacity', parseOpacityValue(fillControl.opacity.node().value));
20970
21312
  addStyleValue(style, 'fill-pattern', patternControl.getCode(getControlValue(fillControl.input)));
21313
+ glowControl.addStyleValues(style);
20971
21314
  }
20972
21315
  return style;
20973
21316
  }
@@ -21079,9 +21422,13 @@
21079
21422
  updateControls();
21080
21423
  }
21081
21424
 
21425
+ // An empty layer has nothing for the controls to act on, so they are shown
21426
+ // disabled rather than left to do nothing.
21082
21427
  function updateEditingStatus(count) {
21083
- editingStatus.text(count > 0 ? 'Editing: ' + count + ' selected' : 'Editing: all');
21428
+ var total = targetLayer ? internal.getFeatureCount(targetLayer) : 0;
21429
+ editingStatus.text(formatEditingStatus({selected: count, total: total}));
21084
21430
  clearLink.classed('hidden', count === 0);
21431
+ panel.classed('no-targets', total === 0);
21085
21432
  }
21086
21433
 
21087
21434
  function getActiveLayer() {
@@ -21308,8 +21655,17 @@
21308
21655
  var control = makeColorRow(circlesSection, {
21309
21656
  label: label,
21310
21657
  // Every circle property is applied together, from what the controls are
21311
- // showing: the fields are one style, not five.
21312
- onColor: applyCircleStyles,
21658
+ // showing: the fields are one style, not five. An emptied colour field
21659
+ // unsets the colour, which applying the fields would leave alone.
21660
+ onColor: function(color) {
21661
+ if (color) {
21662
+ applyCircleStyles();
21663
+ } else if (!styleFieldIsUnset(field)) {
21664
+ runStyleCommand([field + '=' + quoteCommandValue('')], 'Remove circle ' + field);
21665
+ } else {
21666
+ updateControls();
21667
+ }
21668
+ },
21313
21669
  onOpacity: applyCircleStyles,
21314
21670
  revert: updateControls
21315
21671
  });
@@ -21659,9 +22015,13 @@
21659
22015
  updateControls();
21660
22016
  }
21661
22017
 
22018
+ // As in the line and polygon panels: an empty layer's controls are shown
22019
+ // disabled rather than left to do nothing.
21662
22020
  function updateEditingStatus(count) {
21663
- editingStatus.text(count > 0 ? 'Editing: ' + count + ' selected' : 'Editing: all');
22021
+ var total = targetLayer ? internal.getFeatureCount(targetLayer) : 0;
22022
+ editingStatus.text(formatEditingStatus({selected: count, total: total}));
21664
22023
  clearLink.classed('hidden', count === 0);
22024
+ panel.classed('no-targets', total === 0);
21665
22025
  }
21666
22026
 
21667
22027
  function setCircleColor(control, color) {
@@ -29259,6 +29619,7 @@
29259
29619
  selection.turnOff();
29260
29620
  abandonCurve();
29261
29621
  armed = null;
29622
+ setLabelToolArmed(gui, null);
29262
29623
  updateButtons();
29263
29624
  hideInstructions();
29264
29625
  drag = null;
@@ -29324,6 +29685,7 @@
29324
29685
  if (armed == mode) return;
29325
29686
  if (drawingCurve()) abandonCurve();
29326
29687
  armed = mode;
29688
+ setLabelToolArmed(gui, mode);
29327
29689
  updateButtons();
29328
29690
  showInstructions();
29329
29691
  }
@@ -31080,6 +31442,11 @@
31080
31442
  // rest, because the style object is reused from one feature to the next.
31081
31443
  var arrowFields = getLineArrowFields(lyr);
31082
31444
  var hasStrokeFields = fields.includes('stroke') || fields.includes('stroke-width');
31445
+ // Glows are drawn from the record rather than set as attributes, like the
31446
+ // arrowheads. An outer glow that the whole layer shares is drawn once for the
31447
+ // layer (see svg-glow.mjs), so the shapes are then given none of their own.
31448
+ var hasGlows = layerHasGlowFields(lyr);
31449
+ var layerOuterGlow = hasGlows ? internal.svg.getLayerOuterGlow(lyr) : null;
31083
31450
 
31084
31451
  var styler = function(style, i) {
31085
31452
  var rec = records[i];
@@ -31089,6 +31456,10 @@
31089
31456
  style.lineEnd = rec && rec['line-end'];
31090
31457
  style.lineEndSize = rec && rec['line-end-size'];
31091
31458
  }
31459
+ if (hasGlows) {
31460
+ style.outerGlow = layerOuterGlow ? null : internal.svg.getPolygonGlow(rec, 'outer');
31461
+ style.innerGlow = internal.svg.getPolygonGlow(rec, 'inner');
31462
+ }
31092
31463
  for (var j=0; j<fields.length; j++) {
31093
31464
  fname = fields[j];
31094
31465
  val = rec && rec[fname];
@@ -31110,6 +31481,12 @@
31110
31481
  }
31111
31482
  };
31112
31483
  var style = {styler: styler, type: 'styled'};
31484
+ if (hasGlows) {
31485
+ style.layerOuterGlow = layerOuterGlow;
31486
+ // How far past its shapes the layer draws, which is how far outside the
31487
+ // view a shape can be and still reach into it.
31488
+ style.glowReach = internal.svg.getMaxGlowWidth(records) * internal.svg.GLOW_REACH;
31489
+ }
31113
31490
  // A line layer styled with nothing but arrowheads is drawn the way SVG
31114
31491
  // export draws it, with the black 1px line the layer's group gives it.
31115
31492
  if (arrowFields && !hasStrokeFields) {
@@ -31131,7 +31508,14 @@
31131
31508
  return fields.includes('fill') || fields.includes('r'); // support colored squares
31132
31509
  }
31133
31510
  return utils$1.difference(fields, ['opacity', 'class']).length > 0 ||
31134
- !!getLineArrowFields(lyr);
31511
+ !!getLineArrowFields(lyr) || layerHasGlowFields(lyr);
31512
+ }
31513
+
31514
+ function layerHasGlowFields(lyr) {
31515
+ if (lyr.geometry_type != 'polygon' || !lyr.data) return false;
31516
+ return lyr.data.getFields().some(function(f) {
31517
+ return internal.svg.glowFields.includes(f);
31518
+ });
31135
31519
  }
31136
31520
 
31137
31521
  // The arrowhead fields a line layer has, or null
@@ -32048,6 +32432,102 @@
32048
32432
  }
32049
32433
  }
32050
32434
 
32435
+ // Canvas rendering of polygon glows, matching the SVG filters in svg-glow.mjs.
32436
+ //
32437
+ // What glows is drawn first into a scratch canvas: one shape, or a whole layer
32438
+ // for a layer's outer glow. The glows are then made from that image's alpha,
32439
+ // the way a filter works from SourceAlpha:
32440
+ //
32441
+ // outer the image's shadow, with the image itself cut out of it
32442
+ // inner the shadow of the image's inverse, kept only inside the image
32443
+ //
32444
+ // A shadow is only drawn with the image that casts it, so the image is drawn
32445
+ // off the edge of the canvas and its shadow is offset back into place. Drawn
32446
+ // in place and clipped, the image leaks into its own antialiased edge.
32447
+ //
32448
+ // A shadow's shadowBlur is twice the standard deviation of its blur, and a
32449
+ // glow's width is twice the standard deviation of the filter's, so the two
32450
+ // are the same number. Shadows are drawn in device pixels whatever the
32451
+ // transform, and the scratch canvases are never transformed when one is.
32452
+
32453
+ var scratch = {};
32454
+
32455
+ function getScratchCanvas(name, w, h) {
32456
+ var o = scratch[name];
32457
+ if (!o) {
32458
+ o = scratch[name] = {canvas: document.createElement('canvas')};
32459
+ o.ctx = o.canvas.getContext('2d');
32460
+ }
32461
+ // Grown and never shrunk: resizing a canvas reallocates it, and there can
32462
+ // be a resize for every shape drawn.
32463
+ if (o.canvas.width < w || o.canvas.height < h) {
32464
+ o.canvas.width = Math.max(o.canvas.width, w);
32465
+ o.canvas.height = Math.max(o.canvas.height, h);
32466
+ }
32467
+ o.ctx.setTransform(1, 0, 0, 1, 0, 0);
32468
+ o.ctx.clearRect(0, 0, w, h);
32469
+ return o;
32470
+ }
32471
+
32472
+ // A canvas to draw what glows into, @w by @h device pixels. Separate from the
32473
+ // ones compositeGlows() uses, so a shape can be drawn into it while a layer is
32474
+ // being drawn into another.
32475
+ // @name: 'shape' or 'layer'
32476
+ function getGlowSourceCanvas(name, w, h) {
32477
+ return getScratchCanvas('source-' + name, w, h);
32478
+ }
32479
+
32480
+ // How far past a shape its glows reach, in device pixels.
32481
+ function getGlowMargin(outer, inner, pxScale) {
32482
+ var width = Math.max(outer ? outer.width : 0, inner ? inner.width : 0);
32483
+ return Math.ceil(width * pxScale * internal.svg.GLOW_REACH) + 2;
32484
+ }
32485
+
32486
+ // Draws @src (the top-left @w by @h pixels of a canvas) into @ctx at @x, @y,
32487
+ // with its glows: the outer one underneath and the inner one on top.
32488
+ // @pxScale: device pixels per px of glow width
32489
+ function compositeGlows(ctx, src, x, y, w, h, outer, inner, pxScale) {
32490
+ var glow, inverse;
32491
+ if (outer) {
32492
+ glow = getScratchCanvas('glow', w, h);
32493
+ drawShadow(glow.ctx, src, w, h, outer, pxScale);
32494
+ glow.ctx.globalCompositeOperation = 'destination-out';
32495
+ glow.ctx.drawImage(src, 0, 0, w, h, 0, 0, w, h);
32496
+ glow.ctx.globalCompositeOperation = 'source-over';
32497
+ ctx.drawImage(glow.canvas, 0, 0, w, h, x, y, w, h);
32498
+ }
32499
+ ctx.drawImage(src, 0, 0, w, h, x, y, w, h);
32500
+ if (inner) {
32501
+ inverse = getScratchCanvas('inverse', w, h);
32502
+ inverse.ctx.fillStyle = '#000';
32503
+ inverse.ctx.fillRect(0, 0, w, h);
32504
+ inverse.ctx.globalCompositeOperation = 'destination-out';
32505
+ inverse.ctx.drawImage(src, 0, 0, w, h, 0, 0, w, h);
32506
+ inverse.ctx.globalCompositeOperation = 'source-over';
32507
+ glow = getScratchCanvas('glow', w, h);
32508
+ drawShadow(glow.ctx, inverse.canvas, w, h, inner, pxScale);
32509
+ glow.ctx.globalCompositeOperation = 'destination-in';
32510
+ glow.ctx.drawImage(src, 0, 0, w, h, 0, 0, w, h);
32511
+ glow.ctx.globalCompositeOperation = 'source-over';
32512
+ ctx.drawImage(glow.canvas, 0, 0, w, h, x, y, w, h);
32513
+ }
32514
+ }
32515
+
32516
+ // The shadow of the top-left @w by @h pixels of @img, with no image. The glow's
32517
+ // opacity is globalAlpha rather than part of the shadow's color, which can be
32518
+ // any CSS color and would have to be parsed to take one.
32519
+ function drawShadow(ctx, img, w, h, glow, pxScale) {
32520
+ var offset = w + glow.width * pxScale * 4 + 10;
32521
+ ctx.save();
32522
+ ctx.globalAlpha = glow.opacity;
32523
+ ctx.shadowColor = glow.color;
32524
+ ctx.shadowBlur = glow.width * pxScale;
32525
+ ctx.shadowOffsetX = offset;
32526
+ ctx.shadowOffsetY = 0;
32527
+ ctx.drawImage(img, 0, 0, w, h, -offset, 0, w, h);
32528
+ ctx.restore();
32529
+ }
32530
+
32051
32531
  // TODO: consider moving this upstream
32052
32532
  function getArcsForRendering(lyr, ext) {
32053
32533
  var dataset = lyr.gui.source.dataset;
@@ -32098,7 +32578,7 @@
32098
32578
  }
32099
32579
  } else {
32100
32580
  arcs = getArcsForRendering(lyr, ext);
32101
- filter = getShapeFilter(arcs, layer.shapes, ext);
32581
+ filter = getShapeFilter(arcs, layer.shapes, ext, style.overlay ? 0 : style.glowReach);
32102
32582
  canv.drawStyledPaths(layer.shapes, arcs, style, filter);
32103
32583
  if (style.vertices || style.vertex_overlay || style.pending_snip) {
32104
32584
  canv.drawVertices(layer.shapes, arcs, style, filter);
@@ -32136,10 +32616,17 @@
32136
32616
  // view. The filter takes a shape index and tests the shape's bbox against
32137
32617
  // the viewport. At nearly full extent the test is a no-op, so we return
32138
32618
  // null to let the caller skip it entirely.
32139
- function getShapeFilter(arcs, shapes, ext) {
32619
+ // reach: how far past its shapes the layer draws, in px (e.g. a glow)
32620
+ function getShapeFilter(arcs, shapes, ext, reach) {
32140
32621
  if (ext.scale() < 1.1) return null;
32141
32622
  var view = ext.getBounds();
32142
32623
  var b = new Bounds$1();
32624
+ var pad;
32625
+ if (reach > 0) {
32626
+ pad = reach * (ext.getSymbolScale() || 1) * ext.getPixelSize();
32627
+ view = new Bounds$1(view.toArray());
32628
+ view.padBounds(pad, pad, pad, pad);
32629
+ }
32143
32630
  return function(i) {
32144
32631
  var shp = shapes[i];
32145
32632
  if (!shp) return false;
@@ -32303,52 +32790,136 @@
32303
32790
  var key, item, shp;
32304
32791
  var styler = style.styler || null;
32305
32792
  var drawStyle = styler ? utils$1.defaults({}, style) : style;
32306
- for (var i=0; i<shapes.length; i++) {
32307
- shp = shapes[i];
32308
- if (!shp || filter && !filter(i)) continue;
32309
- if (styler) {
32310
- styler(drawStyle, i);
32311
- }
32312
- // A line with arrowheads is drawn on its own, trimmed to meet its heads;
32313
- // see svg-line-arrows.mjs. Only lines styled with line-start or line-end
32314
- // get here, so the batch below is untouched for everything else.
32315
- if (drawStyle.lineStart || drawStyle.lineEnd) {
32316
- if (!arrowPencil) arrowPencil = getArrowLinePencil(arcs, _ext, lineScale);
32317
- if (arrowPencil(shp, _ctx, drawStyle, startPath, draw)) continue;
32318
- }
32319
- if (!drawStyle.batchOverlay && (drawStyle.overlay ||
32320
- drawStyle.opacity < 1 || drawStyle.fillOpacity < 1 ||
32321
- drawStyle.strokeOpacity < 1 || drawStyle.fillEffect)) {
32322
- // don't batch shapes with opacity, in case they overlap
32323
- drawPaths([shp], startPath, draw, drawStyle);
32324
- continue;
32325
- }
32326
- key = getStyleKey(drawStyle);
32327
- if (key in styleIndex === false) {
32328
- styleIndex[key] = {
32329
- style: utils$1.defaults({}, drawStyle),
32330
- shapes: []
32331
- };
32793
+ // Hover and selection overlays copy the layer's style; they do not glow.
32794
+ var glows = !style.overlay;
32795
+ var glowScale = getCanvasStrokeScale(GUI.getPixelRatio(), lineScale);
32796
+ var layerGlow = glows && style.layerOuterGlow ?
32797
+ startLayerGlow(style.layerOuterGlow, glowScale) : null;
32798
+ try {
32799
+ drawShapes();
32800
+ } finally {
32801
+ if (layerGlow) endLayerGlow(layerGlow);
32802
+ }
32803
+ if (layerGlow) compositeLayerGlow(layerGlow);
32804
+
32805
+ function drawShapes() {
32806
+ var glowPencil = null;
32807
+ for (var i=0; i<shapes.length; i++) {
32808
+ shp = shapes[i];
32809
+ if (!shp || filter && !filter(i)) continue;
32810
+ if (styler) {
32811
+ styler(drawStyle, i);
32812
+ }
32813
+ if (glows && (drawStyle.outerGlow || drawStyle.innerGlow)) {
32814
+ // Shapes before this one are drawn first, as they are in SVG, where
32815
+ // a shape's outer glow lies over the shapes that precede it.
32816
+ flushBatches();
32817
+ if (!glowPencil) glowPencil = getGlowShapePencil(arcs, startPath, draw, glowScale);
32818
+ glowPencil(shp, drawStyle);
32819
+ continue;
32820
+ }
32821
+ // A line with arrowheads is drawn on its own, trimmed to meet its heads;
32822
+ // see svg-line-arrows.mjs. Only lines styled with line-start or line-end
32823
+ // get here, so the batch below is untouched for everything else.
32824
+ if (drawStyle.lineStart || drawStyle.lineEnd) {
32825
+ if (!arrowPencil) arrowPencil = getArrowLinePencil(arcs, _ext, lineScale);
32826
+ if (arrowPencil(shp, _ctx, drawStyle, startPath, draw)) continue;
32827
+ }
32828
+ if (!drawStyle.batchOverlay && (drawStyle.overlay ||
32829
+ drawStyle.opacity < 1 || drawStyle.fillOpacity < 1 ||
32830
+ drawStyle.strokeOpacity < 1 || drawStyle.fillEffect)) {
32831
+ // don't batch shapes with opacity, in case they overlap
32832
+ drawPaths([shp], startPath, draw, drawStyle);
32833
+ continue;
32834
+ }
32835
+ key = getStyleKey(drawStyle);
32836
+ if (key in styleIndex === false) {
32837
+ styleIndex[key] = {
32838
+ style: utils$1.defaults({}, drawStyle),
32839
+ shapes: []
32840
+ };
32841
+ }
32842
+ item = styleIndex[key];
32843
+ item.shapes.push(shp);
32844
+ if (item.shapes.length >= batchSize) {
32845
+ drawPaths(item.shapes, startPath, draw, item.style);
32846
+ item.shapes = [];
32847
+ }
32332
32848
  }
32333
- item = styleIndex[key];
32334
- item.shapes.push(shp);
32335
- if (item.shapes.length >= batchSize) {
32849
+ flushBatches();
32850
+ }
32851
+
32852
+ function flushBatches() {
32853
+ Object.keys(styleIndex).forEach(function(key) {
32854
+ var item = styleIndex[key];
32855
+ if (item.shapes.length === 0) return;
32336
32856
  drawPaths(item.shapes, startPath, draw, item.style);
32337
32857
  item.shapes = [];
32338
- }
32858
+ });
32339
32859
  }
32340
- Object.keys(styleIndex).forEach(function(key) {
32341
- var item = styleIndex[key];
32342
- drawPaths(item.shapes, startPath, draw, item.style);
32343
- });
32344
32860
  };
32345
32861
 
32346
- function drawPaths(shapes, beginPath, drawShape, style) {
32347
- beginPath(_ctx, style);
32862
+ function drawPaths(shapes, beginPath, drawShape, style, ctx) {
32863
+ ctx = ctx || _ctx;
32864
+ beginPath(ctx, style);
32348
32865
  for (var i=0, n=shapes.length; i<n; i++) {
32349
- drawShape(shapes[i], _ctx, style);
32866
+ drawShape(shapes[i], ctx, style);
32350
32867
  }
32351
- endPath(_ctx, style);
32868
+ endPath(ctx, style);
32869
+ }
32870
+
32871
+ // A layer's own outer glow is made from the whole layer, so the layer is
32872
+ // drawn into a canvas of its own and then onto this one with its glow. The
32873
+ // canvas is larger than this one by the reach of the glow, so that shapes
32874
+ // just outside the view still cast theirs into it.
32875
+ function startLayerGlow(glow, pxScale) {
32876
+ var margin = getGlowMargin(glow, null, pxScale);
32877
+ var w = _canvas.width + margin * 2;
32878
+ var h = _canvas.height + margin * 2;
32879
+ var src = getGlowSourceCanvas('layer', w, h);
32880
+ var o = {glow: glow, pxScale: pxScale, margin: margin, w: w, h: h,
32881
+ src: src, ctx: _ctx};
32882
+ src.ctx.setTransform(1, 0, 0, 1, margin, margin);
32883
+ _ctx = src.ctx;
32884
+ return o;
32885
+ }
32886
+
32887
+ function endLayerGlow(o) {
32888
+ o.src.ctx.setTransform(1, 0, 0, 1, 0, 0);
32889
+ _ctx = o.ctx;
32890
+ }
32891
+
32892
+ function compositeLayerGlow(o) {
32893
+ compositeGlows(_ctx, o.src.canvas, -o.margin, -o.margin, o.w, o.h,
32894
+ o.glow, null, o.pxScale);
32895
+ }
32896
+
32897
+ // Draws one shape with its own glows, which are made from the shape alone.
32898
+ // The shape is drawn into a canvas the size of its bounding box and the
32899
+ // reach of its glows, cut down to the part that can be seen.
32900
+ function getGlowShapePencil(arcs, startPath, draw, pxScale) {
32901
+ var t = getScaledTransform(_ext);
32902
+ var b = new Bounds$1();
32903
+ return function(shp, style) {
32904
+ var outer = style.outerGlow, inner = style.innerGlow;
32905
+ var margin = getGlowMargin(outer, inner, pxScale) +
32906
+ Math.ceil((style.strokeWidth > 0 ? style.strokeWidth * pxScale : 0) / 2);
32907
+ var viewMargin = margin * 2;
32908
+ var x0, y0, x1, y1, src;
32909
+ b.empty();
32910
+ arcs.getMultiShapeBounds(shp, b);
32911
+ if (!b.hasBounds()) return;
32912
+ x0 = Math.floor(Math.max(b.xmin * t.mx + t.bx - margin, -viewMargin));
32913
+ x1 = Math.ceil(Math.min(b.xmax * t.mx + t.bx + margin, _canvas.width + viewMargin));
32914
+ y0 = Math.floor(Math.max(b.ymax * t.my + t.by - margin, -viewMargin));
32915
+ y1 = Math.ceil(Math.min(b.ymin * t.my + t.by + margin, _canvas.height + viewMargin));
32916
+ if (x1 <= x0 || y1 <= y0) return;
32917
+ src = getGlowSourceCanvas('shape', x1 - x0, y1 - y0);
32918
+ src.ctx.setTransform(1, 0, 0, 1, -x0, -y0);
32919
+ drawPaths([shp], startPath, draw, style, src.ctx);
32920
+ src.ctx.setTransform(1, 0, 0, 1, 0, 0);
32921
+ compositeGlows(_ctx, src.canvas, x0, y0, x1 - x0, y1 - y0, outer, inner, pxScale);
32922
+ };
32352
32923
  }
32353
32924
 
32354
32925
  _self.drawSquareDots = function(shapes, style) {
@@ -36034,8 +36605,9 @@
36034
36605
  var appearance = makePanelSection(form, 'Appearance');
36035
36606
  backgroundControl = makeColorRow(appearance, {
36036
36607
  label: 'Background',
36608
+ // An emptied colour field unsets the colour.
36037
36609
  onColor: function(color) {
36038
- if (color) applyFrameStyle([['fill', color]]);
36610
+ applyFrameStyle([['fill', color]]);
36039
36611
  },
36040
36612
  onOpacity: function(value) {
36041
36613
  applyFrameStyle([['fill-opacity', value]]);
@@ -36045,9 +36617,8 @@
36045
36617
  neatlineControl = makeColorRow(appearance, {
36046
36618
  label: 'Neatline',
36047
36619
  onColor: function(color) {
36048
- if (!color) return;
36049
36620
  var styles = [['stroke', color]];
36050
- if (!getStyleValue('stroke-width')) styles.push(['stroke-width', 1]);
36621
+ if (color && !getStyleValue('stroke-width')) styles.push(['stroke-width', 1]);
36051
36622
  applyFrameStyle(styles);
36052
36623
  },
36053
36624
  onOpacity: function(value) {
@@ -36427,7 +36998,10 @@
36427
36998
  colorInput = El('input').attr('type', 'text').attr('aria-label', 'Scale bar color')
36428
36999
  .on('change', function() {
36429
37000
  var color = colorInput.node().value.trim();
36430
- if (!color) {
37001
+ var d = getScalebarRecord();
37002
+ // An emptied field unsets the colour, so the scalebar is drawn in
37003
+ // the default one.
37004
+ if (!color && !(d && d.color)) {
36431
37005
  updateControls();
36432
37006
  return;
36433
37007
  }