mapshaper 0.7.71 → 0.7.74
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/mapshaper.js +3366 -832
- package/package.json +6 -1
- package/www/index.html +2 -3
- package/www/mapshaper-gui.js +3202 -265
- package/www/mapshaper.js +3366 -832
- package/www/page.css +467 -36
package/www/mapshaper-gui.js
CHANGED
|
@@ -2577,7 +2577,7 @@
|
|
|
2577
2577
|
return obj === Object(obj); // via underscore
|
|
2578
2578
|
}
|
|
2579
2579
|
|
|
2580
|
-
function clamp$
|
|
2580
|
+
function clamp$7(val, min, max) {
|
|
2581
2581
|
return val < min ? min : (val > max ? max : val);
|
|
2582
2582
|
}
|
|
2583
2583
|
|
|
@@ -2595,7 +2595,7 @@
|
|
|
2595
2595
|
}
|
|
2596
2596
|
|
|
2597
2597
|
// Similar to isFinite() but does not coerce strings or other types
|
|
2598
|
-
function isFiniteNumber(val) {
|
|
2598
|
+
function isFiniteNumber$1(val) {
|
|
2599
2599
|
return isValidNumber(val) && val !== Infinity && val !== -Infinity;
|
|
2600
2600
|
}
|
|
2601
2601
|
|
|
@@ -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
|
|
|
@@ -3076,7 +3076,7 @@
|
|
|
3076
3076
|
return fmt;
|
|
3077
3077
|
}
|
|
3078
3078
|
|
|
3079
|
-
function shuffle(arr) {
|
|
3079
|
+
function shuffle$1(arr) {
|
|
3080
3080
|
var tmp, i, j;
|
|
3081
3081
|
for (i = arr.length - 1; i > 0; i--) {
|
|
3082
3082
|
j = Math.floor(Math.random() * (i + 1));
|
|
@@ -3228,7 +3228,7 @@
|
|
|
3228
3228
|
function findValueByRank(arr, rank) {
|
|
3229
3229
|
if (!arr.length || rank < 1 || rank > arr.length) error$1("[findValueByRank()] invalid input");
|
|
3230
3230
|
|
|
3231
|
-
rank = clamp$
|
|
3231
|
+
rank = clamp$7(rank | 0, 1, arr.length);
|
|
3232
3232
|
var k = rank - 1, // conv. rank to array index
|
|
3233
3233
|
n = arr.length,
|
|
3234
3234
|
l = 0,
|
|
@@ -3602,7 +3602,7 @@
|
|
|
3602
3602
|
// self-import and the resulting Rollup circular-dependency warning.
|
|
3603
3603
|
var utils = {
|
|
3604
3604
|
addThousandsSep, addslashes, arrayToIndex,
|
|
3605
|
-
clamp: clamp$
|
|
3605
|
+
clamp: clamp$7, cleanNumericString, contains, copyElements, countValues, createBuffer,
|
|
3606
3606
|
defaults, difference,
|
|
3607
3607
|
endsWith, every, expandoBuffer, extend, extendBuffer,
|
|
3608
3608
|
find, findMedian, findQuantile, findRankByValue, findStringPrefix,
|
|
@@ -3613,7 +3613,7 @@
|
|
|
3613
3613
|
getSortedIds, getUniqueName, groupBy,
|
|
3614
3614
|
htmlEscape,
|
|
3615
3615
|
indexOf, indexOn, inherit, initializeArray, intersection,
|
|
3616
|
-
isArray, isArrayLike, isBoolean, isDate, isEven, isFiniteNumber, isFunction,
|
|
3616
|
+
isArray, isArrayLike, isBoolean, isDate, isEven, isFiniteNumber: isFiniteNumber$1, isFunction,
|
|
3617
3617
|
isInteger, isNonNegNumber, isNumber, isObject, isOdd, isPromise, isString,
|
|
3618
3618
|
isValidNumber,
|
|
3619
3619
|
lpad, ltrim,
|
|
@@ -3624,8 +3624,8 @@
|
|
|
3624
3624
|
quicksort, quicksortPartition,
|
|
3625
3625
|
range, regexEscape, reorderArray: reorderArray$1, repeat, repeatString,
|
|
3626
3626
|
replaceArray, rpad, rtrim,
|
|
3627
|
-
shuffle, some, sortArrayIndex, sortOn, splitLines, sum,
|
|
3628
|
-
toArray, toBuffer, trim, trimQuotes,
|
|
3627
|
+
shuffle: shuffle$1, some, sortArrayIndex, sortOn, splitLines, sum,
|
|
3628
|
+
toArray, toBuffer, trim: trim$1, trimQuotes,
|
|
3629
3629
|
uniq, uniqifyNames,
|
|
3630
3630
|
wildcardToRegExp
|
|
3631
3631
|
};
|
|
@@ -9527,8 +9527,11 @@
|
|
|
9527
9527
|
var globals = {}; // share user-defined globals (job.defs) between runs
|
|
9528
9528
|
var sharedVars = {}; // share -vars / -defaults templating scope between runs
|
|
9529
9529
|
|
|
9530
|
+
var activeSession = null; // see createCommandSession()
|
|
9531
|
+
|
|
9530
9532
|
// expose this function, so other components can run commands (e.g. box tool)
|
|
9531
9533
|
this.runMapshaperCommands = runMapshaperCommands;
|
|
9534
|
+
this.createCommandSession = createCommandSession;
|
|
9532
9535
|
|
|
9533
9536
|
// Open the console (if closed) and run a command, as if the user had
|
|
9534
9537
|
// typed it. Used by UI controls that surface console functionality, e.g.
|
|
@@ -9924,8 +9927,118 @@
|
|
|
9924
9927
|
}
|
|
9925
9928
|
}
|
|
9926
9929
|
|
|
9927
|
-
|
|
9930
|
+
// A command session is for a panel that applies its edits as they are made
|
|
9931
|
+
// (a live preview) but should leave one undo state and one history command
|
|
9932
|
+
// for the whole set: the commands run with session.run() share one undo
|
|
9933
|
+
// transaction, which keeps the state from before the first of them; each
|
|
9934
|
+
// one replaces the session's previous command in the session history; and
|
|
9935
|
+
// finish() adds the transaction to the undo history as one state.
|
|
9936
|
+
//
|
|
9937
|
+
// Nothing else may add an undo state while a session's is still to come, or
|
|
9938
|
+
// undoing that state would also take back the session's edits without
|
|
9939
|
+
// saying so. So another command finishes the session before it runs, and
|
|
9940
|
+
// Undo and Redo finish it before they act (Undo#addPendingCommit()). The
|
|
9941
|
+
// panel finishes it when it closes. A finished session can be run again,
|
|
9942
|
+
// which starts a new set.
|
|
9943
|
+
//
|
|
9944
|
+
// opts.label the undo state's label
|
|
9945
|
+
// opts.onCommit called as the state is added; may return {onUndo, onRedo},
|
|
9946
|
+
// called after the state is undone or redone, for GUI state
|
|
9947
|
+
// that goes with the edits
|
|
9948
|
+
function createCommandSession(optsArg) {
|
|
9949
|
+
var opts = optsArg || {};
|
|
9950
|
+
var busy = Promise.resolve();
|
|
9951
|
+
var queued = null;
|
|
9952
|
+
var unregisterCommit = null;
|
|
9953
|
+
var session = {
|
|
9954
|
+
tx: null,
|
|
9955
|
+
historyIds: [],
|
|
9956
|
+
flags: {},
|
|
9957
|
+
// Runs a command string. A command submitted while an earlier one is
|
|
9958
|
+
// still running waits for it, and replaces any other that is waiting,
|
|
9959
|
+
// so that a burst of edits runs the first and the last of them.
|
|
9960
|
+
run: function(cmd, done) {
|
|
9961
|
+
if (queued) {
|
|
9962
|
+
queued.cmd = cmd;
|
|
9963
|
+
queued.callbacks.push(done);
|
|
9964
|
+
return;
|
|
9965
|
+
}
|
|
9966
|
+
queued = {cmd: cmd, callbacks: [done]};
|
|
9967
|
+
busy = busy.then(function() {
|
|
9968
|
+
var job = queued;
|
|
9969
|
+
queued = null;
|
|
9970
|
+
return new Promise(function(resolve) {
|
|
9971
|
+
runMapshaperCommands(job.cmd, function(err, flags) {
|
|
9972
|
+
job.callbacks.forEach(function(cb) {
|
|
9973
|
+
if (cb) cb(err, flags);
|
|
9974
|
+
});
|
|
9975
|
+
resolve();
|
|
9976
|
+
}, {session: session});
|
|
9977
|
+
});
|
|
9978
|
+
});
|
|
9979
|
+
},
|
|
9980
|
+
finish: function() {
|
|
9981
|
+
return busy.then(commit);
|
|
9982
|
+
},
|
|
9983
|
+
isPending: function() {
|
|
9984
|
+
return activeSession == session;
|
|
9985
|
+
},
|
|
9986
|
+
// for runMapshaperCommands()
|
|
9987
|
+
activate: activate
|
|
9988
|
+
};
|
|
9989
|
+
return session;
|
|
9990
|
+
|
|
9991
|
+
function commit() {
|
|
9992
|
+
var tx = session.tx,
|
|
9993
|
+
historyIds = session.historyIds,
|
|
9994
|
+
flags = session.flags,
|
|
9995
|
+
callbacks;
|
|
9996
|
+
session.tx = null;
|
|
9997
|
+
session.historyIds = [];
|
|
9998
|
+
session.flags = {};
|
|
9999
|
+
if (activeSession == session) activeSession = null;
|
|
10000
|
+
if (unregisterCommit) unregisterCommit();
|
|
10001
|
+
unregisterCommit = null;
|
|
10002
|
+
if (!tx || !isCommandUndoEnabled()) return;
|
|
10003
|
+
callbacks = opts.onCommit ? opts.onCommit() || {} : {};
|
|
10004
|
+
return getStoredUndoHistory(gui).addTransaction(tx, {
|
|
10005
|
+
flags: flags,
|
|
10006
|
+
entryPrefix: 'command-session',
|
|
10007
|
+
onUndo: function() {
|
|
10008
|
+
gui.session.setCommandsActive(historyIds, false);
|
|
10009
|
+
if (callbacks.onUndo) callbacks.onUndo();
|
|
10010
|
+
},
|
|
10011
|
+
onRedo: function() {
|
|
10012
|
+
gui.session.setCommandsActive(historyIds, true);
|
|
10013
|
+
if (callbacks.onRedo) callbacks.onRedo();
|
|
10014
|
+
}
|
|
10015
|
+
}).catch(function(e) {
|
|
10016
|
+
console.error(e);
|
|
10017
|
+
consoleWarn('Undo state was not saved:', e.message || e);
|
|
10018
|
+
});
|
|
10019
|
+
}
|
|
10020
|
+
|
|
10021
|
+
function activate() {
|
|
10022
|
+
if (activeSession == session) return;
|
|
10023
|
+
activeSession = session;
|
|
10024
|
+
session.tx = createCommandUndoTransaction(opts.label || '');
|
|
10025
|
+
if (gui.undo && gui.undo.addPendingCommit) {
|
|
10026
|
+
unregisterCommit = gui.undo.addPendingCommit(session.finish);
|
|
10027
|
+
}
|
|
10028
|
+
}
|
|
10029
|
+
|
|
10030
|
+
}
|
|
10031
|
+
|
|
10032
|
+
function runMapshaperCommands(str, done, runOpts) {
|
|
10033
|
+
var session = runOpts && runOpts.session || null;
|
|
9928
10034
|
var commands;
|
|
10035
|
+
if (activeSession && activeSession != session) {
|
|
10036
|
+
activeSession.finish().then(function() {
|
|
10037
|
+
runMapshaperCommands(str, done, runOpts);
|
|
10038
|
+
});
|
|
10039
|
+
return;
|
|
10040
|
+
}
|
|
10041
|
+
if (session) session.activate();
|
|
9929
10042
|
try {
|
|
9930
10043
|
commands = internal.parseConsoleCommands(str);
|
|
9931
10044
|
// don't add info commands to console history
|
|
@@ -9936,7 +10049,7 @@
|
|
|
9936
10049
|
return done(e, {});
|
|
9937
10050
|
}
|
|
9938
10051
|
if (commands.length === 0) return done();
|
|
9939
|
-
applyParsedCommands(commands, str, function(err, flags) {
|
|
10052
|
+
applyParsedCommands(commands, str, session, function(err, flags) {
|
|
9940
10053
|
if (flags) {
|
|
9941
10054
|
model.updated(flags); // info commands do not return flags
|
|
9942
10055
|
}
|
|
@@ -9962,13 +10075,13 @@
|
|
|
9962
10075
|
});
|
|
9963
10076
|
}
|
|
9964
10077
|
|
|
9965
|
-
function applyParsedCommands(commands, commandString, done) {
|
|
10078
|
+
function applyParsedCommands(commands, commandString, session, done) {
|
|
9966
10079
|
var active = model.getActiveLayer(),
|
|
9967
10080
|
prevArcs = active?.dataset.arcs,
|
|
9968
10081
|
prevTable = active?.layer.data,
|
|
9969
10082
|
prevTableSize = prevTable ? prevTable.size() : 0,
|
|
9970
10083
|
prevArcCount = prevArcs ? prevArcs.size() : 0,
|
|
9971
|
-
undoTransaction = createCommandUndoTransaction(commandString),
|
|
10084
|
+
undoTransaction = session ? session.tx : createCommandUndoTransaction(commandString),
|
|
9972
10085
|
job = new internal.Job(model),
|
|
9973
10086
|
commandStart = Date.now();
|
|
9974
10087
|
|
|
@@ -10029,12 +10142,25 @@
|
|
|
10029
10142
|
// the data.
|
|
10030
10143
|
if (!err) {
|
|
10031
10144
|
commandString = internal.standardizeConsoleCommands(commandString);
|
|
10145
|
+
if (session) {
|
|
10146
|
+
// the session's previous command is at the end of the history, and
|
|
10147
|
+
// an inactive tail is discarded by the next command added
|
|
10148
|
+
gui.session.setCommandsActive(session.historyIds, false);
|
|
10149
|
+
}
|
|
10032
10150
|
historyIds.push(gui.session.consoleCommands(commandString));
|
|
10033
10151
|
// kludge to terminate unclosed -if blocks
|
|
10034
10152
|
if (commandString.includes('-if') && !commandString.includes('-endif')) {
|
|
10035
10153
|
historyIds.push(gui.session.consoleCommands('-endif'));
|
|
10036
10154
|
}
|
|
10037
10155
|
}
|
|
10156
|
+
if (session) {
|
|
10157
|
+
if (!err) session.historyIds = historyIds;
|
|
10158
|
+
Object.assign(session.flags, flags);
|
|
10159
|
+
if (!undoTransaction) discardHistoryAfterUnrecordedCommand();
|
|
10160
|
+
logCommandTiming(commandString, commands, err, Date.now() - commandStart, null);
|
|
10161
|
+
done(err, flags);
|
|
10162
|
+
return;
|
|
10163
|
+
}
|
|
10038
10164
|
addCommandUndoHistory(undoTransaction, err, flags, historyIds).then(function(undoTiming) {
|
|
10039
10165
|
logCommandTiming(commandString, commands, err, Date.now() - commandStart, undoTiming);
|
|
10040
10166
|
done(err, flags);
|
|
@@ -10707,8 +10833,6 @@
|
|
|
10707
10833
|
var toggleBtn = null; // checkbox <input> for toggling layer selection
|
|
10708
10834
|
var exportBtn = gui.container.findChild('.export-btn').addClass('disabled');
|
|
10709
10835
|
var ofileName = gui.container.findChild('#ofile-name');
|
|
10710
|
-
var frameInfo = menu.findChild('.export-frame-info').hide();
|
|
10711
|
-
menu.findChild('.advanced-options').on('input', updateFrameInfo);
|
|
10712
10836
|
new SimpleButton(menu.findChild('.close2-btn')).on('click', gui.clearMode);
|
|
10713
10837
|
|
|
10714
10838
|
if (!GUI.exportIsSupported()) {
|
|
@@ -10777,7 +10901,6 @@
|
|
|
10777
10901
|
formatPickedByUser = false;
|
|
10778
10902
|
// initZipOption();
|
|
10779
10903
|
initFormatMenu();
|
|
10780
|
-
updateFrameInfo();
|
|
10781
10904
|
updateExportCheckboxes();
|
|
10782
10905
|
menu.show();
|
|
10783
10906
|
}
|
|
@@ -10861,10 +10984,10 @@
|
|
|
10861
10984
|
opts.history = snapshot;
|
|
10862
10985
|
targets = addFrameTarget(targets);
|
|
10863
10986
|
}
|
|
10864
|
-
if (opts.format == 'svg' || opts.format == 'topojson') {
|
|
10987
|
+
if (opts.format == 'svg' || opts.format == 'html' || opts.format == 'topojson') {
|
|
10865
10988
|
opts.gui_frame = getGuiFrameContext();
|
|
10866
10989
|
}
|
|
10867
|
-
if (opts.format == 'svg') {
|
|
10990
|
+
if (opts.format == 'svg' || opts.format == 'html') {
|
|
10868
10991
|
targets = addFrameFurnitureTargets(targets, opts.gui_frame);
|
|
10869
10992
|
}
|
|
10870
10993
|
try {
|
|
@@ -11004,7 +11127,7 @@
|
|
|
11004
11127
|
}
|
|
11005
11128
|
|
|
11006
11129
|
function getExportFormats() {
|
|
11007
|
-
var formats = ['shapefile', 'json', 'geojson', 'dsv', 'topojson', 'flatgeobuf', 'geopackage', 'geoparquet', 'kml', '
|
|
11130
|
+
var formats = ['shapefile', 'json', 'geojson', 'dsv', 'topojson', 'flatgeobuf', 'geopackage', 'geoparquet', 'svg', 'kml', 'html', internal.PACKAGE_EXT];
|
|
11008
11131
|
// GeoTIFF is the one format here that only accepts raster layers, so it is
|
|
11009
11132
|
// offered only when there is a raster to export.
|
|
11010
11133
|
if (getExportFormatLayers().some(hasRaster)) formats.push('geotiff');
|
|
@@ -11052,13 +11175,11 @@
|
|
|
11052
11175
|
// changes afterwards.
|
|
11053
11176
|
formatPickedByUser = true;
|
|
11054
11177
|
updateExportCheckboxes();
|
|
11055
|
-
updateFrameInfo();
|
|
11056
11178
|
}
|
|
11057
11179
|
|
|
11058
11180
|
function setSelectedFormat(fmt) {
|
|
11059
11181
|
var el = menu.findChild('.export-formats input[value="' + fmt + '"]');
|
|
11060
11182
|
if (el) el.node().checked = true;
|
|
11061
|
-
updateFrameInfo();
|
|
11062
11183
|
}
|
|
11063
11184
|
|
|
11064
11185
|
// Which formats apply depends on what is checked for export, so unchecking
|
|
@@ -11164,21 +11285,6 @@
|
|
|
11164
11285
|
});
|
|
11165
11286
|
return targets;
|
|
11166
11287
|
}
|
|
11167
|
-
|
|
11168
|
-
function updateFrameInfo() {
|
|
11169
|
-
if (!frameInfo) return;
|
|
11170
|
-
var context = getGuiFrameContext();
|
|
11171
|
-
var format = getSelectedFormat();
|
|
11172
|
-
var topoUsesPixels = format == 'topojson' &&
|
|
11173
|
-
/\b(?:width|height)\s*=/.test(getExportOptsAsString());
|
|
11174
|
-
if (context && (format == 'svg' || topoUsesPixels)) {
|
|
11175
|
-
frameInfo.text('Map frame: ' +
|
|
11176
|
-
internal.formatFrameSizeForDisplay(context.data)).show();
|
|
11177
|
-
} else {
|
|
11178
|
-
frameInfo.hide();
|
|
11179
|
-
}
|
|
11180
|
-
}
|
|
11181
|
-
|
|
11182
11288
|
};
|
|
11183
11289
|
|
|
11184
11290
|
function DomCache() {
|
|
@@ -11667,10 +11773,18 @@
|
|
|
11667
11773
|
return retn;
|
|
11668
11774
|
}
|
|
11669
11775
|
|
|
11776
|
+
// opts.session a command session from gui.console.createCommandSession(),
|
|
11777
|
+
// for an edit that is one of a set recorded as one undo state
|
|
11670
11778
|
function runGuiEditCommand(gui, cmd, optsArg) {
|
|
11671
11779
|
var opts = optsArg || {};
|
|
11672
11780
|
if (!gui.console) return;
|
|
11673
|
-
|
|
11781
|
+
if (opts.session) {
|
|
11782
|
+
opts.session.run(cmd, onDone);
|
|
11783
|
+
} else {
|
|
11784
|
+
gui.console.runMapshaperCommands(cmd, onDone);
|
|
11785
|
+
}
|
|
11786
|
+
|
|
11787
|
+
function onDone(err, flags) {
|
|
11674
11788
|
if (err) {
|
|
11675
11789
|
showPopupAlert(err.message || String(err), opts.title || 'Command error');
|
|
11676
11790
|
if (opts.onError) opts.onError(err);
|
|
@@ -11680,7 +11794,7 @@
|
|
|
11680
11794
|
if (opts.onDone) {
|
|
11681
11795
|
opts.onDone(err, flags);
|
|
11682
11796
|
}
|
|
11683
|
-
}
|
|
11797
|
+
}
|
|
11684
11798
|
}
|
|
11685
11799
|
|
|
11686
11800
|
function LayerControl(gui) {
|
|
@@ -12928,19 +13042,28 @@
|
|
|
12928
13042
|
return fields.sort();
|
|
12929
13043
|
}
|
|
12930
13044
|
|
|
13045
|
+
// A -classify that assigns colors or values rewrites its output field in every
|
|
13046
|
+
// feature, so it supersedes an earlier one that wrote the same field -- unless
|
|
13047
|
+
// it reads that field, or writes a key file that would be lost.
|
|
12931
13048
|
function getClassifyCullInfo(opts) {
|
|
12932
|
-
|
|
13049
|
+
var saveAs = opts.save_as || opts['save-as'] || '';
|
|
13050
|
+
var kind = opts.colors ? 'colors' : opts.values ? 'values' : '';
|
|
13051
|
+
if (opts.where || !kind || classifyWritesKey(opts)) return null;
|
|
13052
|
+
if ([opts.field, opts.index_field].some(function(field) {
|
|
13053
|
+
return field && (saveAs ? field == saveAs : ['fill', 'stroke', 'class'].includes(field));
|
|
13054
|
+
})) return null;
|
|
12933
13055
|
return {
|
|
12934
13056
|
type: 'classify',
|
|
12935
13057
|
target: getTargetKey(opts),
|
|
12936
13058
|
ids: getIdsKey(opts),
|
|
12937
|
-
key: '
|
|
13059
|
+
key: kind + ':' + saveAs
|
|
12938
13060
|
};
|
|
12939
13061
|
}
|
|
12940
13062
|
|
|
12941
|
-
function
|
|
12942
|
-
return
|
|
12943
|
-
|
|
13063
|
+
function classifyWritesKey(opts) {
|
|
13064
|
+
return Object.keys(opts).some(function(name) {
|
|
13065
|
+
return name == 'key' || name.indexOf('key_') === 0 || name.indexOf('key-') === 0;
|
|
13066
|
+
});
|
|
12944
13067
|
}
|
|
12945
13068
|
|
|
12946
13069
|
function getTargetKey(opts) {
|
|
@@ -13250,6 +13373,8 @@
|
|
|
13250
13373
|
|
|
13251
13374
|
function Undo(gui) {
|
|
13252
13375
|
var history, offset, stashedUndo, editSession;
|
|
13376
|
+
var pendingCommits = [];
|
|
13377
|
+
var self = this;
|
|
13253
13378
|
editSession = createEditSessionUndo();
|
|
13254
13379
|
reset();
|
|
13255
13380
|
|
|
@@ -13475,6 +13600,29 @@
|
|
|
13475
13600
|
addHistoryState(undo, redo, cleanup, opts);
|
|
13476
13601
|
};
|
|
13477
13602
|
|
|
13603
|
+
// An edit whose history state has not been added yet -- a panel session
|
|
13604
|
+
// that records one state when it ends -- registers a function that adds it.
|
|
13605
|
+
// Undo and redo run it, and wait for it, before they act; otherwise they
|
|
13606
|
+
// would take back the state before the edit and leave the edit in place.
|
|
13607
|
+
// Returns a function that unregisters it.
|
|
13608
|
+
this.addPendingCommit = function(commit) {
|
|
13609
|
+
pendingCommits.push(commit);
|
|
13610
|
+
return function() {
|
|
13611
|
+
var i = pendingCommits.indexOf(commit);
|
|
13612
|
+
if (i > -1) pendingCommits.splice(i, 1);
|
|
13613
|
+
};
|
|
13614
|
+
};
|
|
13615
|
+
|
|
13616
|
+
function flushPendingCommits() {
|
|
13617
|
+
var commits = pendingCommits.splice(0);
|
|
13618
|
+
if (commits.length === 0) return null;
|
|
13619
|
+
return Promise.all(commits.map(function(commit) {
|
|
13620
|
+
return Promise.resolve(commit()).catch(function(e) {
|
|
13621
|
+
console.error(e);
|
|
13622
|
+
});
|
|
13623
|
+
}));
|
|
13624
|
+
}
|
|
13625
|
+
|
|
13478
13626
|
this.evictOldestHistoryState = function(opts) {
|
|
13479
13627
|
return evictOldestHistoryState(opts || {});
|
|
13480
13628
|
};
|
|
@@ -13545,6 +13693,8 @@
|
|
|
13545
13693
|
}
|
|
13546
13694
|
|
|
13547
13695
|
this.undo = function() {
|
|
13696
|
+
var pending = flushPendingCommits();
|
|
13697
|
+
if (pending) return pending.then(function() { return self.undo(); });
|
|
13548
13698
|
// firing even if history is empty
|
|
13549
13699
|
// (because this event may trigger a new history state)
|
|
13550
13700
|
gui.dispatchEvent('undo_redo_pre', {type: 'undo'});
|
|
@@ -13558,6 +13708,8 @@
|
|
|
13558
13708
|
};
|
|
13559
13709
|
|
|
13560
13710
|
this.redo = function() {
|
|
13711
|
+
var pending = flushPendingCommits();
|
|
13712
|
+
if (pending) return pending.then(function() { return self.redo(); });
|
|
13561
13713
|
gui.dispatchEvent('undo_redo_pre', {type: 'redo'});
|
|
13562
13714
|
if (offset <= 0) return;
|
|
13563
13715
|
offset--;
|
|
@@ -14726,16 +14878,16 @@
|
|
|
14726
14878
|
}
|
|
14727
14879
|
|
|
14728
14880
|
// The label tool is offered where a label would join the active layer (see
|
|
14729
|
-
// menus above),
|
|
14730
|
-
//
|
|
14731
|
-
//
|
|
14732
|
-
// next label
|
|
14881
|
+
// menus above), and it stays open only there: selecting a layer that cannot
|
|
14882
|
+
// hold labels closes it, as selecting another kind of layer closes the line
|
|
14883
|
+
// and polygon tools. Left open, its panel went on styling a layer that has
|
|
14884
|
+
// no labels, and the next label would have started a label layer of its own
|
|
14885
|
+
// beside whatever had been selected. With no layer at all the tool stays,
|
|
14886
|
+
// since it creates the layer it needs.
|
|
14733
14887
|
function labelModeIsAvailable() {
|
|
14734
14888
|
var o = gui.model.getActiveLayer();
|
|
14735
14889
|
if (_editMode != 'label') return false;
|
|
14736
|
-
|
|
14737
|
-
// A table is the one target with no map to click on.
|
|
14738
|
-
return internal.layerHasRaster(o.layer) || !!o.layer.geometry_type;
|
|
14890
|
+
return !o || !o.layer || labelWouldJoin(o.layer);
|
|
14739
14891
|
}
|
|
14740
14892
|
|
|
14741
14893
|
function labelStyleModeIsAvailable() {
|
|
@@ -15846,6 +15998,175 @@
|
|
|
15846
15998
|
return Math.round(val * 100) / 100;
|
|
15847
15999
|
}
|
|
15848
16000
|
|
|
16001
|
+
// Reads a color typed or pasted into a color field, and returns it as a
|
|
16002
|
+
// six-digit hex value (#rrggbb), or null if it isn't a color. Accepts what a
|
|
16003
|
+
// user is likely to have copied from somewhere else: hex with or without the
|
|
16004
|
+
// "#" and with 3, 4, 6 or 8 digits, CSS color names, rgb() and hsl() in
|
|
16005
|
+
// either the comma or the space-separated syntax, and oklch(). Alpha is
|
|
16006
|
+
// accepted and dropped, because the field holds an opaque color. An oklch()
|
|
16007
|
+
// color outside the sRGB gamut loses chroma, keeping its hue and lightness.
|
|
16008
|
+
function parseColorInput(str) {
|
|
16009
|
+
var s = String(str || '').trim().toLowerCase();
|
|
16010
|
+
var rgb;
|
|
16011
|
+
if (!s) return null;
|
|
16012
|
+
if (/^([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/.test(s)) {
|
|
16013
|
+
s = '#' + s;
|
|
16014
|
+
}
|
|
16015
|
+
if (s[0] == '#') {
|
|
16016
|
+
rgb = /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/.test(s) ? internal.parseHexColor(s) : null;
|
|
16017
|
+
} else if (/^rgba?\(/.test(s)) {
|
|
16018
|
+
rgb = parseRgbFunction(s);
|
|
16019
|
+
} else if (/^hsla?\(/.test(s)) {
|
|
16020
|
+
rgb = parseHslFunction(s);
|
|
16021
|
+
} else if (/^oklch\(/.test(s)) {
|
|
16022
|
+
rgb = parseOklchFunction(s);
|
|
16023
|
+
} else if (/^[a-z]+$/.test(s)) {
|
|
16024
|
+
rgb = internal.parseColor(s);
|
|
16025
|
+
}
|
|
16026
|
+
return rgb ? formatHex(rgb) : null;
|
|
16027
|
+
}
|
|
16028
|
+
|
|
16029
|
+
// The formats a color field can show a color in
|
|
16030
|
+
var colorInputFormats = [
|
|
16031
|
+
{name: 'hex', label: 'HEX'},
|
|
16032
|
+
{name: 'rgb', label: 'RGB'},
|
|
16033
|
+
{name: 'hsl', label: 'HSL'},
|
|
16034
|
+
{name: 'oklch', label: 'OKLCH'}
|
|
16035
|
+
];
|
|
16036
|
+
|
|
16037
|
+
// Shows a #rrggbb color in one of colorInputFormats, in a form that
|
|
16038
|
+
// parseColorInput() reads back.
|
|
16039
|
+
function formatColorInput(hex, format) {
|
|
16040
|
+
var rgb = internal.parseHexColor(hex);
|
|
16041
|
+
var hsl, lch;
|
|
16042
|
+
if (!rgb) return hex;
|
|
16043
|
+
if (format == 'rgb') {
|
|
16044
|
+
return 'rgb(' + [rgb.r, rgb.g, rgb.b].join(', ') + ')';
|
|
16045
|
+
}
|
|
16046
|
+
if (format == 'hsl') {
|
|
16047
|
+
hsl = rgbToHsl(rgb);
|
|
16048
|
+
return 'hsl(' + round$2(hsl.h, 1) + ', ' + round$2(hsl.s * 100, 1) + '%, ' +
|
|
16049
|
+
round$2(hsl.l * 100, 1) + '%)';
|
|
16050
|
+
}
|
|
16051
|
+
if (format == 'oklch') {
|
|
16052
|
+
lch = internal.oklabToOklch(internal.rgbToOklab(rgb));
|
|
16053
|
+
// a gray has no hue
|
|
16054
|
+
return 'oklch(' + lch.l.toFixed(4) + ' ' + lch.c.toFixed(4) + ' ' +
|
|
16055
|
+
(lch.c < 0.00005 ? 0 : round$2(lch.h, 2)) + ')';
|
|
16056
|
+
}
|
|
16057
|
+
return internal.formatColor({r: rgb.r, g: rgb.g, b: rgb.b, a: 1});
|
|
16058
|
+
}
|
|
16059
|
+
|
|
16060
|
+
function getFunctionArgs(s) {
|
|
16061
|
+
var match = /^[a-z]+\(([^)]*)\)$/.exec(s);
|
|
16062
|
+
var args;
|
|
16063
|
+
if (!match) return null;
|
|
16064
|
+
args = match[1].split(/[\s,/]+/).filter(Boolean);
|
|
16065
|
+
return args.length == 3 || args.length == 4 ? args : null;
|
|
16066
|
+
}
|
|
16067
|
+
|
|
16068
|
+
function parseRgbFunction(s) {
|
|
16069
|
+
var args = getFunctionArgs(s);
|
|
16070
|
+
var channels;
|
|
16071
|
+
if (!args) return null;
|
|
16072
|
+
channels = args.slice(0, 3).map(function(arg) {
|
|
16073
|
+
var val = parseFloat(arg);
|
|
16074
|
+
if (!/^-?[0-9.]+%?$/.test(arg) || !isFinite(val)) return NaN;
|
|
16075
|
+
return clamp$6(/%$/.test(arg) ? val / 100 * 255 : val, 0, 255);
|
|
16076
|
+
});
|
|
16077
|
+
if (channels.some(isNaN)) return null;
|
|
16078
|
+
return {r: channels[0], g: channels[1], b: channels[2]};
|
|
16079
|
+
}
|
|
16080
|
+
|
|
16081
|
+
function parseHslFunction(s) {
|
|
16082
|
+
var args = getFunctionArgs(s);
|
|
16083
|
+
var h, sat, light;
|
|
16084
|
+
if (!args) return null;
|
|
16085
|
+
h = parseHue(args[0]);
|
|
16086
|
+
sat = parsePercent(args[1]);
|
|
16087
|
+
light = parsePercent(args[2]);
|
|
16088
|
+
if (isNaN(h) || isNaN(sat) || isNaN(light)) return null;
|
|
16089
|
+
return hslToRgb(h, sat, light);
|
|
16090
|
+
}
|
|
16091
|
+
|
|
16092
|
+
// L is 0-1 or a percent; C is a number, or a percent of 0.4, as in CSS
|
|
16093
|
+
function parseOklchFunction(s) {
|
|
16094
|
+
var args = getFunctionArgs(s);
|
|
16095
|
+
var l, c, h;
|
|
16096
|
+
if (!args) return null;
|
|
16097
|
+
l = parseNumberOrPercent(args[0], 1);
|
|
16098
|
+
c = parseNumberOrPercent(args[1], 0.4);
|
|
16099
|
+
h = args[2] == 'none' ? 0 : parseHue(args[2]);
|
|
16100
|
+
if (isNaN(l) || isNaN(c) || isNaN(h)) return null;
|
|
16101
|
+
return internal.oklchToRgb({l: clamp$6(l, 0, 1), c: Math.max(c, 0), h: h});
|
|
16102
|
+
}
|
|
16103
|
+
|
|
16104
|
+
function parseNumberOrPercent(arg, pctScale) {
|
|
16105
|
+
if (arg == 'none') return 0;
|
|
16106
|
+
if (!/^-?[0-9.]+%?$/.test(arg) || !isFinite(parseFloat(arg))) return NaN;
|
|
16107
|
+
return /%$/.test(arg) ? parseFloat(arg) / 100 * pctScale : parseFloat(arg);
|
|
16108
|
+
}
|
|
16109
|
+
|
|
16110
|
+
// degrees, from a number or an angle with a CSS unit
|
|
16111
|
+
function parseHue(arg) {
|
|
16112
|
+
var match = /^(-?[0-9.]+)(deg|rad|grad|turn)?$/.exec(arg);
|
|
16113
|
+
var val, unit;
|
|
16114
|
+
if (!match) return NaN;
|
|
16115
|
+
val = parseFloat(match[1]);
|
|
16116
|
+
unit = match[2] || 'deg';
|
|
16117
|
+
if (unit == 'rad') val = val * 180 / Math.PI;
|
|
16118
|
+
if (unit == 'grad') val = val * 0.9;
|
|
16119
|
+
if (unit == 'turn') val = val * 360;
|
|
16120
|
+
return ((val % 360) + 360) % 360;
|
|
16121
|
+
}
|
|
16122
|
+
|
|
16123
|
+
// 0-1; the percent sign is optional, as in the space-separated syntax
|
|
16124
|
+
function parsePercent(arg) {
|
|
16125
|
+
if (!/^-?[0-9.]+%?$/.test(arg)) return NaN;
|
|
16126
|
+
return clamp$6(parseFloat(arg), 0, 100) / 100;
|
|
16127
|
+
}
|
|
16128
|
+
|
|
16129
|
+
function hslToRgb(h, s, l) {
|
|
16130
|
+
var c = (1 - Math.abs(2 * l - 1)) * s;
|
|
16131
|
+
var x = c * (1 - Math.abs((h / 60) % 2 - 1));
|
|
16132
|
+
var m = l - c / 2;
|
|
16133
|
+
var rgb = h < 60 ? [c, x, 0] :
|
|
16134
|
+
h < 120 ? [x, c, 0] :
|
|
16135
|
+
h < 180 ? [0, c, x] :
|
|
16136
|
+
h < 240 ? [0, x, c] :
|
|
16137
|
+
h < 300 ? [x, 0, c] : [c, 0, x];
|
|
16138
|
+
return {r: (rgb[0] + m) * 255, g: (rgb[1] + m) * 255, b: (rgb[2] + m) * 255};
|
|
16139
|
+
}
|
|
16140
|
+
|
|
16141
|
+
function rgbToHsl(rgb) {
|
|
16142
|
+
var r = rgb.r / 255, g = rgb.g / 255, b = rgb.b / 255;
|
|
16143
|
+
var max = Math.max(r, g, b), min = Math.min(r, g, b);
|
|
16144
|
+
var d = max - min;
|
|
16145
|
+
var l = (max + min) / 2;
|
|
16146
|
+
var h = 0, s = 0;
|
|
16147
|
+
if (d > 0) {
|
|
16148
|
+
s = d / (1 - Math.abs(2 * l - 1));
|
|
16149
|
+
if (max == r) h = ((g - b) / d + 6) % 6;
|
|
16150
|
+
else if (max == g) h = (b - r) / d + 2;
|
|
16151
|
+
else h = (r - g) / d + 4;
|
|
16152
|
+
h *= 60;
|
|
16153
|
+
}
|
|
16154
|
+
return {h: h, s: s, l: l};
|
|
16155
|
+
}
|
|
16156
|
+
|
|
16157
|
+
function round$2(val, decimals) {
|
|
16158
|
+
var k = Math.pow(10, decimals);
|
|
16159
|
+
return Math.round(val * k) / k;
|
|
16160
|
+
}
|
|
16161
|
+
|
|
16162
|
+
function formatHex(rgb) {
|
|
16163
|
+
return internal.formatColor({r: rgb.r, g: rgb.g, b: rgb.b, a: 1});
|
|
16164
|
+
}
|
|
16165
|
+
|
|
16166
|
+
function clamp$6(val, min, max) {
|
|
16167
|
+
return Math.max(min, Math.min(max, val));
|
|
16168
|
+
}
|
|
16169
|
+
|
|
15849
16170
|
var grayscaleColorPresets = [
|
|
15850
16171
|
'#000000', '#111111', '#222222', '#333333',
|
|
15851
16172
|
'#444444', '#555555', '#666666', '#777777',
|
|
@@ -15880,18 +16201,29 @@
|
|
|
15880
16201
|
// this holds at most one entry.
|
|
15881
16202
|
var openPickers = [];
|
|
15882
16203
|
|
|
16204
|
+
// The format color fields show colors in, chosen with the picker's tabs. It
|
|
16205
|
+
// is shared by every picker, so it carries over from one to the next.
|
|
16206
|
+
var colorFieldFormat = 'hex';
|
|
16207
|
+
|
|
15883
16208
|
function closeOpenPickers(except) {
|
|
15884
16209
|
openPickers.slice().forEach(function(picker) {
|
|
15885
16210
|
if (picker !== except) picker.hide();
|
|
15886
16211
|
});
|
|
15887
16212
|
}
|
|
15888
16213
|
|
|
16214
|
+
// opts.presetRows rows of preset colors; [] for none
|
|
16215
|
+
// opts.onPreview(hex), opts.onChange(hex), opts.onHide()
|
|
15889
16216
|
function ColorPicker(parent, opts) {
|
|
15890
16217
|
opts = opts || {};
|
|
15891
16218
|
var self = this;
|
|
15892
16219
|
var colorPicker = El('div').addClass('label-color-picker').appendTo(parent).hide();
|
|
15893
|
-
var sbCanvas, hueCanvas, sbMarker, hueMarker,
|
|
16220
|
+
var sbCanvas, hueCanvas, sbMarker, hueMarker, colorInput, formatTabs;
|
|
15894
16221
|
var pickerColor = {h: 0, s: 0, b: 0};
|
|
16222
|
+
// The hex value the picker was set to, until it is moved off it. HSB is
|
|
16223
|
+
// held in bytes, which cannot represent every RGB colour, so a colour that
|
|
16224
|
+
// was typed or picked from a preset would otherwise come back as a
|
|
16225
|
+
// neighbour of itself (#ff8800 as #ff8a00).
|
|
16226
|
+
var pickerHex = null;
|
|
15895
16227
|
var presetRows = opts.presetRows || [grayscaleColorPresets];
|
|
15896
16228
|
|
|
15897
16229
|
init();
|
|
@@ -15902,31 +16234,45 @@
|
|
|
15902
16234
|
} else {
|
|
15903
16235
|
closeOpenPickers(self);
|
|
15904
16236
|
colorPicker.show();
|
|
16237
|
+
setPanelRaised(true);
|
|
15905
16238
|
positionPicker();
|
|
15906
16239
|
drawColorPicker();
|
|
16240
|
+
updatePickerFields();
|
|
15907
16241
|
if (openPickers.indexOf(self) == -1) openPickers.push(self);
|
|
15908
16242
|
}
|
|
15909
16243
|
};
|
|
15910
16244
|
|
|
15911
16245
|
this.hide = function() {
|
|
16246
|
+
var wasVisible = colorPicker.visible();
|
|
15912
16247
|
colorPicker.hide();
|
|
16248
|
+
setPanelRaised(false);
|
|
15913
16249
|
var i = openPickers.indexOf(self);
|
|
15914
16250
|
if (i > -1) openPickers.splice(i, 1);
|
|
16251
|
+
if (wasVisible && opts.onHide) opts.onHide();
|
|
15915
16252
|
};
|
|
15916
16253
|
|
|
16254
|
+
// The picker's z-index only places it within its panel, which is a stacking
|
|
16255
|
+
// context of its own (see .label-style-panel.color-picker-open).
|
|
16256
|
+
function setPanelRaised(raised) {
|
|
16257
|
+
var panel = colorPicker.node().closest('.label-style-panel');
|
|
16258
|
+
if (panel) panel.classList.toggle('color-picker-open', raised);
|
|
16259
|
+
}
|
|
16260
|
+
|
|
15917
16261
|
this.visible = function() {
|
|
15918
16262
|
return colorPicker.visible();
|
|
15919
16263
|
};
|
|
15920
16264
|
|
|
15921
16265
|
this.setColor = function(color) {
|
|
15922
16266
|
if (isHexColor(color)) {
|
|
15923
|
-
setPickerColor(hexToHsb(color));
|
|
16267
|
+
setPickerColor(hexToHsb(color), color);
|
|
15924
16268
|
}
|
|
15925
16269
|
};
|
|
15926
16270
|
|
|
15927
|
-
this.getColor =
|
|
15928
|
-
|
|
15929
|
-
|
|
16271
|
+
this.getColor = getPickerHex;
|
|
16272
|
+
|
|
16273
|
+
function getPickerHex() {
|
|
16274
|
+
return pickerHex || hsbToHex(pickerColor);
|
|
16275
|
+
}
|
|
15930
16276
|
|
|
15931
16277
|
// The picker is placed in viewport coordinates (see .label-color-picker), so
|
|
15932
16278
|
// it has to be put against the field it belongs to each time it opens. The
|
|
@@ -15953,14 +16299,14 @@
|
|
|
15953
16299
|
var hueWrap = El('div').addClass('label-color-canvas-wrap').appendTo(colorPicker);
|
|
15954
16300
|
hueCanvas = El('canvas').attr('width', '256').attr('height', '18').appendTo(hueWrap);
|
|
15955
16301
|
hueMarker = makePickerMarker().appendTo(hueWrap);
|
|
15956
|
-
renderPresetRows(colorPicker);
|
|
15957
|
-
|
|
15958
|
-
|
|
15959
|
-
|
|
15960
|
-
|
|
15961
|
-
|
|
15962
|
-
El('button').appendTo(
|
|
15963
|
-
|
|
16302
|
+
if (presetRows.length > 0) renderPresetRows(colorPicker);
|
|
16303
|
+
// a field for a typed or pasted color, with tabs for the format it shows
|
|
16304
|
+
// colors in
|
|
16305
|
+
addFormatTabs(colorPicker);
|
|
16306
|
+
var fieldRow = El('div').addClass('label-color-picker-fields').appendTo(colorPicker);
|
|
16307
|
+
addColorInput(fieldRow);
|
|
16308
|
+
El('button').appendTo(fieldRow).text('Close').on('click', function() {
|
|
16309
|
+
self.hide();
|
|
15964
16310
|
});
|
|
15965
16311
|
sbCanvas.on('mousedown', function(e) {
|
|
15966
16312
|
startCanvasDrag(e, updateSbFromEvent);
|
|
@@ -15991,28 +16337,52 @@
|
|
|
15991
16337
|
}
|
|
15992
16338
|
|
|
15993
16339
|
function applyPreset(color) {
|
|
15994
|
-
setPickerColor(hexToHsb(color));
|
|
16340
|
+
setPickerColor(hexToHsb(color), color);
|
|
15995
16341
|
commitPickerColor();
|
|
15996
16342
|
}
|
|
15997
16343
|
|
|
15998
|
-
function
|
|
15999
|
-
var
|
|
16000
|
-
|
|
16001
|
-
|
|
16002
|
-
.attr('type', '
|
|
16003
|
-
|
|
16004
|
-
|
|
16005
|
-
|
|
16006
|
-
|
|
16007
|
-
var b = parseNumberField(pickerHsbInputs.b.node().value);
|
|
16008
|
-
if (!isFinite(h) || !isFinite(s) || !isFinite(b)) return;
|
|
16009
|
-
setPickerColor({
|
|
16010
|
-
h: degreesToByte(h),
|
|
16011
|
-
s: pctToByte(s),
|
|
16012
|
-
b: pctToByte(b)
|
|
16344
|
+
function addFormatTabs(parent) {
|
|
16345
|
+
var row = El('div').addClass('label-color-format-tabs').attr('role', 'group')
|
|
16346
|
+
.attr('aria-label', 'Color format').appendTo(parent);
|
|
16347
|
+
formatTabs = colorInputFormats.map(function(format) {
|
|
16348
|
+
return El('button').addClass('label-color-format-tab').attr('type', 'button')
|
|
16349
|
+
.attr('data-format', format.name).text(format.label).appendTo(row)
|
|
16350
|
+
.on('click', function() {
|
|
16351
|
+
colorFieldFormat = format.name;
|
|
16352
|
+
updatePickerFields();
|
|
16013
16353
|
});
|
|
16014
|
-
|
|
16354
|
+
});
|
|
16355
|
+
}
|
|
16356
|
+
|
|
16357
|
+
// A color is applied when it is pasted, or when the field is left (Enter
|
|
16358
|
+
// leaves it, in the style panels). Whatever was accepted is shown in the
|
|
16359
|
+
// chosen format; something that isn't a color puts the field back.
|
|
16360
|
+
function addColorInput(row) {
|
|
16361
|
+
colorInput = El('input').attr('type', 'text').addClass('label-color-picker-input')
|
|
16362
|
+
.attr('aria-label', 'Color: hex, name, rgb(), hsl() or oklch()')
|
|
16363
|
+
.attr('spellcheck', 'false')
|
|
16364
|
+
.appendTo(row)
|
|
16365
|
+
.on('change', applyColorInput)
|
|
16366
|
+
.on('paste', function() {
|
|
16367
|
+
setTimeout(applyColorInput, 0);
|
|
16015
16368
|
});
|
|
16369
|
+
colorInput.node().addEventListener('keydown', function(e) {
|
|
16370
|
+
if (e.key == 'Escape') updatePickerFields();
|
|
16371
|
+
});
|
|
16372
|
+
}
|
|
16373
|
+
|
|
16374
|
+
function applyColorInput() {
|
|
16375
|
+
var hex = parseColorInput(colorInput.node().value);
|
|
16376
|
+
if (!hex) {
|
|
16377
|
+
updatePickerFields();
|
|
16378
|
+
return;
|
|
16379
|
+
}
|
|
16380
|
+
if (hex == getPickerHex()) {
|
|
16381
|
+
updatePickerFields();
|
|
16382
|
+
return;
|
|
16383
|
+
}
|
|
16384
|
+
setPickerColor(hexToHsb(hex), hex);
|
|
16385
|
+
commitPickerColor();
|
|
16016
16386
|
}
|
|
16017
16387
|
|
|
16018
16388
|
function startCanvasDrag(e, update) {
|
|
@@ -16056,26 +16426,30 @@
|
|
|
16056
16426
|
function getCanvasPoint(canvas, evt) {
|
|
16057
16427
|
var rect = canvas.getBoundingClientRect();
|
|
16058
16428
|
return {
|
|
16059
|
-
x: clamp$
|
|
16060
|
-
y: clamp$
|
|
16429
|
+
x: clamp$5(Math.round((evt.clientX - rect.left) / rect.width * (canvas.width - 1)), 0, canvas.width - 1),
|
|
16430
|
+
y: clamp$5(Math.round((evt.clientY - rect.top) / rect.height * (canvas.height - 1)), 0, canvas.height - 1)
|
|
16061
16431
|
};
|
|
16062
16432
|
}
|
|
16063
16433
|
|
|
16064
|
-
|
|
16434
|
+
// @hex: the exact colour, when @hsb was made from one
|
|
16435
|
+
function setPickerColor(hsb, hex) {
|
|
16065
16436
|
pickerColor = {
|
|
16066
|
-
h: clamp$
|
|
16067
|
-
s: clamp$
|
|
16068
|
-
b: clamp$
|
|
16437
|
+
h: clamp$5(Math.round(hsb.h), 0, 255),
|
|
16438
|
+
s: clamp$5(Math.round(hsb.s), 0, 255),
|
|
16439
|
+
b: clamp$5(Math.round(hsb.b), 0, 255)
|
|
16069
16440
|
};
|
|
16441
|
+
pickerHex = hex || null;
|
|
16070
16442
|
drawColorPicker();
|
|
16071
16443
|
updatePickerFields();
|
|
16072
|
-
if (opts.onPreview) opts.onPreview(
|
|
16444
|
+
if (opts.onPreview) opts.onPreview(getPickerHex());
|
|
16073
16445
|
}
|
|
16074
16446
|
|
|
16075
16447
|
function updatePickerFields() {
|
|
16076
|
-
|
|
16077
|
-
|
|
16078
|
-
|
|
16448
|
+
colorInput.node().value = formatColorInput(getPickerHex(), colorFieldFormat);
|
|
16449
|
+
formatTabs.forEach(function(tab) {
|
|
16450
|
+
var selected = tab.node().getAttribute('data-format') == colorFieldFormat;
|
|
16451
|
+
tab.classed('selected', selected).attr('aria-pressed', String(selected));
|
|
16452
|
+
});
|
|
16079
16453
|
}
|
|
16080
16454
|
|
|
16081
16455
|
function drawColorPicker() {
|
|
@@ -16125,7 +16499,7 @@
|
|
|
16125
16499
|
}
|
|
16126
16500
|
|
|
16127
16501
|
function commitPickerColor() {
|
|
16128
|
-
if (opts.onChange) opts.onChange(
|
|
16502
|
+
if (opts.onChange) opts.onChange(getPickerHex());
|
|
16129
16503
|
}
|
|
16130
16504
|
|
|
16131
16505
|
function getMarkerColor(rgb) {
|
|
@@ -16201,27 +16575,7 @@
|
|
|
16201
16575
|
};
|
|
16202
16576
|
}
|
|
16203
16577
|
|
|
16204
|
-
function
|
|
16205
|
-
return Math.round(val / 255 * 100);
|
|
16206
|
-
}
|
|
16207
|
-
|
|
16208
|
-
function pctToByte(val) {
|
|
16209
|
-
return Math.round(clamp$4(val, 0, 100) / 100 * 255);
|
|
16210
|
-
}
|
|
16211
|
-
|
|
16212
|
-
function byteToDegrees(val) {
|
|
16213
|
-
return Math.round(val / 255 * 360);
|
|
16214
|
-
}
|
|
16215
|
-
|
|
16216
|
-
function degreesToByte(val) {
|
|
16217
|
-
return Math.round(clamp$4(val, 0, 360) / 360 * 255);
|
|
16218
|
-
}
|
|
16219
|
-
|
|
16220
|
-
function parseNumberField(str) {
|
|
16221
|
-
return Number(String(str).replace(/[°%]/g, '').trim());
|
|
16222
|
-
}
|
|
16223
|
-
|
|
16224
|
-
function clamp$4(val, min, max) {
|
|
16578
|
+
function clamp$5(val, min, max) {
|
|
16225
16579
|
return isFinite(val) ? Math.max(min, Math.min(max, val)) : min;
|
|
16226
16580
|
}
|
|
16227
16581
|
|
|
@@ -16894,7 +17248,7 @@
|
|
|
16894
17248
|
});
|
|
16895
17249
|
}
|
|
16896
17250
|
|
|
16897
|
-
// A button with a word on it rather than a glyph: Clear style, Random
|
|
17251
|
+
// A button with a word on it rather than a glyph: Clear style, Random fill,
|
|
16898
17252
|
// Create. Shaped like the panel's fields rather than like its 19px icon
|
|
16899
17253
|
// buttons, because what it does is named rather than drawn, and it is as wide
|
|
16900
17254
|
// as the name.
|
|
@@ -16952,7 +17306,10 @@
|
|
|
16952
17306
|
// split row. Fill and Stroke are each one of these.
|
|
16953
17307
|
//
|
|
16954
17308
|
// opts.label the caption over the field
|
|
16955
|
-
// opts.onColor(hex) a colour was typed, picked or previewed to a finish
|
|
17309
|
+
// opts.onColor(hex) a colour was typed, picked or previewed to a finish;
|
|
17310
|
+
// '' when the field was emptied, which unsets a colour
|
|
17311
|
+
// that can be unset and restores the default of one
|
|
17312
|
+
// that cannot
|
|
16956
17313
|
// opts.onOpacity(frac) a usable percentage was typed
|
|
16957
17314
|
// opts.revert() one that was not, so put the row back as it was
|
|
16958
17315
|
// opts.noOpacity a colour with no opacity of its own, like a pattern's,
|
|
@@ -17220,6 +17577,36 @@
|
|
|
17220
17577
|
return gui.state.label_text_session || null;
|
|
17221
17578
|
}
|
|
17222
17579
|
|
|
17580
|
+
// The label tool's armed placement tool: 'anchor', 'block', 'path' or null.
|
|
17581
|
+
// The tool owns it; the panel reads it, because an armed tool with nothing
|
|
17582
|
+
// selected is what points the panel at new labels rather than at all of them.
|
|
17583
|
+
function setLabelToolArmed(gui, mode) {
|
|
17584
|
+
var val = mode || null;
|
|
17585
|
+
if ((gui.state.label_tool_armed || null) === val) return;
|
|
17586
|
+
gui.state.label_tool_armed = val;
|
|
17587
|
+
gui.dispatchEvent('label_tool_armed_change');
|
|
17588
|
+
}
|
|
17589
|
+
|
|
17590
|
+
function getLabelToolArmed(gui) {
|
|
17591
|
+
return gui.state.label_tool_armed || null;
|
|
17592
|
+
}
|
|
17593
|
+
|
|
17594
|
+
// The "Editing:" line at the top of the style panels, which says what a
|
|
17595
|
+
// change to the panel's controls will act on. One wording for every panel.
|
|
17596
|
+
//
|
|
17597
|
+
// o.selected how many features are selected
|
|
17598
|
+
// o.total how many features the layer has
|
|
17599
|
+
// o.newLabels (label panel) a placement tool is armed with nothing
|
|
17600
|
+
// selected, so the controls set the style of the next label
|
|
17601
|
+
// o.editingText (label panel) a label is open for typing
|
|
17602
|
+
function formatEditingStatus(o) {
|
|
17603
|
+
var what = o.editingText ? 'this label' :
|
|
17604
|
+
o.selected > 0 ? o.selected + ' selected' :
|
|
17605
|
+
o.newLabels ? 'new labels' :
|
|
17606
|
+
o.total > 0 ? 'all' : 'none (empty layer)';
|
|
17607
|
+
return 'Editing: ' + what;
|
|
17608
|
+
}
|
|
17609
|
+
|
|
17223
17610
|
// Dragging an anchored label's text away from its anchor: what the drag
|
|
17224
17611
|
// writes, and which of the nine positions it ends up nearest.
|
|
17225
17612
|
//
|
|
@@ -17792,6 +18179,9 @@
|
|
|
17792
18179
|
}, {
|
|
17793
18180
|
name: 'open-arrow',
|
|
17794
18181
|
title: 'open arrowhead at the anchor'
|
|
18182
|
+
}, {
|
|
18183
|
+
name: 'ring',
|
|
18184
|
+
title: 'ring around the anchor'
|
|
17795
18185
|
}];
|
|
17796
18186
|
// The anchor is at the lower left of the shapes and at the left of the ends,
|
|
17797
18187
|
// which is where the markers go. The arrowheads are drawn to the renderer's
|
|
@@ -17803,7 +18193,8 @@
|
|
|
17803
18193
|
curve: '<path d="M3.5 12.5Q4.5 4.5 12.5 4"></path>',
|
|
17804
18194
|
none: '<path d="M3 8H13"></path>',
|
|
17805
18195
|
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>'
|
|
18196
|
+
'open-arrow': '<path d="M3.2 8H13"></path><path d="M7.38 5.07L3.2 8L7.38 10.93"></path>',
|
|
18197
|
+
ring: '<path d="M8.4 8H13"></path><circle cx="5.6" cy="8" r="2.8"></circle>'
|
|
17807
18198
|
};
|
|
17808
18199
|
var iconButtonSymbols = {
|
|
17809
18200
|
circle: '<circle cx="8" cy="8" r="4.25"></circle>',
|
|
@@ -17854,6 +18245,11 @@
|
|
|
17854
18245
|
// a label opened for typing becomes what the controls act on
|
|
17855
18246
|
if (panel.visible()) updateControls();
|
|
17856
18247
|
});
|
|
18248
|
+
gui.on('label_tool_armed_change', function() {
|
|
18249
|
+
// arming a tool points an empty selection at new labels, and disarming
|
|
18250
|
+
// points it back at the whole layer
|
|
18251
|
+
if (panel.visible()) updateControls();
|
|
18252
|
+
});
|
|
17857
18253
|
gui.on('label_position_mode_change', function() {
|
|
17858
18254
|
// The tool sets the mode back to Fixed when it opens, and the toggle is
|
|
17859
18255
|
// the only thing on screen that says which of the two is on.
|
|
@@ -18003,12 +18399,9 @@
|
|
|
18003
18399
|
});
|
|
18004
18400
|
colorInput.on('change', function() {
|
|
18005
18401
|
var color = colorInput.node().value.trim();
|
|
18006
|
-
|
|
18007
|
-
|
|
18008
|
-
|
|
18009
|
-
}
|
|
18010
|
-
applyLabelColor(color);
|
|
18011
|
-
}
|
|
18402
|
+
// An emptied field unsets the colour.
|
|
18403
|
+
if (isHexColor(color)) colorPicker.setColor(color);
|
|
18404
|
+
applyLabelColor(color);
|
|
18012
18405
|
});
|
|
18013
18406
|
colorPicker = initColorPicker(textColorCell, colorChit, colorInput, applyLabelColor);
|
|
18014
18407
|
|
|
@@ -18075,12 +18468,8 @@
|
|
|
18075
18468
|
});
|
|
18076
18469
|
haloColorInput.on('change', function() {
|
|
18077
18470
|
var color = haloColorInput.node().value.trim();
|
|
18078
|
-
if (color)
|
|
18079
|
-
|
|
18080
|
-
haloColorPicker.setColor(color);
|
|
18081
|
-
}
|
|
18082
|
-
applyHaloColor(color);
|
|
18083
|
-
}
|
|
18471
|
+
if (isHexColor(color)) haloColorPicker.setColor(color);
|
|
18472
|
+
applyHaloColor(color);
|
|
18084
18473
|
});
|
|
18085
18474
|
haloColorPicker = initColorPicker(haloColorCell, haloColorChit, haloColorInput, applyHaloColor);
|
|
18086
18475
|
|
|
@@ -18140,12 +18529,8 @@
|
|
|
18140
18529
|
});
|
|
18141
18530
|
iconColorInput.on('change', function() {
|
|
18142
18531
|
var color = iconColorInput.node().value.trim();
|
|
18143
|
-
if (color)
|
|
18144
|
-
|
|
18145
|
-
iconColorPicker.setColor(color);
|
|
18146
|
-
}
|
|
18147
|
-
applyIconColor(color);
|
|
18148
|
-
}
|
|
18532
|
+
if (isHexColor(color)) iconColorPicker.setColor(color);
|
|
18533
|
+
applyIconColor(color);
|
|
18149
18534
|
});
|
|
18150
18535
|
iconColorPicker = initColorPicker(iconColorCell, iconColorChit, iconColorInput, applyIconColor);
|
|
18151
18536
|
|
|
@@ -18259,7 +18644,7 @@
|
|
|
18259
18644
|
var endSizeCell = El('div').addClass('label-split-cell label-spacing-row label-callout-end-size-row').appendTo(endRow);
|
|
18260
18645
|
El('span').appendTo(endSizeCell).text('Size');
|
|
18261
18646
|
calloutEndSizeInput = new SizeField(endSizeCell, {
|
|
18262
|
-
title: '
|
|
18647
|
+
title: 'Marker size in px: an arrowhead\'s sides, or a ring\'s diameter',
|
|
18263
18648
|
min: 1,
|
|
18264
18649
|
max: 60,
|
|
18265
18650
|
step: 1,
|
|
@@ -18283,12 +18668,8 @@
|
|
|
18283
18668
|
});
|
|
18284
18669
|
calloutColorInput.on('change', function() {
|
|
18285
18670
|
var color = calloutColorInput.node().value.trim();
|
|
18286
|
-
if (color)
|
|
18287
|
-
|
|
18288
|
-
calloutColorPicker.setColor(color);
|
|
18289
|
-
}
|
|
18290
|
-
applyCalloutColor(color);
|
|
18291
|
-
}
|
|
18671
|
+
if (isHexColor(color)) calloutColorPicker.setColor(color);
|
|
18672
|
+
applyCalloutColor(color);
|
|
18292
18673
|
});
|
|
18293
18674
|
calloutColorPicker = initColorPicker(colorCell, calloutColorChit, calloutColorInput, applyCalloutColor);
|
|
18294
18675
|
|
|
@@ -18410,6 +18791,16 @@
|
|
|
18410
18791
|
return !!(gui.interaction && gui.interaction.getMode() == labelMode);
|
|
18411
18792
|
}
|
|
18412
18793
|
|
|
18794
|
+
// Whether an empty selection means the next label rather than every label:
|
|
18795
|
+
// only while a placement tool is armed, or a label not yet made is being
|
|
18796
|
+
// typed. With no tool armed the panel styles the whole layer, as the line
|
|
18797
|
+
// and polygon panels do.
|
|
18798
|
+
function editingNewLabels() {
|
|
18799
|
+
var session = getLabelTextSession(gui);
|
|
18800
|
+
if (!labelModeIsOn()) return false;
|
|
18801
|
+
return !!getLabelToolArmed(gui) || !!(session && session.id == -1);
|
|
18802
|
+
}
|
|
18803
|
+
|
|
18413
18804
|
function updateVisibility() {
|
|
18414
18805
|
var enabled = activeLayerHasLabels();
|
|
18415
18806
|
textBtn.classed('disabled', !enabled);
|
|
@@ -18493,13 +18884,14 @@
|
|
|
18493
18884
|
if (session) return [];
|
|
18494
18885
|
ids = getSelectionIds();
|
|
18495
18886
|
if (ids.length > 0) return ids;
|
|
18496
|
-
return
|
|
18887
|
+
return editingNewLabels() ? [] : getAllLabelIds();
|
|
18497
18888
|
}
|
|
18498
18889
|
|
|
18499
|
-
// With nothing selected the controls edit the style that
|
|
18500
|
-
// so they stay live even when there is no label, and
|
|
18890
|
+
// With a tool armed and nothing selected the controls edit the style that
|
|
18891
|
+
// new labels are given, so they stay live even when there is no label, and
|
|
18892
|
+
// no layer, yet. With no tool armed and no labels, there is nothing to edit.
|
|
18501
18893
|
function controlsEnabled() {
|
|
18502
|
-
return getTargetIds().length > 0 ||
|
|
18894
|
+
return getTargetIds().length > 0 || editingNewLabels();
|
|
18503
18895
|
}
|
|
18504
18896
|
|
|
18505
18897
|
function getAllLabelIds() {
|
|
@@ -18729,22 +19121,21 @@
|
|
|
18729
19121
|
});
|
|
18730
19122
|
}
|
|
18731
19123
|
|
|
19124
|
+
// What the controls will act on. With a tool armed an empty selection is the
|
|
19125
|
+
// next label rather than the whole layer, and saying "all" there is what
|
|
19126
|
+
// would make the panel untrustworthy.
|
|
18732
19127
|
function updateEditingStatus(count, editingText) {
|
|
18733
|
-
editingStatus.text(
|
|
19128
|
+
editingStatus.text(formatEditingStatus({
|
|
19129
|
+
selected: count,
|
|
19130
|
+
total: getActiveLayer() ? internal.getFeatureCount(getActiveLayer()) : 0,
|
|
19131
|
+
newLabels: editingNewLabels(),
|
|
19132
|
+
editingText: editingText
|
|
19133
|
+
}));
|
|
18734
19134
|
// "deselect" is for a selection the user made; a label being typed into is
|
|
18735
19135
|
// left by clicking away or pressing Escape, not from here.
|
|
18736
19136
|
clearLink.classed('hidden', count === 0 || editingText);
|
|
18737
19137
|
}
|
|
18738
19138
|
|
|
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
19139
|
function updateFontSizeControls(shown) {
|
|
18749
19140
|
fontSizeInput.setValue(shown.value || '');
|
|
18750
19141
|
fontSizeInput.setPlaceholder(shown.mixed ? MIXED_TEXT : '');
|
|
@@ -18953,7 +19344,7 @@
|
|
|
18953
19344
|
var ids = getTargetIds();
|
|
18954
19345
|
var table = getActiveTable();
|
|
18955
19346
|
if (ids.length === 0) {
|
|
18956
|
-
return internal.svg.labelHasHalo(
|
|
19347
|
+
return internal.svg.labelHasHalo(editingNewLabels() ? getNewLabelStyle(gui) : null) ? 'on' : 'off';
|
|
18957
19348
|
}
|
|
18958
19349
|
return getToggleState(ids.map(function(id) {
|
|
18959
19350
|
return internal.svg.labelHasHalo(table && table.getRecordAt(id));
|
|
@@ -19013,7 +19404,7 @@
|
|
|
19013
19404
|
var ids = getTargetIds();
|
|
19014
19405
|
var table = getActiveTable();
|
|
19015
19406
|
if (ids.length === 0) {
|
|
19016
|
-
return internal.svg.labelHasCallout(
|
|
19407
|
+
return internal.svg.labelHasCallout(editingNewLabels() ? getNewLabelStyle(gui) : null) ? 'on' : 'off';
|
|
19017
19408
|
}
|
|
19018
19409
|
return getToggleState(ids.map(function(id) {
|
|
19019
19410
|
return internal.svg.labelHasCallout(table && table.getRecordAt(id));
|
|
@@ -19155,7 +19546,7 @@
|
|
|
19155
19546
|
function updateCalloutEndSizeControl(off) {
|
|
19156
19547
|
var ids = getTargetIds().length > 0 ? getCalloutEndTargetIds() : [];
|
|
19157
19548
|
var noMarker = getTargetIds().length > 0 ? ids.length === 0 :
|
|
19158
|
-
getCalloutEnd(
|
|
19549
|
+
getCalloutEnd(editingNewLabels() ? getNewLabelStyle(gui) : null) == 'none';
|
|
19159
19550
|
var shown = off || noMarker ? {value: '', mixed: false} : getCalloutEndSizeShown(ids);
|
|
19160
19551
|
calloutEndSizeInput.setValue(shown.value);
|
|
19161
19552
|
calloutEndSizeInput.setPlaceholder(shown.mixed ? MIXED_TEXT : '');
|
|
@@ -19194,7 +19585,7 @@
|
|
|
19194
19585
|
var vals;
|
|
19195
19586
|
if (!controlsEnabled()) return {value: '', mixed: false};
|
|
19196
19587
|
if (ids.length === 0) {
|
|
19197
|
-
return {value: fn(
|
|
19588
|
+
return {value: fn(editingNewLabels() ? getNewLabelStyle(gui) : null), mixed: false};
|
|
19198
19589
|
}
|
|
19199
19590
|
vals = ids.map(function(id) {
|
|
19200
19591
|
return fn(table && table.getRecordAt(id));
|
|
@@ -19270,7 +19661,7 @@
|
|
|
19270
19661
|
var records = getActiveTable() && getActiveTable().getRecords();
|
|
19271
19662
|
var value, val;
|
|
19272
19663
|
if (ids.length === 0) {
|
|
19273
|
-
return resolveAlignment(
|
|
19664
|
+
return resolveAlignment(editingNewLabels() ? getNewLabelStyle(gui) : null);
|
|
19274
19665
|
}
|
|
19275
19666
|
for (var i = 0; i < ids.length; i++) {
|
|
19276
19667
|
val = resolveAlignment(records && records[ids[i]]);
|
|
@@ -19290,7 +19681,7 @@
|
|
|
19290
19681
|
// label will be created with, falling back to the same default an existing
|
|
19291
19682
|
// label would show.
|
|
19292
19683
|
function getNewLabelValue(field, opts) {
|
|
19293
|
-
var style =
|
|
19684
|
+
var style = editingNewLabels() ? getNewLabelStyle(gui) : null;
|
|
19294
19685
|
var val = style ? style[field] : '';
|
|
19295
19686
|
if (val || val === 0) return val;
|
|
19296
19687
|
return opts && opts.useDefault ? opts.defaultValue : '';
|
|
@@ -19381,7 +19772,7 @@
|
|
|
19381
19772
|
var records = getActiveTable() && getActiveTable().getRecords();
|
|
19382
19773
|
var ids = getTargetIds();
|
|
19383
19774
|
if (ids.length === 0) {
|
|
19384
|
-
return !!(
|
|
19775
|
+
return !!(editingNewLabels() && getNewLabelStyle(gui)[textAnchorField]);
|
|
19385
19776
|
}
|
|
19386
19777
|
return ids.some(function(id) {
|
|
19387
19778
|
var rec = records && records[id];
|
|
@@ -19921,13 +20312,13 @@
|
|
|
19921
20312
|
// no fill gets a clear one, so that the pattern is drawn over whatever is
|
|
19922
20313
|
// underneath.
|
|
19923
20314
|
function getPatternBackground(fill) {
|
|
19924
|
-
return isBlank$
|
|
20315
|
+
return isBlank$2(fill) ? 'none' : String(fill).trim();
|
|
19925
20316
|
}
|
|
19926
20317
|
|
|
19927
20318
|
// Whether the settings describe a pattern the renderer will draw. A hatch with
|
|
19928
20319
|
// no gap would be a solid fill, and the parser refuses a stripe of no width.
|
|
19929
20320
|
function isValidPatternControls(o) {
|
|
19930
|
-
if (!o || patternTypes.indexOf(o.type) == -1 || isBlank$
|
|
20321
|
+
if (!o || patternTypes.indexOf(o.type) == -1 || isBlank$2(o.color)) return false;
|
|
19931
20322
|
if (!(o.size > 0) || !isFinite(o.angle)) return false;
|
|
19932
20323
|
return o.type == 'hatches' ? o.gap > 0 : o.gap >= 0;
|
|
19933
20324
|
}
|
|
@@ -20021,7 +20412,7 @@
|
|
|
20021
20412
|
return str == 'transparent' ? 'none' : str;
|
|
20022
20413
|
}
|
|
20023
20414
|
|
|
20024
|
-
function isBlank$
|
|
20415
|
+
function isBlank$2(val) {
|
|
20025
20416
|
return val === undefined || val === null || String(val).trim() === '';
|
|
20026
20417
|
}
|
|
20027
20418
|
|
|
@@ -20038,7 +20429,7 @@
|
|
|
20038
20429
|
// opts.revert() put the panel back as the data has it
|
|
20039
20430
|
// opts.releaseFocus()
|
|
20040
20431
|
function PatternFillControl(parent, opts) {
|
|
20041
|
-
var section = makePanelSection(parent, '
|
|
20432
|
+
var section = makePanelSection(parent, 'Patterns');
|
|
20042
20433
|
var shown = {type: 'none'};
|
|
20043
20434
|
// What switching the pattern on applies when the features have none: the
|
|
20044
20435
|
// last pattern the section showed, so that off and on again is a round trip.
|
|
@@ -20083,7 +20474,7 @@
|
|
|
20083
20474
|
var code = rec && rec['fill-pattern'];
|
|
20084
20475
|
var edit = {id: id, styles: styles};
|
|
20085
20476
|
var refilled;
|
|
20086
|
-
if (refill && !isBlank(code)) {
|
|
20477
|
+
if (refill && !isBlank$1(code)) {
|
|
20087
20478
|
refilled = refillPattern(internal.parsePattern(code), rec.fill, fill);
|
|
20088
20479
|
if (refilled) edit.styles = styles.concat([['fill-pattern', refilled]]);
|
|
20089
20480
|
}
|
|
@@ -20100,7 +20491,7 @@
|
|
|
20100
20491
|
ids.forEach(function(id) {
|
|
20101
20492
|
var rec = records[id];
|
|
20102
20493
|
var code = rec && rec['fill-pattern'];
|
|
20103
|
-
var o = isBlank(code) ? null : getPatternControls(internal.parsePattern(code), rec.fill);
|
|
20494
|
+
var o = isBlank$1(code) ? null : getPatternControls(internal.parsePattern(code), rec.fill);
|
|
20104
20495
|
if (o) edits.push({id: id, styles: [['fill-pattern', formatFillPatternExpression(o)]]});
|
|
20105
20496
|
});
|
|
20106
20497
|
return edits;
|
|
@@ -20130,7 +20521,12 @@
|
|
|
20130
20521
|
label: 'Color',
|
|
20131
20522
|
noOpacity: true,
|
|
20132
20523
|
onColor: function(color) {
|
|
20133
|
-
|
|
20524
|
+
// A pattern has to have a colour, so an emptied field is its default.
|
|
20525
|
+
if (!color) {
|
|
20526
|
+
color = defaultPatternColor;
|
|
20527
|
+
colorControl.showColor(color);
|
|
20528
|
+
}
|
|
20529
|
+
applyChange({color: color});
|
|
20134
20530
|
},
|
|
20135
20531
|
revert: opts.revert
|
|
20136
20532
|
});
|
|
@@ -20258,7 +20654,7 @@
|
|
|
20258
20654
|
|
|
20259
20655
|
function hasPattern(id) {
|
|
20260
20656
|
var rec = opts.getRecords()[id];
|
|
20261
|
-
return !isBlank(rec && rec['fill-pattern']);
|
|
20657
|
+
return !isBlank$1(rec && rec['fill-pattern']);
|
|
20262
20658
|
}
|
|
20263
20659
|
|
|
20264
20660
|
function applyChange(change) {
|
|
@@ -20330,7 +20726,7 @@
|
|
|
20330
20726
|
|
|
20331
20727
|
function readPatternState(rec) {
|
|
20332
20728
|
var code = rec && rec['fill-pattern'];
|
|
20333
|
-
if (isBlank(code)) return {type: 'none'};
|
|
20729
|
+
if (isBlank$1(code)) return {type: 'none'};
|
|
20334
20730
|
return getPatternControls(internal.parsePattern(code), rec.fill) ||
|
|
20335
20731
|
{type: 'custom', code: String(code)};
|
|
20336
20732
|
}
|
|
@@ -20346,12 +20742,2071 @@
|
|
|
20346
20742
|
return undefined;
|
|
20347
20743
|
}
|
|
20348
20744
|
|
|
20745
|
+
function isBlank$1(val) {
|
|
20746
|
+
return val === undefined || val === null || String(val).trim() === '';
|
|
20747
|
+
}
|
|
20748
|
+
|
|
20749
|
+
// The polygon panel's "Effects" section: an outer and an inner glow, each a
|
|
20750
|
+
// color, an opacity and a width. See svg-glow.mjs for how they are drawn.
|
|
20751
|
+
//
|
|
20752
|
+
// A glow is there when its color is set, so each glow is a color row with no
|
|
20753
|
+
// switch of its own: picking a color adds the glow, and emptying the color
|
|
20754
|
+
// field removes it. The section's switch opens the section, and switching it
|
|
20755
|
+
// off removes both glows and their settings.
|
|
20756
|
+
//
|
|
20757
|
+
// A width or opacity typed while no feature has the glow is kept, and goes
|
|
20758
|
+
// with the color when one is picked.
|
|
20759
|
+
//
|
|
20760
|
+
// opts.getRecords() the target layer's records
|
|
20761
|
+
// opts.getTargetIds() the features being styled
|
|
20762
|
+
// opts.applyEdits(edits, title) run per-feature edits: [{id, styles}]
|
|
20763
|
+
// opts.revert() put the panel back as the data has it
|
|
20764
|
+
// opts.releaseFocus()
|
|
20765
|
+
|
|
20766
|
+
var glowTypes = ['outer', 'inner'];
|
|
20767
|
+
var glowTitles = {outer: 'Outer glow', inner: 'Inner glow'};
|
|
20768
|
+
|
|
20769
|
+
function GlowEffectsControl(parent, opts) {
|
|
20770
|
+
var section = makePanelSection(parent, 'Effects');
|
|
20771
|
+
var rows = {};
|
|
20772
|
+
// Switched on, with no glow to show yet.
|
|
20773
|
+
var opened = false;
|
|
20774
|
+
var pending = {outer: {}, inner: {}};
|
|
20775
|
+
var toggle = makePanelToggle(section.findChild('.label-style-section-title'), {
|
|
20776
|
+
title: 'Add glow effects',
|
|
20777
|
+
className: 'layer-effects-toggle',
|
|
20778
|
+
onChange: function(on) {
|
|
20779
|
+
var edits;
|
|
20780
|
+
opened = on;
|
|
20781
|
+
pending = {outer: {}, inner: {}};
|
|
20782
|
+
edits = on ? [] : getEffectsOffEdits(opts.getRecords(), opts.getTargetIds());
|
|
20783
|
+
if (edits.length > 0) {
|
|
20784
|
+
opts.applyEdits(edits, 'Remove glow effects');
|
|
20785
|
+
} else {
|
|
20786
|
+
update();
|
|
20787
|
+
}
|
|
20788
|
+
}
|
|
20789
|
+
});
|
|
20790
|
+
glowTypes.forEach(function(type) {
|
|
20791
|
+
rows[type] = addGlowRow(type);
|
|
20792
|
+
});
|
|
20793
|
+
|
|
20794
|
+
this.section = section;
|
|
20795
|
+
this.update = update;
|
|
20796
|
+
this.hidePickers = hidePickers;
|
|
20797
|
+
|
|
20798
|
+
this.reset = function() {
|
|
20799
|
+
opened = false;
|
|
20800
|
+
pending = {outer: {}, inner: {}};
|
|
20801
|
+
hidePickers();
|
|
20802
|
+
};
|
|
20803
|
+
|
|
20804
|
+
// The glows every target has, for a saved style.
|
|
20805
|
+
this.addStyleValues = function(style) {
|
|
20806
|
+
var records = opts.getRecords();
|
|
20807
|
+
var ids = opts.getTargetIds();
|
|
20808
|
+
glowTypes.forEach(function(type) {
|
|
20809
|
+
if (getGlowState(records, ids, type) != 'on') return;
|
|
20810
|
+
['color', 'width', 'opacity'].forEach(function(name) {
|
|
20811
|
+
var field = getGlowField(type, name);
|
|
20812
|
+
var val = getCommonValue(records, ids, field);
|
|
20813
|
+
if (val || val === 0) style[field] = val;
|
|
20814
|
+
});
|
|
20815
|
+
});
|
|
20816
|
+
};
|
|
20817
|
+
|
|
20818
|
+
function hidePickers() {
|
|
20819
|
+
rows.outer.picker.hide();
|
|
20820
|
+
rows.inner.picker.hide();
|
|
20821
|
+
}
|
|
20822
|
+
|
|
20823
|
+
function addGlowRow(type) {
|
|
20824
|
+
var o = makeColorRow(section, {
|
|
20825
|
+
label: glowTitles[type],
|
|
20826
|
+
onColor: function(color) {
|
|
20827
|
+
applyGlowColor(type, color);
|
|
20828
|
+
},
|
|
20829
|
+
onOpacity: function(value) {
|
|
20830
|
+
applyGlowValue(type, 'opacity', value);
|
|
20831
|
+
},
|
|
20832
|
+
revert: opts.revert
|
|
20833
|
+
});
|
|
20834
|
+
o.row.addClass('layer-glow-row layer-' + type + '-glow-row');
|
|
20835
|
+
El('span').appendTo(o.aside).text('Width');
|
|
20836
|
+
o.width = new SizeField(o.aside, {
|
|
20837
|
+
min: 0.5,
|
|
20838
|
+
max: 200,
|
|
20839
|
+
step: 1,
|
|
20840
|
+
bigStep: 5,
|
|
20841
|
+
decimals: 1,
|
|
20842
|
+
title: 'How far the glow reaches from the edge, in px',
|
|
20843
|
+
onSet: function(value) {
|
|
20844
|
+
applyGlowValue(type, 'width', value);
|
|
20845
|
+
},
|
|
20846
|
+
onStep: function(delta) {
|
|
20847
|
+
var shown = Number(o.width.getValue());
|
|
20848
|
+
if (!(shown > 0)) shown = internal.svg.DEFAULT_GLOW_WIDTH;
|
|
20849
|
+
applyGlowValue(type, 'width', Math.max(0.5, Math.round(shown + delta)));
|
|
20850
|
+
},
|
|
20851
|
+
onDone: opts.releaseFocus
|
|
20852
|
+
});
|
|
20853
|
+
return o;
|
|
20854
|
+
}
|
|
20855
|
+
|
|
20856
|
+
function applyGlowColor(type, color) {
|
|
20857
|
+
var edits = getGlowColorEdits(opts.getRecords(), opts.getTargetIds(), type, color, pending[type]);
|
|
20858
|
+
if (color) pending[type] = {};
|
|
20859
|
+
if (edits.length > 0) {
|
|
20860
|
+
opts.applyEdits(edits, color ? glowTitles[type] : 'Remove ' + glowTitles[type].toLowerCase());
|
|
20861
|
+
} else {
|
|
20862
|
+
update();
|
|
20863
|
+
}
|
|
20864
|
+
}
|
|
20865
|
+
|
|
20866
|
+
// A width or opacity goes to the features that have the glow, as the label
|
|
20867
|
+
// panel's halo settings go only to the labels that have a halo.
|
|
20868
|
+
function applyGlowValue(type, name, value) {
|
|
20869
|
+
var records = opts.getRecords();
|
|
20870
|
+
var ids = opts.getTargetIds().filter(function(id) {
|
|
20871
|
+
return hasGlow(records[id], type);
|
|
20872
|
+
});
|
|
20873
|
+
if (ids.length === 0) {
|
|
20874
|
+
pending[type][name] = value;
|
|
20875
|
+
update();
|
|
20876
|
+
return;
|
|
20877
|
+
}
|
|
20878
|
+
opts.applyEdits(ids.map(function(id) {
|
|
20879
|
+
return {id: id, styles: [[getGlowField(type, name), formatGlowValue(name, value)]]};
|
|
20880
|
+
}));
|
|
20881
|
+
}
|
|
20882
|
+
|
|
20883
|
+
function update() {
|
|
20884
|
+
var records = opts.getRecords();
|
|
20885
|
+
var ids = opts.getTargetIds();
|
|
20886
|
+
var state = getEffectsState(records, ids);
|
|
20887
|
+
if (state != 'off') opened = true;
|
|
20888
|
+
toggle.setState(state == 'off' && opened ? 'on' : state);
|
|
20889
|
+
section.classed('collapsed', state == 'off' && !opened);
|
|
20890
|
+
if (state == 'off' && !opened) hidePickers();
|
|
20891
|
+
glowTypes.forEach(function(type) {
|
|
20892
|
+
updateGlowRow(type, records, ids);
|
|
20893
|
+
});
|
|
20894
|
+
}
|
|
20895
|
+
|
|
20896
|
+
function updateGlowRow(type, records, ids) {
|
|
20897
|
+
var o = rows[type];
|
|
20898
|
+
var shown = getShownGlow(records, ids, type, pending[type]);
|
|
20899
|
+
o.showColor(shown.color);
|
|
20900
|
+
o.input.attr('placeholder', shown.mixed ? 'mixed' : '');
|
|
20901
|
+
o.chit.classed('mixed', shown.mixed);
|
|
20902
|
+
o.opacity.node().value = shown.opacity === '' ? '' : formatOpacityPct(shown.opacity);
|
|
20903
|
+
o.width.setValue(shown.width === '' ? '' : String(shown.width));
|
|
20904
|
+
o.width.setPlaceholder(shown.width === '' ? 'mixed' : '');
|
|
20905
|
+
}
|
|
20906
|
+
}
|
|
20907
|
+
|
|
20908
|
+
function getGlowField(type, name) {
|
|
20909
|
+
return type + '-glow-' + name;
|
|
20910
|
+
}
|
|
20911
|
+
|
|
20912
|
+
// In the panel, a feature has a glow when the glow's color is set.
|
|
20913
|
+
function hasGlow(rec, type) {
|
|
20914
|
+
return !!rec && !isBlank(rec[getGlowField(type, 'color')]);
|
|
20915
|
+
}
|
|
20916
|
+
|
|
20917
|
+
// 'on', 'off' or 'mixed', over the features @ids
|
|
20918
|
+
function getGlowState(records, ids, type) {
|
|
20919
|
+
var n = ids.filter(function(id) { return hasGlow(records[id], type); }).length;
|
|
20920
|
+
return n === 0 ? 'off' : n < ids.length ? 'mixed' : 'on';
|
|
20921
|
+
}
|
|
20922
|
+
|
|
20923
|
+
function getEffectsState(records, ids) {
|
|
20924
|
+
var n = ids.filter(function(id) {
|
|
20925
|
+
return hasGlow(records[id], 'outer') || hasGlow(records[id], 'inner');
|
|
20926
|
+
}).length;
|
|
20927
|
+
return n === 0 ? 'off' : n < ids.length ? 'mixed' : 'on';
|
|
20928
|
+
}
|
|
20929
|
+
|
|
20930
|
+
// Per-feature edits for a color typed or picked for one glow. A color adds the
|
|
20931
|
+
// glow to every feature, with the @pending width and opacity where a feature
|
|
20932
|
+
// has none of its own; an empty color removes the glow's color.
|
|
20933
|
+
function getGlowColorEdits(records, ids, type, color, pending) {
|
|
20934
|
+
var colorField = getGlowField(type, 'color');
|
|
20935
|
+
var edits = [];
|
|
20936
|
+
ids.forEach(function(id) {
|
|
20937
|
+
var rec = records[id] || {};
|
|
20938
|
+
var styles = [];
|
|
20939
|
+
if (!color) {
|
|
20940
|
+
if (!isBlank(rec[colorField])) styles.push([colorField, '']);
|
|
20941
|
+
} else {
|
|
20942
|
+
if (String(rec[colorField]).trim() != color) styles.push([colorField, color]);
|
|
20943
|
+
['width', 'opacity'].forEach(function(name) {
|
|
20944
|
+
var field = getGlowField(type, name);
|
|
20945
|
+
if (pending && name in pending && isBlank(rec[field])) {
|
|
20946
|
+
var val = formatGlowValue(name, pending[name]);
|
|
20947
|
+
if (val !== '') styles.push([field, val]);
|
|
20948
|
+
}
|
|
20949
|
+
});
|
|
20950
|
+
}
|
|
20951
|
+
if (styles.length > 0) edits.push({id: id, styles: styles});
|
|
20952
|
+
});
|
|
20953
|
+
return edits;
|
|
20954
|
+
}
|
|
20955
|
+
|
|
20956
|
+
// Switching Effects off removes every glow setting, which mean nothing
|
|
20957
|
+
// without a color.
|
|
20958
|
+
function getEffectsOffEdits(records, ids) {
|
|
20959
|
+
var edits = [];
|
|
20960
|
+
ids.forEach(function(id) {
|
|
20961
|
+
var rec = records[id] || {};
|
|
20962
|
+
var styles = internal.svg.glowFields.filter(function(field) {
|
|
20963
|
+
return !isBlank(rec[field]);
|
|
20964
|
+
}).map(function(field) {
|
|
20965
|
+
return [field, ''];
|
|
20966
|
+
});
|
|
20967
|
+
if (styles.length > 0) edits.push({id: id, styles: styles});
|
|
20968
|
+
});
|
|
20969
|
+
return edits;
|
|
20970
|
+
}
|
|
20971
|
+
|
|
20972
|
+
// What a glow's row shows. The color is blank where no feature has the glow,
|
|
20973
|
+
// and blank and mixed where only some do or their colors differ. The width
|
|
20974
|
+
// and opacity are those the glowing features agree on, '' where they differ;
|
|
20975
|
+
// with no glowing features, they are the @pending ones, the default width and
|
|
20976
|
+
// no opacity.
|
|
20977
|
+
function getShownGlow(records, ids, type, pending) {
|
|
20978
|
+
var glowIds = ids.filter(function(id) { return hasGlow(records[id], type); });
|
|
20979
|
+
var defaultWidth = internal.svg.DEFAULT_GLOW_WIDTH;
|
|
20980
|
+
var color, mixed;
|
|
20981
|
+
pending = pending || {};
|
|
20982
|
+
if (glowIds.length === 0) {
|
|
20983
|
+
return {
|
|
20984
|
+
color: '',
|
|
20985
|
+
mixed: false,
|
|
20986
|
+
width: 'width' in pending ? pending.width : defaultWidth,
|
|
20987
|
+
opacity: 'opacity' in pending ? pending.opacity : ''
|
|
20988
|
+
};
|
|
20989
|
+
}
|
|
20990
|
+
color = getCommonValue(records, glowIds, getGlowField(type, 'color'), undefined, trim);
|
|
20991
|
+
mixed = glowIds.length < ids.length || color === '';
|
|
20992
|
+
return {
|
|
20993
|
+
color: mixed ? '' : color,
|
|
20994
|
+
mixed: mixed,
|
|
20995
|
+
width: getCommonValue(records, glowIds, getGlowField(type, 'width'), defaultWidth, Number),
|
|
20996
|
+
opacity: getCommonValue(records, glowIds, getGlowField(type, 'opacity'), 1, Number)
|
|
20997
|
+
};
|
|
20998
|
+
}
|
|
20999
|
+
|
|
21000
|
+
// Full opacity is the default, and stored as no opacity at all.
|
|
21001
|
+
function formatGlowValue(name, value) {
|
|
21002
|
+
return name == 'opacity' && value >= 1 ? '' : value;
|
|
21003
|
+
}
|
|
21004
|
+
|
|
21005
|
+
// The value of @field that the features @ids share, or '' if they differ.
|
|
21006
|
+
function getCommonValue(records, ids, field, defaultValue, convert) {
|
|
21007
|
+
var value, val;
|
|
21008
|
+
for (var i=0; i<ids.length; i++) {
|
|
21009
|
+
val = records[ids[i]] && records[ids[i]][field];
|
|
21010
|
+
if (isBlank(val)) val = defaultValue === undefined ? '' : defaultValue;
|
|
21011
|
+
if (convert && val !== '' && val !== null) val = convert(val);
|
|
21012
|
+
if (i === 0) {
|
|
21013
|
+
value = val;
|
|
21014
|
+
} else if (val !== value) {
|
|
21015
|
+
return '';
|
|
21016
|
+
}
|
|
21017
|
+
}
|
|
21018
|
+
return ids.length > 0 ? value : '';
|
|
21019
|
+
}
|
|
21020
|
+
|
|
21021
|
+
function trim(val) {
|
|
21022
|
+
return String(val).trim();
|
|
21023
|
+
}
|
|
21024
|
+
|
|
20349
21025
|
function isBlank(val) {
|
|
20350
21026
|
return val === undefined || val === null || String(val).trim() === '';
|
|
20351
21027
|
}
|
|
20352
21028
|
|
|
21029
|
+
// The state behind the color scheme panel, as plain objects and functions
|
|
21030
|
+
// that return new ones. See docs/development/color-scheme-panel-design.md
|
|
21031
|
+
//
|
|
21032
|
+
// A sequential scheme is either a preset (a named d3 ramp, possibly reversed)
|
|
21033
|
+
// or a custom ramp defined by pins ([{t, color}], see src/color/color-ramps.mjs).
|
|
21034
|
+
// Editing a tile of a preset turns it into a custom ramp, pinned at the
|
|
21035
|
+
// preset's two ends and at the edited tile.
|
|
21036
|
+
//
|
|
21037
|
+
// A categorical scheme is a palette of swatches, either a d3 categorical
|
|
21038
|
+
// scheme (in its own order, or rearranged) or a custom list, of which the
|
|
21039
|
+
// first n are used. Its 'categorical' method gives each unique value of the
|
|
21040
|
+
// field a swatch, taking them in turn (so there are never more swatches in
|
|
21041
|
+
// use than values); 'non-adjacent' has no field, and colors neighboring
|
|
21042
|
+
// polygons differently. Editing a swatch of a preset turns it into a custom
|
|
21043
|
+
// list.
|
|
21044
|
+
//
|
|
21045
|
+
// A diverging scheme is a ramp of 2K+1 tiles: K tiles on either side of a
|
|
21046
|
+
// center color, where K is the number of classes on the longer side of the
|
|
21047
|
+
// pivot. The shorter side uses the tiles nearest the center, and the center
|
|
21048
|
+
// tile colors the pivot class (if there is one). How many classes each side
|
|
21049
|
+
// has depends on the data, so the scheme keeps the layout that -classify
|
|
21050
|
+
// will find (see updateDivergingLayout()). A custom diverging ramp is pinned
|
|
21051
|
+
// at its ends and its center.
|
|
21052
|
+
//
|
|
21053
|
+
// A continuous sequential or diverging scheme gives unclassed colors: its
|
|
21054
|
+
// tiles are color stops, at the data's min, the class breaks and its max,
|
|
21055
|
+
// with colors interpolated between them. Each side of a diverging scheme is
|
|
21056
|
+
// a ramp of its own, a tile longer than its classes (so K is the longer
|
|
21057
|
+
// side's classes + 1); the center tile is used only by a pivot class, which
|
|
21058
|
+
// is a flat band of the center color.
|
|
21059
|
+
//
|
|
21060
|
+
// scheme: {
|
|
21061
|
+
// type: 'sequential', 'diverging' or 'categorical',
|
|
21062
|
+
// field, method, n, (n: the number of colors, or for
|
|
21063
|
+
// diverging schemes, of classes:
|
|
21064
|
+
// the total, or per side)
|
|
21065
|
+
// preset: name or null, reversed: boolean, (presets)
|
|
21066
|
+
// pins: [{t, color}] or null, (custom sequential ramps)
|
|
21067
|
+
// vibrance, longHue (custom sequential ramps, see
|
|
21068
|
+
// getOklchInterpolator())
|
|
21069
|
+
// order: [...] or null (categorical presets: indexes
|
|
21070
|
+
// of the preset's colors, as
|
|
21071
|
+
// rearranged)
|
|
21072
|
+
// swatches: [...] or null (custom categorical lists)
|
|
21073
|
+
// continuous: boolean (sequential and diverging)
|
|
21074
|
+
// pivot: 'auto', 'median', 'mean' or a number,
|
|
21075
|
+
// neutral, continuousNeutral: boolean (a pivot class, for classed
|
|
21076
|
+
// and continuous schemes; see
|
|
21077
|
+
// getSchemeNeutral())
|
|
21078
|
+
// split: 'size' or 'count' (diverging: n is the total
|
|
21079
|
+
// number of classes, split so
|
|
21080
|
+
// they're the same size on both
|
|
21081
|
+
// sides, or the number per side)
|
|
21082
|
+
// layout: {pivot, neutral, below, above, breaks} or null
|
|
21083
|
+
// (diverging: the classes the
|
|
21084
|
+
// data gives, see
|
|
21085
|
+
// getDivergingLayout())
|
|
21086
|
+
// nullColor the color of features with no data (both types; not
|
|
21087
|
+
// used by non-adjacent schemes)
|
|
21088
|
+
// colors: [...] the colors that were applied, for checking a layer later
|
|
21089
|
+
// fillHash continuous schemes: a fingerprint of the fills they gave
|
|
21090
|
+
// the layer (interpolated fills aren't the scheme's colors)
|
|
21091
|
+
// }
|
|
21092
|
+
|
|
21093
|
+
var schemeTypes = [
|
|
21094
|
+
{name: 'sequential', label: 'Sequential'},
|
|
21095
|
+
{name: 'diverging', label: 'Diverging'},
|
|
21096
|
+
{name: 'categorical', label: 'Categorical'}
|
|
21097
|
+
];
|
|
21098
|
+
|
|
21099
|
+
var classifyMethods = [
|
|
21100
|
+
{name: 'quantile', label: 'Quantile'},
|
|
21101
|
+
{name: 'equal-interval', label: 'Equal interval'},
|
|
21102
|
+
{name: 'nice', label: 'Nice breaks'},
|
|
21103
|
+
{name: 'hybrid', label: 'Hybrid'}
|
|
21104
|
+
];
|
|
21105
|
+
|
|
21106
|
+
var pivotOptions = [
|
|
21107
|
+
{name: 'auto', label: 'Auto (0 or median)'},
|
|
21108
|
+
{name: 'median', label: 'Median'},
|
|
21109
|
+
{name: 'mean', label: 'Mean'},
|
|
21110
|
+
{name: 'value', label: 'Value'}
|
|
21111
|
+
];
|
|
21112
|
+
|
|
21113
|
+
var divergingSplits = [
|
|
21114
|
+
{name: 'size', label: 'Same class size'},
|
|
21115
|
+
{name: 'count', label: 'Same number of classes'}
|
|
21116
|
+
];
|
|
21117
|
+
|
|
21118
|
+
var categoricalMethods = [
|
|
21119
|
+
{name: 'categorical', label: 'Categories'},
|
|
21120
|
+
{name: 'non-adjacent', label: 'Non-adjacent'}
|
|
21121
|
+
];
|
|
21122
|
+
|
|
21123
|
+
// -classify's default null value for colors, as hex
|
|
21124
|
+
var defaultNullColor = '#eeeeee';
|
|
21125
|
+
|
|
21126
|
+
var minSchemeColors = 2;
|
|
21127
|
+
var maxSchemeColors = 12;
|
|
21128
|
+
var maxCategoricalColors = 20;
|
|
21129
|
+
var defaultCategoricalPreset = 'Tableau10';
|
|
21130
|
+
var defaultNonAdjacentColors = 5;
|
|
21131
|
+
var defaultDivergingPreset = 'RdBu';
|
|
21132
|
+
var defaultDivergingClasses = 7;
|
|
21133
|
+
var defaultDivergingSideClasses = 3;
|
|
21134
|
+
var maxDivergingSideClasses = 6;
|
|
21135
|
+
// colors for growing a custom categorical list past the palette it came from
|
|
21136
|
+
var extraSwatchSource = 'Tableau20';
|
|
21137
|
+
// vibrance is OKLCH chroma added to the colors of each interpolated segment
|
|
21138
|
+
// (-classify's vibrance= is the same, scaled to 0-1)
|
|
21139
|
+
var defaultVibrance = 0;
|
|
21140
|
+
var maxVibrance = internal.MAX_VIBRANCE_CHROMA;
|
|
21141
|
+
var defaultEnds = ['#344a72', '#f0d26b'];
|
|
21142
|
+
// Cyclic ramps start and end on the same hue, which reads as a category
|
|
21143
|
+
// rather than an order.
|
|
21144
|
+
var excludedPresets = ['Rainbow', 'Sinebow'];
|
|
21145
|
+
|
|
21146
|
+
// Layers keep the scheme that colored them, so that the panel can reopen it
|
|
21147
|
+
// and the style panel can show it. Undo and redo of a scheme edit put back
|
|
21148
|
+
// the scheme that went with the colors (see the panel's command session).
|
|
21149
|
+
var layerSchemes = new WeakMap();
|
|
21150
|
+
|
|
21151
|
+
function getSequentialPresetNames() {
|
|
21152
|
+
return internal.getColorSchemeNames('sequential')
|
|
21153
|
+
.concat(internal.getColorSchemeNames('rainbow'))
|
|
21154
|
+
.filter(function(name) {
|
|
21155
|
+
return !excludedPresets.includes(name);
|
|
21156
|
+
});
|
|
21157
|
+
}
|
|
21158
|
+
|
|
21159
|
+
function getDivergingPresetNames() {
|
|
21160
|
+
return internal.getColorSchemeNames('diverging');
|
|
21161
|
+
}
|
|
21162
|
+
|
|
21163
|
+
function getCategoricalPresetNames() {
|
|
21164
|
+
return internal.getColorSchemeNames('categorical');
|
|
21165
|
+
}
|
|
21166
|
+
|
|
21167
|
+
// The scheme a panel tab starts with on a layer
|
|
21168
|
+
function getDefaultSchemeOfType(type, lyr) {
|
|
21169
|
+
if (type == 'categorical') return getDefaultCategoricalScheme(lyr);
|
|
21170
|
+
if (type == 'diverging') return getDefaultDivergingScheme(lyr);
|
|
21171
|
+
return getDefaultScheme(getNumericFields(lyr)[0]);
|
|
21172
|
+
}
|
|
21173
|
+
|
|
21174
|
+
function getDefaultDivergingScheme(lyr) {
|
|
21175
|
+
return updateDivergingLayout({
|
|
21176
|
+
type: 'diverging',
|
|
21177
|
+
field: getNumericFields(lyr)[0] || null,
|
|
21178
|
+
method: 'quantile',
|
|
21179
|
+
n: defaultDivergingClasses,
|
|
21180
|
+
split: 'size',
|
|
21181
|
+
pivot: 'auto',
|
|
21182
|
+
neutral: true,
|
|
21183
|
+
preset: defaultDivergingPreset,
|
|
21184
|
+
reversed: false,
|
|
21185
|
+
pins: null,
|
|
21186
|
+
vibrance: defaultVibrance
|
|
21187
|
+
}, lyr);
|
|
21188
|
+
}
|
|
21189
|
+
|
|
21190
|
+
// The layout of a diverging scheme's classes on the layer's data, as
|
|
21191
|
+
// -classify finds it; null if there are no data to classify. Done after
|
|
21192
|
+
// every change that can move the classes (the field, method, pivot, pivot
|
|
21193
|
+
// class, number of classes), and when the panel opens, since the data may
|
|
21194
|
+
// have changed.
|
|
21195
|
+
function updateDivergingLayout(scheme, lyr) {
|
|
21196
|
+
if (scheme.type != 'diverging') return scheme;
|
|
21197
|
+
return Object.assign({}, scheme, {layout: getDivergingLayout(scheme, lyr)});
|
|
21198
|
+
}
|
|
21199
|
+
|
|
21200
|
+
function getDivergingLayout(scheme, lyr) {
|
|
21201
|
+
var values = getAscendingValues(lyr, scheme.field);
|
|
21202
|
+
if (values.length === 0) return null;
|
|
21203
|
+
var neutral = getSchemeNeutral(scheme);
|
|
21204
|
+
try {
|
|
21205
|
+
return internal.getDivergingLayout(values, scheme.method, {
|
|
21206
|
+
pivot: scheme.pivot,
|
|
21207
|
+
continuous: !!scheme.continuous,
|
|
21208
|
+
pivot_class: neutral,
|
|
21209
|
+
no_pivot_class: !neutral,
|
|
21210
|
+
classes: getDivergingClassesOption(scheme)
|
|
21211
|
+
});
|
|
21212
|
+
} catch(e) {
|
|
21213
|
+
return null;
|
|
21214
|
+
}
|
|
21215
|
+
}
|
|
21216
|
+
|
|
21217
|
+
// Whether a diverging scheme has a pivot class. Classed and continuous
|
|
21218
|
+
// schemes each keep their own setting, so that switching between them keeps
|
|
21219
|
+
// both: on by default for classes, off for continuous colors.
|
|
21220
|
+
function getSchemeNeutral(scheme) {
|
|
21221
|
+
return scheme.continuous ? !!scheme.continuousNeutral : scheme.neutral !== false;
|
|
21222
|
+
}
|
|
21223
|
+
|
|
21224
|
+
function isContinuousScheme(scheme) {
|
|
21225
|
+
return scheme.type != 'categorical' && !!scheme.continuous;
|
|
21226
|
+
}
|
|
21227
|
+
|
|
21228
|
+
// A sequential ramp starts with at least this many stops, since -classify
|
|
21229
|
+
// interpolates straight between them, so vibrance and the long hue path
|
|
21230
|
+
// show only at the stops.
|
|
21231
|
+
var minContinuousStartColors = 5;
|
|
21232
|
+
|
|
21233
|
+
function setSchemeContinuous(scheme, continuous) {
|
|
21234
|
+
var next = Object.assign({}, scheme, {continuous: !!continuous});
|
|
21235
|
+
if (continuous && next.type == 'sequential' && next.n < minContinuousStartColors) {
|
|
21236
|
+
next.n = minContinuousStartColors;
|
|
21237
|
+
}
|
|
21238
|
+
return next;
|
|
21239
|
+
}
|
|
21240
|
+
|
|
21241
|
+
function getAscendingValues(lyr, field) {
|
|
21242
|
+
if (!lyr || !lyr.data || !field || !lyr.data.fieldExists(field)) return [];
|
|
21243
|
+
return lyr.data.getRecords().map(function(rec) {
|
|
21244
|
+
return rec ? rec[field] : undefined;
|
|
21245
|
+
}).filter(isFiniteNumber).sort(function(a, b) { return a - b; });
|
|
21246
|
+
}
|
|
21247
|
+
|
|
21248
|
+
// -classify's classes= for a diverging scheme
|
|
21249
|
+
function getDivergingClassesOption(scheme) {
|
|
21250
|
+
return scheme.split == 'count' ? [scheme.n, scheme.n] : scheme.n;
|
|
21251
|
+
}
|
|
21252
|
+
|
|
21253
|
+
// The number of tiles of a ramp: for a diverging scheme, the center tile and
|
|
21254
|
+
// enough on each side for the longer side's classes (or a continuous
|
|
21255
|
+
// side's stops)
|
|
21256
|
+
function getRampSize(scheme) {
|
|
21257
|
+
var layout = scheme.layout;
|
|
21258
|
+
var k;
|
|
21259
|
+
if (scheme.type != 'diverging') return scheme.n;
|
|
21260
|
+
k = layout ? Math.max(layout.below, layout.above) :
|
|
21261
|
+
scheme.split == 'count' ? scheme.n : Math.floor(scheme.n / 2);
|
|
21262
|
+
k = scheme.continuous ? k + 1 : Math.max(k, 1);
|
|
21263
|
+
return k * 2 + 1;
|
|
21264
|
+
}
|
|
21265
|
+
|
|
21266
|
+
// The numbers of tiles that each side of a diverging scheme uses
|
|
21267
|
+
function getSideTiles(scheme) {
|
|
21268
|
+
var layout = scheme.layout;
|
|
21269
|
+
if (!scheme.continuous) return [layout.below, layout.above];
|
|
21270
|
+
return [internal.getContinuousSideStops(layout.below), internal.getContinuousSideStops(layout.above)];
|
|
21271
|
+
}
|
|
21272
|
+
|
|
21273
|
+
// One boolean per tile of a diverging scheme: whether a class (or a color
|
|
21274
|
+
// stop) uses it
|
|
21275
|
+
function getDivergingTileUse(scheme) {
|
|
21276
|
+
var size = getRampSize(scheme);
|
|
21277
|
+
var k = (size - 1) / 2;
|
|
21278
|
+
var layout = scheme.layout;
|
|
21279
|
+
var sides = layout ? getSideTiles(scheme) : [0, 0];
|
|
21280
|
+
var use = [];
|
|
21281
|
+
for (var i=0; i<size; i++) {
|
|
21282
|
+
use.push(!!layout && (i < k ? i >= k - sides[0] : i > k ? i <= k + sides[1] : !!layout.neutral));
|
|
21283
|
+
}
|
|
21284
|
+
return use;
|
|
21285
|
+
}
|
|
21286
|
+
|
|
21287
|
+
// The data value at each tile of a continuous scheme: {value, min, max}
|
|
21288
|
+
// (min and max: the value is the data's min or max), or for a diverging
|
|
21289
|
+
// scheme's center tile, the pivot class's range: {range}. Tiles that no stop
|
|
21290
|
+
// uses are null. Returns null if there are no data.
|
|
21291
|
+
function getContinuousTileStops(scheme, lyr) {
|
|
21292
|
+
var values, breaks, layout, stops, k, tiles;
|
|
21293
|
+
if (scheme.type == 'sequential') {
|
|
21294
|
+
values = getAscendingValues(lyr, scheme.field);
|
|
21295
|
+
if (values.length === 0) return null;
|
|
21296
|
+
try {
|
|
21297
|
+
breaks = internal.getSequentialBreaks(values, scheme.method, scheme.n - 2);
|
|
21298
|
+
} catch(e) {
|
|
21299
|
+
return null;
|
|
21300
|
+
}
|
|
21301
|
+
return markDataRange([values[0]].concat(breaks, [values[values.length - 1]]), values);
|
|
21302
|
+
}
|
|
21303
|
+
layout = scheme.layout;
|
|
21304
|
+
if (!layout) return null;
|
|
21305
|
+
stops = internal.getDivergingStops(layout);
|
|
21306
|
+
k = (getRampSize(scheme) - 1) / 2;
|
|
21307
|
+
tiles = new Array(k * 2 + 1).fill(null);
|
|
21308
|
+
markDataRange(stops.below, layout.dataRange).forEach(function(stop, i) {
|
|
21309
|
+
tiles[k - stops.below.length + i] = stop;
|
|
21310
|
+
});
|
|
21311
|
+
if (layout.neutral) tiles[k] = {range: layout.neutral};
|
|
21312
|
+
markDataRange(stops.above, layout.dataRange).forEach(function(stop, i) {
|
|
21313
|
+
tiles[k + 1 + i] = stop;
|
|
21314
|
+
});
|
|
21315
|
+
return tiles;
|
|
21316
|
+
}
|
|
21317
|
+
|
|
21318
|
+
// range: ascending values, of which the first is the min and the last the max
|
|
21319
|
+
function markDataRange(stops, range) {
|
|
21320
|
+
var min = range[0], max = range[range.length - 1];
|
|
21321
|
+
return stops.map(function(val) {
|
|
21322
|
+
return {value: val, min: val == min, max: val == max};
|
|
21323
|
+
});
|
|
21324
|
+
}
|
|
21325
|
+
|
|
21326
|
+
// The gradients to draw over the tiles of a continuous scheme, in order:
|
|
21327
|
+
// [{start, end, flatStart, flatEnd, samples: [{pos, color}]}], with positions
|
|
21328
|
+
// in tiles (0 is the center of the first tile, 1 of the second...). A
|
|
21329
|
+
// gradient runs from stop to stop, and holds its end colors over the outer
|
|
21330
|
+
// halves of its end tiles (flatStart, flatEnd) -- except where the data ends
|
|
21331
|
+
// between two stops (the shorter side of an equal-interval or nice
|
|
21332
|
+
// diverging scheme). A pivot class is a gradient of one color, over its
|
|
21333
|
+
// tile. The samples are of the interpolation -classify does between the
|
|
21334
|
+
// stops (samplesPerTile to each pair).
|
|
21335
|
+
function getContinuousSegments(scheme, samplesPerTile) {
|
|
21336
|
+
var colors = getSchemeColors(scheme);
|
|
21337
|
+
var layout = scheme.layout;
|
|
21338
|
+
var steps = samplesPerTile || 6;
|
|
21339
|
+
var segments = [];
|
|
21340
|
+
var stops, k, first, last;
|
|
21341
|
+
if (!isContinuousScheme(scheme)) return [];
|
|
21342
|
+
if (scheme.type == 'sequential') {
|
|
21343
|
+
return [makeSegment(colors, 0, 0, colors.length - 1, steps)];
|
|
21344
|
+
}
|
|
21345
|
+
if (!layout) return [];
|
|
21346
|
+
stops = internal.getDivergingStops(layout);
|
|
21347
|
+
k = (colors.length - 1) / 2;
|
|
21348
|
+
if (stops.below.length > 0) {
|
|
21349
|
+
first = k - stops.below.length;
|
|
21350
|
+
segments.push(makeSegment(colors.slice(first, k), first,
|
|
21351
|
+
first + internal.getStopPosition(stops.below, layout.dataRange[0]), k - 1, steps));
|
|
21352
|
+
}
|
|
21353
|
+
if (layout.neutral) {
|
|
21354
|
+
segments.push(makeSegment([colors[k]], k, k, k, steps));
|
|
21355
|
+
}
|
|
21356
|
+
if (stops.above.length > 0) {
|
|
21357
|
+
last = k + stops.above.length;
|
|
21358
|
+
segments.push(makeSegment(colors.slice(k + 1, last + 1), k + 1,
|
|
21359
|
+
k + 1, k + 1 + internal.getStopPosition(stops.above, layout.dataRange[1]), steps));
|
|
21360
|
+
}
|
|
21361
|
+
return segments;
|
|
21362
|
+
}
|
|
21363
|
+
|
|
21364
|
+
// colors: the stops' colors, the first at tile first
|
|
21365
|
+
// start, end: where the gradient starts and ends, in tiles
|
|
21366
|
+
function makeSegment(colors, first, start, end, steps) {
|
|
21367
|
+
var last = first + colors.length - 1;
|
|
21368
|
+
var getColor = colors.length > 1 ?
|
|
21369
|
+
internal.getInterpolatedValueGetter(colors, null, {interpolation: 'oklch'}) : null;
|
|
21370
|
+
var samples = [];
|
|
21371
|
+
var sample = function(pos) {
|
|
21372
|
+
samples.push({pos: pos, color: getColor ? toHex(getColor(pos - first)) : colors[0]});
|
|
21373
|
+
};
|
|
21374
|
+
sample(start);
|
|
21375
|
+
for (var i=Math.floor(start * steps) + 1; i / steps < end; i++) {
|
|
21376
|
+
sample(i / steps);
|
|
21377
|
+
}
|
|
21378
|
+
if (end > start) sample(end);
|
|
21379
|
+
return {start: start, end: end, flatStart: start == first, flatEnd: end == last, samples: samples};
|
|
21380
|
+
}
|
|
21381
|
+
|
|
21382
|
+
// The data range of each class of a diverging layout, in class order:
|
|
21383
|
+
// [low, high), with -Infinity and Infinity at the outer ends
|
|
21384
|
+
function getDivergingClassRanges(layout) {
|
|
21385
|
+
return getBreakRanges(layout ? layout.breaks : []);
|
|
21386
|
+
}
|
|
21387
|
+
|
|
21388
|
+
// The data range of each class of a sequential scheme, as -classify finds
|
|
21389
|
+
// them (see getDivergingClassRanges()), or null if there are no data
|
|
21390
|
+
function getSequentialClassRanges(scheme, lyr) {
|
|
21391
|
+
var values = getAscendingValues(lyr, scheme.field);
|
|
21392
|
+
if (values.length === 0) return null;
|
|
21393
|
+
try {
|
|
21394
|
+
return getBreakRanges(internal.getSequentialBreaks(values, scheme.method, scheme.n - 1));
|
|
21395
|
+
} catch(e) {
|
|
21396
|
+
return null;
|
|
21397
|
+
}
|
|
21398
|
+
}
|
|
21399
|
+
|
|
21400
|
+
function getBreakRanges(breaks) {
|
|
21401
|
+
var ranges = [];
|
|
21402
|
+
for (var i=0; i<=breaks.length; i++) {
|
|
21403
|
+
ranges.push([i > 0 ? breaks[i - 1] : -Infinity, i < breaks.length ? breaks[i] : Infinity]);
|
|
21404
|
+
}
|
|
21405
|
+
return ranges;
|
|
21406
|
+
}
|
|
21407
|
+
|
|
21408
|
+
// How the data falls around the pivot of a diverging scheme:
|
|
21409
|
+
// {pivot, below, above} (features below the pivot, and at or above it)
|
|
21410
|
+
function getPivotSummary(scheme, lyr) {
|
|
21411
|
+
var values = getAscendingValues(lyr, scheme.field);
|
|
21412
|
+
var pivot = scheme.layout ? scheme.layout.pivot : null;
|
|
21413
|
+
if (pivot === null) return null;
|
|
21414
|
+
return {
|
|
21415
|
+
pivot: pivot,
|
|
21416
|
+
below: values.filter(function(val) { return val < pivot; }).length,
|
|
21417
|
+
above: values.filter(function(val) { return val >= pivot; }).length
|
|
21418
|
+
};
|
|
21419
|
+
}
|
|
21420
|
+
|
|
21421
|
+
// pivot: 'auto', 'median', 'mean' or a number
|
|
21422
|
+
function setSchemePivot(scheme, pivot) {
|
|
21423
|
+
return Object.assign({}, scheme, {pivot: pivot});
|
|
21424
|
+
}
|
|
21425
|
+
|
|
21426
|
+
// sets the pivot class of the scheme's mode (see getSchemeNeutral())
|
|
21427
|
+
function setSchemeNeutral(scheme, neutral) {
|
|
21428
|
+
return Object.assign({}, scheme, scheme.continuous ? {continuousNeutral: !!neutral} : {neutral: !!neutral});
|
|
21429
|
+
}
|
|
21430
|
+
|
|
21431
|
+
// Switching between a total and a number per side starts over with the
|
|
21432
|
+
// default number
|
|
21433
|
+
function setSchemeSplit(scheme, split) {
|
|
21434
|
+
if (scheme.split == split) return scheme;
|
|
21435
|
+
return Object.assign({}, scheme, {
|
|
21436
|
+
split: split,
|
|
21437
|
+
n: split == 'count' ? defaultDivergingSideClasses : defaultDivergingClasses
|
|
21438
|
+
});
|
|
21439
|
+
}
|
|
21440
|
+
|
|
21441
|
+
// By the first text field, or else the first field that can be classified
|
|
21442
|
+
// by value, or else non-adjacent
|
|
21443
|
+
function getDefaultCategoricalScheme(lyr) {
|
|
21444
|
+
var fields = getCategoryFields(lyr);
|
|
21445
|
+
var records = lyr && lyr.data ? lyr.data.getRecords() : [];
|
|
21446
|
+
var field = fields.find(function(name) {
|
|
21447
|
+
return internal.getColumnType(name, records) == 'string';
|
|
21448
|
+
}) || fields[0] || null;
|
|
21449
|
+
var scheme = {
|
|
21450
|
+
type: 'categorical',
|
|
21451
|
+
field: field,
|
|
21452
|
+
method: field ? 'categorical' : 'non-adjacent',
|
|
21453
|
+
n: defaultNonAdjacentColors,
|
|
21454
|
+
preset: defaultCategoricalPreset,
|
|
21455
|
+
order: null,
|
|
21456
|
+
swatches: null
|
|
21457
|
+
};
|
|
21458
|
+
return field ? setSchemeField(scheme, field, getCategories(lyr, field).length) : scheme;
|
|
21459
|
+
}
|
|
21460
|
+
|
|
21461
|
+
function getMinSchemeColors(scheme) {
|
|
21462
|
+
return scheme.type == 'diverging' && scheme.split == 'count' ? 1 : minSchemeColors;
|
|
21463
|
+
}
|
|
21464
|
+
|
|
21465
|
+
function getSchemeMethods(scheme) {
|
|
21466
|
+
return scheme.type == 'categorical' ? categoricalMethods : classifyMethods;
|
|
21467
|
+
}
|
|
21468
|
+
|
|
21469
|
+
// count: the number of categories, for the categorical method
|
|
21470
|
+
function getMaxSchemeColors(scheme, count) {
|
|
21471
|
+
var max = maxSchemeColors;
|
|
21472
|
+
if (scheme.type == 'diverging' && scheme.split == 'count') return maxDivergingSideClasses;
|
|
21473
|
+
if (scheme.type != 'categorical') return max;
|
|
21474
|
+
max = scheme.preset ? internal.getCategoricalColors(scheme.preset).length : maxCategoricalColors;
|
|
21475
|
+
if (scheme.method == 'categorical' && count >= 0) {
|
|
21476
|
+
max = Math.min(max, Math.max(count, minSchemeColors));
|
|
21477
|
+
}
|
|
21478
|
+
return max;
|
|
21479
|
+
}
|
|
21480
|
+
|
|
21481
|
+
function clampColorCount(scheme, n, count) {
|
|
21482
|
+
var min = getMinSchemeColors(scheme);
|
|
21483
|
+
return clamp$4(Math.round(n) || min, min, getMaxSchemeColors(scheme, count));
|
|
21484
|
+
}
|
|
21485
|
+
|
|
21486
|
+
// A new field for a categorical scheme gets a swatch for each of its values,
|
|
21487
|
+
// up to as many as the palette has.
|
|
21488
|
+
// count: the number of categories in the field
|
|
21489
|
+
function setSchemeField(scheme, field, count) {
|
|
21490
|
+
var next = Object.assign({}, scheme, {field: field});
|
|
21491
|
+
if (next.type == 'categorical' && next.method == 'categorical') {
|
|
21492
|
+
next.n = clampColorCount(next, count, count);
|
|
21493
|
+
}
|
|
21494
|
+
return next;
|
|
21495
|
+
}
|
|
21496
|
+
|
|
21497
|
+
// field, count: the field to use, and its number of categories, when a
|
|
21498
|
+
// categorical scheme changes to the categorical method
|
|
21499
|
+
function setSchemeMethod(scheme, method, field, count) {
|
|
21500
|
+
var next = Object.assign({}, scheme, {method: method});
|
|
21501
|
+
if (next.type != 'categorical') return next;
|
|
21502
|
+
if (method == 'categorical') {
|
|
21503
|
+
return setSchemeField(next, next.field || field, count);
|
|
21504
|
+
}
|
|
21505
|
+
return setTileCount(next, defaultNonAdjacentColors);
|
|
21506
|
+
}
|
|
21507
|
+
|
|
21508
|
+
// Fields that a categorical scheme can classify: text and numbers. The
|
|
21509
|
+
// fill field is left out, since the scheme writes over it.
|
|
21510
|
+
function getCategoryFields(lyr) {
|
|
21511
|
+
var records = lyr && lyr.data ? lyr.data.getRecords() : [];
|
|
21512
|
+
if (!lyr || !lyr.data) return [];
|
|
21513
|
+
return lyr.data.getFields().filter(function(field) {
|
|
21514
|
+
var type = internal.getColumnType(field, records);
|
|
21515
|
+
return field != 'fill' && (type == 'string' || type == 'number');
|
|
21516
|
+
});
|
|
21517
|
+
}
|
|
21518
|
+
|
|
21519
|
+
// The values of a field, in the order -classify gives them swatches. Empty
|
|
21520
|
+
// values are left out, as -classify leaves them out: they are no data.
|
|
21521
|
+
function getCategories(lyr, field) {
|
|
21522
|
+
if (!lyr || !lyr.data || !field || !lyr.data.fieldExists(field)) return [];
|
|
21523
|
+
return internal.getUniqFieldValues(lyr.data.getRecords(), field).filter(isCategoryValue);
|
|
21524
|
+
}
|
|
21525
|
+
|
|
21526
|
+
// The same test as -classify's (0 is a value; null, undefined, '' and NaN
|
|
21527
|
+
// are not)
|
|
21528
|
+
function isCategoryValue(val) {
|
|
21529
|
+
return !!val || val === 0;
|
|
21530
|
+
}
|
|
21531
|
+
|
|
21532
|
+
// How many features the scheme gives the no-data color
|
|
21533
|
+
function getNoDataCount(lyr, scheme) {
|
|
21534
|
+
var records, test;
|
|
21535
|
+
if (!lyr || !lyr.data || !scheme.field || scheme.method == 'non-adjacent' ||
|
|
21536
|
+
!lyr.data.fieldExists(scheme.field)) return 0;
|
|
21537
|
+
records = lyr.data.getRecords();
|
|
21538
|
+
test = scheme.type == 'categorical' ? isCategoryValue : isFiniteNumber;
|
|
21539
|
+
return records.filter(function(rec) {
|
|
21540
|
+
return !test(rec ? rec[scheme.field] : undefined);
|
|
21541
|
+
}).length;
|
|
21542
|
+
}
|
|
21543
|
+
|
|
21544
|
+
function isFiniteNumber(val) {
|
|
21545
|
+
return typeof val == 'number' && isFinite(val);
|
|
21546
|
+
}
|
|
21547
|
+
|
|
21548
|
+
function getSchemeNullColor(scheme) {
|
|
21549
|
+
return scheme.nullColor || defaultNullColor;
|
|
21550
|
+
}
|
|
21551
|
+
|
|
21552
|
+
// An empty or unreadable color is the default
|
|
21553
|
+
function setSchemeNullColor(scheme, color) {
|
|
21554
|
+
var rgb = color ? internal.parseColor(color) : null;
|
|
21555
|
+
return Object.assign({}, scheme, {nullColor: rgb ? internal.formatColor(rgb) : defaultNullColor});
|
|
21556
|
+
}
|
|
21557
|
+
|
|
21558
|
+
// The categories each swatch colors, as -classify assigns them
|
|
21559
|
+
function getSwatchCategories(categories, n) {
|
|
21560
|
+
var groups = [];
|
|
21561
|
+
for (var i=0; i<n; i++) groups.push([]);
|
|
21562
|
+
categories.forEach(function(val, i) {
|
|
21563
|
+
groups[i % n].push(val);
|
|
21564
|
+
});
|
|
21565
|
+
return groups;
|
|
21566
|
+
}
|
|
21567
|
+
|
|
21568
|
+
function getDefaultScheme(field) {
|
|
21569
|
+
return {
|
|
21570
|
+
type: 'sequential',
|
|
21571
|
+
field: field || null,
|
|
21572
|
+
method: 'quantile',
|
|
21573
|
+
n: 5,
|
|
21574
|
+
preset: null,
|
|
21575
|
+
reversed: false,
|
|
21576
|
+
pins: [{t: 0, color: defaultEnds[0]}, {t: 1, color: defaultEnds[1]}],
|
|
21577
|
+
vibrance: defaultVibrance
|
|
21578
|
+
};
|
|
21579
|
+
}
|
|
21580
|
+
|
|
21581
|
+
function getSchemeColors(scheme) {
|
|
21582
|
+
return getSchemeTiles(scheme).map(function(tile) {
|
|
21583
|
+
return tile.color;
|
|
21584
|
+
});
|
|
21585
|
+
}
|
|
21586
|
+
|
|
21587
|
+
// One object per tile: {color, pinned, adjusted, ...}, where adjusted means
|
|
21588
|
+
// an interpolated color was moved to fit the sRGB gamut (see resolveRampTiles())
|
|
21589
|
+
function getSchemeTiles(scheme) {
|
|
21590
|
+
var colors;
|
|
21591
|
+
if (scheme.type == 'categorical') {
|
|
21592
|
+
return getCategoricalPalette(scheme).slice(0, scheme.n).map(function(color) {
|
|
21593
|
+
return {color: color, pinned: false, adjusted: false};
|
|
21594
|
+
});
|
|
21595
|
+
}
|
|
21596
|
+
if (scheme.preset) {
|
|
21597
|
+
colors = getPresetColors(scheme.preset, getRampSize(scheme));
|
|
21598
|
+
if (scheme.reversed) colors.reverse();
|
|
21599
|
+
return colors.map(function(color) {
|
|
21600
|
+
return {color: color, pinned: false, adjusted: false};
|
|
21601
|
+
});
|
|
21602
|
+
}
|
|
21603
|
+
return internal.resolveRampTiles(scheme.pins, getRampSize(scheme), {
|
|
21604
|
+
vibrance: getSchemeVibrance(scheme),
|
|
21605
|
+
hue: scheme.longHue && scheme.type != 'diverging' ? 'longer' : 'shorter'
|
|
21606
|
+
});
|
|
21607
|
+
}
|
|
21608
|
+
|
|
21609
|
+
function getSchemeVibrance(scheme) {
|
|
21610
|
+
return scheme.vibrance >= 0 ? scheme.vibrance : defaultVibrance;
|
|
21611
|
+
}
|
|
21612
|
+
|
|
21613
|
+
function setSchemeVibrance(scheme, vibrance) {
|
|
21614
|
+
vibrance = clamp$4(+vibrance || 0, 0, maxVibrance);
|
|
21615
|
+
return Object.assign({}, scheme, {vibrance: vibrance});
|
|
21616
|
+
}
|
|
21617
|
+
|
|
21618
|
+
// longHue: hues go the long way around the color wheel between pinned colors
|
|
21619
|
+
function setSchemeLongHue(scheme, longHue) {
|
|
21620
|
+
return Object.assign({}, scheme, {longHue: !!longHue});
|
|
21621
|
+
}
|
|
21622
|
+
|
|
21623
|
+
function clamp$4(val, min, max) {
|
|
21624
|
+
return Math.min(Math.max(val, min), max);
|
|
21625
|
+
}
|
|
21626
|
+
|
|
21627
|
+
function getPresetColors(name, n) {
|
|
21628
|
+
return internal.getColorRamp(name, n).map(toHex);
|
|
21629
|
+
}
|
|
21630
|
+
|
|
21631
|
+
// All the swatches of a categorical scheme, in order: the first n are used
|
|
21632
|
+
function getCategoricalPalette(scheme) {
|
|
21633
|
+
var colors;
|
|
21634
|
+
if (!scheme.preset) return scheme.swatches.concat();
|
|
21635
|
+
colors = getCategoricalPresetColors(scheme.preset);
|
|
21636
|
+
if (!scheme.order) return colors;
|
|
21637
|
+
return scheme.order.map(function(i) { return colors[i]; });
|
|
21638
|
+
}
|
|
21639
|
+
|
|
21640
|
+
function getCategoricalPresetColors(name) {
|
|
21641
|
+
return internal.getCategoricalColors(name).map(toHex);
|
|
21642
|
+
}
|
|
21643
|
+
|
|
21644
|
+
// A list of n colors, starting with the given ones and going on with colors
|
|
21645
|
+
// they don't have yet
|
|
21646
|
+
function extendSwatches(colors, n) {
|
|
21647
|
+
var swatches = colors.slice(0, n);
|
|
21648
|
+
getCategoricalPresetColors(extraSwatchSource).forEach(function(color) {
|
|
21649
|
+
if (swatches.length < n && !swatches.includes(color)) {
|
|
21650
|
+
swatches.push(color);
|
|
21651
|
+
}
|
|
21652
|
+
});
|
|
21653
|
+
return internal.wrapColors(swatches, n);
|
|
21654
|
+
}
|
|
21655
|
+
|
|
21656
|
+
// One boolean per tile: whether the user set its color.
|
|
21657
|
+
function getPinnedTiles(scheme) {
|
|
21658
|
+
if (scheme.preset || scheme.type == 'categorical') {
|
|
21659
|
+
return new Array(getRampSize(scheme)).fill(false);
|
|
21660
|
+
}
|
|
21661
|
+
return internal.getPinnedSlots(scheme.pins, getRampSize(scheme)).map(function(pinId) {
|
|
21662
|
+
return pinId > -1;
|
|
21663
|
+
});
|
|
21664
|
+
}
|
|
21665
|
+
|
|
21666
|
+
// count: the number of categories, for a categorical scheme
|
|
21667
|
+
function choosePreset(scheme, name, count) {
|
|
21668
|
+
var next;
|
|
21669
|
+
if (scheme.type == 'categorical') {
|
|
21670
|
+
next = Object.assign({}, scheme, {preset: name, order: null, swatches: null});
|
|
21671
|
+
next.n = clampColorCount(next, next.n, count);
|
|
21672
|
+
return next;
|
|
21673
|
+
}
|
|
21674
|
+
return Object.assign({}, scheme, {preset: name, reversed: false, pins: null});
|
|
21675
|
+
}
|
|
21676
|
+
|
|
21677
|
+
// A preset as a custom ramp, pinned at its ends, for editing.
|
|
21678
|
+
function makeSchemeCustom(scheme) {
|
|
21679
|
+
var colors;
|
|
21680
|
+
if (!scheme.preset) return scheme;
|
|
21681
|
+
if (scheme.type == 'categorical') {
|
|
21682
|
+
return Object.assign({}, scheme, {preset: null, order: null, swatches: getCategoricalPalette(scheme)});
|
|
21683
|
+
}
|
|
21684
|
+
colors = getSchemeColors(scheme);
|
|
21685
|
+
return Object.assign({}, scheme, {
|
|
21686
|
+
preset: null,
|
|
21687
|
+
reversed: false,
|
|
21688
|
+
pins: scheme.type == 'diverging' ?
|
|
21689
|
+
[{t: 0, color: colors[0]}, {t: 0.5, color: colors[(colors.length - 1) / 2]},
|
|
21690
|
+
{t: 1, color: colors[colors.length - 1]}] :
|
|
21691
|
+
[{t: 0, color: colors[0]}, {t: 1, color: colors[colors.length - 1]}]
|
|
21692
|
+
});
|
|
21693
|
+
}
|
|
21694
|
+
|
|
21695
|
+
// The center tile of a diverging ramp, which stays pinned
|
|
21696
|
+
function getCenterTile(scheme) {
|
|
21697
|
+
return scheme.type == 'diverging' ? (getRampSize(scheme) - 1) / 2 : -1;
|
|
21698
|
+
}
|
|
21699
|
+
|
|
21700
|
+
// The color to edit for tile i: a pinned tile's own color, which vibrance
|
|
21701
|
+
// may have raised in the ramp, or else the tile's color
|
|
21702
|
+
function getTileEditColor(scheme, i) {
|
|
21703
|
+
var slot = scheme.preset || scheme.type == 'categorical' ? -1 :
|
|
21704
|
+
internal.getPinnedSlots(scheme.pins, getRampSize(scheme))[i];
|
|
21705
|
+
if (slot > -1) return toHex(scheme.pins[slot].color);
|
|
21706
|
+
return getSchemeColors(scheme)[i];
|
|
21707
|
+
}
|
|
21708
|
+
|
|
21709
|
+
function setTileColor(scheme, i, color) {
|
|
21710
|
+
var custom = makeSchemeCustom(scheme);
|
|
21711
|
+
var swatches;
|
|
21712
|
+
if (custom.type == 'categorical') {
|
|
21713
|
+
swatches = custom.swatches.concat();
|
|
21714
|
+
swatches[i] = toHex(color);
|
|
21715
|
+
return Object.assign({}, custom, {swatches: swatches});
|
|
21716
|
+
}
|
|
21717
|
+
return Object.assign({}, custom, {
|
|
21718
|
+
pins: internal.setRampPin(custom.pins, getRampSize(custom), i, toHex(color))
|
|
21719
|
+
});
|
|
21720
|
+
}
|
|
21721
|
+
|
|
21722
|
+
function clearTileColor(scheme, i) {
|
|
21723
|
+
if (scheme.preset || scheme.type == 'categorical' || i == getCenterTile(scheme)) return scheme;
|
|
21724
|
+
return Object.assign({}, scheme, {
|
|
21725
|
+
pins: internal.clearRampPin(scheme.pins, getRampSize(scheme), i)
|
|
21726
|
+
});
|
|
21727
|
+
}
|
|
21728
|
+
|
|
21729
|
+
// A custom categorical list grows to have at least n swatches.
|
|
21730
|
+
// count: the number of categories, for a categorical scheme
|
|
21731
|
+
function setTileCount(scheme, n, count) {
|
|
21732
|
+
var next = Object.assign({}, scheme, {n: clampColorCount(scheme, n, count)});
|
|
21733
|
+
if (next.type == 'categorical' && !next.preset && next.swatches.length < next.n) {
|
|
21734
|
+
next.swatches = extendSwatches(next.swatches, next.n);
|
|
21735
|
+
}
|
|
21736
|
+
return next;
|
|
21737
|
+
}
|
|
21738
|
+
|
|
21739
|
+
// Moves a categorical swatch to a new place in the palette. Moving one
|
|
21740
|
+
// into the swatches in use pushes the last of them out of use, and moving one
|
|
21741
|
+
// out of use brings the first unused one in.
|
|
21742
|
+
// to: the swatch's index after the move
|
|
21743
|
+
function moveSwatch(scheme, from, to) {
|
|
21744
|
+
return rearrangeSwatches(scheme, function(items) {
|
|
21745
|
+
var item = items.splice(from, 1)[0];
|
|
21746
|
+
items.splice(to, 0, item);
|
|
21747
|
+
return items;
|
|
21748
|
+
});
|
|
21749
|
+
}
|
|
21750
|
+
|
|
21751
|
+
// Shuffles all of a categorical palette's swatches, in use or not, into a
|
|
21752
|
+
// new order.
|
|
21753
|
+
// random: a function like Math.random (for testing)
|
|
21754
|
+
function shuffleScheme(scheme, random) {
|
|
21755
|
+
var rand = random || Math.random;
|
|
21756
|
+
return rearrangeSwatches(scheme, function(items) {
|
|
21757
|
+
var shuffled, tries = 0;
|
|
21758
|
+
do {
|
|
21759
|
+
shuffled = shuffle(items.concat(), rand);
|
|
21760
|
+
} while (items.length > 1 && ++tries < 10 && shuffled.every(function(item, i) {
|
|
21761
|
+
return item == items[i];
|
|
21762
|
+
}));
|
|
21763
|
+
return shuffled;
|
|
21764
|
+
});
|
|
21765
|
+
}
|
|
21766
|
+
|
|
21767
|
+
function shuffle(arr, rand) {
|
|
21768
|
+
var j, tmp;
|
|
21769
|
+
for (var i=arr.length - 1; i>0; i--) {
|
|
21770
|
+
j = Math.floor(rand() * (i + 1));
|
|
21771
|
+
tmp = arr[i];
|
|
21772
|
+
arr[i] = arr[j];
|
|
21773
|
+
arr[j] = tmp;
|
|
21774
|
+
}
|
|
21775
|
+
return arr;
|
|
21776
|
+
}
|
|
21777
|
+
|
|
21778
|
+
// Presets are rearranged by index, so that they keep their name
|
|
21779
|
+
function rearrangeSwatches(scheme, rearrange) {
|
|
21780
|
+
var order;
|
|
21781
|
+
if (scheme.type != 'categorical') return scheme;
|
|
21782
|
+
if (!scheme.preset) {
|
|
21783
|
+
return Object.assign({}, scheme, {swatches: rearrange(scheme.swatches.concat())});
|
|
21784
|
+
}
|
|
21785
|
+
order = scheme.order || getCategoricalPresetColors(scheme.preset).map(function(c, i) { return i; });
|
|
21786
|
+
return Object.assign({}, scheme, {order: rearrange(order.concat())});
|
|
21787
|
+
}
|
|
21788
|
+
|
|
21789
|
+
function reverseScheme(scheme) {
|
|
21790
|
+
if (scheme.type == 'categorical') return scheme;
|
|
21791
|
+
if (scheme.preset) {
|
|
21792
|
+
return Object.assign({}, scheme, {reversed: !scheme.reversed});
|
|
21793
|
+
}
|
|
21794
|
+
return Object.assign({}, scheme, {
|
|
21795
|
+
pins: scheme.pins.map(function(pin) {
|
|
21796
|
+
return {t: 1 - pin.t, color: pin.color};
|
|
21797
|
+
}).reverse()
|
|
21798
|
+
});
|
|
21799
|
+
}
|
|
21800
|
+
|
|
21801
|
+
// The colors to give -classify: no more than there are categories, for the
|
|
21802
|
+
// categorical method. A diverging scheme gives all its tiles, from which
|
|
21803
|
+
// -classify takes the same ones the classes use here (see
|
|
21804
|
+
// getDivergingClassValues()).
|
|
21805
|
+
// count: the number of categories
|
|
21806
|
+
function getAppliedColors(scheme, count) {
|
|
21807
|
+
var colors = getSchemeColors(scheme);
|
|
21808
|
+
if (scheme.type == 'categorical' && scheme.method == 'categorical' && count > 0) {
|
|
21809
|
+
colors = colors.slice(0, count);
|
|
21810
|
+
}
|
|
21811
|
+
return colors;
|
|
21812
|
+
}
|
|
21813
|
+
|
|
21814
|
+
// opts.target a -target value, when the layer is not the active one
|
|
21815
|
+
function formatSchemeCommand(scheme, colors, opts) {
|
|
21816
|
+
var parts = ['-classify'];
|
|
21817
|
+
if (scheme.method != 'non-adjacent') {
|
|
21818
|
+
parts.push('field=' + quoteCommandValue(scheme.field));
|
|
21819
|
+
}
|
|
21820
|
+
parts.push('method=' + scheme.method);
|
|
21821
|
+
if (scheme.type == 'diverging') {
|
|
21822
|
+
parts.push('pivot=' + scheme.pivot, 'classes=' + getDivergingClassesOption(scheme));
|
|
21823
|
+
// continuous output has no pivot class by default, classes have one
|
|
21824
|
+
if (!getSchemeNeutral(scheme)) parts.push('no-pivot-class');
|
|
21825
|
+
else if (scheme.continuous) parts.push('pivot-class');
|
|
21826
|
+
} else if (isContinuousScheme(scheme)) {
|
|
21827
|
+
parts.push('classes=' + (scheme.n - 1));
|
|
21828
|
+
}
|
|
21829
|
+
if (isContinuousScheme(scheme)) {
|
|
21830
|
+
// the tiles already have their vibrance, so no vibrance=
|
|
21831
|
+
parts.push('continuous', 'interpolation=oklch');
|
|
21832
|
+
}
|
|
21833
|
+
parts.push('colors=' + colors.join(','));
|
|
21834
|
+
if (scheme.method != 'non-adjacent' && getSchemeNullColor(scheme) != defaultNullColor) {
|
|
21835
|
+
parts.push('null-value=' + getSchemeNullColor(scheme));
|
|
21836
|
+
}
|
|
21837
|
+
if (opts && opts.target) {
|
|
21838
|
+
parts.push('target=' + opts.target);
|
|
21839
|
+
}
|
|
21840
|
+
return parts.join(' ');
|
|
21841
|
+
}
|
|
21842
|
+
|
|
21843
|
+
function getNumericFields(lyr) {
|
|
21844
|
+
var records = lyr && lyr.data ? lyr.data.getRecords() : [];
|
|
21845
|
+
if (!lyr || !lyr.data) return [];
|
|
21846
|
+
return lyr.data.getFields().filter(function(field) {
|
|
21847
|
+
return internal.getColumnType(field, records) == 'number';
|
|
21848
|
+
});
|
|
21849
|
+
}
|
|
21850
|
+
|
|
21851
|
+
function setLayerScheme(lyr, scheme) {
|
|
21852
|
+
if (!lyr) return;
|
|
21853
|
+
if (scheme) {
|
|
21854
|
+
layerSchemes.set(lyr, scheme);
|
|
21855
|
+
} else {
|
|
21856
|
+
layerSchemes.delete(lyr);
|
|
21857
|
+
}
|
|
21858
|
+
}
|
|
21859
|
+
|
|
21860
|
+
// The scheme that colored a layer, or null if it has none, or if its fills
|
|
21861
|
+
// have since been changed some other way.
|
|
21862
|
+
function getLayerScheme(lyr) {
|
|
21863
|
+
var scheme = lyr ? layerSchemes.get(lyr) : null;
|
|
21864
|
+
if (!scheme || !schemeMatchesLayer(scheme, lyr)) return null;
|
|
21865
|
+
return scheme;
|
|
21866
|
+
}
|
|
21867
|
+
|
|
21868
|
+
// Checked from the data rather than kept in step with every edit, so that
|
|
21869
|
+
// anything that changes the fills -- a -style command, a typed fill color,
|
|
21870
|
+
// an undo past the scheme -- retires it.
|
|
21871
|
+
function schemeMatchesLayer(scheme, lyr) {
|
|
21872
|
+
var records, colors, val;
|
|
21873
|
+
if (!lyr || !lyr.data || !scheme.colors) return false;
|
|
21874
|
+
if (scheme.method != 'non-adjacent' && !lyr.data.fieldExists(scheme.field)) return false;
|
|
21875
|
+
if (isContinuousScheme(scheme)) {
|
|
21876
|
+
return !!scheme.fillHash && scheme.fillHash == getFillFingerprint(lyr);
|
|
21877
|
+
}
|
|
21878
|
+
colors = scheme.colors.map(function(c) { return c.toLowerCase(); });
|
|
21879
|
+
records = lyr.data.getRecords();
|
|
21880
|
+
for (var i=0; i<records.length; i++) {
|
|
21881
|
+
val = records[i] && records[i].fill;
|
|
21882
|
+
if (!val || (!colors.includes(String(val).toLowerCase()) && !isNullFill(val, scheme))) {
|
|
21883
|
+
return false;
|
|
21884
|
+
}
|
|
21885
|
+
}
|
|
21886
|
+
return true;
|
|
21887
|
+
}
|
|
21888
|
+
|
|
21889
|
+
// A scheme as applied to a layer, with what's needed to check it later
|
|
21890
|
+
// colors: the colors given to -classify
|
|
21891
|
+
function getAppliedScheme(scheme, colors, lyr) {
|
|
21892
|
+
var applied = Object.assign({}, scheme, {colors: colors});
|
|
21893
|
+
if (isContinuousScheme(scheme)) {
|
|
21894
|
+
applied.fillHash = getFillFingerprint(lyr);
|
|
21895
|
+
} else {
|
|
21896
|
+
delete applied.fillHash;
|
|
21897
|
+
}
|
|
21898
|
+
return applied;
|
|
21899
|
+
}
|
|
21900
|
+
|
|
21901
|
+
// A hash of a layer's fill values (FNV-1a, 32 bits)
|
|
21902
|
+
function getFillFingerprint(lyr) {
|
|
21903
|
+
var records = lyr && lyr.data ? lyr.data.getRecords() : [];
|
|
21904
|
+
var hash = 0x811c9dc5;
|
|
21905
|
+
var str;
|
|
21906
|
+
for (var i=0; i<records.length; i++) {
|
|
21907
|
+
str = String(records[i] ? records[i].fill : '') + '\n';
|
|
21908
|
+
for (var j=0; j<str.length; j++) {
|
|
21909
|
+
hash ^= str.charCodeAt(j);
|
|
21910
|
+
hash = Math.imul(hash, 0x01000193);
|
|
21911
|
+
}
|
|
21912
|
+
}
|
|
21913
|
+
return records.length + ':' + (hash >>> 0).toString(16);
|
|
21914
|
+
}
|
|
21915
|
+
|
|
21916
|
+
function isNullFill(val, scheme) {
|
|
21917
|
+
var color = getSchemeNullColor(scheme);
|
|
21918
|
+
val = String(val).toLowerCase();
|
|
21919
|
+
// -classify writes its default as #eee
|
|
21920
|
+
return val == color || color == defaultNullColor && val == '#eee';
|
|
21921
|
+
}
|
|
21922
|
+
|
|
21923
|
+
function toHex(color) {
|
|
21924
|
+
return internal.formatColor(internal.parseColor(color));
|
|
21925
|
+
}
|
|
21926
|
+
|
|
21927
|
+
// A popup beside the polygon style panel for coloring a layer's fills by a
|
|
21928
|
+
// data field, with -classify. Edits are applied as they are made, in a command
|
|
21929
|
+
// session, so that the whole time the panel is open on a layer is one undo
|
|
21930
|
+
// state and one command in the session history.
|
|
21931
|
+
// See docs/development/color-scheme-panel-design.md
|
|
21932
|
+
//
|
|
21933
|
+
// opts.getExtraCommands() commands to run after -classify, in the same step
|
|
21934
|
+
// (the style panel's pattern backgrounds follow the
|
|
21935
|
+
// fills)
|
|
21936
|
+
// opts.getSelectionCount() features selected in the style panel
|
|
21937
|
+
// opts.onUpdate() the scheme or the fills changed
|
|
21938
|
+
// opts.onClose() the panel was closed
|
|
21939
|
+
var reverseIcon = '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" ' +
|
|
21940
|
+
'stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">' +
|
|
21941
|
+
'<path d="M2 4.5h9.5M9 2l2.5 2.5L9 7"/><path d="M12 9.5H2.5M5 7L2.5 9.5 5 12"/></svg>';
|
|
21942
|
+
var shuffleIcon = '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" ' +
|
|
21943
|
+
'stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">' +
|
|
21944
|
+
'<path d="M1.5 3.5h2.2c1.4 0 2.3.7 3 2l1.6 3c.7 1.3 1.6 2 3 2h1.2"/>' +
|
|
21945
|
+
'<path d="M1.5 10.5h2.2c1.2 0 2-.5 2.6-1.4M8.2 4.9c.6-.9 1.4-1.4 2.6-1.4h1.7"/>' +
|
|
21946
|
+
'<path d="M11 1.5l1.7 2-1.7 2M11 8.5l1.7 2-1.7 2"/></svg>';
|
|
21947
|
+
var longHueIcon = '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" ' +
|
|
21948
|
+
'stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">' +
|
|
21949
|
+
'<path d="M11.5 4.6A5 5 0 1 0 12 7"/><path d="M12 1.8v3h-3"/></svg>';
|
|
21950
|
+
var continuousIcon = '<svg width="14" height="14" viewBox="0 0 14 14" fill="none">' +
|
|
21951
|
+
'<defs><linearGradient id="color-scheme-continuous-icon">' +
|
|
21952
|
+
'<stop offset="0" stop-color="currentColor" stop-opacity="0"/>' +
|
|
21953
|
+
'<stop offset="1" stop-color="currentColor"/></linearGradient></defs>' +
|
|
21954
|
+
'<rect x="1.5" y="3.5" width="11" height="7" rx="1.5" fill="url(#color-scheme-continuous-icon)" ' +
|
|
21955
|
+
'stroke="currentColor" stroke-width="1.2"/></svg>';
|
|
21956
|
+
|
|
21957
|
+
function ColorSchemePanel(gui, opts) {
|
|
21958
|
+
var parent = gui.container.findChild('.mshp-main-map');
|
|
21959
|
+
var panel = El('div').addClass('label-style-panel color-scheme-panel').appendTo(parent).hide();
|
|
21960
|
+
var session = null; // made on first use: the console is made after the panel
|
|
21961
|
+
var targetLayer = null;
|
|
21962
|
+
var scheme = null;
|
|
21963
|
+
var sessionLayer = null; // the layer the session's edits are to
|
|
21964
|
+
var baseScheme = null; // its scheme when they began
|
|
21965
|
+
var selectedTile = -1;
|
|
21966
|
+
// the scheme last used on each tab while the panel is open, for switching back
|
|
21967
|
+
var tabSchemes = {};
|
|
21968
|
+
var tabs = {};
|
|
21969
|
+
var paletteBtn, paletteMenu, countField, tileRow, gradientEl, tilesEl, picker, fieldRow, fieldSelect, methodSelect,
|
|
21970
|
+
noFieldsNote, selectionNote, controlsEl, vibranceRow, vibranceInput,
|
|
21971
|
+
longHueBtn, continuousBtn, reverseBtn, shuffleBtn, dropMarker, nullControl, nullCount, countLabel,
|
|
21972
|
+
divergingEl, pivotSelect, pivotInput, pivotNote, neutralToggle, splitSelect;
|
|
21973
|
+
// set while a tile is being dragged, so that letting go isn't a click
|
|
21974
|
+
var tileDrag = null;
|
|
21975
|
+
|
|
21976
|
+
initPanel();
|
|
21977
|
+
|
|
21978
|
+
gui.on('undo_redo_post', function() {
|
|
21979
|
+
var restored;
|
|
21980
|
+
if (!targetLayer) return;
|
|
21981
|
+
restored = getLayerScheme(targetLayer);
|
|
21982
|
+
if (restored) {
|
|
21983
|
+
scheme = updateDivergingLayout(restored, targetLayer);
|
|
21984
|
+
render();
|
|
21985
|
+
} else {
|
|
21986
|
+
close();
|
|
21987
|
+
}
|
|
21988
|
+
});
|
|
21989
|
+
|
|
21990
|
+
this.open = function(lyr) {
|
|
21991
|
+
var fields;
|
|
21992
|
+
if (targetLayer && targetLayer != lyr) close();
|
|
21993
|
+
targetLayer = lyr;
|
|
21994
|
+
selectedTile = -1;
|
|
21995
|
+
tabSchemes = {};
|
|
21996
|
+
scheme = getLayerScheme(lyr);
|
|
21997
|
+
panel.show();
|
|
21998
|
+
if (scheme) {
|
|
21999
|
+
scheme = updateDivergingLayout(scheme, lyr);
|
|
22000
|
+
} else {
|
|
22001
|
+
fields = getNumericFields(lyr);
|
|
22002
|
+
scheme = getDefaultSchemeOfType(fields.length > 0 ? 'sequential' : 'categorical', lyr);
|
|
22003
|
+
if (canApply(scheme)) apply();
|
|
22004
|
+
}
|
|
22005
|
+
render();
|
|
22006
|
+
};
|
|
22007
|
+
|
|
22008
|
+
this.close = close;
|
|
22009
|
+
|
|
22010
|
+
this.isOpen = function() {
|
|
22011
|
+
return !!targetLayer;
|
|
22012
|
+
};
|
|
22013
|
+
|
|
22014
|
+
function close() {
|
|
22015
|
+
var wasOpen = !!targetLayer;
|
|
22016
|
+
picker.hide();
|
|
22017
|
+
nullControl.picker.hide();
|
|
22018
|
+
hidePaletteMenu();
|
|
22019
|
+
panel.hide();
|
|
22020
|
+
targetLayer = null;
|
|
22021
|
+
scheme = null;
|
|
22022
|
+
if (session) session.finish();
|
|
22023
|
+
if (wasOpen && opts.onClose) opts.onClose();
|
|
22024
|
+
}
|
|
22025
|
+
|
|
22026
|
+
function getSession() {
|
|
22027
|
+
if (!session && gui.console && gui.console.createCommandSession) {
|
|
22028
|
+
session = gui.console.createCommandSession({label: 'Color scheme', onCommit: onCommit});
|
|
22029
|
+
}
|
|
22030
|
+
return session;
|
|
22031
|
+
}
|
|
22032
|
+
|
|
22033
|
+
function initPanel() {
|
|
22034
|
+
claimFieldKeys(panel.node(), {
|
|
22035
|
+
revert: render,
|
|
22036
|
+
release: releaseFocus
|
|
22037
|
+
});
|
|
22038
|
+
panel.node().addEventListener('click', function(e) {
|
|
22039
|
+
if (isTextInput(document.activeElement) || opensAMenu(e.target)) return;
|
|
22040
|
+
releaseFocus();
|
|
22041
|
+
});
|
|
22042
|
+
|
|
22043
|
+
var header = El('div').addClass('label-style-panel-title').appendTo(panel);
|
|
22044
|
+
El('span').appendTo(header).text('Color palettes');
|
|
22045
|
+
El('button').addClass('label-style-close').appendTo(header).text('×').on('click', function() {
|
|
22046
|
+
close();
|
|
22047
|
+
opts.onUpdate();
|
|
22048
|
+
});
|
|
22049
|
+
|
|
22050
|
+
var tabRow = El('div').addClass('label-style-row color-scheme-tabs').attr('role', 'tablist').appendTo(panel);
|
|
22051
|
+
schemeTypes.forEach(function(type) {
|
|
22052
|
+
tabs[type.name] = El('button').addClass('color-scheme-tab').attr('type', 'button')
|
|
22053
|
+
.attr('role', 'tab').text(type.label).appendTo(tabRow)
|
|
22054
|
+
.on('click', function() {
|
|
22055
|
+
switchType(type.name);
|
|
22056
|
+
});
|
|
22057
|
+
});
|
|
22058
|
+
|
|
22059
|
+
noFieldsNote = El('div').addClass('label-style-row color-scheme-note').appendTo(panel)
|
|
22060
|
+
.text('This layer has no numeric fields to classify.');
|
|
22061
|
+
controlsEl = El('div').appendTo(panel);
|
|
22062
|
+
|
|
22063
|
+
fieldRow = El('div').addClass('label-style-row color-scheme-select-row').appendTo(controlsEl);
|
|
22064
|
+
El('span').appendTo(fieldRow).text('Field');
|
|
22065
|
+
fieldSelect = El('select').attr('aria-label', 'Data field').appendTo(fieldRow).on('change', function() {
|
|
22066
|
+
var field = fieldSelect.node().value;
|
|
22067
|
+
changeScheme(setSchemeField(scheme, field, getCategoryCount(field)));
|
|
22068
|
+
});
|
|
22069
|
+
|
|
22070
|
+
var methodRow = El('div').addClass('label-style-row color-scheme-select-row').appendTo(controlsEl);
|
|
22071
|
+
El('span').appendTo(methodRow).text('Method');
|
|
22072
|
+
methodSelect = El('select').attr('aria-label', 'Classification method').appendTo(methodRow).on('change', function() {
|
|
22073
|
+
var field = scheme.field || getCategoryFields(targetLayer)[0];
|
|
22074
|
+
changeScheme(setSchemeMethod(scheme, methodSelect.node().value, field, getCategoryCount(field)));
|
|
22075
|
+
});
|
|
22076
|
+
|
|
22077
|
+
initDivergingControls();
|
|
22078
|
+
|
|
22079
|
+
var paletteRow = El('div').addClass('label-style-row label-split-row').appendTo(controlsEl);
|
|
22080
|
+
var paletteCell = El('div').addClass('label-split-cell color-scheme-palette-cell').appendTo(paletteRow);
|
|
22081
|
+
var countCell = El('div').addClass('label-split-cell').appendTo(paletteRow);
|
|
22082
|
+
El('span').appendTo(paletteCell).text('Palette');
|
|
22083
|
+
paletteBtn = El('div').addClass('color-scheme-palette-btn').attr('role', 'button')
|
|
22084
|
+
.attr('aria-label', 'Palette').appendTo(paletteCell).on('click', togglePaletteMenu);
|
|
22085
|
+
paletteMenu = El('div').addClass('color-scheme-palette-menu').appendTo(paletteCell).hide();
|
|
22086
|
+
countLabel = El('span').appendTo(countCell).text('Colors');
|
|
22087
|
+
// the scheme sets the limits (see getMinSchemeColors(), getMaxSchemeColors())
|
|
22088
|
+
countField = new SizeField(countCell, {
|
|
22089
|
+
min: 1,
|
|
22090
|
+
max: maxCategoricalColors,
|
|
22091
|
+
step: 1,
|
|
22092
|
+
bigStep: 2,
|
|
22093
|
+
decimals: 0,
|
|
22094
|
+
title: 'Number of classes',
|
|
22095
|
+
onSet: function(n) {
|
|
22096
|
+
changeScheme(setTileCount(scheme, n, getCategoryCount()));
|
|
22097
|
+
},
|
|
22098
|
+
onStep: function(delta) {
|
|
22099
|
+
changeScheme(setTileCount(scheme, scheme.n + (delta > 0 ? 1 : -1), getCategoryCount()));
|
|
22100
|
+
},
|
|
22101
|
+
onDone: releaseFocus
|
|
22102
|
+
});
|
|
22103
|
+
|
|
22104
|
+
tileRow = El('div').addClass('color-scheme-tile-row').appendTo(controlsEl);
|
|
22105
|
+
gradientEl = El('div').addClass('color-scheme-gradient').appendTo(tileRow);
|
|
22106
|
+
tilesEl = El('div').addClass('color-scheme-tiles').appendTo(tileRow);
|
|
22107
|
+
picker = new ColorPicker(tileRow, {
|
|
22108
|
+
presetRows: [],
|
|
22109
|
+
onPreview: function(hex) {
|
|
22110
|
+
var tile = getTileCells()[selectedTile];
|
|
22111
|
+
if (tile) tile.querySelector('.color-scheme-tile').style.backgroundColor = hex;
|
|
22112
|
+
},
|
|
22113
|
+
onChange: function(hex) {
|
|
22114
|
+
if (selectedTile > -1) changeScheme(setTileColor(scheme, selectedTile, hex));
|
|
22115
|
+
},
|
|
22116
|
+
// the highlight says which tile the picker is editing
|
|
22117
|
+
onHide: function() {
|
|
22118
|
+
selectedTile = -1;
|
|
22119
|
+
render();
|
|
22120
|
+
}
|
|
22121
|
+
});
|
|
22122
|
+
|
|
22123
|
+
// the slider works in thousandths of OKLCH chroma
|
|
22124
|
+
vibranceRow = addSliderRow('Vibrance', 'Makes all the colors more vivid',
|
|
22125
|
+
0, Math.round(maxVibrance * 1000), 5, function(val) {
|
|
22126
|
+
changeScheme(setSchemeVibrance(scheme, val / 1000));
|
|
22127
|
+
});
|
|
22128
|
+
vibranceInput = vibranceRow.findChild('input');
|
|
22129
|
+
longHueBtn = addIconButton(vibranceRow, 'Go the long way around the color wheel', longHueIcon, function() {
|
|
22130
|
+
changeScheme(setSchemeLongHue(scheme, !scheme.longHue));
|
|
22131
|
+
}).addClass('long-hue-btn');
|
|
22132
|
+
continuousBtn = addIconButton(vibranceRow, 'Continuous colors (unclassed)', continuousIcon, function() {
|
|
22133
|
+
changeScheme(setSchemeContinuous(scheme, !scheme.continuous));
|
|
22134
|
+
}).addClass('continuous-btn');
|
|
22135
|
+
reverseBtn = addIconButton(vibranceRow, 'Reverse the colors', reverseIcon, function() {
|
|
22136
|
+
changeScheme(reverseScheme(scheme));
|
|
22137
|
+
});
|
|
22138
|
+
shuffleBtn = addIconButton(vibranceRow, 'Shuffle the colors', shuffleIcon, function() {
|
|
22139
|
+
changeScheme(shuffleScheme(scheme));
|
|
22140
|
+
});
|
|
22141
|
+
// the color of features with no data, and how many there are
|
|
22142
|
+
nullControl = makeColorRow(controlsEl, {
|
|
22143
|
+
label: 'No data',
|
|
22144
|
+
noOpacity: true,
|
|
22145
|
+
onColor: function(color) {
|
|
22146
|
+
changeScheme(setSchemeNullColor(scheme, color));
|
|
22147
|
+
}
|
|
22148
|
+
});
|
|
22149
|
+
nullControl.row.addClass('color-scheme-null-row');
|
|
22150
|
+
nullControl.chit.on('click', function() {
|
|
22151
|
+
picker.hide();
|
|
22152
|
+
});
|
|
22153
|
+
nullCount = El('span').addClass('color-scheme-note color-scheme-null-count').appendTo(nullControl.aside);
|
|
22154
|
+
|
|
22155
|
+
selectionNote = El('div').addClass('label-style-row color-scheme-note').appendTo(controlsEl)
|
|
22156
|
+
.text('Colors apply to every feature in the layer, not only the selected ones.');
|
|
22157
|
+
|
|
22158
|
+
document.addEventListener('mousedown', function(e) {
|
|
22159
|
+
if (paletteMenu.visible() && !paletteMenu.node().contains(e.target) &&
|
|
22160
|
+
!paletteBtn.node().contains(e.target)) {
|
|
22161
|
+
hidePaletteMenu();
|
|
22162
|
+
}
|
|
22163
|
+
});
|
|
22164
|
+
}
|
|
22165
|
+
|
|
22166
|
+
// The pivot, the pivot class and how the classes are split between the
|
|
22167
|
+
// sides
|
|
22168
|
+
function initDivergingControls() {
|
|
22169
|
+
divergingEl = El('div').appendTo(controlsEl);
|
|
22170
|
+
var pivotRow = El('div').addClass('label-style-row label-split-row').appendTo(divergingEl);
|
|
22171
|
+
var pivotCell = El('div').addClass('label-split-cell color-scheme-select-row').appendTo(pivotRow);
|
|
22172
|
+
var valueCell = El('div').addClass('label-split-cell color-scheme-select-row').appendTo(pivotRow);
|
|
22173
|
+
El('span').appendTo(pivotCell).text('Pivot');
|
|
22174
|
+
pivotSelect = El('select').attr('aria-label', 'Pivot').appendTo(pivotCell).on('change', function() {
|
|
22175
|
+
var name = pivotSelect.node().value;
|
|
22176
|
+
var current = scheme.layout ? scheme.layout.pivot : 0;
|
|
22177
|
+
changeScheme(setSchemePivot(scheme, name == 'value' ? current : name));
|
|
22178
|
+
});
|
|
22179
|
+
pivotOptions.forEach(function(item) {
|
|
22180
|
+
El('option').attr('value', item.name).text(item.label).appendTo(pivotSelect);
|
|
22181
|
+
});
|
|
22182
|
+
El('span').appendTo(valueCell).text('Value');
|
|
22183
|
+
pivotInput = El('input').attr('type', 'text').attr('aria-label', 'Pivot value')
|
|
22184
|
+
.addClass('color-scheme-pivot-input').appendTo(valueCell)
|
|
22185
|
+
.on('change', function() {
|
|
22186
|
+
var val = parseFloat(pivotInput.node().value);
|
|
22187
|
+
if (isFinite(val)) {
|
|
22188
|
+
changeScheme(setSchemePivot(scheme, val));
|
|
22189
|
+
} else {
|
|
22190
|
+
render();
|
|
22191
|
+
}
|
|
22192
|
+
})
|
|
22193
|
+
.on('keydown', function(e) {
|
|
22194
|
+
if (e.key == 'Enter') pivotInput.node().blur();
|
|
22195
|
+
});
|
|
22196
|
+
pivotNote = El('div').addClass('label-style-row color-scheme-note').appendTo(divergingEl);
|
|
22197
|
+
|
|
22198
|
+
var optionRow = El('div').addClass('label-style-row label-split-row').appendTo(divergingEl);
|
|
22199
|
+
var splitCell = El('div').addClass('label-split-cell color-scheme-select-row').appendTo(optionRow);
|
|
22200
|
+
var neutralCell = El('div').addClass('label-split-cell color-scheme-select-row').appendTo(optionRow);
|
|
22201
|
+
var neutralLabel = El('span').addClass('color-scheme-hint-label').appendTo(neutralCell).text('Pivot class')
|
|
22202
|
+
.attr('data-tooltip', 'A class around the pivot, in the center color')
|
|
22203
|
+
.on('mouseenter', function() { keepTooltipInWindow(neutralLabel.node()); });
|
|
22204
|
+
var neutralLine = El('div').addClass('color-scheme-toggle-line').appendTo(neutralCell);
|
|
22205
|
+
neutralToggle = makePanelToggle(neutralLine, {
|
|
22206
|
+
title: 'Pivot class',
|
|
22207
|
+
onChange: function(on) {
|
|
22208
|
+
changeScheme(setSchemeNeutral(scheme, on));
|
|
22209
|
+
}
|
|
22210
|
+
});
|
|
22211
|
+
El('span').appendTo(splitCell).text('Sides');
|
|
22212
|
+
splitSelect = El('select').attr('aria-label', 'Classes on each side').appendTo(splitCell).on('change', function() {
|
|
22213
|
+
changeScheme(setSchemeSplit(scheme, splitSelect.node().value));
|
|
22214
|
+
});
|
|
22215
|
+
divergingSplits.forEach(function(item) {
|
|
22216
|
+
El('option').attr('value', item.name).text(item.label).appendTo(splitSelect);
|
|
22217
|
+
});
|
|
22218
|
+
}
|
|
22219
|
+
|
|
22220
|
+
function renderDivergingControls() {
|
|
22221
|
+
var diverging = scheme.type == 'diverging';
|
|
22222
|
+
var pivot = scheme.pivot;
|
|
22223
|
+
var option = typeof pivot == 'number' ? 'value' : pivot;
|
|
22224
|
+
var summary;
|
|
22225
|
+
divergingEl.classed('hidden', !diverging);
|
|
22226
|
+
if (!diverging) return;
|
|
22227
|
+
pivotSelect.node().value = option;
|
|
22228
|
+
pivotInput.node().disabled = option != 'value';
|
|
22229
|
+
if (document.activeElement != pivotInput.node()) {
|
|
22230
|
+
pivotInput.node().value = scheme.layout ? formatNumber(scheme.layout.pivot) : '';
|
|
22231
|
+
}
|
|
22232
|
+
summary = getPivotSummary(scheme, targetLayer);
|
|
22233
|
+
pivotNote.text(summary ? formatCount(summary.below) + ' below the pivot, ' +
|
|
22234
|
+
summary.above + ' at or above' : 'No numeric data to classify.');
|
|
22235
|
+
neutralToggle.setState(getSchemeNeutral(scheme) ? 'on' : 'off');
|
|
22236
|
+
splitSelect.node().value = scheme.split;
|
|
22237
|
+
}
|
|
22238
|
+
|
|
22239
|
+
function formatCount(n) {
|
|
22240
|
+
return n + (n == 1 ? ' feature' : ' features');
|
|
22241
|
+
}
|
|
22242
|
+
|
|
22243
|
+
function formatNumber(val) {
|
|
22244
|
+
return String(+val.toPrecision(6));
|
|
22245
|
+
}
|
|
22246
|
+
|
|
22247
|
+
function render() {
|
|
22248
|
+
var categorical, fields, colors, tiles;
|
|
22249
|
+
if (!targetLayer || !scheme) return;
|
|
22250
|
+
categorical = scheme.type == 'categorical';
|
|
22251
|
+
Object.keys(tabs).forEach(function(type) {
|
|
22252
|
+
tabs[type].classed('selected', type == scheme.type)
|
|
22253
|
+
.attr('aria-selected', type == scheme.type ? 'true' : 'false');
|
|
22254
|
+
});
|
|
22255
|
+
fields = categorical ? getCategoryFields(targetLayer) : getNumericFields(targetLayer);
|
|
22256
|
+
noFieldsNote.classed('hidden', categorical || fields.length > 0);
|
|
22257
|
+
controlsEl.classed('hidden', !categorical && fields.length === 0);
|
|
22258
|
+
if (!categorical && fields.length === 0) return;
|
|
22259
|
+
renderFieldOptions(fields);
|
|
22260
|
+
fieldRow.classed('hidden', scheme.method == 'non-adjacent');
|
|
22261
|
+
renderMethodOptions(fields);
|
|
22262
|
+
renderDivergingControls();
|
|
22263
|
+
countLabel.text(scheme.type != 'diverging' ? 'Colors' : scheme.split == 'count' ? 'Per side' : 'Classes');
|
|
22264
|
+
countField.setValue(String(scheme.n));
|
|
22265
|
+
tiles = getSchemeTiles(scheme);
|
|
22266
|
+
colors = tiles.map(function(tile) { return tile.color; });
|
|
22267
|
+
renderPaletteButton(colors);
|
|
22268
|
+
renderTiles(tiles);
|
|
22269
|
+
renderVibrance();
|
|
22270
|
+
renderNullColor();
|
|
22271
|
+
selectionNote.classed('hidden', !(opts.getSelectionCount() > 0));
|
|
22272
|
+
}
|
|
22273
|
+
|
|
22274
|
+
function renderNullColor() {
|
|
22275
|
+
var count = getNoDataCount(targetLayer, scheme);
|
|
22276
|
+
nullControl.row.classed('hidden', scheme.method == 'non-adjacent');
|
|
22277
|
+
if (scheme.method == 'non-adjacent') nullControl.picker.hide();
|
|
22278
|
+
nullControl.showColor(getSchemeNullColor(scheme));
|
|
22279
|
+
nullCount.text(count === 0 ? 'None in this layer' : formatCount(count));
|
|
22280
|
+
}
|
|
22281
|
+
|
|
22282
|
+
function renderMethodOptions(fields) {
|
|
22283
|
+
methodSelect.empty();
|
|
22284
|
+
getSchemeMethods(scheme).forEach(function(item) {
|
|
22285
|
+
var opt = El('option').attr('value', item.name).text(item.label).appendTo(methodSelect);
|
|
22286
|
+
// classifying by category needs a field
|
|
22287
|
+
if (item.name == 'categorical' && fields.length === 0) opt.attr('disabled', true);
|
|
22288
|
+
});
|
|
22289
|
+
methodSelect.node().value = scheme.method;
|
|
22290
|
+
}
|
|
22291
|
+
|
|
22292
|
+
// Each tab keeps its own scheme while the panel is open, and starts from a
|
|
22293
|
+
// default the first time
|
|
22294
|
+
function switchType(type) {
|
|
22295
|
+
var next;
|
|
22296
|
+
if (type == scheme.type) return;
|
|
22297
|
+
tabSchemes[scheme.type] = scheme;
|
|
22298
|
+
next = tabSchemes[type] || getDefaultSchemeOfType(type, targetLayer);
|
|
22299
|
+
// one no-data color for both tabs
|
|
22300
|
+
next = updateDivergingLayout(Object.assign({}, next, {nullColor: scheme.nullColor}), targetLayer);
|
|
22301
|
+
picker.hide();
|
|
22302
|
+
selectedTile = -1;
|
|
22303
|
+
scheme = next;
|
|
22304
|
+
if (canApply(scheme)) apply();
|
|
22305
|
+
render();
|
|
22306
|
+
}
|
|
22307
|
+
|
|
22308
|
+
// a diverging scheme needs data that its classes can divide
|
|
22309
|
+
function canApply(s) {
|
|
22310
|
+
if (s.type == 'diverging' && !s.layout) return false;
|
|
22311
|
+
return s.method == 'non-adjacent' || !!s.field;
|
|
22312
|
+
}
|
|
22313
|
+
|
|
22314
|
+
// The number of categories in a field (the scheme's, by default), for a
|
|
22315
|
+
// categorical scheme
|
|
22316
|
+
function getCategoryCount(field) {
|
|
22317
|
+
if (scheme.type != 'categorical') return -1;
|
|
22318
|
+
return getCategories(targetLayer, field || scheme.field).length;
|
|
22319
|
+
}
|
|
22320
|
+
|
|
22321
|
+
function addSliderRow(label, tooltip, min, max, step, onInput) {
|
|
22322
|
+
var row = El('div').addClass('label-style-row color-scheme-slider-row').appendTo(controlsEl);
|
|
22323
|
+
var labelEl = El('span').addClass('color-scheme-slider-label').attr('data-tooltip', tooltip)
|
|
22324
|
+
.appendTo(row).text(label)
|
|
22325
|
+
.on('mouseenter', function() { keepTooltipInWindow(labelEl.node()); });
|
|
22326
|
+
var input = El('input').attr('type', 'range').attr('min', min).attr('max', max).attr('step', step)
|
|
22327
|
+
.attr('aria-label', label).appendTo(row)
|
|
22328
|
+
.on('input', function() {
|
|
22329
|
+
onInput(+input.node().value);
|
|
22330
|
+
})
|
|
22331
|
+
.on('change', releaseFocus);
|
|
22332
|
+
return row;
|
|
22333
|
+
}
|
|
22334
|
+
|
|
22335
|
+
function addIconButton(parent, label, icon, onClick) {
|
|
22336
|
+
var btn = El('button').addClass('color-scheme-icon-btn').attr('type', 'button')
|
|
22337
|
+
.attr('aria-label', label).attr('data-tooltip', label).html(icon).appendTo(parent)
|
|
22338
|
+
.on('click', onClick)
|
|
22339
|
+
.on('mouseenter', function() { keepTooltipInWindow(btn.node()); });
|
|
22340
|
+
return btn;
|
|
22341
|
+
}
|
|
22342
|
+
|
|
22343
|
+
// Presets and categorical schemes have no interpolated tiles for vibrance
|
|
22344
|
+
// or the hue path to change; the row keeps only the reverse button for them.
|
|
22345
|
+
function renderVibrance() {
|
|
22346
|
+
var vibrance = getSchemeVibrance(scheme);
|
|
22347
|
+
setSliderValue(vibranceInput, Math.round(vibrance * 1000));
|
|
22348
|
+
vibranceRow.classed('preset', !!scheme.preset || scheme.type == 'categorical');
|
|
22349
|
+
reverseBtn.classed('hidden', scheme.type == 'categorical');
|
|
22350
|
+
shuffleBtn.classed('hidden', scheme.type != 'categorical');
|
|
22351
|
+
// each half of a diverging ramp runs from a low-chroma center, where the
|
|
22352
|
+
// hue path makes little difference
|
|
22353
|
+
longHueBtn.classed('hidden', scheme.type == 'diverging')
|
|
22354
|
+
.classed('selected', !!scheme.longHue)
|
|
22355
|
+
.attr('aria-pressed', scheme.longHue ? 'true' : 'false');
|
|
22356
|
+
continuousBtn.classed('hidden', scheme.type == 'categorical')
|
|
22357
|
+
.classed('selected', isContinuousScheme(scheme))
|
|
22358
|
+
.attr('aria-pressed', isContinuousScheme(scheme) ? 'true' : 'false');
|
|
22359
|
+
}
|
|
22360
|
+
|
|
22361
|
+
function setSliderValue(input, val) {
|
|
22362
|
+
if (input.node().value != val) input.node().value = val;
|
|
22363
|
+
}
|
|
22364
|
+
|
|
22365
|
+
function renderFieldOptions(fields) {
|
|
22366
|
+
fieldSelect.empty();
|
|
22367
|
+
fields.forEach(function(field) {
|
|
22368
|
+
El('option').attr('value', field).text(field).appendTo(fieldSelect);
|
|
22369
|
+
});
|
|
22370
|
+
fieldSelect.node().value = scheme.field || '';
|
|
22371
|
+
}
|
|
22372
|
+
|
|
22373
|
+
function renderPaletteButton(colors) {
|
|
22374
|
+
paletteBtn.empty();
|
|
22375
|
+
El('div').addClass('color-scheme-strip').appendTo(paletteBtn)
|
|
22376
|
+
.css('background-image', isContinuousScheme(scheme) ?
|
|
22377
|
+
getGradientStripBackground(scheme) : getStripBackground(colors));
|
|
22378
|
+
El('span').addClass('color-scheme-palette-name').appendTo(paletteBtn)
|
|
22379
|
+
.text(scheme.preset || 'Custom');
|
|
22380
|
+
}
|
|
22381
|
+
|
|
22382
|
+
function renderTiles(tiles) {
|
|
22383
|
+
var categorical = scheme.type == 'categorical';
|
|
22384
|
+
var diverging = scheme.type == 'diverging';
|
|
22385
|
+
var continuous = isContinuousScheme(scheme);
|
|
22386
|
+
var used = tiles.length;
|
|
22387
|
+
var tileUse = diverging ? getDivergingTileUse(scheme) : null;
|
|
22388
|
+
var center = getCenterTile(scheme);
|
|
22389
|
+
var classLabels = categorical ? null : continuous ? getStopLabels() : getClassLabels(tileUse);
|
|
22390
|
+
var groups = categorical && scheme.method == 'categorical' ?
|
|
22391
|
+
getSwatchCategories(getCategories(targetLayer, scheme.field), used) : null;
|
|
22392
|
+
var n, perRow;
|
|
22393
|
+
// a categorical scheme shows its whole palette, and marks the swatches
|
|
22394
|
+
// in use
|
|
22395
|
+
if (categorical) {
|
|
22396
|
+
tiles = getCategoricalPalette(scheme).map(function(color) {
|
|
22397
|
+
return {color: color, pinned: false, adjusted: false};
|
|
22398
|
+
});
|
|
22399
|
+
}
|
|
22400
|
+
n = tiles.length;
|
|
22401
|
+
// the stops of a continuous ramp stay in one row, under the gradient
|
|
22402
|
+
perRow = n > 12 && !continuous ? Math.ceil(n / 2) : n;
|
|
22403
|
+
tilesEl.empty();
|
|
22404
|
+
tilesEl.classed('two-rows', perRow < n);
|
|
22405
|
+
tilesEl.classed('continuous', continuous);
|
|
22406
|
+
tilesEl.node().style.setProperty('--tiles-per-row', perRow);
|
|
22407
|
+
dropMarker = El('div').addClass('color-scheme-drop-marker').appendTo(tilesEl).hide();
|
|
22408
|
+
tiles.forEach(function(tile, i) {
|
|
22409
|
+
var color = tile.color;
|
|
22410
|
+
var inUse = tileUse ? tileUse[i] : !categorical || i < used;
|
|
22411
|
+
var cell = El('div').addClass('color-scheme-tile-cell').appendTo(tilesEl);
|
|
22412
|
+
var tileEl = El('div').addClass('color-scheme-tile').attr('role', 'button')
|
|
22413
|
+
.attr('aria-label', 'Color ' + (i + 1) + ': ' + color + (inUse ? '' : ' (not used)'))
|
|
22414
|
+
.classed('selected', i == selectedTile)
|
|
22415
|
+
.css('background-color', color)
|
|
22416
|
+
.appendTo(cell)
|
|
22417
|
+
.on('click', function() {
|
|
22418
|
+
if (tileDrag && tileDrag.moved) return;
|
|
22419
|
+
selectTile(i, color);
|
|
22420
|
+
});
|
|
22421
|
+
if (categorical) {
|
|
22422
|
+
tileEl.on('pointerdown', function(e) {
|
|
22423
|
+
startTileDrag(e, i);
|
|
22424
|
+
});
|
|
22425
|
+
}
|
|
22426
|
+
if (groups && inUse || classLabels && classLabels[i]) {
|
|
22427
|
+
tileEl.attr('data-tooltip', groups ? formatCategories(groups[i]) : classLabels[i])
|
|
22428
|
+
.on('mouseenter', function() { keepTooltipInWindow(tileEl.node()); });
|
|
22429
|
+
}
|
|
22430
|
+
if (diverging && !continuous) {
|
|
22431
|
+
// the bar under the tiles that classes use; a gap at the center
|
|
22432
|
+
// when there is no pivot class (continuous ramps have the gradient)
|
|
22433
|
+
El('div').addClass('color-scheme-bar').appendTo(cell)
|
|
22434
|
+
.classed('used', inUse)
|
|
22435
|
+
.classed('joined', inUse && i < n - 1 && tileUse[i + 1]);
|
|
22436
|
+
}
|
|
22437
|
+
var pin = El('div').addClass('color-scheme-pin').appendTo(cell)
|
|
22438
|
+
.on('mouseenter', function() { keepTooltipInWindow(pin.node()); });
|
|
22439
|
+
var end = i === 0 || i == n - 1 || i == center;
|
|
22440
|
+
if (categorical) {
|
|
22441
|
+
// a bar under the swatches in use, unbroken to the end of each row
|
|
22442
|
+
pin.classed('used', inUse)
|
|
22443
|
+
.classed('joined', inUse && i < used - 1 && (i + 1) % perRow > 0);
|
|
22444
|
+
// a pinned tile between the ends needs its dot for unpinning, even if
|
|
22445
|
+
// vibrance was clipped
|
|
22446
|
+
} else if (tile.pinned && !end) {
|
|
22447
|
+
pin.addClass('pinned').attr('role', 'button').attr('aria-label', 'Unpin this color')
|
|
22448
|
+
.attr('data-tooltip', 'Unpin this color')
|
|
22449
|
+
.on('click', function() {
|
|
22450
|
+
changeScheme(clearTileColor(scheme, i));
|
|
22451
|
+
});
|
|
22452
|
+
} else if (tile.adjusted) {
|
|
22453
|
+
pin.addClass('adjusted').attr('data-tooltip', describeAdjustment(tile));
|
|
22454
|
+
}
|
|
22455
|
+
});
|
|
22456
|
+
renderGradient(continuous ? getContinuousSegments(scheme) : null, n);
|
|
22457
|
+
}
|
|
22458
|
+
|
|
22459
|
+
// The gradient over a continuous ramp's tiles, each stop over the center
|
|
22460
|
+
// of its tile; one bar per side of a diverging ramp, and one for a pivot
|
|
22461
|
+
// class
|
|
22462
|
+
function renderGradient(segments, n) {
|
|
22463
|
+
var width = tilesEl.node().clientWidth;
|
|
22464
|
+
var gap = 3;
|
|
22465
|
+
var tileWidth = (width - (n - 1) * gap) / n;
|
|
22466
|
+
var center = function(pos) { return pos * (tileWidth + gap) + tileWidth / 2; };
|
|
22467
|
+
gradientEl.empty();
|
|
22468
|
+
gradientEl.classed('hidden', !segments);
|
|
22469
|
+
if (!segments || !(width > 0)) return;
|
|
22470
|
+
segments.forEach(function(seg) {
|
|
22471
|
+
var left = seg.flatStart ? center(seg.start) - tileWidth / 2 : center(seg.start);
|
|
22472
|
+
var right = seg.flatEnd ? center(seg.end) + tileWidth / 2 : center(seg.end);
|
|
22473
|
+
var stops = seg.samples.map(function(sample) {
|
|
22474
|
+
return sample.color + ' ' + round(center(sample.pos) - left) + 'px';
|
|
22475
|
+
});
|
|
22476
|
+
if (stops.length == 1) stops.push(stops[0]);
|
|
22477
|
+
El('div').addClass('color-scheme-gradient-segment').appendTo(gradientEl).css({
|
|
22478
|
+
left: round(left) + 'px',
|
|
22479
|
+
width: round(right - left) + 'px',
|
|
22480
|
+
'background-image': 'linear-gradient(to right, ' + stops.join(', ') + ')'
|
|
22481
|
+
});
|
|
22482
|
+
});
|
|
22483
|
+
}
|
|
22484
|
+
|
|
22485
|
+
function round(val) {
|
|
22486
|
+
return Math.round(val * 10) / 10;
|
|
22487
|
+
}
|
|
22488
|
+
|
|
22489
|
+
// The data value at each stop of a continuous ramp, and the range of a
|
|
22490
|
+
// pivot class
|
|
22491
|
+
function getStopLabels() {
|
|
22492
|
+
var stops = getContinuousTileStops(scheme, targetLayer) || [];
|
|
22493
|
+
return stops.map(function(stop) {
|
|
22494
|
+
if (!stop) return null;
|
|
22495
|
+
if (stop.range) return formatClassRange(stop.range);
|
|
22496
|
+
return formatNumber(stop.value) + (stop.min ? ' (min)' : stop.max ? ' (max)' : '');
|
|
22497
|
+
});
|
|
22498
|
+
}
|
|
22499
|
+
|
|
22500
|
+
// The data range of each tile's class, for the tiles that classes use
|
|
22501
|
+
// (sequential tiles are all used, one class each)
|
|
22502
|
+
function getClassLabels(tileUse) {
|
|
22503
|
+
var ranges, j = 0;
|
|
22504
|
+
if (scheme.type == 'sequential') {
|
|
22505
|
+
ranges = getSequentialClassRanges(scheme, targetLayer) || [];
|
|
22506
|
+
return ranges.map(formatClassRange);
|
|
22507
|
+
}
|
|
22508
|
+
ranges = getDivergingClassRanges(scheme.layout);
|
|
22509
|
+
return tileUse.map(function(used) {
|
|
22510
|
+
return used ? formatClassRange(ranges[j++]) : null;
|
|
22511
|
+
});
|
|
22512
|
+
}
|
|
22513
|
+
|
|
22514
|
+
function formatClassRange(range) {
|
|
22515
|
+
if (!range) return null;
|
|
22516
|
+
if (range[0] == -Infinity && range[1] == Infinity) return 'All values';
|
|
22517
|
+
if (range[0] == -Infinity) return 'Below ' + formatNumber(range[1]);
|
|
22518
|
+
if (range[1] == Infinity) return formatNumber(range[0]) + ' and above';
|
|
22519
|
+
return formatNumber(range[0]) + ' to ' + formatNumber(range[1]);
|
|
22520
|
+
}
|
|
22521
|
+
|
|
22522
|
+
// Dragging a categorical swatch moves it to where it's dropped, which may
|
|
22523
|
+
// be into or out of the swatches in use
|
|
22524
|
+
function startTileDrag(e, from) {
|
|
22525
|
+
var x0 = e.clientX, y0 = e.clientY;
|
|
22526
|
+
var drag = tileDrag = {from: from, moved: false, before: -1};
|
|
22527
|
+
if (e.button !== 0) return;
|
|
22528
|
+
document.addEventListener('pointermove', onMove);
|
|
22529
|
+
document.addEventListener('pointerup', onUp);
|
|
22530
|
+
document.addEventListener('pointercancel', onCancel);
|
|
22531
|
+
|
|
22532
|
+
function onMove(e) {
|
|
22533
|
+
if (!drag.moved && Math.abs(e.clientX - x0) + Math.abs(e.clientY - y0) < 5) return;
|
|
22534
|
+
if (!drag.moved) {
|
|
22535
|
+
drag.moved = true;
|
|
22536
|
+
tilesEl.addClass('dragging');
|
|
22537
|
+
getTileCells()[from].classList.add('drag-source');
|
|
22538
|
+
}
|
|
22539
|
+
drag.before = findDropPlace(e.clientX, e.clientY);
|
|
22540
|
+
showDropMarker(drag.before);
|
|
22541
|
+
}
|
|
22542
|
+
|
|
22543
|
+
function onUp() {
|
|
22544
|
+
var to = drag.before > from ? drag.before - 1 : drag.before;
|
|
22545
|
+
finish();
|
|
22546
|
+
if (drag.moved && drag.before > -1 && to != from) {
|
|
22547
|
+
picker.hide();
|
|
22548
|
+
selectedTile = -1;
|
|
22549
|
+
changeScheme(moveSwatch(scheme, from, to));
|
|
22550
|
+
}
|
|
22551
|
+
// the click that follows a drag is ignored, then dragging is over
|
|
22552
|
+
setTimeout(function() {
|
|
22553
|
+
if (tileDrag == drag) tileDrag = null;
|
|
22554
|
+
}, 0);
|
|
22555
|
+
}
|
|
22556
|
+
|
|
22557
|
+
function onCancel() {
|
|
22558
|
+
finish();
|
|
22559
|
+
tileDrag = null;
|
|
22560
|
+
}
|
|
22561
|
+
|
|
22562
|
+
function finish() {
|
|
22563
|
+
document.removeEventListener('pointermove', onMove);
|
|
22564
|
+
document.removeEventListener('pointerup', onUp);
|
|
22565
|
+
document.removeEventListener('pointercancel', onCancel);
|
|
22566
|
+
tilesEl.removeClass('dragging');
|
|
22567
|
+
getTileCells().forEach(function(cell) { cell.classList.remove('drag-source'); });
|
|
22568
|
+
dropMarker.hide();
|
|
22569
|
+
}
|
|
22570
|
+
}
|
|
22571
|
+
|
|
22572
|
+
function getTileCells() {
|
|
22573
|
+
return Array.from(tilesEl.node().querySelectorAll('.color-scheme-tile-cell'));
|
|
22574
|
+
}
|
|
22575
|
+
|
|
22576
|
+
// The index of the tile that a swatch dropped at (x, y) goes in front of
|
|
22577
|
+
// (the number of tiles, after the last), on the row nearest the pointer
|
|
22578
|
+
function findDropPlace(x, y) {
|
|
22579
|
+
var rects = getTileCells().map(function(cell) {
|
|
22580
|
+
return cell.querySelector('.color-scheme-tile').getBoundingClientRect();
|
|
22581
|
+
});
|
|
22582
|
+
var best = -1, bestDist = Infinity;
|
|
22583
|
+
rects.forEach(function(rect, i) {
|
|
22584
|
+
var dy = y < rect.top ? rect.top - y : y > rect.bottom ? y - rect.bottom : 0;
|
|
22585
|
+
var dx = x < rect.left ? rect.left - x : x > rect.right ? x - rect.right : 0;
|
|
22586
|
+
var dist = dy * 1000 + dx;
|
|
22587
|
+
if (dist < bestDist) {
|
|
22588
|
+
bestDist = dist;
|
|
22589
|
+
best = x < (rect.left + rect.right) / 2 ? i : i + 1;
|
|
22590
|
+
}
|
|
22591
|
+
});
|
|
22592
|
+
return best;
|
|
22593
|
+
}
|
|
22594
|
+
|
|
22595
|
+
// A line in the gap where the swatch would go
|
|
22596
|
+
function showDropMarker(before) {
|
|
22597
|
+
var cells = getTileCells();
|
|
22598
|
+
var box = tilesEl.node().getBoundingClientRect();
|
|
22599
|
+
var ref = cells[Math.min(before, cells.length - 1)].querySelector('.color-scheme-tile');
|
|
22600
|
+
var rect = ref.getBoundingClientRect();
|
|
22601
|
+
var left = before < cells.length ? rect.left - 2 : rect.right + 1;
|
|
22602
|
+
dropMarker.css({
|
|
22603
|
+
left: (left - box.left) + 'px',
|
|
22604
|
+
top: (rect.top - box.top) + 'px',
|
|
22605
|
+
height: rect.height + 'px'
|
|
22606
|
+
}).show();
|
|
22607
|
+
}
|
|
22608
|
+
|
|
22609
|
+
// A tooltip is centered under its pin or button, unless that would run it past the
|
|
22610
|
+
// edge of the window
|
|
22611
|
+
function keepTooltipInWindow(el) {
|
|
22612
|
+
var margin = 4;
|
|
22613
|
+
el.style.removeProperty('--tooltip-shift');
|
|
22614
|
+
if (!el.hasAttribute('data-tooltip')) return;
|
|
22615
|
+
var width = parseFloat(getComputedStyle(el, '::after').width) || 0;
|
|
22616
|
+
var rect = el.getBoundingClientRect();
|
|
22617
|
+
var center = rect.left + rect.width / 2;
|
|
22618
|
+
var left = center - width / 2;
|
|
22619
|
+
var right = center + width / 2;
|
|
22620
|
+
var maxRight = document.documentElement.clientWidth - margin;
|
|
22621
|
+
var shift = 0;
|
|
22622
|
+
if (right > maxRight) shift = maxRight - right;
|
|
22623
|
+
else if (left < margin) shift = margin - left;
|
|
22624
|
+
if (shift) el.style.setProperty('--tooltip-shift', shift + 'px');
|
|
22625
|
+
}
|
|
22626
|
+
|
|
22627
|
+
function formatCategories(values) {
|
|
22628
|
+
// "and 1 more" would take a line that the value itself can have
|
|
22629
|
+
var max = values.length == 5 ? 5 : 4;
|
|
22630
|
+
var labels = values.slice(0, max).map(function(val) {
|
|
22631
|
+
return val === '' || val === null || val === undefined ? '(no value)' : String(val);
|
|
22632
|
+
});
|
|
22633
|
+
if (values.length > max) labels.push('and ' + (values.length - max) + ' more');
|
|
22634
|
+
return labels.join('\n');
|
|
22635
|
+
}
|
|
22636
|
+
|
|
22637
|
+
function describeAdjustment(tile) {
|
|
22638
|
+
var parts = [];
|
|
22639
|
+
var dl = tile.l - tile.ideal.l;
|
|
22640
|
+
var dc = tile.c - tile.ideal.c;
|
|
22641
|
+
if (Math.abs(dl) >= 0.0005) parts.push('lightness ' + formatShift(dl));
|
|
22642
|
+
if (Math.abs(dc) >= 0.0005) parts.push('chroma ' + formatShift(dc));
|
|
22643
|
+
return 'Shifted to fit the sRGB gamut:\n' + parts.join(', ');
|
|
22644
|
+
}
|
|
22645
|
+
|
|
22646
|
+
function formatShift(val) {
|
|
22647
|
+
return (val > 0 ? '+' : '') + val.toFixed(3);
|
|
22648
|
+
}
|
|
22649
|
+
|
|
22650
|
+
function selectTile(i, color) {
|
|
22651
|
+
nullControl.picker.hide();
|
|
22652
|
+
if (selectedTile == i && picker.visible()) {
|
|
22653
|
+
picker.hide();
|
|
22654
|
+
} else {
|
|
22655
|
+
selectedTile = i;
|
|
22656
|
+
picker.setColor(getTileEditColor(scheme, i) || color);
|
|
22657
|
+
if (!picker.visible()) picker.toggle();
|
|
22658
|
+
}
|
|
22659
|
+
render();
|
|
22660
|
+
}
|
|
22661
|
+
|
|
22662
|
+
function togglePaletteMenu() {
|
|
22663
|
+
if (paletteMenu.visible()) {
|
|
22664
|
+
hidePaletteMenu();
|
|
22665
|
+
} else {
|
|
22666
|
+
renderPaletteMenu();
|
|
22667
|
+
paletteMenu.show();
|
|
22668
|
+
}
|
|
22669
|
+
}
|
|
22670
|
+
|
|
22671
|
+
function hidePaletteMenu() {
|
|
22672
|
+
paletteMenu.hide();
|
|
22673
|
+
}
|
|
22674
|
+
|
|
22675
|
+
function renderPaletteMenu() {
|
|
22676
|
+
var categorical = scheme.type == 'categorical';
|
|
22677
|
+
var names = categorical ? getCategoricalPresetNames() :
|
|
22678
|
+
scheme.type == 'diverging' ? getDivergingPresetNames() : getSequentialPresetNames();
|
|
22679
|
+
paletteMenu.empty();
|
|
22680
|
+
addPaletteItem('Custom', getSchemeColors(makeSchemeCustom(scheme)), !scheme.preset, function() {
|
|
22681
|
+
changeScheme(makeSchemeCustom(scheme));
|
|
22682
|
+
});
|
|
22683
|
+
names.forEach(function(name) {
|
|
22684
|
+
var colors = categorical ? getCategoricalPresetColors(name) : getPresetColors(name, 7);
|
|
22685
|
+
addPaletteItem(name, colors, scheme.preset == name, function() {
|
|
22686
|
+
changeScheme(choosePreset(scheme, name, getCategoryCount()));
|
|
22687
|
+
});
|
|
22688
|
+
});
|
|
22689
|
+
}
|
|
22690
|
+
|
|
22691
|
+
function addPaletteItem(name, colors, selected, onSelect) {
|
|
22692
|
+
var item = El('div').addClass('color-scheme-palette-item').attr('role', 'button')
|
|
22693
|
+
.classed('selected', selected).appendTo(paletteMenu)
|
|
22694
|
+
.on('click', function() {
|
|
22695
|
+
hidePaletteMenu();
|
|
22696
|
+
onSelect();
|
|
22697
|
+
});
|
|
22698
|
+
El('div').addClass('color-scheme-strip').appendTo(item)
|
|
22699
|
+
.css('background-image', getStripBackground(colors));
|
|
22700
|
+
El('span').appendTo(item).text(name);
|
|
22701
|
+
}
|
|
22702
|
+
|
|
22703
|
+
function changeScheme(next) {
|
|
22704
|
+
var prevSize = getSchemeTiles(scheme).length;
|
|
22705
|
+
scheme = updateDivergingLayout(next, targetLayer);
|
|
22706
|
+
if (getSchemeTiles(scheme).length != prevSize) {
|
|
22707
|
+
picker.hide();
|
|
22708
|
+
selectedTile = -1;
|
|
22709
|
+
}
|
|
22710
|
+
apply();
|
|
22711
|
+
render();
|
|
22712
|
+
}
|
|
22713
|
+
|
|
22714
|
+
function apply() {
|
|
22715
|
+
var lyr = targetLayer;
|
|
22716
|
+
var applying = scheme;
|
|
22717
|
+
var colors = getAppliedColors(scheme, getCategoryCount());
|
|
22718
|
+
var cmd, extra;
|
|
22719
|
+
if (!lyr || !canApply(scheme) || !gui.console) return;
|
|
22720
|
+
getSession();
|
|
22721
|
+
if (!session || !session.isPending()) {
|
|
22722
|
+
sessionLayer = lyr;
|
|
22723
|
+
baseScheme = getLayerScheme(lyr);
|
|
22724
|
+
}
|
|
22725
|
+
cmd = formatSchemeCommand(scheme, colors, {target: getTargetOption(lyr)});
|
|
22726
|
+
extra = opts.getExtraCommands ? opts.getExtraCommands() : '';
|
|
22727
|
+
if (extra) cmd += ' ' + extra;
|
|
22728
|
+
runGuiEditCommand(gui, cmd, {
|
|
22729
|
+
title: 'Color scheme',
|
|
22730
|
+
session: session,
|
|
22731
|
+
onDone: function(err) {
|
|
22732
|
+
if (!err) setLayerScheme(lyr, getAppliedScheme(applying, colors, lyr));
|
|
22733
|
+
opts.onUpdate();
|
|
22734
|
+
}
|
|
22735
|
+
});
|
|
22736
|
+
}
|
|
22737
|
+
|
|
22738
|
+
// The scheme on each side of the session's undo state, to go with the fills
|
|
22739
|
+
// that undo and redo put back.
|
|
22740
|
+
function onCommit() {
|
|
22741
|
+
var lyr = sessionLayer;
|
|
22742
|
+
var before = baseScheme;
|
|
22743
|
+
var after = lyr ? getLayerScheme(lyr) : null;
|
|
22744
|
+
if (!lyr) return null;
|
|
22745
|
+
return {
|
|
22746
|
+
onUndo: function() {
|
|
22747
|
+
setLayerScheme(lyr, before);
|
|
22748
|
+
},
|
|
22749
|
+
onRedo: function() {
|
|
22750
|
+
setLayerScheme(lyr, after);
|
|
22751
|
+
}
|
|
22752
|
+
};
|
|
22753
|
+
}
|
|
22754
|
+
|
|
22755
|
+
function getTargetOption(lyr) {
|
|
22756
|
+
var active = gui.model.getActiveLayer();
|
|
22757
|
+
if (active && active.layer == lyr) return '';
|
|
22758
|
+
return internal.formatOptionValue(internal.getLayerTargetId(gui.model, lyr));
|
|
22759
|
+
}
|
|
22760
|
+
|
|
22761
|
+
function releaseFocus() {
|
|
22762
|
+
releasePanelFocus(panel.node());
|
|
22763
|
+
}
|
|
22764
|
+
}
|
|
22765
|
+
|
|
22766
|
+
// A scheme's colors as a CSS background: blocks, or for a continuous scheme,
|
|
22767
|
+
// its gradients, side by side
|
|
22768
|
+
function getSchemeStripBackground(scheme) {
|
|
22769
|
+
return isContinuousScheme(scheme) ? getGradientStripBackground(scheme) : getStripBackground(scheme.colors);
|
|
22770
|
+
}
|
|
22771
|
+
|
|
22772
|
+
// Each gradient takes room for its length, from its first stop to its last
|
|
22773
|
+
// (see getContinuousSegments()), without the flat ends the panel's bar has
|
|
22774
|
+
// over its end tiles; a pivot class takes a tile's width. Hard edges between
|
|
22775
|
+
// them.
|
|
22776
|
+
function getGradientStripBackground(scheme) {
|
|
22777
|
+
var segments = getContinuousSegments(scheme);
|
|
22778
|
+
var lengths = segments.map(function(seg) {
|
|
22779
|
+
return seg.end > seg.start ? seg.end - seg.start : 1;
|
|
22780
|
+
});
|
|
22781
|
+
var total = lengths.reduce(function(memo, len) { return memo + len; }, 0);
|
|
22782
|
+
var offset = 0;
|
|
22783
|
+
var stops = [];
|
|
22784
|
+
var pct = function(pos) { return (Math.round(pos / total * 10000) / 100) + '%'; };
|
|
22785
|
+
if (!(total > 0)) return 'none';
|
|
22786
|
+
segments.forEach(function(seg, i) {
|
|
22787
|
+
var first = seg.samples[0].color;
|
|
22788
|
+
var last = seg.samples[seg.samples.length - 1].color;
|
|
22789
|
+
stops.push(first + ' ' + pct(offset));
|
|
22790
|
+
seg.samples.forEach(function(sample) {
|
|
22791
|
+
stops.push(sample.color + ' ' + pct(offset + sample.pos - seg.start));
|
|
22792
|
+
});
|
|
22793
|
+
offset += lengths[i];
|
|
22794
|
+
stops.push(last + ' ' + pct(offset));
|
|
22795
|
+
});
|
|
22796
|
+
return 'linear-gradient(to right, ' + stops.join(', ') + ')';
|
|
22797
|
+
}
|
|
22798
|
+
|
|
22799
|
+
// A row of hard-edged color blocks, as a CSS background
|
|
22800
|
+
function getStripBackground(colors) {
|
|
22801
|
+
var n = colors.length;
|
|
22802
|
+
var stops = colors.map(function(color, i) {
|
|
22803
|
+
return color + ' ' + (i / n * 100) + '%, ' + color + ' ' + ((i + 1) / n * 100) + '%';
|
|
22804
|
+
});
|
|
22805
|
+
return 'linear-gradient(to right, ' + stops.join(', ') + ')';
|
|
22806
|
+
}
|
|
22807
|
+
|
|
20353
22808
|
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'];
|
|
22809
|
+
var styleFields = ['stroke', 'stroke-width', 'stroke-opacity', 'stroke-dasharray', 'stroke-linecap', 'line-start', 'line-end', 'line-end-size', 'line-fade', 'fill', 'fill-opacity', 'fill-pattern'].concat(internal.svg.glowFields);
|
|
20355
22810
|
var arrowShapes = [{
|
|
20356
22811
|
name: 'arrow',
|
|
20357
22812
|
title: 'solid arrowheads'
|
|
@@ -20382,14 +22837,28 @@
|
|
|
20382
22837
|
label: 'Both',
|
|
20383
22838
|
title: 'arrowheads at both ends of the line'
|
|
20384
22839
|
}];
|
|
22840
|
+
var lineCaps = [{
|
|
22841
|
+
name: 'round',
|
|
22842
|
+
label: 'Round',
|
|
22843
|
+
title: 'round line caps'
|
|
22844
|
+
}, {
|
|
22845
|
+
name: 'butt',
|
|
22846
|
+
label: 'Butt',
|
|
22847
|
+
title: 'flat line caps that end at the line\'s endpoints'
|
|
22848
|
+
}, {
|
|
22849
|
+
name: 'square',
|
|
22850
|
+
label: 'Square',
|
|
22851
|
+
title: 'flat line caps that extend past the line\'s endpoints'
|
|
22852
|
+
}];
|
|
20385
22853
|
|
|
20386
22854
|
function LayerStyleTool(gui) {
|
|
20387
22855
|
var parent = gui.container.findChild('.mshp-main-map');
|
|
20388
22856
|
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,
|
|
22857
|
+
var title, editingStatus, clearLink, strokeControl, fillControl, strokeWidthField, capControl, dashControl, arrowControl, presetControl, patternControl, glowControl, hit;
|
|
22858
|
+
var schemeBtn, schemeStrip, schemePanel;
|
|
20390
22859
|
var targetLayer = null;
|
|
20391
22860
|
// What the arrowhead switch turns on, for lines that have no heads
|
|
20392
|
-
var lastArrow = {shape: 'arrow', position: 'end'};
|
|
22861
|
+
var lastArrow = {shape: 'arrow', position: 'end', fade: 0};
|
|
20393
22862
|
|
|
20394
22863
|
initPanel();
|
|
20395
22864
|
hit = gui.map.getHitControl && gui.map.getHitControl();
|
|
@@ -20440,11 +22909,13 @@
|
|
|
20440
22909
|
}
|
|
20441
22910
|
|
|
20442
22911
|
function turnOff() {
|
|
22912
|
+
schemePanel.close();
|
|
20443
22913
|
panel.hide();
|
|
20444
22914
|
strokeControl.picker.hide();
|
|
20445
22915
|
fillControl.picker.hide();
|
|
20446
22916
|
patternControl.hidePicker();
|
|
20447
22917
|
patternControl.reset();
|
|
22918
|
+
glowControl.reset();
|
|
20448
22919
|
targetLayer = null;
|
|
20449
22920
|
}
|
|
20450
22921
|
|
|
@@ -20474,14 +22945,17 @@
|
|
|
20474
22945
|
editingStatus = El('span').addClass('label-editing-status').appendTo(editRow);
|
|
20475
22946
|
clearLink = El('span').addClass('label-editing-clear colored-text').appendTo(editRow).text('deselect').on('click', clearSelection);
|
|
20476
22947
|
|
|
20477
|
-
fillControl = addColorControl(panel, 'Fill', 'fill', '');
|
|
20478
22948
|
strokeControl = addColorControl(panel, 'Stroke', 'stroke', '#000000');
|
|
20479
22949
|
strokeWidthField = addStrokeWidthControl(strokeControl.aside);
|
|
20480
|
-
|
|
22950
|
+
fillControl = addColorControl(panel, 'Fill', 'fill', '');
|
|
22951
|
+
addColorSchemeControl();
|
|
22952
|
+
var lineRow = El('div').addClass('label-style-row label-split-row layer-line-row').appendTo(panel);
|
|
22953
|
+
capControl = addLineCapControl(lineRow);
|
|
22954
|
+
dashControl = addDashArrayControl(lineRow);
|
|
22955
|
+
dashControl.row = lineRow;
|
|
20481
22956
|
arrowControl = addArrowControl(panel);
|
|
20482
22957
|
|
|
20483
22958
|
var buttonRow = El('div').addClass('label-style-row label-panel-button-row').appendTo(panel);
|
|
20484
|
-
randomFillBtn = makePanelActionButton(buttonRow, 'Random fills', applyRandomFillColors);
|
|
20485
22959
|
makePanelActionButton(buttonRow, 'Clear style', clearLayerStyle);
|
|
20486
22960
|
|
|
20487
22961
|
patternControl = new PatternFillControl(panel, {
|
|
@@ -20494,6 +22968,14 @@
|
|
|
20494
22968
|
releaseFocus: releaseFocus
|
|
20495
22969
|
});
|
|
20496
22970
|
|
|
22971
|
+
glowControl = new GlowEffectsControl(panel, {
|
|
22972
|
+
getRecords: getTargetRecords,
|
|
22973
|
+
getTargetIds: getTargetIds,
|
|
22974
|
+
applyEdits: runStyleEdits,
|
|
22975
|
+
revert: updateControls,
|
|
22976
|
+
releaseFocus: releaseFocus
|
|
22977
|
+
});
|
|
22978
|
+
|
|
20497
22979
|
presetControl = new StylePresetControl(panel, {
|
|
20498
22980
|
storageKey: savedStylesKey,
|
|
20499
22981
|
type: getStyleType,
|
|
@@ -20515,10 +22997,9 @@
|
|
|
20515
22997
|
var control = makeColorRow(parent, {
|
|
20516
22998
|
label: label,
|
|
20517
22999
|
onColor: function(color) {
|
|
20518
|
-
//
|
|
20519
|
-
//
|
|
20520
|
-
|
|
20521
|
-
if (color) applyColorControlStyle(control, color);
|
|
23000
|
+
// An emptied field unsets the colour: -style reads an empty value as
|
|
23001
|
+
// "remove this".
|
|
23002
|
+
applyColorControlStyle(control, color);
|
|
20522
23003
|
},
|
|
20523
23004
|
onOpacity: function(value) {
|
|
20524
23005
|
applyLayerStyle(field + '-opacity', value);
|
|
@@ -20530,6 +23011,74 @@
|
|
|
20530
23011
|
return control;
|
|
20531
23012
|
}
|
|
20532
23013
|
|
|
23014
|
+
// Fills colored by a data field. While a scheme is applied, its colors take
|
|
23015
|
+
// the place of the fill's swatch and hex value, which have no one value to
|
|
23016
|
+
// show; the opacity still applies. Clicking them reopens the scheme.
|
|
23017
|
+
function addColorSchemeControl() {
|
|
23018
|
+
var box = fillControl.input.parent();
|
|
23019
|
+
schemeStrip = El('div').addClass('layer-scheme-strip').attr('role', 'button')
|
|
23020
|
+
.attr('aria-label', 'Edit color scheme');
|
|
23021
|
+
box.node().insertBefore(schemeStrip.node(), fillControl.input.node());
|
|
23022
|
+
schemeStrip.on('click', openColorSchemePanel);
|
|
23023
|
+
El('div').addClass('layer-scheme-colors').appendTo(schemeStrip);
|
|
23024
|
+
El('div').addClass('layer-scheme-remove').attr('role', 'button')
|
|
23025
|
+
.attr('aria-label', 'Remove color scheme').attr('title', 'Remove color scheme')
|
|
23026
|
+
.text('×').appendTo(schemeStrip)
|
|
23027
|
+
.on('click', function(e) {
|
|
23028
|
+
e.stopPropagation();
|
|
23029
|
+
removeColorScheme();
|
|
23030
|
+
});
|
|
23031
|
+
schemeBtn = makePanelActionButton(fillControl.aside, 'Palettes', openColorSchemePanel)
|
|
23032
|
+
.addClass('layer-palettes-btn');
|
|
23033
|
+
schemePanel = new ColorSchemePanel(gui, {
|
|
23034
|
+
getExtraCommands: function() {
|
|
23035
|
+
if (!targetLayer) return '';
|
|
23036
|
+
return formatStyleEditCommands(patternControl.getRefillExpressionEdits(getAllFeatureIds(targetLayer)));
|
|
23037
|
+
},
|
|
23038
|
+
getSelectionCount: function() {
|
|
23039
|
+
return getSelectionIds().length;
|
|
23040
|
+
},
|
|
23041
|
+
onUpdate: function() {
|
|
23042
|
+
if (panel.visible()) updateControls();
|
|
23043
|
+
},
|
|
23044
|
+
onClose: function() {
|
|
23045
|
+
panel.removeClass('replaced');
|
|
23046
|
+
}
|
|
23047
|
+
});
|
|
23048
|
+
}
|
|
23049
|
+
|
|
23050
|
+
function openColorSchemePanel() {
|
|
23051
|
+
syncTargetLayer();
|
|
23052
|
+
if (!targetLayer || targetLayer.geometry_type != 'polygon') return;
|
|
23053
|
+
fillControl.picker.hide();
|
|
23054
|
+
strokeControl.picker.hide();
|
|
23055
|
+
patternControl.hidePicker();
|
|
23056
|
+
// The scheme panel takes this panel's place until it closes
|
|
23057
|
+
panel.addClass('replaced');
|
|
23058
|
+
schemePanel.open(targetLayer);
|
|
23059
|
+
}
|
|
23060
|
+
|
|
23061
|
+
// Back to a single fill, unset, as when the fill field is emptied
|
|
23062
|
+
function removeColorScheme() {
|
|
23063
|
+
schemePanel.close();
|
|
23064
|
+
applyColorControlStyle(fillControl, '');
|
|
23065
|
+
}
|
|
23066
|
+
|
|
23067
|
+
function updateColorSchemeControl() {
|
|
23068
|
+
var isPolygon = targetLayer && targetLayer.geometry_type == 'polygon';
|
|
23069
|
+
var scheme = isPolygon ? getLayerScheme(targetLayer) : null;
|
|
23070
|
+
schemeBtn.classed('hidden', !isPolygon);
|
|
23071
|
+
fillControl.input.parent().classed('has-scheme', !!scheme);
|
|
23072
|
+
schemeBtn.classed('selected', schemePanel.isOpen());
|
|
23073
|
+
if (scheme) {
|
|
23074
|
+
schemeStrip.findChild('.layer-scheme-colors')
|
|
23075
|
+
.css('background-image', getSchemeStripBackground(scheme));
|
|
23076
|
+
schemeStrip.attr('title', scheme.method == 'non-adjacent' ?
|
|
23077
|
+
'Neighbors colored differently' : 'Colored by ' + scheme.field);
|
|
23078
|
+
fillControl.picker.hide();
|
|
23079
|
+
}
|
|
23080
|
+
}
|
|
23081
|
+
|
|
20533
23082
|
// In the narrow column beside the stroke's colour. Stepping runs up a ladder
|
|
20534
23083
|
// of widths rather than by a fixed amount, because the useful ones are close
|
|
20535
23084
|
// together at the hairline end and far apart above 2px.
|
|
@@ -20549,12 +23098,60 @@
|
|
|
20549
23098
|
});
|
|
20550
23099
|
}
|
|
20551
23100
|
|
|
20552
|
-
//
|
|
20553
|
-
//
|
|
20554
|
-
function
|
|
20555
|
-
var
|
|
20556
|
-
var
|
|
20557
|
-
El('
|
|
23101
|
+
// Round is the default, except on a dashed line, which the renderers give
|
|
23102
|
+
// butt caps unless it has a cap of its own.
|
|
23103
|
+
function addLineCapControl(row) {
|
|
23104
|
+
var cell = El('div').addClass('label-split-cell label-align-row').appendTo(row);
|
|
23105
|
+
var control = {btns: {}};
|
|
23106
|
+
El('span').appendTo(cell).text('Caps');
|
|
23107
|
+
var group = El('div').addClass('label-btn-group layer-cap-buttons').appendTo(cell);
|
|
23108
|
+
lineCaps.forEach(function(item) {
|
|
23109
|
+
control.btns[item.name] = makePanelButton(group, item.label, function() {
|
|
23110
|
+
applyLineCap(item.name);
|
|
23111
|
+
}).attr('data-line-cap', item.name).attr('aria-label', item.title);
|
|
23112
|
+
});
|
|
23113
|
+
return control;
|
|
23114
|
+
}
|
|
23115
|
+
|
|
23116
|
+
function getDefaultLineCap(rec) {
|
|
23117
|
+
return rec && rec['stroke-dasharray'] ? 'butt' : 'round';
|
|
23118
|
+
}
|
|
23119
|
+
|
|
23120
|
+
function getLineCap(rec) {
|
|
23121
|
+
return rec && rec['stroke-linecap'] || getDefaultLineCap(rec);
|
|
23122
|
+
}
|
|
23123
|
+
|
|
23124
|
+
// A cap that is the line's default is unset rather than stored, so that a
|
|
23125
|
+
// layer left at Round gets no stroke-linecap column.
|
|
23126
|
+
function applyLineCap(cap) {
|
|
23127
|
+
var records = getTargetRecords();
|
|
23128
|
+
var edits = [];
|
|
23129
|
+
getTargetIds().forEach(function(id) {
|
|
23130
|
+
var rec = records[id] || {};
|
|
23131
|
+
var value = cap == getDefaultLineCap(rec) ? '' : cap;
|
|
23132
|
+
if (value != (rec['stroke-linecap'] || '')) {
|
|
23133
|
+
edits.push({id: id, styles: [['stroke-linecap', value]]});
|
|
23134
|
+
}
|
|
23135
|
+
});
|
|
23136
|
+
runStyleEdits(edits);
|
|
23137
|
+
}
|
|
23138
|
+
|
|
23139
|
+
function updateLineCapControl() {
|
|
23140
|
+
var records = getTargetRecords();
|
|
23141
|
+
var cap;
|
|
23142
|
+
getTargetIds().forEach(function(id, i) {
|
|
23143
|
+
var val = getLineCap(records[id]);
|
|
23144
|
+
cap = i === 0 || val == cap ? val : '';
|
|
23145
|
+
});
|
|
23146
|
+
lineCaps.forEach(function(item) {
|
|
23147
|
+
capControl.btns[item.name].classed('selected', item.name == cap);
|
|
23148
|
+
});
|
|
23149
|
+
}
|
|
23150
|
+
|
|
23151
|
+
// A literal stroke-dasharray, in the narrow column beside the caps. A blank
|
|
23152
|
+
// field makes the line solid again.
|
|
23153
|
+
function addDashArrayControl(row) {
|
|
23154
|
+
var cell = El('div').addClass('label-split-cell layer-dash-cell').appendTo(row);
|
|
20558
23155
|
var caption = El('div').addClass('label-style-row-label').appendTo(cell).text('Dashes');
|
|
20559
23156
|
makeFieldTip(caption,
|
|
20560
23157
|
'Dash and gap lengths in pixels, separated\n' +
|
|
@@ -20569,13 +23166,14 @@
|
|
|
20569
23166
|
}
|
|
20570
23167
|
applyDashArrayStyle(value);
|
|
20571
23168
|
});
|
|
20572
|
-
return {
|
|
23169
|
+
return {input: input};
|
|
20573
23170
|
}
|
|
20574
23171
|
|
|
20575
23172
|
// A switch in the heading says whether the lines have arrowheads; the rows
|
|
20576
23173
|
// under it are the head's shape with its size beside it, then which ends
|
|
20577
|
-
// get it. A line has one shape for both
|
|
20578
|
-
// though -style can give the two ends
|
|
23174
|
+
// get it with the line's fade beside that. A line has one shape for both
|
|
23175
|
+
// ends, which is all the panel sets, though -style can give the two ends
|
|
23176
|
+
// different ones.
|
|
20579
23177
|
function addArrowControl(parent) {
|
|
20580
23178
|
var section = makePanelSection(parent, 'Arrowheads');
|
|
20581
23179
|
var toggle = makePanelToggle(section.findChild('.label-style-section-title'), {
|
|
@@ -20588,8 +23186,8 @@
|
|
|
20588
23186
|
var sizeCell = El('div').addClass('label-split-cell label-spacing-row layer-arrow-size-row').appendTo(shapeRow);
|
|
20589
23187
|
var posRow = El('div').addClass('label-style-row label-split-row').appendTo(section);
|
|
20590
23188
|
var posCell = El('div').addClass('label-split-cell label-align-row').appendTo(posRow);
|
|
23189
|
+
var fadeCell = El('div').addClass('label-split-cell layer-arrow-fade-cell').appendTo(posRow);
|
|
20591
23190
|
var control = {section: section, toggle: toggle, shapeBtns: {}, posBtns: {}};
|
|
20592
|
-
El('div').addClass('label-split-cell').appendTo(posRow);
|
|
20593
23191
|
El('span').appendTo(shapeCell).text('Shape');
|
|
20594
23192
|
var shapeGroup = El('div').addClass('label-btn-group label-callout-buttons layer-arrow-shape-buttons').appendTo(shapeCell);
|
|
20595
23193
|
arrowShapes.forEach(function(item) {
|
|
@@ -20618,6 +23216,22 @@
|
|
|
20618
23216
|
onStep: nudgeArrowSize,
|
|
20619
23217
|
onDone: releaseFocus
|
|
20620
23218
|
});
|
|
23219
|
+
var fadeCaption = El('div').addClass('label-style-row-label').appendTo(fadeCell).text('Fade %');
|
|
23220
|
+
makeFieldTip(fadeCaption,
|
|
23221
|
+
'How much of the line fades in from its\n' +
|
|
23222
|
+
'tail, as a percent of its length. The fade\n' +
|
|
23223
|
+
'is a straight gradient, so it follows\n' +
|
|
23224
|
+
'straight and gently curved lines best.');
|
|
23225
|
+
control.fadeField = new SizeField(fadeCell, {
|
|
23226
|
+
title: 'Percent of the line that fades in from its tail',
|
|
23227
|
+
min: 0,
|
|
23228
|
+
max: 100,
|
|
23229
|
+
step: 5,
|
|
23230
|
+
bigStep: 25,
|
|
23231
|
+
onSet: applyArrowFade,
|
|
23232
|
+
onStep: nudgeArrowFade,
|
|
23233
|
+
onDone: releaseFocus
|
|
23234
|
+
});
|
|
20621
23235
|
return control;
|
|
20622
23236
|
}
|
|
20623
23237
|
|
|
@@ -20632,10 +23246,11 @@
|
|
|
20632
23246
|
return {shape: shape, position: position};
|
|
20633
23247
|
}
|
|
20634
23248
|
|
|
20635
|
-
// Switching on gives the lines that have no heads the last shape and
|
|
20636
|
-
// the section showed, so that off and on again is a round trip; the
|
|
20637
|
-
// that have heads keep theirs. Switching off leaves line-end-size
|
|
20638
|
-
// for the same reason
|
|
23249
|
+
// Switching on gives the lines that have no heads the last shape, ends and
|
|
23250
|
+
// fade the section showed, so that off and on again is a round trip; the
|
|
23251
|
+
// lines that have heads keep theirs. Switching off leaves line-end-size
|
|
23252
|
+
// alone, for the same reason, but removes the fade, which would otherwise
|
|
23253
|
+
// go on showing with its control hidden.
|
|
20639
23254
|
function setArrowsOn(on) {
|
|
20640
23255
|
applyArrowEdits(function(info) {
|
|
20641
23256
|
return on ? fillArrowInfo(info) : {shape: 'none', position: ''};
|
|
@@ -20667,7 +23282,8 @@
|
|
|
20667
23282
|
var edits = [];
|
|
20668
23283
|
getTargetIds().forEach(function(id) {
|
|
20669
23284
|
var rec = records[id] || {};
|
|
20670
|
-
var
|
|
23285
|
+
var info = getArrowInfo(rec);
|
|
23286
|
+
var next = getNext(info);
|
|
20671
23287
|
var on = next.shape != 'none';
|
|
20672
23288
|
var styles = [
|
|
20673
23289
|
['line-start', on && next.position != 'end' ? next.shape : ''],
|
|
@@ -20676,6 +23292,11 @@
|
|
|
20676
23292
|
// no need to remove what is not there
|
|
20677
23293
|
return style[1] || rec[style[0]];
|
|
20678
23294
|
});
|
|
23295
|
+
if (!on && rec['line-fade']) {
|
|
23296
|
+
styles.push(['line-fade', '']);
|
|
23297
|
+
} else if (on && info.shape == 'none' && lastArrow.fade > 0 && !rec['line-fade']) {
|
|
23298
|
+
styles.push(['line-fade', lastArrow.fade]);
|
|
23299
|
+
}
|
|
20679
23300
|
if (on && addStroke) styles.push(['stroke', strokeControl.defaultColor]);
|
|
20680
23301
|
if (styles.length > 0) edits.push({id: id, styles: styles});
|
|
20681
23302
|
});
|
|
@@ -20697,6 +23318,34 @@
|
|
|
20697
23318
|
applyArrowSize(Math.max(1, Math.round(size + delta)));
|
|
20698
23319
|
}
|
|
20699
23320
|
|
|
23321
|
+
// The field is a percent; line-fade is a share of the line, 0-1. No fade
|
|
23322
|
+
// is unset rather than stored as 0.
|
|
23323
|
+
function applyArrowFade(pct) {
|
|
23324
|
+
var ids = getArrowTargetIds();
|
|
23325
|
+
var value = pct > 0 ? Math.round(pct * 10) / 1000 : '';
|
|
23326
|
+
if (ids.length === 0) return;
|
|
23327
|
+
runStyleEdits(ids.map(function(id) {
|
|
23328
|
+
return {id: id, styles: [['line-fade', value]]};
|
|
23329
|
+
}));
|
|
23330
|
+
}
|
|
23331
|
+
|
|
23332
|
+
function nudgeArrowFade(delta) {
|
|
23333
|
+
var shown = getArrowFadeShown(getArrowTargetIds());
|
|
23334
|
+
if (shown.value === '') return;
|
|
23335
|
+
applyArrowFade(Math.max(0, Math.min(100, Math.round(shown.value + delta))));
|
|
23336
|
+
}
|
|
23337
|
+
|
|
23338
|
+
function getArrowFadeShown(ids) {
|
|
23339
|
+
var records = getTargetRecords();
|
|
23340
|
+
var value = '', pct;
|
|
23341
|
+
for (var i=0; i<ids.length; i++) {
|
|
23342
|
+
pct = Math.round(internal.svg.parseLineFade(records[ids[i]] && records[ids[i]]['line-fade']) * 1000) / 10;
|
|
23343
|
+
if (i > 0 && pct !== value) return {value: '', mixed: true};
|
|
23344
|
+
value = pct;
|
|
23345
|
+
}
|
|
23346
|
+
return {value: value, mixed: false};
|
|
23347
|
+
}
|
|
23348
|
+
|
|
20700
23349
|
function getArrowTargetIds() {
|
|
20701
23350
|
var records = getTargetRecords();
|
|
20702
23351
|
return getTargetIds().filter(function(id) {
|
|
@@ -20757,6 +23406,11 @@
|
|
|
20757
23406
|
arrowControl.sizeField.setValue(shown.value);
|
|
20758
23407
|
arrowControl.sizeField.setPlaceholder(shown.mixed ? 'mixed' : '');
|
|
20759
23408
|
arrowControl.sizeField.setDisabled(arrowIds.length === 0);
|
|
23409
|
+
var fade = arrowIds.length > 0 ? getArrowFadeShown(arrowIds) : {value: '', mixed: false};
|
|
23410
|
+
if (fade.value !== '') lastArrow.fade = fade.value / 100;
|
|
23411
|
+
arrowControl.fadeField.setValue(fade.value);
|
|
23412
|
+
arrowControl.fadeField.setPlaceholder(fade.mixed ? 'mixed' : '');
|
|
23413
|
+
arrowControl.fadeField.setDisabled(arrowIds.length === 0);
|
|
20760
23414
|
}
|
|
20761
23415
|
|
|
20762
23416
|
function getTargetRecords() {
|
|
@@ -20782,10 +23436,15 @@
|
|
|
20782
23436
|
updateColorControl(fillControl);
|
|
20783
23437
|
updateStrokeWidthControl();
|
|
20784
23438
|
updateDashArrayControl();
|
|
20785
|
-
if (geom == 'polyline')
|
|
20786
|
-
|
|
23439
|
+
if (geom == 'polyline') {
|
|
23440
|
+
updateLineCapControl();
|
|
23441
|
+
updateArrowControl();
|
|
23442
|
+
}
|
|
23443
|
+
updateColorSchemeControl();
|
|
20787
23444
|
patternControl.section.classed('hidden', geom != 'polygon');
|
|
20788
23445
|
if (geom == 'polygon') patternControl.update();
|
|
23446
|
+
glowControl.section.classed('hidden', geom != 'polygon');
|
|
23447
|
+
if (geom == 'polygon') glowControl.update();
|
|
20789
23448
|
presetControl.render();
|
|
20790
23449
|
updateSavedStyleControls();
|
|
20791
23450
|
}
|
|
@@ -20846,7 +23505,7 @@
|
|
|
20846
23505
|
|
|
20847
23506
|
function applyColorControlStyle(control, color) {
|
|
20848
23507
|
var styles = [[control.field, color]];
|
|
20849
|
-
if (control.field == 'stroke' && strokeWidthIsUnsetForTargets()) {
|
|
23508
|
+
if (color && control.field == 'stroke' && strokeWidthIsUnsetForTargets()) {
|
|
20850
23509
|
styles.push(['stroke-width', 1]);
|
|
20851
23510
|
}
|
|
20852
23511
|
if (control.field == 'fill') {
|
|
@@ -20918,6 +23577,8 @@
|
|
|
20918
23577
|
}
|
|
20919
23578
|
}
|
|
20920
23579
|
|
|
23580
|
+
// No button runs this for now; the Random fill button is waiting for a new
|
|
23581
|
+
// place in the panel.
|
|
20921
23582
|
// Patterns keep their backgrounds in step with the fills, and the new fills
|
|
20922
23583
|
// are not known until -classify has chosen them, so the patterns are
|
|
20923
23584
|
// rewritten by an expression that reads each feature's fill when it runs.
|
|
@@ -20962,12 +23623,14 @@
|
|
|
20962
23623
|
addStyleValue(style, 'stroke-opacity', parseOpacityValue(strokeControl.opacity.node().value));
|
|
20963
23624
|
if (targetLayer && targetLayer.geometry_type == 'polyline') {
|
|
20964
23625
|
addStyleValue(style, 'stroke-dasharray', getControlValue(dashControl.input));
|
|
23626
|
+
addStyleValue(style, 'stroke-linecap', getCommonStyleValue('stroke-linecap'));
|
|
20965
23627
|
addArrowStyleValues(style);
|
|
20966
23628
|
}
|
|
20967
23629
|
if (targetLayer && targetLayer.geometry_type == 'polygon') {
|
|
20968
23630
|
addStyleValue(style, 'fill', getControlValue(fillControl.input));
|
|
20969
23631
|
addStyleValue(style, 'fill-opacity', parseOpacityValue(fillControl.opacity.node().value));
|
|
20970
23632
|
addStyleValue(style, 'fill-pattern', patternControl.getCode(getControlValue(fillControl.input)));
|
|
23633
|
+
glowControl.addStyleValues(style);
|
|
20971
23634
|
}
|
|
20972
23635
|
return style;
|
|
20973
23636
|
}
|
|
@@ -20975,7 +23638,7 @@
|
|
|
20975
23638
|
// Only when every target agrees, as with the other controls; a size is
|
|
20976
23639
|
// saved only if the lines have their own.
|
|
20977
23640
|
function addArrowStyleValues(style) {
|
|
20978
|
-
['line-start', 'line-end', 'line-end-size'].forEach(function(field) {
|
|
23641
|
+
['line-start', 'line-end', 'line-end-size', 'line-fade'].forEach(function(field) {
|
|
20979
23642
|
addStyleValue(style, field, getCommonStyleValue(field));
|
|
20980
23643
|
});
|
|
20981
23644
|
}
|
|
@@ -21079,9 +23742,13 @@
|
|
|
21079
23742
|
updateControls();
|
|
21080
23743
|
}
|
|
21081
23744
|
|
|
23745
|
+
// An empty layer has nothing for the controls to act on, so they are shown
|
|
23746
|
+
// disabled rather than left to do nothing.
|
|
21082
23747
|
function updateEditingStatus(count) {
|
|
21083
|
-
|
|
23748
|
+
var total = targetLayer ? internal.getFeatureCount(targetLayer) : 0;
|
|
23749
|
+
editingStatus.text(formatEditingStatus({selected: count, total: total}));
|
|
21084
23750
|
clearLink.classed('hidden', count === 0);
|
|
23751
|
+
panel.classed('no-targets', total === 0);
|
|
21085
23752
|
}
|
|
21086
23753
|
|
|
21087
23754
|
function getActiveLayer() {
|
|
@@ -21092,6 +23759,7 @@
|
|
|
21092
23759
|
function syncTargetLayer() {
|
|
21093
23760
|
var lyr = getActiveLayer();
|
|
21094
23761
|
if (lyr == targetLayer) return;
|
|
23762
|
+
schemePanel.close();
|
|
21095
23763
|
if (layerCanBeStyled(lyr)) {
|
|
21096
23764
|
targetLayer = lyr;
|
|
21097
23765
|
if (hit) hit.clearSelection();
|
|
@@ -21308,8 +23976,17 @@
|
|
|
21308
23976
|
var control = makeColorRow(circlesSection, {
|
|
21309
23977
|
label: label,
|
|
21310
23978
|
// Every circle property is applied together, from what the controls are
|
|
21311
|
-
// showing: the fields are one style, not five.
|
|
21312
|
-
|
|
23979
|
+
// showing: the fields are one style, not five. An emptied colour field
|
|
23980
|
+
// unsets the colour, which applying the fields would leave alone.
|
|
23981
|
+
onColor: function(color) {
|
|
23982
|
+
if (color) {
|
|
23983
|
+
applyCircleStyles();
|
|
23984
|
+
} else if (!styleFieldIsUnset(field)) {
|
|
23985
|
+
runStyleCommand([field + '=' + quoteCommandValue('')], 'Remove circle ' + field);
|
|
23986
|
+
} else {
|
|
23987
|
+
updateControls();
|
|
23988
|
+
}
|
|
23989
|
+
},
|
|
21313
23990
|
onOpacity: applyCircleStyles,
|
|
21314
23991
|
revert: updateControls
|
|
21315
23992
|
});
|
|
@@ -21659,9 +24336,13 @@
|
|
|
21659
24336
|
updateControls();
|
|
21660
24337
|
}
|
|
21661
24338
|
|
|
24339
|
+
// As in the line and polygon panels: an empty layer's controls are shown
|
|
24340
|
+
// disabled rather than left to do nothing.
|
|
21662
24341
|
function updateEditingStatus(count) {
|
|
21663
|
-
|
|
24342
|
+
var total = targetLayer ? internal.getFeatureCount(targetLayer) : 0;
|
|
24343
|
+
editingStatus.text(formatEditingStatus({selected: count, total: total}));
|
|
21664
24344
|
clearLink.classed('hidden', count === 0);
|
|
24345
|
+
panel.classed('no-targets', total === 0);
|
|
21665
24346
|
}
|
|
21666
24347
|
|
|
21667
24348
|
function setCircleColor(control, color) {
|
|
@@ -29259,6 +31940,7 @@
|
|
|
29259
31940
|
selection.turnOff();
|
|
29260
31941
|
abandonCurve();
|
|
29261
31942
|
armed = null;
|
|
31943
|
+
setLabelToolArmed(gui, null);
|
|
29262
31944
|
updateButtons();
|
|
29263
31945
|
hideInstructions();
|
|
29264
31946
|
drag = null;
|
|
@@ -29324,6 +32006,7 @@
|
|
|
29324
32006
|
if (armed == mode) return;
|
|
29325
32007
|
if (drawingCurve()) abandonCurve();
|
|
29326
32008
|
armed = mode;
|
|
32009
|
+
setLabelToolArmed(gui, mode);
|
|
29327
32010
|
updateButtons();
|
|
29328
32011
|
showInstructions();
|
|
29329
32012
|
}
|
|
@@ -31080,6 +33763,11 @@
|
|
|
31080
33763
|
// rest, because the style object is reused from one feature to the next.
|
|
31081
33764
|
var arrowFields = getLineArrowFields(lyr);
|
|
31082
33765
|
var hasStrokeFields = fields.includes('stroke') || fields.includes('stroke-width');
|
|
33766
|
+
// Glows are drawn from the record rather than set as attributes, like the
|
|
33767
|
+
// arrowheads. An outer glow that the whole layer shares is drawn once for the
|
|
33768
|
+
// layer (see svg-glow.mjs), so the shapes are then given none of their own.
|
|
33769
|
+
var hasGlows = layerHasGlowFields(lyr);
|
|
33770
|
+
var layerOuterGlow = hasGlows ? internal.svg.getLayerOuterGlow(lyr) : null;
|
|
31083
33771
|
|
|
31084
33772
|
var styler = function(style, i) {
|
|
31085
33773
|
var rec = records[i];
|
|
@@ -31088,6 +33776,11 @@
|
|
|
31088
33776
|
style.lineStart = rec && rec['line-start'];
|
|
31089
33777
|
style.lineEnd = rec && rec['line-end'];
|
|
31090
33778
|
style.lineEndSize = rec && rec['line-end-size'];
|
|
33779
|
+
style.lineFade = rec && rec['line-fade'];
|
|
33780
|
+
}
|
|
33781
|
+
if (hasGlows) {
|
|
33782
|
+
style.outerGlow = layerOuterGlow ? null : internal.svg.getPolygonGlow(rec, 'outer');
|
|
33783
|
+
style.innerGlow = internal.svg.getPolygonGlow(rec, 'inner');
|
|
31091
33784
|
}
|
|
31092
33785
|
for (var j=0; j<fields.length; j++) {
|
|
31093
33786
|
fname = fields[j];
|
|
@@ -31110,6 +33803,12 @@
|
|
|
31110
33803
|
}
|
|
31111
33804
|
};
|
|
31112
33805
|
var style = {styler: styler, type: 'styled'};
|
|
33806
|
+
if (hasGlows) {
|
|
33807
|
+
style.layerOuterGlow = layerOuterGlow;
|
|
33808
|
+
// How far past its shapes the layer draws, which is how far outside the
|
|
33809
|
+
// view a shape can be and still reach into it.
|
|
33810
|
+
style.glowReach = internal.svg.getMaxGlowWidth(records) * internal.svg.GLOW_REACH;
|
|
33811
|
+
}
|
|
31113
33812
|
// A line layer styled with nothing but arrowheads is drawn the way SVG
|
|
31114
33813
|
// export draws it, with the black 1px line the layer's group gives it.
|
|
31115
33814
|
if (arrowFields && !hasStrokeFields) {
|
|
@@ -31131,7 +33830,14 @@
|
|
|
31131
33830
|
return fields.includes('fill') || fields.includes('r'); // support colored squares
|
|
31132
33831
|
}
|
|
31133
33832
|
return utils$1.difference(fields, ['opacity', 'class']).length > 0 ||
|
|
31134
|
-
!!getLineArrowFields(lyr);
|
|
33833
|
+
!!getLineArrowFields(lyr) || layerHasGlowFields(lyr);
|
|
33834
|
+
}
|
|
33835
|
+
|
|
33836
|
+
function layerHasGlowFields(lyr) {
|
|
33837
|
+
if (lyr.geometry_type != 'polygon' || !lyr.data) return false;
|
|
33838
|
+
return lyr.data.getFields().some(function(f) {
|
|
33839
|
+
return internal.svg.glowFields.includes(f);
|
|
33840
|
+
});
|
|
31135
33841
|
}
|
|
31136
33842
|
|
|
31137
33843
|
// The arrowhead fields a line layer has, or null
|
|
@@ -32048,6 +34754,102 @@
|
|
|
32048
34754
|
}
|
|
32049
34755
|
}
|
|
32050
34756
|
|
|
34757
|
+
// Canvas rendering of polygon glows, matching the SVG filters in svg-glow.mjs.
|
|
34758
|
+
//
|
|
34759
|
+
// What glows is drawn first into a scratch canvas: one shape, or a whole layer
|
|
34760
|
+
// for a layer's outer glow. The glows are then made from that image's alpha,
|
|
34761
|
+
// the way a filter works from SourceAlpha:
|
|
34762
|
+
//
|
|
34763
|
+
// outer the image's shadow, with the image itself cut out of it
|
|
34764
|
+
// inner the shadow of the image's inverse, kept only inside the image
|
|
34765
|
+
//
|
|
34766
|
+
// A shadow is only drawn with the image that casts it, so the image is drawn
|
|
34767
|
+
// off the edge of the canvas and its shadow is offset back into place. Drawn
|
|
34768
|
+
// in place and clipped, the image leaks into its own antialiased edge.
|
|
34769
|
+
//
|
|
34770
|
+
// A shadow's shadowBlur is twice the standard deviation of its blur, and a
|
|
34771
|
+
// glow's width is twice the standard deviation of the filter's, so the two
|
|
34772
|
+
// are the same number. Shadows are drawn in device pixels whatever the
|
|
34773
|
+
// transform, and the scratch canvases are never transformed when one is.
|
|
34774
|
+
|
|
34775
|
+
var scratch = {};
|
|
34776
|
+
|
|
34777
|
+
function getScratchCanvas(name, w, h) {
|
|
34778
|
+
var o = scratch[name];
|
|
34779
|
+
if (!o) {
|
|
34780
|
+
o = scratch[name] = {canvas: document.createElement('canvas')};
|
|
34781
|
+
o.ctx = o.canvas.getContext('2d');
|
|
34782
|
+
}
|
|
34783
|
+
// Grown and never shrunk: resizing a canvas reallocates it, and there can
|
|
34784
|
+
// be a resize for every shape drawn.
|
|
34785
|
+
if (o.canvas.width < w || o.canvas.height < h) {
|
|
34786
|
+
o.canvas.width = Math.max(o.canvas.width, w);
|
|
34787
|
+
o.canvas.height = Math.max(o.canvas.height, h);
|
|
34788
|
+
}
|
|
34789
|
+
o.ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
34790
|
+
o.ctx.clearRect(0, 0, w, h);
|
|
34791
|
+
return o;
|
|
34792
|
+
}
|
|
34793
|
+
|
|
34794
|
+
// A canvas to draw what glows into, @w by @h device pixels. Separate from the
|
|
34795
|
+
// ones compositeGlows() uses, so a shape can be drawn into it while a layer is
|
|
34796
|
+
// being drawn into another.
|
|
34797
|
+
// @name: 'shape' or 'layer'
|
|
34798
|
+
function getGlowSourceCanvas(name, w, h) {
|
|
34799
|
+
return getScratchCanvas('source-' + name, w, h);
|
|
34800
|
+
}
|
|
34801
|
+
|
|
34802
|
+
// How far past a shape its glows reach, in device pixels.
|
|
34803
|
+
function getGlowMargin(outer, inner, pxScale) {
|
|
34804
|
+
var width = Math.max(outer ? outer.width : 0, inner ? inner.width : 0);
|
|
34805
|
+
return Math.ceil(width * pxScale * internal.svg.GLOW_REACH) + 2;
|
|
34806
|
+
}
|
|
34807
|
+
|
|
34808
|
+
// Draws @src (the top-left @w by @h pixels of a canvas) into @ctx at @x, @y,
|
|
34809
|
+
// with its glows: the outer one underneath and the inner one on top.
|
|
34810
|
+
// @pxScale: device pixels per px of glow width
|
|
34811
|
+
function compositeGlows(ctx, src, x, y, w, h, outer, inner, pxScale) {
|
|
34812
|
+
var glow, inverse;
|
|
34813
|
+
if (outer) {
|
|
34814
|
+
glow = getScratchCanvas('glow', w, h);
|
|
34815
|
+
drawShadow(glow.ctx, src, w, h, outer, pxScale);
|
|
34816
|
+
glow.ctx.globalCompositeOperation = 'destination-out';
|
|
34817
|
+
glow.ctx.drawImage(src, 0, 0, w, h, 0, 0, w, h);
|
|
34818
|
+
glow.ctx.globalCompositeOperation = 'source-over';
|
|
34819
|
+
ctx.drawImage(glow.canvas, 0, 0, w, h, x, y, w, h);
|
|
34820
|
+
}
|
|
34821
|
+
ctx.drawImage(src, 0, 0, w, h, x, y, w, h);
|
|
34822
|
+
if (inner) {
|
|
34823
|
+
inverse = getScratchCanvas('inverse', w, h);
|
|
34824
|
+
inverse.ctx.fillStyle = '#000';
|
|
34825
|
+
inverse.ctx.fillRect(0, 0, w, h);
|
|
34826
|
+
inverse.ctx.globalCompositeOperation = 'destination-out';
|
|
34827
|
+
inverse.ctx.drawImage(src, 0, 0, w, h, 0, 0, w, h);
|
|
34828
|
+
inverse.ctx.globalCompositeOperation = 'source-over';
|
|
34829
|
+
glow = getScratchCanvas('glow', w, h);
|
|
34830
|
+
drawShadow(glow.ctx, inverse.canvas, w, h, inner, pxScale);
|
|
34831
|
+
glow.ctx.globalCompositeOperation = 'destination-in';
|
|
34832
|
+
glow.ctx.drawImage(src, 0, 0, w, h, 0, 0, w, h);
|
|
34833
|
+
glow.ctx.globalCompositeOperation = 'source-over';
|
|
34834
|
+
ctx.drawImage(glow.canvas, 0, 0, w, h, x, y, w, h);
|
|
34835
|
+
}
|
|
34836
|
+
}
|
|
34837
|
+
|
|
34838
|
+
// The shadow of the top-left @w by @h pixels of @img, with no image. The glow's
|
|
34839
|
+
// opacity is globalAlpha rather than part of the shadow's color, which can be
|
|
34840
|
+
// any CSS color and would have to be parsed to take one.
|
|
34841
|
+
function drawShadow(ctx, img, w, h, glow, pxScale) {
|
|
34842
|
+
var offset = w + glow.width * pxScale * 4 + 10;
|
|
34843
|
+
ctx.save();
|
|
34844
|
+
ctx.globalAlpha = glow.opacity;
|
|
34845
|
+
ctx.shadowColor = glow.color;
|
|
34846
|
+
ctx.shadowBlur = glow.width * pxScale;
|
|
34847
|
+
ctx.shadowOffsetX = offset;
|
|
34848
|
+
ctx.shadowOffsetY = 0;
|
|
34849
|
+
ctx.drawImage(img, 0, 0, w, h, -offset, 0, w, h);
|
|
34850
|
+
ctx.restore();
|
|
34851
|
+
}
|
|
34852
|
+
|
|
32051
34853
|
// TODO: consider moving this upstream
|
|
32052
34854
|
function getArcsForRendering(lyr, ext) {
|
|
32053
34855
|
var dataset = lyr.gui.source.dataset;
|
|
@@ -32098,7 +34900,7 @@
|
|
|
32098
34900
|
}
|
|
32099
34901
|
} else {
|
|
32100
34902
|
arcs = getArcsForRendering(lyr, ext);
|
|
32101
|
-
filter = getShapeFilter(arcs, layer.shapes, ext);
|
|
34903
|
+
filter = getShapeFilter(arcs, layer.shapes, ext, style.overlay ? 0 : style.glowReach);
|
|
32102
34904
|
canv.drawStyledPaths(layer.shapes, arcs, style, filter);
|
|
32103
34905
|
if (style.vertices || style.vertex_overlay || style.pending_snip) {
|
|
32104
34906
|
canv.drawVertices(layer.shapes, arcs, style, filter);
|
|
@@ -32136,10 +34938,17 @@
|
|
|
32136
34938
|
// view. The filter takes a shape index and tests the shape's bbox against
|
|
32137
34939
|
// the viewport. At nearly full extent the test is a no-op, so we return
|
|
32138
34940
|
// null to let the caller skip it entirely.
|
|
32139
|
-
|
|
34941
|
+
// reach: how far past its shapes the layer draws, in px (e.g. a glow)
|
|
34942
|
+
function getShapeFilter(arcs, shapes, ext, reach) {
|
|
32140
34943
|
if (ext.scale() < 1.1) return null;
|
|
32141
34944
|
var view = ext.getBounds();
|
|
32142
34945
|
var b = new Bounds$1();
|
|
34946
|
+
var pad;
|
|
34947
|
+
if (reach > 0) {
|
|
34948
|
+
pad = reach * (ext.getSymbolScale() || 1) * ext.getPixelSize();
|
|
34949
|
+
view = new Bounds$1(view.toArray());
|
|
34950
|
+
view.padBounds(pad, pad, pad, pad);
|
|
34951
|
+
}
|
|
32143
34952
|
return function(i) {
|
|
32144
34953
|
var shp = shapes[i];
|
|
32145
34954
|
if (!shp) return false;
|
|
@@ -32303,52 +35112,137 @@
|
|
|
32303
35112
|
var key, item, shp;
|
|
32304
35113
|
var styler = style.styler || null;
|
|
32305
35114
|
var drawStyle = styler ? utils$1.defaults({}, style) : style;
|
|
32306
|
-
|
|
32307
|
-
|
|
32308
|
-
|
|
32309
|
-
|
|
32310
|
-
|
|
32311
|
-
|
|
32312
|
-
|
|
32313
|
-
|
|
32314
|
-
|
|
32315
|
-
|
|
32316
|
-
|
|
32317
|
-
|
|
32318
|
-
|
|
32319
|
-
|
|
32320
|
-
|
|
32321
|
-
|
|
32322
|
-
|
|
32323
|
-
|
|
32324
|
-
|
|
32325
|
-
|
|
32326
|
-
|
|
32327
|
-
|
|
32328
|
-
|
|
32329
|
-
|
|
32330
|
-
|
|
32331
|
-
|
|
35115
|
+
// Hover and selection overlays copy the layer's style; they do not glow.
|
|
35116
|
+
var glows = !style.overlay;
|
|
35117
|
+
var glowScale = getCanvasStrokeScale(GUI.getPixelRatio(), lineScale);
|
|
35118
|
+
var layerGlow = glows && style.layerOuterGlow ?
|
|
35119
|
+
startLayerGlow(style.layerOuterGlow, glowScale) : null;
|
|
35120
|
+
try {
|
|
35121
|
+
drawShapes();
|
|
35122
|
+
} finally {
|
|
35123
|
+
if (layerGlow) endLayerGlow(layerGlow);
|
|
35124
|
+
}
|
|
35125
|
+
if (layerGlow) compositeLayerGlow(layerGlow);
|
|
35126
|
+
|
|
35127
|
+
function drawShapes() {
|
|
35128
|
+
var glowPencil = null;
|
|
35129
|
+
for (var i=0; i<shapes.length; i++) {
|
|
35130
|
+
shp = shapes[i];
|
|
35131
|
+
if (!shp || filter && !filter(i)) continue;
|
|
35132
|
+
if (styler) {
|
|
35133
|
+
styler(drawStyle, i);
|
|
35134
|
+
}
|
|
35135
|
+
if (glows && (drawStyle.outerGlow || drawStyle.innerGlow)) {
|
|
35136
|
+
// Shapes before this one are drawn first, as they are in SVG, where
|
|
35137
|
+
// a shape's outer glow lies over the shapes that precede it.
|
|
35138
|
+
flushBatches();
|
|
35139
|
+
if (!glowPencil) glowPencil = getGlowShapePencil(arcs, startPath, draw, glowScale);
|
|
35140
|
+
glowPencil(shp, drawStyle);
|
|
35141
|
+
continue;
|
|
35142
|
+
}
|
|
35143
|
+
// A line with arrowheads is drawn on its own, trimmed to meet its heads;
|
|
35144
|
+
// see svg-line-arrows.mjs. Only lines styled with line-start, line-end
|
|
35145
|
+
// or line-fade get here, so the batch below is untouched for
|
|
35146
|
+
// everything else.
|
|
35147
|
+
if (drawStyle.lineStart || drawStyle.lineEnd || drawStyle.lineFade) {
|
|
35148
|
+
if (!arrowPencil) arrowPencil = getArrowLinePencil(arcs, _ext, lineScale);
|
|
35149
|
+
if (arrowPencil(shp, _ctx, drawStyle, startPath, draw)) continue;
|
|
35150
|
+
}
|
|
35151
|
+
if (!drawStyle.batchOverlay && (drawStyle.overlay ||
|
|
35152
|
+
drawStyle.opacity < 1 || drawStyle.fillOpacity < 1 ||
|
|
35153
|
+
drawStyle.strokeOpacity < 1 || drawStyle.fillEffect)) {
|
|
35154
|
+
// don't batch shapes with opacity, in case they overlap
|
|
35155
|
+
drawPaths([shp], startPath, draw, drawStyle);
|
|
35156
|
+
continue;
|
|
35157
|
+
}
|
|
35158
|
+
key = getStyleKey(drawStyle);
|
|
35159
|
+
if (key in styleIndex === false) {
|
|
35160
|
+
styleIndex[key] = {
|
|
35161
|
+
style: utils$1.defaults({}, drawStyle),
|
|
35162
|
+
shapes: []
|
|
35163
|
+
};
|
|
35164
|
+
}
|
|
35165
|
+
item = styleIndex[key];
|
|
35166
|
+
item.shapes.push(shp);
|
|
35167
|
+
if (item.shapes.length >= batchSize) {
|
|
35168
|
+
drawPaths(item.shapes, startPath, draw, item.style);
|
|
35169
|
+
item.shapes = [];
|
|
35170
|
+
}
|
|
32332
35171
|
}
|
|
32333
|
-
|
|
32334
|
-
|
|
32335
|
-
|
|
35172
|
+
flushBatches();
|
|
35173
|
+
}
|
|
35174
|
+
|
|
35175
|
+
function flushBatches() {
|
|
35176
|
+
Object.keys(styleIndex).forEach(function(key) {
|
|
35177
|
+
var item = styleIndex[key];
|
|
35178
|
+
if (item.shapes.length === 0) return;
|
|
32336
35179
|
drawPaths(item.shapes, startPath, draw, item.style);
|
|
32337
35180
|
item.shapes = [];
|
|
32338
|
-
}
|
|
35181
|
+
});
|
|
32339
35182
|
}
|
|
32340
|
-
Object.keys(styleIndex).forEach(function(key) {
|
|
32341
|
-
var item = styleIndex[key];
|
|
32342
|
-
drawPaths(item.shapes, startPath, draw, item.style);
|
|
32343
|
-
});
|
|
32344
35183
|
};
|
|
32345
35184
|
|
|
32346
|
-
function drawPaths(shapes, beginPath, drawShape, style) {
|
|
32347
|
-
|
|
35185
|
+
function drawPaths(shapes, beginPath, drawShape, style, ctx) {
|
|
35186
|
+
ctx = ctx || _ctx;
|
|
35187
|
+
beginPath(ctx, style);
|
|
32348
35188
|
for (var i=0, n=shapes.length; i<n; i++) {
|
|
32349
|
-
drawShape(shapes[i],
|
|
35189
|
+
drawShape(shapes[i], ctx, style);
|
|
32350
35190
|
}
|
|
32351
|
-
endPath(
|
|
35191
|
+
endPath(ctx, style);
|
|
35192
|
+
}
|
|
35193
|
+
|
|
35194
|
+
// A layer's own outer glow is made from the whole layer, so the layer is
|
|
35195
|
+
// drawn into a canvas of its own and then onto this one with its glow. The
|
|
35196
|
+
// canvas is larger than this one by the reach of the glow, so that shapes
|
|
35197
|
+
// just outside the view still cast theirs into it.
|
|
35198
|
+
function startLayerGlow(glow, pxScale) {
|
|
35199
|
+
var margin = getGlowMargin(glow, null, pxScale);
|
|
35200
|
+
var w = _canvas.width + margin * 2;
|
|
35201
|
+
var h = _canvas.height + margin * 2;
|
|
35202
|
+
var src = getGlowSourceCanvas('layer', w, h);
|
|
35203
|
+
var o = {glow: glow, pxScale: pxScale, margin: margin, w: w, h: h,
|
|
35204
|
+
src: src, ctx: _ctx};
|
|
35205
|
+
src.ctx.setTransform(1, 0, 0, 1, margin, margin);
|
|
35206
|
+
_ctx = src.ctx;
|
|
35207
|
+
return o;
|
|
35208
|
+
}
|
|
35209
|
+
|
|
35210
|
+
function endLayerGlow(o) {
|
|
35211
|
+
o.src.ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
35212
|
+
_ctx = o.ctx;
|
|
35213
|
+
}
|
|
35214
|
+
|
|
35215
|
+
function compositeLayerGlow(o) {
|
|
35216
|
+
compositeGlows(_ctx, o.src.canvas, -o.margin, -o.margin, o.w, o.h,
|
|
35217
|
+
o.glow, null, o.pxScale);
|
|
35218
|
+
}
|
|
35219
|
+
|
|
35220
|
+
// Draws one shape with its own glows, which are made from the shape alone.
|
|
35221
|
+
// The shape is drawn into a canvas the size of its bounding box and the
|
|
35222
|
+
// reach of its glows, cut down to the part that can be seen.
|
|
35223
|
+
function getGlowShapePencil(arcs, startPath, draw, pxScale) {
|
|
35224
|
+
var t = getScaledTransform(_ext);
|
|
35225
|
+
var b = new Bounds$1();
|
|
35226
|
+
return function(shp, style) {
|
|
35227
|
+
var outer = style.outerGlow, inner = style.innerGlow;
|
|
35228
|
+
var margin = getGlowMargin(outer, inner, pxScale) +
|
|
35229
|
+
Math.ceil((style.strokeWidth > 0 ? style.strokeWidth * pxScale : 0) / 2);
|
|
35230
|
+
var viewMargin = margin * 2;
|
|
35231
|
+
var x0, y0, x1, y1, src;
|
|
35232
|
+
b.empty();
|
|
35233
|
+
arcs.getMultiShapeBounds(shp, b);
|
|
35234
|
+
if (!b.hasBounds()) return;
|
|
35235
|
+
x0 = Math.floor(Math.max(b.xmin * t.mx + t.bx - margin, -viewMargin));
|
|
35236
|
+
x1 = Math.ceil(Math.min(b.xmax * t.mx + t.bx + margin, _canvas.width + viewMargin));
|
|
35237
|
+
y0 = Math.floor(Math.max(b.ymax * t.my + t.by - margin, -viewMargin));
|
|
35238
|
+
y1 = Math.ceil(Math.min(b.ymin * t.my + t.by + margin, _canvas.height + viewMargin));
|
|
35239
|
+
if (x1 <= x0 || y1 <= y0) return;
|
|
35240
|
+
src = getGlowSourceCanvas('shape', x1 - x0, y1 - y0);
|
|
35241
|
+
src.ctx.setTransform(1, 0, 0, 1, -x0, -y0);
|
|
35242
|
+
drawPaths([shp], startPath, draw, style, src.ctx);
|
|
35243
|
+
src.ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
35244
|
+
compositeGlows(_ctx, src.canvas, x0, y0, x1 - x0, y1 - y0, outer, inner, pxScale);
|
|
35245
|
+
};
|
|
32352
35246
|
}
|
|
32353
35247
|
|
|
32354
35248
|
_self.drawSquareDots = function(shapes, style) {
|
|
@@ -32698,14 +35592,17 @@
|
|
|
32698
35592
|
};
|
|
32699
35593
|
}
|
|
32700
35594
|
|
|
32701
|
-
// Returns a function that draws one line feature with its arrowheads
|
|
32702
|
-
// returns false if the feature has
|
|
32703
|
-
// stroke), for the caller to draw it the usual way.
|
|
35595
|
+
// Returns a function that draws one line feature with its arrowheads and fade,
|
|
35596
|
+
// and returns false if the feature has neither to draw (a style of 'none', or
|
|
35597
|
+
// no stroke), for the caller to draw it the usual way.
|
|
32704
35598
|
//
|
|
32705
35599
|
// Heads are sized by the same factor as the line width, so that a preview
|
|
32706
35600
|
// matches the exported SVG. Far enough in that paths are clipped to the view
|
|
32707
35601
|
// (see protectIterForDrawing()), the line is drawn clipped and untrimmed and
|
|
32708
|
-
// only the heads are added, since a clipped path's ends are not its ends.
|
|
35602
|
+
// only the heads are added, since a clipped path's ends are not its ends. The
|
|
35603
|
+
// fade is placed by the whole line, so it is unaffected by the clipping.
|
|
35604
|
+
//
|
|
35605
|
+
// A faded part is stroked on its own, with its own gradient, as in SVG.
|
|
32709
35606
|
function getArrowLinePencil(arcs, ext, lineScale) {
|
|
32710
35607
|
var t = getScaledTransform(ext);
|
|
32711
35608
|
var iter = new internal.ShapeIter(arcs);
|
|
@@ -32713,10 +35610,13 @@
|
|
|
32713
35610
|
var clipped = ext.scale() > 100;
|
|
32714
35611
|
return function(shp, ctx, style, startPath, draw) {
|
|
32715
35612
|
var opts = internal.svg.makeLineArrowOpts(style.lineStart, style.lineEnd,
|
|
32716
|
-
style.lineEndSize, style.strokeWidth, strokeScale);
|
|
35613
|
+
style.lineEndSize, style.strokeWidth, strokeScale, style.lineFade);
|
|
35614
|
+
var fadeColors = opts.fade > 0 ? internal.svg.getLineFadeColors(style.strokeColor) : null;
|
|
32717
35615
|
var heads = [];
|
|
35616
|
+
var plain = [];
|
|
35617
|
+
var faded = [];
|
|
32718
35618
|
var i, coords, shape;
|
|
32719
|
-
if (opts.start == 'none' && opts.end == 'none' || !(style.strokeWidth > 0)) {
|
|
35619
|
+
if (opts.start == 'none' && opts.end == 'none' && !fadeColors || !(style.strokeWidth > 0)) {
|
|
32720
35620
|
return false;
|
|
32721
35621
|
}
|
|
32722
35622
|
startPath(ctx, style);
|
|
@@ -32725,15 +35625,38 @@
|
|
|
32725
35625
|
if (coords.length < 2) continue;
|
|
32726
35626
|
shape = internal.svg.getLineArrowShape(coords, opts);
|
|
32727
35627
|
heads = heads.concat(shape.heads);
|
|
32728
|
-
if (
|
|
35628
|
+
if (shape.fade && fadeColors) {
|
|
35629
|
+
faded.push({path: shp[i], shape: shape});
|
|
35630
|
+
} else if (clipped) {
|
|
35631
|
+
plain.push(shp[i]);
|
|
35632
|
+
} else {
|
|
35633
|
+
traceCoords(shape.coords, ctx);
|
|
35634
|
+
}
|
|
32729
35635
|
}
|
|
32730
|
-
if (clipped) draw(
|
|
35636
|
+
if (clipped && plain.length > 0) draw(plain, ctx, style);
|
|
32731
35637
|
endPath(ctx, style);
|
|
35638
|
+
for (i=0; i<faded.length; i++) {
|
|
35639
|
+
startPath(ctx, style);
|
|
35640
|
+
ctx.strokeStyle = getLineFadeGradient(ctx, faded[i].shape.fade, fadeColors);
|
|
35641
|
+
if (clipped) {
|
|
35642
|
+
draw([faded[i].path], ctx, style);
|
|
35643
|
+
} else {
|
|
35644
|
+
traceCoords(faded[i].shape.coords, ctx);
|
|
35645
|
+
}
|
|
35646
|
+
endPath(ctx, style);
|
|
35647
|
+
}
|
|
32732
35648
|
drawArrowHeads(heads, ctx, style, opts.width);
|
|
32733
35649
|
return true;
|
|
32734
35650
|
};
|
|
32735
35651
|
}
|
|
32736
35652
|
|
|
35653
|
+
function getLineFadeGradient(ctx, axis, colors) {
|
|
35654
|
+
var gradient = ctx.createLinearGradient(axis.from[0], axis.from[1], axis.to[0], axis.to[1]);
|
|
35655
|
+
gradient.addColorStop(0, colors.transparent);
|
|
35656
|
+
gradient.addColorStop(1, colors.solid);
|
|
35657
|
+
return gradient;
|
|
35658
|
+
}
|
|
35659
|
+
|
|
32737
35660
|
function getPixelCoords(iter, path, t) {
|
|
32738
35661
|
var coords = [];
|
|
32739
35662
|
var x, y, prev;
|
|
@@ -32866,7 +35789,7 @@
|
|
|
32866
35789
|
ctx.lineWidth = strokeWidth * strokeScale;
|
|
32867
35790
|
ctx.strokeStyle = style.strokeColor;
|
|
32868
35791
|
if (style.lineDash){
|
|
32869
|
-
ctx.lineCap = 'butt';
|
|
35792
|
+
ctx.lineCap = style.lineCap || 'butt';
|
|
32870
35793
|
ctx.setLineDash(scaleCanvasLineDash(style.lineDash, strokeScale));
|
|
32871
35794
|
}
|
|
32872
35795
|
if (style.miterLimit) {
|
|
@@ -35463,8 +38386,9 @@
|
|
|
35463
38386
|
var marginField = toolbar.addTextField('Margin', {
|
|
35464
38387
|
classname: 'frame-toolbar-margin-input',
|
|
35465
38388
|
placeholder: '0',
|
|
35466
|
-
tooltip: 'Padding added when fitting: 20px, 1cm, ' +
|
|
35467
|
-
|
|
38389
|
+
tooltip: 'Padding added when fitting: 2%, 20px, 1cm, or ' +
|
|
38390
|
+
'one to four values in CSS order (top right bottom left). ' +
|
|
38391
|
+
"A percentage is of the frame's width on every side."
|
|
35468
38392
|
});
|
|
35469
38393
|
toolbar.addSeparator();
|
|
35470
38394
|
toolbar.addTextButton('Done', {
|
|
@@ -35714,7 +38638,7 @@
|
|
|
35714
38638
|
return internal.getLayerTargetId(gui.model, o.layer);
|
|
35715
38639
|
}).join(','))
|
|
35716
38640
|
];
|
|
35717
|
-
if (margin) parts.push('
|
|
38641
|
+
if (margin) parts.push('margin=' + quoteCommandValue(margin));
|
|
35718
38642
|
if (!lockSize) parts.push('fix-scale');
|
|
35719
38643
|
parts.push(getTargetOption(target));
|
|
35720
38644
|
runGuiEditCommand(gui, parts.join(' '), {
|
|
@@ -35727,7 +38651,7 @@
|
|
|
35727
38651
|
// cleared so the fit that just ran matches what the toolbar shows.
|
|
35728
38652
|
function getMargin() {
|
|
35729
38653
|
var value = marginField.getValue();
|
|
35730
|
-
if (
|
|
38654
|
+
if (hasNonZeroMargin(value)) return value;
|
|
35731
38655
|
if (value) marginField.setValue('');
|
|
35732
38656
|
return '';
|
|
35733
38657
|
}
|
|
@@ -35778,8 +38702,10 @@
|
|
|
35778
38702
|
marginInput.node().value = '2%';
|
|
35779
38703
|
makeFieldTip(fitRow,
|
|
35780
38704
|
'Space around the layers: 2%, 20px, 1cm.\n' +
|
|
35781
|
-
"A percentage is of the frame's width on
|
|
35782
|
-
'side
|
|
38705
|
+
"A percentage is of the frame's width, on\n" +
|
|
38706
|
+
'every side. Use two to four values for\n' +
|
|
38707
|
+
'uneven margins, in CSS order: 4% 2%, or\n' +
|
|
38708
|
+
'8% 2% 2% 2% (top right bottom left).');
|
|
35783
38709
|
|
|
35784
38710
|
var drawRow = El('div').addClass('frame-create-option-row').appendTo(section);
|
|
35785
38711
|
addButton(drawRow, 'Draw on the map', function() {
|
|
@@ -35811,7 +38737,7 @@
|
|
|
35811
38737
|
|
|
35812
38738
|
function getMargin() {
|
|
35813
38739
|
var value = marginInput.node().value.trim();
|
|
35814
|
-
return
|
|
38740
|
+
return hasNonZeroMargin(value) ? value : '';
|
|
35815
38741
|
}
|
|
35816
38742
|
}
|
|
35817
38743
|
|
|
@@ -35891,7 +38817,7 @@
|
|
|
35891
38817
|
});
|
|
35892
38818
|
var parts = ['-frame', 'width=' + quoteCommandValue(width)];
|
|
35893
38819
|
if (aspect) parts.push('aspect-ratio=' + aspect);
|
|
35894
|
-
if (margin) parts.push('
|
|
38820
|
+
if (margin) parts.push('margin=' + quoteCommandValue(margin));
|
|
35895
38821
|
parts.push('name=frame');
|
|
35896
38822
|
parts.push('target=' + internal.formatOptionValue(ids.join(',')));
|
|
35897
38823
|
runCreateCommand(parts.join(' '));
|
|
@@ -35970,6 +38896,14 @@
|
|
|
35970
38896
|
}
|
|
35971
38897
|
}
|
|
35972
38898
|
|
|
38899
|
+
// A margin field's value is passed to the command when any of its one to four
|
|
38900
|
+
// values is positive, e.g. '0 2%'; a blank or all-zero margin is dropped.
|
|
38901
|
+
function hasNonZeroMargin(value) {
|
|
38902
|
+
return String(value || '').split(/[\s,]+/).some(function(str) {
|
|
38903
|
+
return parseFloat(str) > 0;
|
|
38904
|
+
});
|
|
38905
|
+
}
|
|
38906
|
+
|
|
35973
38907
|
function addButton(parent, label, action) {
|
|
35974
38908
|
El('div').addClass('btn dialog-btn').appendTo(parent)
|
|
35975
38909
|
.text(label).on('click', action);
|
|
@@ -36034,8 +38968,9 @@
|
|
|
36034
38968
|
var appearance = makePanelSection(form, 'Appearance');
|
|
36035
38969
|
backgroundControl = makeColorRow(appearance, {
|
|
36036
38970
|
label: 'Background',
|
|
38971
|
+
// An emptied colour field unsets the colour.
|
|
36037
38972
|
onColor: function(color) {
|
|
36038
|
-
|
|
38973
|
+
applyFrameStyle([['fill', color]]);
|
|
36039
38974
|
},
|
|
36040
38975
|
onOpacity: function(value) {
|
|
36041
38976
|
applyFrameStyle([['fill-opacity', value]]);
|
|
@@ -36045,9 +38980,8 @@
|
|
|
36045
38980
|
neatlineControl = makeColorRow(appearance, {
|
|
36046
38981
|
label: 'Neatline',
|
|
36047
38982
|
onColor: function(color) {
|
|
36048
|
-
if (!color) return;
|
|
36049
38983
|
var styles = [['stroke', color]];
|
|
36050
|
-
if (!getStyleValue('stroke-width')) styles.push(['stroke-width', 1]);
|
|
38984
|
+
if (color && !getStyleValue('stroke-width')) styles.push(['stroke-width', 1]);
|
|
36051
38985
|
applyFrameStyle(styles);
|
|
36052
38986
|
},
|
|
36053
38987
|
onOpacity: function(value) {
|
|
@@ -36427,7 +39361,10 @@
|
|
|
36427
39361
|
colorInput = El('input').attr('type', 'text').attr('aria-label', 'Scale bar color')
|
|
36428
39362
|
.on('change', function() {
|
|
36429
39363
|
var color = colorInput.node().value.trim();
|
|
36430
|
-
|
|
39364
|
+
var d = getScalebarRecord();
|
|
39365
|
+
// An emptied field unsets the colour, so the scalebar is drawn in
|
|
39366
|
+
// the default one.
|
|
39367
|
+
if (!color && !(d && d.color)) {
|
|
36431
39368
|
updateControls();
|
|
36432
39369
|
return;
|
|
36433
39370
|
}
|