mapshaper 0.7.72 → 0.7.75
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 +3115 -790
- package/package.json +6 -1
- package/www/index.html +2 -3
- package/www/mapshaper-gui.js +2750 -151
- package/www/mapshaper.js +3115 -790
- package/www/page.css +593 -35
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
|
|
|
@@ -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,7 +3624,7 @@
|
|
|
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,
|
|
3627
|
+
shuffle: shuffle$1, some, sortArrayIndex, sortOn, splitLines, sum,
|
|
3628
3628
|
toArray, toBuffer, trim: trim$1, trimQuotes,
|
|
3629
3629
|
uniq, uniqifyNames,
|
|
3630
3630
|
wildcardToRegExp
|
|
@@ -5177,7 +5177,7 @@
|
|
|
5177
5177
|
var recipe = getRasterViewRecipe(grid, view && view.recipe);
|
|
5178
5178
|
var stats = getRasterViewScalingStats(raster, recipe);
|
|
5179
5179
|
var sourceRange = recipe.scaling == 'none' ? getPixelTypeRange(grid.pixelType) : null;
|
|
5180
|
-
var displayRange = getDisplayRange(recipe.scaleRange);
|
|
5180
|
+
var displayRange = getDisplayRange$1(recipe.scaleRange);
|
|
5181
5181
|
var offset = pixelId * grid.bands;
|
|
5182
5182
|
var color = [0, 1, 2].map(function(band) {
|
|
5183
5183
|
return scaleSample(grid.samples[offset + band], stats && stats[band], sourceRange, displayRange);
|
|
@@ -5204,7 +5204,7 @@
|
|
|
5204
5204
|
var pixels = new Uint8ClampedArray(width * height * 4);
|
|
5205
5205
|
var stats = statsArg || getScalingStats(samples, bands, noData, recipe);
|
|
5206
5206
|
var sourceRange = recipe.scaling == 'none' ? getPixelTypeRange(grid.pixelType) : null;
|
|
5207
|
-
var displayRange = getDisplayRange(recipe.scaleRange);
|
|
5207
|
+
var displayRange = getDisplayRange$1(recipe.scaleRange);
|
|
5208
5208
|
var src, dest, val, isNoData, j, sample;
|
|
5209
5209
|
for (var y = 0; y < height; y++) {
|
|
5210
5210
|
for (var x = 0; x < width; x++) {
|
|
@@ -5642,7 +5642,7 @@
|
|
|
5642
5642
|
return range;
|
|
5643
5643
|
}
|
|
5644
5644
|
|
|
5645
|
-
function getDisplayRange(scaleRange) {
|
|
5645
|
+
function getDisplayRange$1(scaleRange) {
|
|
5646
5646
|
return [
|
|
5647
5647
|
scaleRange[0] / 100 * 255,
|
|
5648
5648
|
scaleRange[1] / 100 * 255
|
|
@@ -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,17 +16201,23 @@
|
|
|
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};
|
|
15895
16222
|
// The hex value the picker was set to, until it is moved off it. HSB is
|
|
15896
16223
|
// held in bytes, which cannot represent every RGB colour, so a colour that
|
|
@@ -15907,18 +16234,30 @@
|
|
|
15907
16234
|
} else {
|
|
15908
16235
|
closeOpenPickers(self);
|
|
15909
16236
|
colorPicker.show();
|
|
16237
|
+
setPanelRaised(true);
|
|
15910
16238
|
positionPicker();
|
|
15911
16239
|
drawColorPicker();
|
|
16240
|
+
updatePickerFields();
|
|
15912
16241
|
if (openPickers.indexOf(self) == -1) openPickers.push(self);
|
|
15913
16242
|
}
|
|
15914
16243
|
};
|
|
15915
16244
|
|
|
15916
16245
|
this.hide = function() {
|
|
16246
|
+
var wasVisible = colorPicker.visible();
|
|
15917
16247
|
colorPicker.hide();
|
|
16248
|
+
setPanelRaised(false);
|
|
15918
16249
|
var i = openPickers.indexOf(self);
|
|
15919
16250
|
if (i > -1) openPickers.splice(i, 1);
|
|
16251
|
+
if (wasVisible && opts.onHide) opts.onHide();
|
|
15920
16252
|
};
|
|
15921
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
|
+
|
|
15922
16261
|
this.visible = function() {
|
|
15923
16262
|
return colorPicker.visible();
|
|
15924
16263
|
};
|
|
@@ -15960,14 +16299,14 @@
|
|
|
15960
16299
|
var hueWrap = El('div').addClass('label-color-canvas-wrap').appendTo(colorPicker);
|
|
15961
16300
|
hueCanvas = El('canvas').attr('width', '256').attr('height', '18').appendTo(hueWrap);
|
|
15962
16301
|
hueMarker = makePickerMarker().appendTo(hueWrap);
|
|
15963
|
-
renderPresetRows(colorPicker);
|
|
15964
|
-
|
|
15965
|
-
|
|
15966
|
-
|
|
15967
|
-
|
|
15968
|
-
|
|
15969
|
-
El('button').appendTo(
|
|
15970
|
-
|
|
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();
|
|
15971
16310
|
});
|
|
15972
16311
|
sbCanvas.on('mousedown', function(e) {
|
|
15973
16312
|
startCanvasDrag(e, updateSbFromEvent);
|
|
@@ -16002,24 +16341,48 @@
|
|
|
16002
16341
|
commitPickerColor();
|
|
16003
16342
|
}
|
|
16004
16343
|
|
|
16005
|
-
function
|
|
16006
|
-
var
|
|
16007
|
-
|
|
16008
|
-
|
|
16009
|
-
.attr('type', '
|
|
16010
|
-
|
|
16011
|
-
|
|
16012
|
-
|
|
16013
|
-
|
|
16014
|
-
var b = parseNumberField(pickerHsbInputs.b.node().value);
|
|
16015
|
-
if (!isFinite(h) || !isFinite(s) || !isFinite(b)) return;
|
|
16016
|
-
setPickerColor({
|
|
16017
|
-
h: degreesToByte(h),
|
|
16018
|
-
s: pctToByte(s),
|
|
16019
|
-
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();
|
|
16020
16353
|
});
|
|
16021
|
-
|
|
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);
|
|
16022
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();
|
|
16023
16386
|
}
|
|
16024
16387
|
|
|
16025
16388
|
function startCanvasDrag(e, update) {
|
|
@@ -16063,17 +16426,17 @@
|
|
|
16063
16426
|
function getCanvasPoint(canvas, evt) {
|
|
16064
16427
|
var rect = canvas.getBoundingClientRect();
|
|
16065
16428
|
return {
|
|
16066
|
-
x: clamp$
|
|
16067
|
-
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)
|
|
16068
16431
|
};
|
|
16069
16432
|
}
|
|
16070
16433
|
|
|
16071
16434
|
// @hex: the exact colour, when @hsb was made from one
|
|
16072
16435
|
function setPickerColor(hsb, hex) {
|
|
16073
16436
|
pickerColor = {
|
|
16074
|
-
h: clamp$
|
|
16075
|
-
s: clamp$
|
|
16076
|
-
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)
|
|
16077
16440
|
};
|
|
16078
16441
|
pickerHex = hex || null;
|
|
16079
16442
|
drawColorPicker();
|
|
@@ -16082,9 +16445,11 @@
|
|
|
16082
16445
|
}
|
|
16083
16446
|
|
|
16084
16447
|
function updatePickerFields() {
|
|
16085
|
-
|
|
16086
|
-
|
|
16087
|
-
|
|
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
|
+
});
|
|
16088
16453
|
}
|
|
16089
16454
|
|
|
16090
16455
|
function drawColorPicker() {
|
|
@@ -16210,27 +16575,7 @@
|
|
|
16210
16575
|
};
|
|
16211
16576
|
}
|
|
16212
16577
|
|
|
16213
|
-
function
|
|
16214
|
-
return Math.round(val / 255 * 100);
|
|
16215
|
-
}
|
|
16216
|
-
|
|
16217
|
-
function pctToByte(val) {
|
|
16218
|
-
return Math.round(clamp$4(val, 0, 100) / 100 * 255);
|
|
16219
|
-
}
|
|
16220
|
-
|
|
16221
|
-
function byteToDegrees(val) {
|
|
16222
|
-
return Math.round(val / 255 * 360);
|
|
16223
|
-
}
|
|
16224
|
-
|
|
16225
|
-
function degreesToByte(val) {
|
|
16226
|
-
return Math.round(clamp$4(val, 0, 360) / 360 * 255);
|
|
16227
|
-
}
|
|
16228
|
-
|
|
16229
|
-
function parseNumberField(str) {
|
|
16230
|
-
return Number(String(str).replace(/[°%]/g, '').trim());
|
|
16231
|
-
}
|
|
16232
|
-
|
|
16233
|
-
function clamp$4(val, min, max) {
|
|
16578
|
+
function clamp$5(val, min, max) {
|
|
16234
16579
|
return isFinite(val) ? Math.max(min, Math.min(max, val)) : min;
|
|
16235
16580
|
}
|
|
16236
16581
|
|
|
@@ -20681,8 +21026,2011 @@
|
|
|
20681
21026
|
return val === undefined || val === null || String(val).trim() === '';
|
|
20682
21027
|
}
|
|
20683
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
|
+
// range: {start, end, base} or null (presets: the part of the ramp
|
|
21067
|
+
// the tiles are taken from, 0-1
|
|
21068
|
+
// in the preset's own direction;
|
|
21069
|
+
// see getSchemeRange())
|
|
21070
|
+
// pins: [{t, color}] or null, (custom sequential ramps)
|
|
21071
|
+
// vibrance, longHue (custom sequential ramps, see
|
|
21072
|
+
// getOklchInterpolator())
|
|
21073
|
+
// order: [...] or null (categorical presets: indexes
|
|
21074
|
+
// of the preset's colors, as
|
|
21075
|
+
// rearranged)
|
|
21076
|
+
// swatches: [...] or null (custom categorical lists)
|
|
21077
|
+
// continuous: boolean (sequential and diverging)
|
|
21078
|
+
// pivot: 'auto', 'median', 'mean' or a number,
|
|
21079
|
+
// neutral, continuousNeutral: boolean (a pivot class, for classed
|
|
21080
|
+
// and continuous schemes; see
|
|
21081
|
+
// getSchemeNeutral())
|
|
21082
|
+
// split: 'size' or 'count' (diverging: n is the total
|
|
21083
|
+
// number of classes, split so
|
|
21084
|
+
// they're the same size on both
|
|
21085
|
+
// sides, or the number per side)
|
|
21086
|
+
// layout: {pivot, neutral, below, above, breaks} or null
|
|
21087
|
+
// (diverging: the classes the
|
|
21088
|
+
// data gives, see
|
|
21089
|
+
// getDivergingLayout())
|
|
21090
|
+
// nullColor the color of features with no data (both types; not
|
|
21091
|
+
// used by non-adjacent schemes)
|
|
21092
|
+
// colors: [...] the colors that were applied, for checking a layer later
|
|
21093
|
+
// fillHash continuous schemes: a fingerprint of the fills they gave
|
|
21094
|
+
// the layer (interpolated fills aren't the scheme's colors)
|
|
21095
|
+
// }
|
|
21096
|
+
|
|
21097
|
+
var schemeTypes = [
|
|
21098
|
+
{name: 'sequential', label: 'Sequential'},
|
|
21099
|
+
{name: 'diverging', label: 'Diverging'},
|
|
21100
|
+
{name: 'categorical', label: 'Categorical'}
|
|
21101
|
+
];
|
|
21102
|
+
|
|
21103
|
+
var classifyMethods = [
|
|
21104
|
+
{name: 'quantile', label: 'Quantile'},
|
|
21105
|
+
{name: 'equal-interval', label: 'Equal interval'},
|
|
21106
|
+
{name: 'nice', label: 'Nice breaks'},
|
|
21107
|
+
{name: 'hybrid', label: 'Hybrid'}
|
|
21108
|
+
];
|
|
21109
|
+
|
|
21110
|
+
var pivotOptions = [
|
|
21111
|
+
{name: 'auto', label: 'Auto (0 or median)'},
|
|
21112
|
+
{name: 'median', label: 'Median'},
|
|
21113
|
+
{name: 'mean', label: 'Mean'},
|
|
21114
|
+
{name: 'value', label: 'Value'}
|
|
21115
|
+
];
|
|
21116
|
+
|
|
21117
|
+
var divergingSplits = [
|
|
21118
|
+
{name: 'size', label: 'Same class size'},
|
|
21119
|
+
{name: 'count', label: 'Same number of classes'}
|
|
21120
|
+
];
|
|
21121
|
+
|
|
21122
|
+
var categoricalMethods = [
|
|
21123
|
+
{name: 'categorical', label: 'Categories'},
|
|
21124
|
+
{name: 'non-adjacent', label: 'Non-adjacent'}
|
|
21125
|
+
];
|
|
21126
|
+
|
|
21127
|
+
// -classify's default null value for colors, as hex
|
|
21128
|
+
var defaultNullColor = '#eeeeee';
|
|
21129
|
+
|
|
21130
|
+
var minSchemeColors = 2;
|
|
21131
|
+
var maxSchemeColors = 12;
|
|
21132
|
+
var maxCategoricalColors = 20;
|
|
21133
|
+
var defaultCategoricalPreset = 'batlowS';
|
|
21134
|
+
var defaultNonAdjacentColors = 5;
|
|
21135
|
+
var defaultDivergingPreset = 'RdBu';
|
|
21136
|
+
var defaultDivergingClasses = 7;
|
|
21137
|
+
var defaultDivergingSideClasses = 3;
|
|
21138
|
+
var maxDivergingSideClasses = 6;
|
|
21139
|
+
// colors for growing a custom categorical list past the palette it came from
|
|
21140
|
+
var extraSwatchSource = 'Tableau20';
|
|
21141
|
+
// vibrance is OKLCH chroma added to the colors of each interpolated segment
|
|
21142
|
+
// (-classify's vibrance= is the same, scaled to 0-1)
|
|
21143
|
+
var defaultVibrance = 0;
|
|
21144
|
+
var maxVibrance = internal.MAX_VIBRANCE_CHROMA;
|
|
21145
|
+
// the default sequential ramp: cream to blue, through green and teal
|
|
21146
|
+
var defaultEnds = ['#fff8da', '#2d4d8e'];
|
|
21147
|
+
// Cyclic ramps start and end on the same hue, which reads as a category
|
|
21148
|
+
// rather than an order.
|
|
21149
|
+
var excludedPresets = ['Rainbow', 'Sinebow'];
|
|
21150
|
+
|
|
21151
|
+
// Layers keep the scheme that colored them, so that the panel can reopen it
|
|
21152
|
+
// and the style panel can show it. Undo and redo of a scheme edit put back
|
|
21153
|
+
// the scheme that went with the colors (see the panel's command session).
|
|
21154
|
+
var layerSchemes = new WeakMap();
|
|
21155
|
+
|
|
21156
|
+
function getSequentialPresetNames() {
|
|
21157
|
+
return flattenGroups(getPresetGroups('sequential'));
|
|
21158
|
+
}
|
|
21159
|
+
|
|
21160
|
+
function getDivergingPresetNames() {
|
|
21161
|
+
return flattenGroups(getPresetGroups('diverging'));
|
|
21162
|
+
}
|
|
21163
|
+
|
|
21164
|
+
function getCategoricalPresetNames() {
|
|
21165
|
+
return flattenGroups(getPresetGroups('categorical'));
|
|
21166
|
+
}
|
|
21167
|
+
|
|
21168
|
+
// The presets of a scheme type, by source, for the palette menu:
|
|
21169
|
+
// [{source, names}]. Sequential presets include -classify's multi-hue
|
|
21170
|
+
// ('rainbow') schemes.
|
|
21171
|
+
function getPresetGroups(type) {
|
|
21172
|
+
var types = type == 'sequential' ? ['sequential', 'rainbow'] : [type];
|
|
21173
|
+
return internal.getColorSchemeGroups(types).map(function(group) {
|
|
21174
|
+
return {source: group.source, names: group.names.filter(function(name) {
|
|
21175
|
+
return !excludedPresets.includes(name);
|
|
21176
|
+
})};
|
|
21177
|
+
}).filter(function(group) {
|
|
21178
|
+
return group.names.length > 0;
|
|
21179
|
+
});
|
|
21180
|
+
}
|
|
21181
|
+
|
|
21182
|
+
function flattenGroups(groups) {
|
|
21183
|
+
return groups.reduce(function(memo, group) { return memo.concat(group.names); }, []);
|
|
21184
|
+
}
|
|
21185
|
+
|
|
21186
|
+
// The scheme a panel tab starts with on a layer
|
|
21187
|
+
function getDefaultSchemeOfType(type, lyr) {
|
|
21188
|
+
if (type == 'categorical') return getDefaultCategoricalScheme(lyr);
|
|
21189
|
+
if (type == 'diverging') return getDefaultDivergingScheme(lyr);
|
|
21190
|
+
return getDefaultScheme(getNumericFields(lyr)[0]);
|
|
21191
|
+
}
|
|
21192
|
+
|
|
21193
|
+
function getDefaultDivergingScheme(lyr) {
|
|
21194
|
+
return updateDivergingLayout({
|
|
21195
|
+
type: 'diverging',
|
|
21196
|
+
field: getNumericFields(lyr)[0] || null,
|
|
21197
|
+
method: 'quantile',
|
|
21198
|
+
n: defaultDivergingClasses,
|
|
21199
|
+
split: 'size',
|
|
21200
|
+
pivot: 'auto',
|
|
21201
|
+
neutral: true,
|
|
21202
|
+
preset: defaultDivergingPreset,
|
|
21203
|
+
reversed: false,
|
|
21204
|
+
pins: null,
|
|
21205
|
+
vibrance: defaultVibrance
|
|
21206
|
+
}, lyr);
|
|
21207
|
+
}
|
|
21208
|
+
|
|
21209
|
+
// The layout of a diverging scheme's classes on the layer's data, as
|
|
21210
|
+
// -classify finds it; null if there are no data to classify. Done after
|
|
21211
|
+
// every change that can move the classes (the field, method, pivot, pivot
|
|
21212
|
+
// class, number of classes), and when the panel opens, since the data may
|
|
21213
|
+
// have changed.
|
|
21214
|
+
function updateDivergingLayout(scheme, lyr) {
|
|
21215
|
+
if (scheme.type != 'diverging') return scheme;
|
|
21216
|
+
return Object.assign({}, scheme, {layout: getDivergingLayout(scheme, lyr)});
|
|
21217
|
+
}
|
|
21218
|
+
|
|
21219
|
+
function getDivergingLayout(scheme, lyr) {
|
|
21220
|
+
var values = getAscendingValues(lyr, scheme.field);
|
|
21221
|
+
if (values.length === 0) return null;
|
|
21222
|
+
var neutral = getSchemeNeutral(scheme);
|
|
21223
|
+
try {
|
|
21224
|
+
return internal.getDivergingLayout(values, scheme.method, {
|
|
21225
|
+
pivot: scheme.pivot,
|
|
21226
|
+
continuous: !!scheme.continuous,
|
|
21227
|
+
pivot_class: neutral,
|
|
21228
|
+
no_pivot_class: !neutral,
|
|
21229
|
+
classes: getDivergingClassesOption(scheme)
|
|
21230
|
+
});
|
|
21231
|
+
} catch(e) {
|
|
21232
|
+
return null;
|
|
21233
|
+
}
|
|
21234
|
+
}
|
|
21235
|
+
|
|
21236
|
+
// Whether a diverging scheme has a pivot class. Classed and continuous
|
|
21237
|
+
// schemes each keep their own setting, so that switching between them keeps
|
|
21238
|
+
// both: on by default for classes, off for continuous colors.
|
|
21239
|
+
function getSchemeNeutral(scheme) {
|
|
21240
|
+
return scheme.continuous ? !!scheme.continuousNeutral : scheme.neutral !== false;
|
|
21241
|
+
}
|
|
21242
|
+
|
|
21243
|
+
function isContinuousScheme(scheme) {
|
|
21244
|
+
return scheme.type != 'categorical' && !!scheme.continuous;
|
|
21245
|
+
}
|
|
21246
|
+
|
|
21247
|
+
// A sequential ramp starts with at least this many stops, since -classify
|
|
21248
|
+
// interpolates straight between them, so vibrance and the long hue path
|
|
21249
|
+
// show only at the stops.
|
|
21250
|
+
var minContinuousStartColors = 5;
|
|
21251
|
+
|
|
21252
|
+
function setSchemeContinuous(scheme, continuous) {
|
|
21253
|
+
var next = Object.assign({}, scheme, {continuous: !!continuous});
|
|
21254
|
+
if (continuous && next.type == 'sequential' && next.n < minContinuousStartColors) {
|
|
21255
|
+
next.n = minContinuousStartColors;
|
|
21256
|
+
}
|
|
21257
|
+
return next;
|
|
21258
|
+
}
|
|
21259
|
+
|
|
21260
|
+
function getAscendingValues(lyr, field) {
|
|
21261
|
+
if (!lyr || !lyr.data || !field || !lyr.data.fieldExists(field)) return [];
|
|
21262
|
+
return lyr.data.getRecords().map(function(rec) {
|
|
21263
|
+
return rec ? rec[field] : undefined;
|
|
21264
|
+
}).filter(isFiniteNumber).sort(function(a, b) { return a - b; });
|
|
21265
|
+
}
|
|
21266
|
+
|
|
21267
|
+
// -classify's classes= for a diverging scheme
|
|
21268
|
+
function getDivergingClassesOption(scheme) {
|
|
21269
|
+
return scheme.split == 'count' ? [scheme.n, scheme.n] : scheme.n;
|
|
21270
|
+
}
|
|
21271
|
+
|
|
21272
|
+
// The number of tiles of a ramp: for a diverging scheme, the center tile and
|
|
21273
|
+
// enough on each side for the longer side's classes (or a continuous
|
|
21274
|
+
// side's stops)
|
|
21275
|
+
function getRampSize(scheme) {
|
|
21276
|
+
var layout = scheme.layout;
|
|
21277
|
+
var k;
|
|
21278
|
+
if (scheme.type != 'diverging') return scheme.n;
|
|
21279
|
+
k = layout ? Math.max(layout.below, layout.above) :
|
|
21280
|
+
scheme.split == 'count' ? scheme.n : Math.floor(scheme.n / 2);
|
|
21281
|
+
k = scheme.continuous ? k + 1 : Math.max(k, 1);
|
|
21282
|
+
return k * 2 + 1;
|
|
21283
|
+
}
|
|
21284
|
+
|
|
21285
|
+
// The numbers of tiles that each side of a diverging scheme uses
|
|
21286
|
+
function getSideTiles(scheme) {
|
|
21287
|
+
var layout = scheme.layout;
|
|
21288
|
+
if (!scheme.continuous) return [layout.below, layout.above];
|
|
21289
|
+
return [internal.getContinuousSideStops(layout.below), internal.getContinuousSideStops(layout.above)];
|
|
21290
|
+
}
|
|
21291
|
+
|
|
21292
|
+
// One boolean per tile of a diverging scheme: whether a class (or a color
|
|
21293
|
+
// stop) uses it
|
|
21294
|
+
function getDivergingTileUse(scheme) {
|
|
21295
|
+
var size = getRampSize(scheme);
|
|
21296
|
+
var k = (size - 1) / 2;
|
|
21297
|
+
var layout = scheme.layout;
|
|
21298
|
+
var sides = layout ? getSideTiles(scheme) : [0, 0];
|
|
21299
|
+
var use = [];
|
|
21300
|
+
for (var i=0; i<size; i++) {
|
|
21301
|
+
use.push(!!layout && (i < k ? i >= k - sides[0] : i > k ? i <= k + sides[1] : !!layout.neutral));
|
|
21302
|
+
}
|
|
21303
|
+
return use;
|
|
21304
|
+
}
|
|
21305
|
+
|
|
21306
|
+
// The data value at each tile of a continuous scheme: {value, min, max}
|
|
21307
|
+
// (min and max: the value is the data's min or max), or for a diverging
|
|
21308
|
+
// scheme's center tile, the pivot class's range: {range}. Tiles that no stop
|
|
21309
|
+
// uses are null. Returns null if there are no data.
|
|
21310
|
+
function getContinuousTileStops(scheme, lyr) {
|
|
21311
|
+
var values, breaks, layout, stops, k, tiles;
|
|
21312
|
+
if (scheme.type == 'sequential') {
|
|
21313
|
+
values = getAscendingValues(lyr, scheme.field);
|
|
21314
|
+
if (values.length === 0) return null;
|
|
21315
|
+
try {
|
|
21316
|
+
breaks = internal.getSequentialBreaks(values, scheme.method, scheme.n - 2);
|
|
21317
|
+
} catch(e) {
|
|
21318
|
+
return null;
|
|
21319
|
+
}
|
|
21320
|
+
return markDataRange([values[0]].concat(breaks, [values[values.length - 1]]), values);
|
|
21321
|
+
}
|
|
21322
|
+
layout = scheme.layout;
|
|
21323
|
+
if (!layout) return null;
|
|
21324
|
+
stops = internal.getDivergingStops(layout);
|
|
21325
|
+
k = (getRampSize(scheme) - 1) / 2;
|
|
21326
|
+
tiles = new Array(k * 2 + 1).fill(null);
|
|
21327
|
+
markDataRange(stops.below, layout.dataRange).forEach(function(stop, i) {
|
|
21328
|
+
tiles[k - stops.below.length + i] = stop;
|
|
21329
|
+
});
|
|
21330
|
+
if (layout.neutral) tiles[k] = {range: layout.neutral};
|
|
21331
|
+
markDataRange(stops.above, layout.dataRange).forEach(function(stop, i) {
|
|
21332
|
+
tiles[k + 1 + i] = stop;
|
|
21333
|
+
});
|
|
21334
|
+
return tiles;
|
|
21335
|
+
}
|
|
21336
|
+
|
|
21337
|
+
// range: ascending values, of which the first is the min and the last the max
|
|
21338
|
+
function markDataRange(stops, range) {
|
|
21339
|
+
var min = range[0], max = range[range.length - 1];
|
|
21340
|
+
return stops.map(function(val) {
|
|
21341
|
+
return {value: val, min: val == min, max: val == max};
|
|
21342
|
+
});
|
|
21343
|
+
}
|
|
21344
|
+
|
|
21345
|
+
// The gradients to draw over the tiles of a continuous scheme, in order:
|
|
21346
|
+
// [{start, end, flatStart, flatEnd, samples: [{pos, color}]}], with positions
|
|
21347
|
+
// in tiles (0 is the center of the first tile, 1 of the second...). A
|
|
21348
|
+
// gradient runs from stop to stop, and holds its end colors over the outer
|
|
21349
|
+
// halves of its end tiles (flatStart, flatEnd) -- except where the data ends
|
|
21350
|
+
// between two stops (the shorter side of an equal-interval or nice
|
|
21351
|
+
// diverging scheme). A pivot class is a gradient of one color, over its
|
|
21352
|
+
// tile. The samples are of the interpolation -classify does between the
|
|
21353
|
+
// stops (samplesPerTile to each pair).
|
|
21354
|
+
function getContinuousSegments(scheme, samplesPerTile) {
|
|
21355
|
+
var colors = getSchemeColors(scheme);
|
|
21356
|
+
var layout = scheme.layout;
|
|
21357
|
+
var steps = samplesPerTile || 6;
|
|
21358
|
+
var segments = [];
|
|
21359
|
+
var stops, k, first, last;
|
|
21360
|
+
if (!isContinuousScheme(scheme)) return [];
|
|
21361
|
+
if (scheme.type == 'sequential') {
|
|
21362
|
+
return [makeSegment(colors, 0, 0, colors.length - 1, steps)];
|
|
21363
|
+
}
|
|
21364
|
+
if (!layout) return [];
|
|
21365
|
+
stops = internal.getDivergingStops(layout);
|
|
21366
|
+
k = (colors.length - 1) / 2;
|
|
21367
|
+
if (stops.below.length > 0) {
|
|
21368
|
+
first = k - stops.below.length;
|
|
21369
|
+
segments.push(makeSegment(colors.slice(first, k), first,
|
|
21370
|
+
first + internal.getStopPosition(stops.below, layout.dataRange[0]), k - 1, steps));
|
|
21371
|
+
}
|
|
21372
|
+
if (layout.neutral) {
|
|
21373
|
+
segments.push(makeSegment([colors[k]], k, k, k, steps));
|
|
21374
|
+
}
|
|
21375
|
+
if (stops.above.length > 0) {
|
|
21376
|
+
last = k + stops.above.length;
|
|
21377
|
+
segments.push(makeSegment(colors.slice(k + 1, last + 1), k + 1,
|
|
21378
|
+
k + 1, k + 1 + internal.getStopPosition(stops.above, layout.dataRange[1]), steps));
|
|
21379
|
+
}
|
|
21380
|
+
return segments;
|
|
21381
|
+
}
|
|
21382
|
+
|
|
21383
|
+
// colors: the stops' colors, the first at tile first
|
|
21384
|
+
// start, end: where the gradient starts and ends, in tiles
|
|
21385
|
+
function makeSegment(colors, first, start, end, steps) {
|
|
21386
|
+
var last = first + colors.length - 1;
|
|
21387
|
+
var getColor = colors.length > 1 ?
|
|
21388
|
+
internal.getInterpolatedValueGetter(colors, null, {interpolation: 'oklch'}) : null;
|
|
21389
|
+
var samples = [];
|
|
21390
|
+
var sample = function(pos) {
|
|
21391
|
+
samples.push({pos: pos, color: getColor ? toHex(getColor(pos - first)) : colors[0]});
|
|
21392
|
+
};
|
|
21393
|
+
sample(start);
|
|
21394
|
+
for (var i=Math.floor(start * steps) + 1; i / steps < end; i++) {
|
|
21395
|
+
sample(i / steps);
|
|
21396
|
+
}
|
|
21397
|
+
if (end > start) sample(end);
|
|
21398
|
+
return {start: start, end: end, flatStart: start == first, flatEnd: end == last, samples: samples};
|
|
21399
|
+
}
|
|
21400
|
+
|
|
21401
|
+
// The data range of each class of a diverging layout, in class order:
|
|
21402
|
+
// [low, high), with -Infinity and Infinity at the outer ends
|
|
21403
|
+
function getDivergingClassRanges(layout) {
|
|
21404
|
+
return getBreakRanges(layout ? layout.breaks : []);
|
|
21405
|
+
}
|
|
21406
|
+
|
|
21407
|
+
// The data range of each class of a sequential scheme, as -classify finds
|
|
21408
|
+
// them (see getDivergingClassRanges()), or null if there are no data
|
|
21409
|
+
function getSequentialClassRanges(scheme, lyr) {
|
|
21410
|
+
var values = getAscendingValues(lyr, scheme.field);
|
|
21411
|
+
if (values.length === 0) return null;
|
|
21412
|
+
try {
|
|
21413
|
+
return getBreakRanges(internal.getSequentialBreaks(values, scheme.method, scheme.n - 1));
|
|
21414
|
+
} catch(e) {
|
|
21415
|
+
return null;
|
|
21416
|
+
}
|
|
21417
|
+
}
|
|
21418
|
+
|
|
21419
|
+
function getBreakRanges(breaks) {
|
|
21420
|
+
var ranges = [];
|
|
21421
|
+
for (var i=0; i<=breaks.length; i++) {
|
|
21422
|
+
ranges.push([i > 0 ? breaks[i - 1] : -Infinity, i < breaks.length ? breaks[i] : Infinity]);
|
|
21423
|
+
}
|
|
21424
|
+
return ranges;
|
|
21425
|
+
}
|
|
21426
|
+
|
|
21427
|
+
// How the data falls around the pivot of a diverging scheme:
|
|
21428
|
+
// {pivot, below, above} (features below the pivot, and at or above it)
|
|
21429
|
+
function getPivotSummary(scheme, lyr) {
|
|
21430
|
+
var values = getAscendingValues(lyr, scheme.field);
|
|
21431
|
+
var pivot = scheme.layout ? scheme.layout.pivot : null;
|
|
21432
|
+
if (pivot === null) return null;
|
|
21433
|
+
return {
|
|
21434
|
+
pivot: pivot,
|
|
21435
|
+
below: values.filter(function(val) { return val < pivot; }).length,
|
|
21436
|
+
above: values.filter(function(val) { return val >= pivot; }).length
|
|
21437
|
+
};
|
|
21438
|
+
}
|
|
21439
|
+
|
|
21440
|
+
// pivot: 'auto', 'median', 'mean' or a number
|
|
21441
|
+
function setSchemePivot(scheme, pivot) {
|
|
21442
|
+
return Object.assign({}, scheme, {pivot: pivot});
|
|
21443
|
+
}
|
|
21444
|
+
|
|
21445
|
+
// sets the pivot class of the scheme's mode (see getSchemeNeutral())
|
|
21446
|
+
function setSchemeNeutral(scheme, neutral) {
|
|
21447
|
+
return Object.assign({}, scheme, scheme.continuous ? {continuousNeutral: !!neutral} : {neutral: !!neutral});
|
|
21448
|
+
}
|
|
21449
|
+
|
|
21450
|
+
// Switching between a total and a number per side starts over with the
|
|
21451
|
+
// default number
|
|
21452
|
+
function setSchemeSplit(scheme, split) {
|
|
21453
|
+
if (scheme.split == split) return scheme;
|
|
21454
|
+
return Object.assign({}, scheme, {
|
|
21455
|
+
split: split,
|
|
21456
|
+
n: split == 'count' ? defaultDivergingSideClasses : defaultDivergingClasses
|
|
21457
|
+
});
|
|
21458
|
+
}
|
|
21459
|
+
|
|
21460
|
+
// By the first text field, or else the first field that can be classified
|
|
21461
|
+
// by value, or else non-adjacent
|
|
21462
|
+
function getDefaultCategoricalScheme(lyr) {
|
|
21463
|
+
var fields = getCategoryFields(lyr);
|
|
21464
|
+
var records = lyr && lyr.data ? lyr.data.getRecords() : [];
|
|
21465
|
+
var field = fields.find(function(name) {
|
|
21466
|
+
return internal.getColumnType(name, records) == 'string';
|
|
21467
|
+
}) || fields[0] || null;
|
|
21468
|
+
var scheme = {
|
|
21469
|
+
type: 'categorical',
|
|
21470
|
+
field: field,
|
|
21471
|
+
method: field ? 'categorical' : 'non-adjacent',
|
|
21472
|
+
n: defaultNonAdjacentColors,
|
|
21473
|
+
preset: defaultCategoricalPreset,
|
|
21474
|
+
order: null,
|
|
21475
|
+
swatches: null
|
|
21476
|
+
};
|
|
21477
|
+
return field ? setSchemeField(scheme, field, getCategories(lyr, field).length) : scheme;
|
|
21478
|
+
}
|
|
21479
|
+
|
|
21480
|
+
function getMinSchemeColors(scheme) {
|
|
21481
|
+
return scheme.type == 'diverging' && scheme.split == 'count' ? 1 : minSchemeColors;
|
|
21482
|
+
}
|
|
21483
|
+
|
|
21484
|
+
function getSchemeMethods(scheme) {
|
|
21485
|
+
return scheme.type == 'categorical' ? categoricalMethods : classifyMethods;
|
|
21486
|
+
}
|
|
21487
|
+
|
|
21488
|
+
// count: the number of categories, for the categorical method
|
|
21489
|
+
function getMaxSchemeColors(scheme, count) {
|
|
21490
|
+
var max = maxSchemeColors;
|
|
21491
|
+
if (scheme.type == 'diverging' && scheme.split == 'count') return maxDivergingSideClasses;
|
|
21492
|
+
if (scheme.type != 'categorical') return max;
|
|
21493
|
+
max = scheme.preset ? internal.getCategoricalColors(scheme.preset).length : maxCategoricalColors;
|
|
21494
|
+
if (scheme.method == 'categorical' && count >= 0) {
|
|
21495
|
+
max = Math.min(max, Math.max(count, minSchemeColors));
|
|
21496
|
+
}
|
|
21497
|
+
return max;
|
|
21498
|
+
}
|
|
21499
|
+
|
|
21500
|
+
function clampColorCount(scheme, n, count) {
|
|
21501
|
+
var min = getMinSchemeColors(scheme);
|
|
21502
|
+
return clamp$4(Math.round(n) || min, min, getMaxSchemeColors(scheme, count));
|
|
21503
|
+
}
|
|
21504
|
+
|
|
21505
|
+
// A new field for a categorical scheme gets a swatch for each of its values,
|
|
21506
|
+
// up to as many as the palette has.
|
|
21507
|
+
// count: the number of categories in the field
|
|
21508
|
+
function setSchemeField(scheme, field, count) {
|
|
21509
|
+
var next = Object.assign({}, scheme, {field: field});
|
|
21510
|
+
if (next.type == 'categorical' && next.method == 'categorical') {
|
|
21511
|
+
next.n = clampColorCount(next, count, count);
|
|
21512
|
+
}
|
|
21513
|
+
return next;
|
|
21514
|
+
}
|
|
21515
|
+
|
|
21516
|
+
// field, count: the field to use, and its number of categories, when a
|
|
21517
|
+
// categorical scheme changes to the categorical method
|
|
21518
|
+
function setSchemeMethod(scheme, method, field, count) {
|
|
21519
|
+
var next = Object.assign({}, scheme, {method: method});
|
|
21520
|
+
if (next.type != 'categorical') return next;
|
|
21521
|
+
if (method == 'categorical') {
|
|
21522
|
+
return setSchemeField(next, next.field || field, count);
|
|
21523
|
+
}
|
|
21524
|
+
return setTileCount(next, defaultNonAdjacentColors);
|
|
21525
|
+
}
|
|
21526
|
+
|
|
21527
|
+
// Fields that a categorical scheme can classify: text and numbers. The
|
|
21528
|
+
// fill field is left out, since the scheme writes over it.
|
|
21529
|
+
function getCategoryFields(lyr) {
|
|
21530
|
+
var records = lyr && lyr.data ? lyr.data.getRecords() : [];
|
|
21531
|
+
if (!lyr || !lyr.data) return [];
|
|
21532
|
+
return lyr.data.getFields().filter(function(field) {
|
|
21533
|
+
var type = internal.getColumnType(field, records);
|
|
21534
|
+
return field != 'fill' && (type == 'string' || type == 'number');
|
|
21535
|
+
});
|
|
21536
|
+
}
|
|
21537
|
+
|
|
21538
|
+
// The values of a field, in the order -classify gives them swatches. Empty
|
|
21539
|
+
// values are left out, as -classify leaves them out: they are no data.
|
|
21540
|
+
function getCategories(lyr, field) {
|
|
21541
|
+
if (!lyr || !lyr.data || !field || !lyr.data.fieldExists(field)) return [];
|
|
21542
|
+
return internal.getUniqFieldValues(lyr.data.getRecords(), field).filter(isCategoryValue);
|
|
21543
|
+
}
|
|
21544
|
+
|
|
21545
|
+
// The same test as -classify's (0 is a value; null, undefined, '' and NaN
|
|
21546
|
+
// are not)
|
|
21547
|
+
function isCategoryValue(val) {
|
|
21548
|
+
return !!val || val === 0;
|
|
21549
|
+
}
|
|
21550
|
+
|
|
21551
|
+
// How many features the scheme gives the no-data color
|
|
21552
|
+
function getNoDataCount(lyr, scheme) {
|
|
21553
|
+
var records, test;
|
|
21554
|
+
if (!lyr || !lyr.data || !scheme.field || scheme.method == 'non-adjacent' ||
|
|
21555
|
+
!lyr.data.fieldExists(scheme.field)) return 0;
|
|
21556
|
+
records = lyr.data.getRecords();
|
|
21557
|
+
test = scheme.type == 'categorical' ? isCategoryValue : isFiniteNumber;
|
|
21558
|
+
return records.filter(function(rec) {
|
|
21559
|
+
return !test(rec ? rec[scheme.field] : undefined);
|
|
21560
|
+
}).length;
|
|
21561
|
+
}
|
|
21562
|
+
|
|
21563
|
+
function isFiniteNumber(val) {
|
|
21564
|
+
return typeof val == 'number' && isFinite(val);
|
|
21565
|
+
}
|
|
21566
|
+
|
|
21567
|
+
function getSchemeNullColor(scheme) {
|
|
21568
|
+
return scheme.nullColor || defaultNullColor;
|
|
21569
|
+
}
|
|
21570
|
+
|
|
21571
|
+
// An empty or unreadable color is the default
|
|
21572
|
+
function setSchemeNullColor(scheme, color) {
|
|
21573
|
+
var rgb = color ? internal.parseColor(color) : null;
|
|
21574
|
+
return Object.assign({}, scheme, {nullColor: rgb ? internal.formatColor(rgb) : defaultNullColor});
|
|
21575
|
+
}
|
|
21576
|
+
|
|
21577
|
+
// The categories each swatch colors, as -classify assigns them
|
|
21578
|
+
function getSwatchCategories(categories, n) {
|
|
21579
|
+
var groups = [];
|
|
21580
|
+
for (var i=0; i<n; i++) groups.push([]);
|
|
21581
|
+
categories.forEach(function(val, i) {
|
|
21582
|
+
groups[i % n].push(val);
|
|
21583
|
+
});
|
|
21584
|
+
return groups;
|
|
21585
|
+
}
|
|
21586
|
+
|
|
21587
|
+
function getDefaultScheme(field) {
|
|
21588
|
+
return {
|
|
21589
|
+
type: 'sequential',
|
|
21590
|
+
field: field || null,
|
|
21591
|
+
method: 'quantile',
|
|
21592
|
+
n: 5,
|
|
21593
|
+
preset: null,
|
|
21594
|
+
reversed: false,
|
|
21595
|
+
pins: [{t: 0, color: defaultEnds[0]}, {t: 1, color: defaultEnds[1]}],
|
|
21596
|
+
vibrance: defaultVibrance
|
|
21597
|
+
};
|
|
21598
|
+
}
|
|
21599
|
+
|
|
21600
|
+
function getSchemeColors(scheme) {
|
|
21601
|
+
return getSchemeTiles(scheme).map(function(tile) {
|
|
21602
|
+
return tile.color;
|
|
21603
|
+
});
|
|
21604
|
+
}
|
|
21605
|
+
|
|
21606
|
+
// One object per tile: {color, pinned, adjusted, ...}, where adjusted means
|
|
21607
|
+
// an interpolated color was moved to fit the sRGB gamut (see resolveRampTiles())
|
|
21608
|
+
function getSchemeTiles(scheme) {
|
|
21609
|
+
var colors;
|
|
21610
|
+
if (scheme.type == 'categorical') {
|
|
21611
|
+
return getCategoricalPalette(scheme).slice(0, scheme.n).map(function(color) {
|
|
21612
|
+
return {color: color, pinned: false, adjusted: false};
|
|
21613
|
+
});
|
|
21614
|
+
}
|
|
21615
|
+
if (scheme.preset) {
|
|
21616
|
+
colors = scheme.range ? getPresetSection(scheme, getRampSize(scheme), scheme.range) :
|
|
21617
|
+
getPresetColors(scheme.preset, getRampSize(scheme));
|
|
21618
|
+
if (scheme.reversed) colors.reverse();
|
|
21619
|
+
return colors.map(function(color) {
|
|
21620
|
+
return {color: color, pinned: false, adjusted: false};
|
|
21621
|
+
});
|
|
21622
|
+
}
|
|
21623
|
+
return internal.resolveRampTiles(scheme.pins, getRampSize(scheme), {
|
|
21624
|
+
vibrance: getSchemeVibrance(scheme),
|
|
21625
|
+
hue: scheme.longHue && scheme.type != 'diverging' ? 'longer' : 'shorter'
|
|
21626
|
+
});
|
|
21627
|
+
}
|
|
21628
|
+
|
|
21629
|
+
function getSchemeVibrance(scheme) {
|
|
21630
|
+
return scheme.vibrance >= 0 ? scheme.vibrance : defaultVibrance;
|
|
21631
|
+
}
|
|
21632
|
+
|
|
21633
|
+
function setSchemeVibrance(scheme, vibrance) {
|
|
21634
|
+
vibrance = clamp$4(+vibrance || 0, 0, maxVibrance);
|
|
21635
|
+
return Object.assign({}, scheme, {vibrance: vibrance});
|
|
21636
|
+
}
|
|
21637
|
+
|
|
21638
|
+
// longHue: hues go the long way around the color wheel between pinned colors
|
|
21639
|
+
function setSchemeLongHue(scheme, longHue) {
|
|
21640
|
+
return Object.assign({}, scheme, {longHue: !!longHue});
|
|
21641
|
+
}
|
|
21642
|
+
|
|
21643
|
+
function clamp$4(val, min, max) {
|
|
21644
|
+
return Math.min(Math.max(val, min), max);
|
|
21645
|
+
}
|
|
21646
|
+
|
|
21647
|
+
function getPresetColors(name, n) {
|
|
21648
|
+
return internal.getColorRamp(name, n).map(toHex);
|
|
21649
|
+
}
|
|
21650
|
+
|
|
21651
|
+
// The part of a sequential or diverging preset that its tiles are taken
|
|
21652
|
+
// from: {start, end, base}, 0-1 in the preset's own direction (before
|
|
21653
|
+
// reversing). A diverging range is the same on both sides of the center.
|
|
21654
|
+
// base is the number of tiles when the range was first narrowed: a preset
|
|
21655
|
+
// with a hand-picked set of that size (ColorBrewer's) is interpolated
|
|
21656
|
+
// between the set's colors, so that removing an end tile leaves the others
|
|
21657
|
+
// as they were.
|
|
21658
|
+
var minRangeSpan = 0.1;
|
|
21659
|
+
|
|
21660
|
+
function getSchemeRange(scheme) {
|
|
21661
|
+
return scheme.range || {start: 0, end: 1, base: getRampSize(scheme)};
|
|
21662
|
+
}
|
|
21663
|
+
|
|
21664
|
+
function hasSchemeRange(scheme) {
|
|
21665
|
+
return !!scheme.preset && scheme.type != 'categorical';
|
|
21666
|
+
}
|
|
21667
|
+
|
|
21668
|
+
// The range as positions along the tiles, left to right: [left, right]
|
|
21669
|
+
function getDisplayRange(scheme) {
|
|
21670
|
+
var range = getSchemeRange(scheme);
|
|
21671
|
+
return scheme.reversed ? [1 - range.end, 1 - range.start] : [range.start, range.end];
|
|
21672
|
+
}
|
|
21673
|
+
|
|
21674
|
+
// Moves one end of the range, given as a position along the tiles (0-1,
|
|
21675
|
+
// left to right); a diverging range moves at both ends.
|
|
21676
|
+
// side: 'left' or 'right'
|
|
21677
|
+
function setSchemeRangeEnd(scheme, side, pos) {
|
|
21678
|
+
var display = getDisplayRange(scheme);
|
|
21679
|
+
var left = display[0], right = display[1];
|
|
21680
|
+
pos = clamp$4(+pos || 0, 0, 1);
|
|
21681
|
+
if (scheme.type == 'diverging') {
|
|
21682
|
+
left = side == 'left' ? pos : 1 - pos;
|
|
21683
|
+
left = clamp$4(left, 0, (1 - minRangeSpan) / 2);
|
|
21684
|
+
right = 1 - left;
|
|
21685
|
+
} else if (side == 'left') {
|
|
21686
|
+
left = Math.min(pos, right - minRangeSpan);
|
|
21687
|
+
} else {
|
|
21688
|
+
right = Math.max(pos, left + minRangeSpan);
|
|
21689
|
+
}
|
|
21690
|
+
return setDisplayRange(scheme, left, right);
|
|
21691
|
+
}
|
|
21692
|
+
|
|
21693
|
+
function resetSchemeRange(scheme) {
|
|
21694
|
+
return Object.assign({}, scheme, {range: null});
|
|
21695
|
+
}
|
|
21696
|
+
|
|
21697
|
+
// Takes the tile at one end of a sequential preset away, keeping the colors
|
|
21698
|
+
// of the others: the range ends where the next tile was.
|
|
21699
|
+
// side: 'left' or 'right'
|
|
21700
|
+
function removeEndTile(scheme, side) {
|
|
21701
|
+
var display, step;
|
|
21702
|
+
if (!canRemoveEndTile(scheme)) return scheme;
|
|
21703
|
+
display = getDisplayRange(scheme);
|
|
21704
|
+
step = (display[1] - display[0]) / (scheme.n - 1);
|
|
21705
|
+
if (side == 'left') display[0] += step;
|
|
21706
|
+
else display[1] -= step;
|
|
21707
|
+
return Object.assign(setDisplayRange(scheme, display[0], display[1]), {n: scheme.n - 1});
|
|
21708
|
+
}
|
|
21709
|
+
|
|
21710
|
+
function canRemoveEndTile(scheme) {
|
|
21711
|
+
return hasSchemeRange(scheme) && scheme.type == 'sequential' &&
|
|
21712
|
+
scheme.n > getMinSchemeColors(scheme);
|
|
21713
|
+
}
|
|
21714
|
+
|
|
21715
|
+
function setDisplayRange(scheme, left, right) {
|
|
21716
|
+
var base = getSchemeRange(scheme).base;
|
|
21717
|
+
var start = scheme.reversed ? 1 - right : left;
|
|
21718
|
+
var end = scheme.reversed ? 1 - left : right;
|
|
21719
|
+
if (start <= 0 && end >= 1) return resetSchemeRange(scheme);
|
|
21720
|
+
return Object.assign({}, scheme, {range: {start: Math.max(start, 0), end: Math.min(end, 1), base: base}});
|
|
21721
|
+
}
|
|
21722
|
+
|
|
21723
|
+
// The whole of a preset, left to right, as the range's strip shows it
|
|
21724
|
+
function getRangeStripColors(scheme, n) {
|
|
21725
|
+
var colors = getPresetSection(scheme, n, {start: 0, end: 1, base: getSchemeRange(scheme).base});
|
|
21726
|
+
return scheme.reversed ? colors.reverse() : colors;
|
|
21727
|
+
}
|
|
21728
|
+
|
|
21729
|
+
function getPresetSection(scheme, n, range) {
|
|
21730
|
+
return internal.getColorRampSection(scheme.preset, n, range.start, range.end, range.base).map(toHex);
|
|
21731
|
+
}
|
|
21732
|
+
|
|
21733
|
+
// All the swatches of a categorical scheme, in order: the first n are used
|
|
21734
|
+
function getCategoricalPalette(scheme) {
|
|
21735
|
+
var colors;
|
|
21736
|
+
if (!scheme.preset) return scheme.swatches.concat();
|
|
21737
|
+
colors = getCategoricalPresetColors(scheme.preset);
|
|
21738
|
+
if (!scheme.order) return colors;
|
|
21739
|
+
return scheme.order.map(function(i) { return colors[i]; });
|
|
21740
|
+
}
|
|
21741
|
+
|
|
21742
|
+
function getCategoricalPresetColors(name) {
|
|
21743
|
+
return internal.getCategoricalColors(name).map(toHex);
|
|
21744
|
+
}
|
|
21745
|
+
|
|
21746
|
+
// A list of n colors, starting with the given ones and going on with colors
|
|
21747
|
+
// they don't have yet
|
|
21748
|
+
function extendSwatches(colors, n) {
|
|
21749
|
+
var swatches = colors.slice(0, n);
|
|
21750
|
+
getCategoricalPresetColors(extraSwatchSource).forEach(function(color) {
|
|
21751
|
+
if (swatches.length < n && !swatches.includes(color)) {
|
|
21752
|
+
swatches.push(color);
|
|
21753
|
+
}
|
|
21754
|
+
});
|
|
21755
|
+
return internal.wrapColors(swatches, n);
|
|
21756
|
+
}
|
|
21757
|
+
|
|
21758
|
+
// One boolean per tile: whether the user set its color.
|
|
21759
|
+
function getPinnedTiles(scheme) {
|
|
21760
|
+
if (scheme.preset || scheme.type == 'categorical') {
|
|
21761
|
+
return new Array(getRampSize(scheme)).fill(false);
|
|
21762
|
+
}
|
|
21763
|
+
return internal.getPinnedSlots(scheme.pins, getRampSize(scheme)).map(function(pinId) {
|
|
21764
|
+
return pinId > -1;
|
|
21765
|
+
});
|
|
21766
|
+
}
|
|
21767
|
+
|
|
21768
|
+
// count: the number of categories, for a categorical scheme
|
|
21769
|
+
function choosePreset(scheme, name, count) {
|
|
21770
|
+
var next;
|
|
21771
|
+
if (scheme.type == 'categorical') {
|
|
21772
|
+
next = Object.assign({}, scheme, {preset: name, order: null, swatches: null});
|
|
21773
|
+
next.n = clampColorCount(next, next.n, count);
|
|
21774
|
+
return next;
|
|
21775
|
+
}
|
|
21776
|
+
return Object.assign({}, scheme, {preset: name, reversed: false, pins: null, range: null});
|
|
21777
|
+
}
|
|
21778
|
+
|
|
21779
|
+
// A preset as a custom ramp, pinned at its ends, for editing.
|
|
21780
|
+
function makeSchemeCustom(scheme) {
|
|
21781
|
+
var colors;
|
|
21782
|
+
if (!scheme.preset) return scheme;
|
|
21783
|
+
if (scheme.type == 'categorical') {
|
|
21784
|
+
return Object.assign({}, scheme, {preset: null, order: null, swatches: getCategoricalPalette(scheme)});
|
|
21785
|
+
}
|
|
21786
|
+
colors = getSchemeColors(scheme);
|
|
21787
|
+
return Object.assign({}, scheme, {
|
|
21788
|
+
preset: null,
|
|
21789
|
+
reversed: false,
|
|
21790
|
+
range: null,
|
|
21791
|
+
pins: scheme.type == 'diverging' ?
|
|
21792
|
+
[{t: 0, color: colors[0]}, {t: 0.5, color: colors[(colors.length - 1) / 2]},
|
|
21793
|
+
{t: 1, color: colors[colors.length - 1]}] :
|
|
21794
|
+
[{t: 0, color: colors[0]}, {t: 1, color: colors[colors.length - 1]}]
|
|
21795
|
+
});
|
|
21796
|
+
}
|
|
21797
|
+
|
|
21798
|
+
// The center tile of a diverging ramp, which stays pinned
|
|
21799
|
+
function getCenterTile(scheme) {
|
|
21800
|
+
return scheme.type == 'diverging' ? (getRampSize(scheme) - 1) / 2 : -1;
|
|
21801
|
+
}
|
|
21802
|
+
|
|
21803
|
+
// The color to edit for tile i: a pinned tile's own color, which vibrance
|
|
21804
|
+
// may have raised in the ramp, or else the tile's color
|
|
21805
|
+
function getTileEditColor(scheme, i) {
|
|
21806
|
+
var slot = scheme.preset || scheme.type == 'categorical' ? -1 :
|
|
21807
|
+
internal.getPinnedSlots(scheme.pins, getRampSize(scheme))[i];
|
|
21808
|
+
if (slot > -1) return toHex(scheme.pins[slot].color);
|
|
21809
|
+
return getSchemeColors(scheme)[i];
|
|
21810
|
+
}
|
|
21811
|
+
|
|
21812
|
+
function setTileColor(scheme, i, color) {
|
|
21813
|
+
var custom = makeSchemeCustom(scheme);
|
|
21814
|
+
var swatches;
|
|
21815
|
+
if (custom.type == 'categorical') {
|
|
21816
|
+
swatches = custom.swatches.concat();
|
|
21817
|
+
swatches[i] = toHex(color);
|
|
21818
|
+
return Object.assign({}, custom, {swatches: swatches});
|
|
21819
|
+
}
|
|
21820
|
+
return Object.assign({}, custom, {
|
|
21821
|
+
pins: internal.setRampPin(custom.pins, getRampSize(custom), i, toHex(color))
|
|
21822
|
+
});
|
|
21823
|
+
}
|
|
21824
|
+
|
|
21825
|
+
function clearTileColor(scheme, i) {
|
|
21826
|
+
if (scheme.preset || scheme.type == 'categorical' || i == getCenterTile(scheme)) return scheme;
|
|
21827
|
+
return Object.assign({}, scheme, {
|
|
21828
|
+
pins: internal.clearRampPin(scheme.pins, getRampSize(scheme), i)
|
|
21829
|
+
});
|
|
21830
|
+
}
|
|
21831
|
+
|
|
21832
|
+
// A custom categorical list grows to have at least n swatches.
|
|
21833
|
+
// count: the number of categories, for a categorical scheme
|
|
21834
|
+
function setTileCount(scheme, n, count) {
|
|
21835
|
+
var next = Object.assign({}, scheme, {n: clampColorCount(scheme, n, count)});
|
|
21836
|
+
if (next.type == 'categorical' && !next.preset && next.swatches.length < next.n) {
|
|
21837
|
+
next.swatches = extendSwatches(next.swatches, next.n);
|
|
21838
|
+
}
|
|
21839
|
+
return next;
|
|
21840
|
+
}
|
|
21841
|
+
|
|
21842
|
+
// Moves a categorical swatch to a new place in the palette. Moving one
|
|
21843
|
+
// into the swatches in use pushes the last of them out of use, and moving one
|
|
21844
|
+
// out of use brings the first unused one in.
|
|
21845
|
+
// to: the swatch's index after the move
|
|
21846
|
+
function moveSwatch(scheme, from, to) {
|
|
21847
|
+
return rearrangeSwatches(scheme, function(items) {
|
|
21848
|
+
var item = items.splice(from, 1)[0];
|
|
21849
|
+
items.splice(to, 0, item);
|
|
21850
|
+
return items;
|
|
21851
|
+
});
|
|
21852
|
+
}
|
|
21853
|
+
|
|
21854
|
+
// Shuffles all of a categorical palette's swatches, in use or not, into a
|
|
21855
|
+
// new order.
|
|
21856
|
+
// random: a function like Math.random (for testing)
|
|
21857
|
+
function shuffleScheme(scheme, random) {
|
|
21858
|
+
var rand = random || Math.random;
|
|
21859
|
+
return rearrangeSwatches(scheme, function(items) {
|
|
21860
|
+
var shuffled, tries = 0;
|
|
21861
|
+
do {
|
|
21862
|
+
shuffled = shuffle(items.concat(), rand);
|
|
21863
|
+
} while (items.length > 1 && ++tries < 10 && shuffled.every(function(item, i) {
|
|
21864
|
+
return item == items[i];
|
|
21865
|
+
}));
|
|
21866
|
+
return shuffled;
|
|
21867
|
+
});
|
|
21868
|
+
}
|
|
21869
|
+
|
|
21870
|
+
function shuffle(arr, rand) {
|
|
21871
|
+
var j, tmp;
|
|
21872
|
+
for (var i=arr.length - 1; i>0; i--) {
|
|
21873
|
+
j = Math.floor(rand() * (i + 1));
|
|
21874
|
+
tmp = arr[i];
|
|
21875
|
+
arr[i] = arr[j];
|
|
21876
|
+
arr[j] = tmp;
|
|
21877
|
+
}
|
|
21878
|
+
return arr;
|
|
21879
|
+
}
|
|
21880
|
+
|
|
21881
|
+
// Presets are rearranged by index, so that they keep their name
|
|
21882
|
+
function rearrangeSwatches(scheme, rearrange) {
|
|
21883
|
+
var order;
|
|
21884
|
+
if (scheme.type != 'categorical') return scheme;
|
|
21885
|
+
if (!scheme.preset) {
|
|
21886
|
+
return Object.assign({}, scheme, {swatches: rearrange(scheme.swatches.concat())});
|
|
21887
|
+
}
|
|
21888
|
+
order = scheme.order || getCategoricalPresetColors(scheme.preset).map(function(c, i) { return i; });
|
|
21889
|
+
return Object.assign({}, scheme, {order: rearrange(order.concat())});
|
|
21890
|
+
}
|
|
21891
|
+
|
|
21892
|
+
function reverseScheme(scheme) {
|
|
21893
|
+
if (scheme.type == 'categorical') return scheme;
|
|
21894
|
+
if (scheme.preset) {
|
|
21895
|
+
return Object.assign({}, scheme, {reversed: !scheme.reversed});
|
|
21896
|
+
}
|
|
21897
|
+
return Object.assign({}, scheme, {
|
|
21898
|
+
pins: scheme.pins.map(function(pin) {
|
|
21899
|
+
return {t: 1 - pin.t, color: pin.color};
|
|
21900
|
+
}).reverse()
|
|
21901
|
+
});
|
|
21902
|
+
}
|
|
21903
|
+
|
|
21904
|
+
// The colors to give -classify: no more than there are categories, for the
|
|
21905
|
+
// categorical method. A diverging scheme gives all its tiles, from which
|
|
21906
|
+
// -classify takes the same ones the classes use here (see
|
|
21907
|
+
// getDivergingClassValues()).
|
|
21908
|
+
// count: the number of categories
|
|
21909
|
+
function getAppliedColors(scheme, count) {
|
|
21910
|
+
var colors = getSchemeColors(scheme);
|
|
21911
|
+
if (scheme.type == 'categorical' && scheme.method == 'categorical' && count > 0) {
|
|
21912
|
+
colors = colors.slice(0, count);
|
|
21913
|
+
}
|
|
21914
|
+
return colors;
|
|
21915
|
+
}
|
|
21916
|
+
|
|
21917
|
+
// opts.target a -target value, when the layer is not the active one
|
|
21918
|
+
function formatSchemeCommand(scheme, colors, opts) {
|
|
21919
|
+
var parts = ['-classify'];
|
|
21920
|
+
if (scheme.method != 'non-adjacent') {
|
|
21921
|
+
parts.push('field=' + quoteCommandValue(scheme.field));
|
|
21922
|
+
}
|
|
21923
|
+
parts.push('method=' + scheme.method);
|
|
21924
|
+
if (scheme.type == 'diverging') {
|
|
21925
|
+
parts.push('pivot=' + scheme.pivot, 'classes=' + getDivergingClassesOption(scheme));
|
|
21926
|
+
// continuous output has no pivot class by default, classes have one
|
|
21927
|
+
if (!getSchemeNeutral(scheme)) parts.push('no-pivot-class');
|
|
21928
|
+
else if (scheme.continuous) parts.push('pivot-class');
|
|
21929
|
+
} else if (isContinuousScheme(scheme)) {
|
|
21930
|
+
parts.push('classes=' + (scheme.n - 1));
|
|
21931
|
+
}
|
|
21932
|
+
if (isContinuousScheme(scheme)) {
|
|
21933
|
+
// the tiles already have their vibrance, so no vibrance=
|
|
21934
|
+
parts.push('continuous', 'interpolation=oklch');
|
|
21935
|
+
}
|
|
21936
|
+
parts.push('colors=' + colors.join(','));
|
|
21937
|
+
if (scheme.method != 'non-adjacent' && getSchemeNullColor(scheme) != defaultNullColor) {
|
|
21938
|
+
parts.push('null-value=' + getSchemeNullColor(scheme));
|
|
21939
|
+
}
|
|
21940
|
+
if (opts && opts.target) {
|
|
21941
|
+
parts.push('target=' + opts.target);
|
|
21942
|
+
}
|
|
21943
|
+
return parts.join(' ');
|
|
21944
|
+
}
|
|
21945
|
+
|
|
21946
|
+
function getNumericFields(lyr) {
|
|
21947
|
+
var records = lyr && lyr.data ? lyr.data.getRecords() : [];
|
|
21948
|
+
if (!lyr || !lyr.data) return [];
|
|
21949
|
+
return lyr.data.getFields().filter(function(field) {
|
|
21950
|
+
return internal.getColumnType(field, records) == 'number';
|
|
21951
|
+
});
|
|
21952
|
+
}
|
|
21953
|
+
|
|
21954
|
+
function setLayerScheme(lyr, scheme) {
|
|
21955
|
+
if (!lyr) return;
|
|
21956
|
+
if (scheme) {
|
|
21957
|
+
layerSchemes.set(lyr, scheme);
|
|
21958
|
+
} else {
|
|
21959
|
+
layerSchemes.delete(lyr);
|
|
21960
|
+
}
|
|
21961
|
+
}
|
|
21962
|
+
|
|
21963
|
+
// The scheme that colored a layer, or null if it has none, or if its fills
|
|
21964
|
+
// have since been changed some other way.
|
|
21965
|
+
function getLayerScheme(lyr) {
|
|
21966
|
+
var scheme = lyr ? layerSchemes.get(lyr) : null;
|
|
21967
|
+
if (!scheme || !schemeMatchesLayer(scheme, lyr)) return null;
|
|
21968
|
+
return scheme;
|
|
21969
|
+
}
|
|
21970
|
+
|
|
21971
|
+
// Checked from the data rather than kept in step with every edit, so that
|
|
21972
|
+
// anything that changes the fills -- a -style command, a typed fill color,
|
|
21973
|
+
// an undo past the scheme -- retires it.
|
|
21974
|
+
function schemeMatchesLayer(scheme, lyr) {
|
|
21975
|
+
var records, colors, val;
|
|
21976
|
+
if (!lyr || !lyr.data || !scheme.colors) return false;
|
|
21977
|
+
if (scheme.method != 'non-adjacent' && !lyr.data.fieldExists(scheme.field)) return false;
|
|
21978
|
+
if (isContinuousScheme(scheme)) {
|
|
21979
|
+
return !!scheme.fillHash && scheme.fillHash == getFillFingerprint(lyr);
|
|
21980
|
+
}
|
|
21981
|
+
colors = scheme.colors.map(function(c) { return c.toLowerCase(); });
|
|
21982
|
+
records = lyr.data.getRecords();
|
|
21983
|
+
for (var i=0; i<records.length; i++) {
|
|
21984
|
+
val = records[i] && records[i].fill;
|
|
21985
|
+
if (!val || (!colors.includes(String(val).toLowerCase()) && !isNullFill(val, scheme))) {
|
|
21986
|
+
return false;
|
|
21987
|
+
}
|
|
21988
|
+
}
|
|
21989
|
+
return true;
|
|
21990
|
+
}
|
|
21991
|
+
|
|
21992
|
+
// A scheme as applied to a layer, with what's needed to check it later
|
|
21993
|
+
// colors: the colors given to -classify
|
|
21994
|
+
function getAppliedScheme(scheme, colors, lyr) {
|
|
21995
|
+
var applied = Object.assign({}, scheme, {colors: colors});
|
|
21996
|
+
if (isContinuousScheme(scheme)) {
|
|
21997
|
+
applied.fillHash = getFillFingerprint(lyr);
|
|
21998
|
+
} else {
|
|
21999
|
+
delete applied.fillHash;
|
|
22000
|
+
}
|
|
22001
|
+
return applied;
|
|
22002
|
+
}
|
|
22003
|
+
|
|
22004
|
+
// A hash of a layer's fill values (FNV-1a, 32 bits)
|
|
22005
|
+
function getFillFingerprint(lyr) {
|
|
22006
|
+
var records = lyr && lyr.data ? lyr.data.getRecords() : [];
|
|
22007
|
+
var hash = 0x811c9dc5;
|
|
22008
|
+
var str;
|
|
22009
|
+
for (var i=0; i<records.length; i++) {
|
|
22010
|
+
str = String(records[i] ? records[i].fill : '') + '\n';
|
|
22011
|
+
for (var j=0; j<str.length; j++) {
|
|
22012
|
+
hash ^= str.charCodeAt(j);
|
|
22013
|
+
hash = Math.imul(hash, 0x01000193);
|
|
22014
|
+
}
|
|
22015
|
+
}
|
|
22016
|
+
return records.length + ':' + (hash >>> 0).toString(16);
|
|
22017
|
+
}
|
|
22018
|
+
|
|
22019
|
+
function isNullFill(val, scheme) {
|
|
22020
|
+
var color = getSchemeNullColor(scheme);
|
|
22021
|
+
val = String(val).toLowerCase();
|
|
22022
|
+
// -classify writes its default as #eee
|
|
22023
|
+
return val == color || color == defaultNullColor && val == '#eee';
|
|
22024
|
+
}
|
|
22025
|
+
|
|
22026
|
+
function toHex(color) {
|
|
22027
|
+
return internal.formatColor(internal.parseColor(color));
|
|
22028
|
+
}
|
|
22029
|
+
|
|
22030
|
+
// A popup beside the polygon style panel for coloring a layer's fills by a
|
|
22031
|
+
// data field, with -classify. Edits are applied as they are made, in a command
|
|
22032
|
+
// session, so that the whole time the panel is open on a layer is one undo
|
|
22033
|
+
// state and one command in the session history.
|
|
22034
|
+
// See docs/development/color-scheme-panel-design.md
|
|
22035
|
+
//
|
|
22036
|
+
// opts.getExtraCommands() commands to run after -classify, in the same step
|
|
22037
|
+
// (the style panel's pattern backgrounds follow the
|
|
22038
|
+
// fills)
|
|
22039
|
+
// opts.getSelectionCount() features selected in the style panel
|
|
22040
|
+
// opts.onUpdate() the scheme or the fills changed
|
|
22041
|
+
// opts.onClose() the panel was closed
|
|
22042
|
+
var reverseIcon = '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" ' +
|
|
22043
|
+
'stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">' +
|
|
22044
|
+
'<path d="M2 4.5h9.5M9 2l2.5 2.5L9 7"/><path d="M12 9.5H2.5M5 7L2.5 9.5 5 12"/></svg>';
|
|
22045
|
+
var shuffleIcon = '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" ' +
|
|
22046
|
+
'stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">' +
|
|
22047
|
+
'<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"/>' +
|
|
22048
|
+
'<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"/>' +
|
|
22049
|
+
'<path d="M11 1.5l1.7 2-1.7 2M11 8.5l1.7 2-1.7 2"/></svg>';
|
|
22050
|
+
var longHueIcon = '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" ' +
|
|
22051
|
+
'stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">' +
|
|
22052
|
+
'<path d="M11.5 4.6A5 5 0 1 0 12 7"/><path d="M12 1.8v3h-3"/></svg>';
|
|
22053
|
+
var continuousIcon = '<svg width="14" height="14" viewBox="0 0 14 14" fill="none">' +
|
|
22054
|
+
'<defs><linearGradient id="color-scheme-continuous-icon">' +
|
|
22055
|
+
'<stop offset="0" stop-color="currentColor" stop-opacity="0"/>' +
|
|
22056
|
+
'<stop offset="1" stop-color="currentColor"/></linearGradient></defs>' +
|
|
22057
|
+
'<rect x="1.5" y="3.5" width="11" height="7" rx="1.5" fill="url(#color-scheme-continuous-icon)" ' +
|
|
22058
|
+
'stroke="currentColor" stroke-width="1.2"/></svg>';
|
|
22059
|
+
|
|
22060
|
+
function ColorSchemePanel(gui, opts) {
|
|
22061
|
+
var parent = gui.container.findChild('.mshp-main-map');
|
|
22062
|
+
var panel = El('div').addClass('label-style-panel color-scheme-panel').appendTo(parent).hide();
|
|
22063
|
+
var session = null; // made on first use: the console is made after the panel
|
|
22064
|
+
var targetLayer = null;
|
|
22065
|
+
var scheme = null;
|
|
22066
|
+
var sessionLayer = null; // the layer the session's edits are to
|
|
22067
|
+
var baseScheme = null; // its scheme when they began
|
|
22068
|
+
var selectedTile = -1;
|
|
22069
|
+
// the scheme last used on each tab while the panel is open, for switching back
|
|
22070
|
+
var tabSchemes = {};
|
|
22071
|
+
var tabs = {};
|
|
22072
|
+
var paletteBtn, paletteMenu, countField, tileRow, gradientEl, tilesEl, picker, fieldRow, fieldSelect, methodSelect,
|
|
22073
|
+
noFieldsNote, selectionNote, controlsEl, vibranceRow, vibranceInput,
|
|
22074
|
+
longHueBtn, continuousBtn, reverseBtn, shuffleBtn, dropMarker, nullControl, nullCount, countLabel,
|
|
22075
|
+
rangeEl, rangeStrip, rangeShades, rangeHandles,
|
|
22076
|
+
divergingEl, pivotSelect, pivotInput, pivotNote, neutralToggle, splitSelect;
|
|
22077
|
+
// set while a tile is being dragged, so that letting go isn't a click
|
|
22078
|
+
var tileDrag = null;
|
|
22079
|
+
|
|
22080
|
+
initPanel();
|
|
22081
|
+
|
|
22082
|
+
gui.on('undo_redo_post', function() {
|
|
22083
|
+
var restored;
|
|
22084
|
+
if (!targetLayer) return;
|
|
22085
|
+
restored = getLayerScheme(targetLayer);
|
|
22086
|
+
if (restored) {
|
|
22087
|
+
scheme = updateDivergingLayout(restored, targetLayer);
|
|
22088
|
+
render();
|
|
22089
|
+
} else {
|
|
22090
|
+
close();
|
|
22091
|
+
}
|
|
22092
|
+
});
|
|
22093
|
+
|
|
22094
|
+
this.open = function(lyr) {
|
|
22095
|
+
var fields;
|
|
22096
|
+
if (targetLayer && targetLayer != lyr) close();
|
|
22097
|
+
targetLayer = lyr;
|
|
22098
|
+
selectedTile = -1;
|
|
22099
|
+
tabSchemes = {};
|
|
22100
|
+
scheme = getLayerScheme(lyr);
|
|
22101
|
+
panel.show();
|
|
22102
|
+
if (scheme) {
|
|
22103
|
+
scheme = updateDivergingLayout(scheme, lyr);
|
|
22104
|
+
} else {
|
|
22105
|
+
fields = getNumericFields(lyr);
|
|
22106
|
+
scheme = getDefaultSchemeOfType(fields.length > 0 ? 'sequential' : 'categorical', lyr);
|
|
22107
|
+
if (canApply(scheme)) apply();
|
|
22108
|
+
}
|
|
22109
|
+
render();
|
|
22110
|
+
};
|
|
22111
|
+
|
|
22112
|
+
this.close = close;
|
|
22113
|
+
|
|
22114
|
+
this.isOpen = function() {
|
|
22115
|
+
return !!targetLayer;
|
|
22116
|
+
};
|
|
22117
|
+
|
|
22118
|
+
function close() {
|
|
22119
|
+
var wasOpen = !!targetLayer;
|
|
22120
|
+
picker.hide();
|
|
22121
|
+
nullControl.picker.hide();
|
|
22122
|
+
hidePaletteMenu();
|
|
22123
|
+
panel.hide();
|
|
22124
|
+
targetLayer = null;
|
|
22125
|
+
scheme = null;
|
|
22126
|
+
if (session) session.finish();
|
|
22127
|
+
if (wasOpen && opts.onClose) opts.onClose();
|
|
22128
|
+
}
|
|
22129
|
+
|
|
22130
|
+
function getSession() {
|
|
22131
|
+
if (!session && gui.console && gui.console.createCommandSession) {
|
|
22132
|
+
session = gui.console.createCommandSession({label: 'Color scheme', onCommit: onCommit});
|
|
22133
|
+
}
|
|
22134
|
+
return session;
|
|
22135
|
+
}
|
|
22136
|
+
|
|
22137
|
+
function initPanel() {
|
|
22138
|
+
claimFieldKeys(panel.node(), {
|
|
22139
|
+
revert: render,
|
|
22140
|
+
release: releaseFocus
|
|
22141
|
+
});
|
|
22142
|
+
panel.node().addEventListener('click', function(e) {
|
|
22143
|
+
if (isTextInput(document.activeElement) || opensAMenu(e.target)) return;
|
|
22144
|
+
releaseFocus();
|
|
22145
|
+
});
|
|
22146
|
+
|
|
22147
|
+
var header = El('div').addClass('label-style-panel-title').appendTo(panel);
|
|
22148
|
+
El('span').appendTo(header).text('Color palettes');
|
|
22149
|
+
El('button').addClass('label-style-close').appendTo(header).text('×').on('click', function() {
|
|
22150
|
+
close();
|
|
22151
|
+
opts.onUpdate();
|
|
22152
|
+
});
|
|
22153
|
+
|
|
22154
|
+
var tabRow = El('div').addClass('label-style-row color-scheme-tabs').attr('role', 'tablist').appendTo(panel);
|
|
22155
|
+
schemeTypes.forEach(function(type) {
|
|
22156
|
+
tabs[type.name] = El('button').addClass('color-scheme-tab').attr('type', 'button')
|
|
22157
|
+
.attr('role', 'tab').text(type.label).appendTo(tabRow)
|
|
22158
|
+
.on('click', function() {
|
|
22159
|
+
switchType(type.name);
|
|
22160
|
+
});
|
|
22161
|
+
});
|
|
22162
|
+
|
|
22163
|
+
noFieldsNote = El('div').addClass('label-style-row color-scheme-note').appendTo(panel)
|
|
22164
|
+
.text('This layer has no numeric fields to classify.');
|
|
22165
|
+
controlsEl = El('div').appendTo(panel);
|
|
22166
|
+
|
|
22167
|
+
fieldRow = El('div').addClass('label-style-row color-scheme-select-row').appendTo(controlsEl);
|
|
22168
|
+
El('span').appendTo(fieldRow).text('Field');
|
|
22169
|
+
fieldSelect = El('select').attr('aria-label', 'Data field').appendTo(fieldRow).on('change', function() {
|
|
22170
|
+
var field = fieldSelect.node().value;
|
|
22171
|
+
changeScheme(setSchemeField(scheme, field, getCategoryCount(field)));
|
|
22172
|
+
});
|
|
22173
|
+
|
|
22174
|
+
var methodRow = El('div').addClass('label-style-row color-scheme-select-row').appendTo(controlsEl);
|
|
22175
|
+
El('span').appendTo(methodRow).text('Method');
|
|
22176
|
+
methodSelect = El('select').attr('aria-label', 'Classification method').appendTo(methodRow).on('change', function() {
|
|
22177
|
+
var field = scheme.field || getCategoryFields(targetLayer)[0];
|
|
22178
|
+
changeScheme(setSchemeMethod(scheme, methodSelect.node().value, field, getCategoryCount(field)));
|
|
22179
|
+
});
|
|
22180
|
+
|
|
22181
|
+
initDivergingControls();
|
|
22182
|
+
|
|
22183
|
+
var paletteRow = El('div').addClass('label-style-row label-split-row').appendTo(controlsEl);
|
|
22184
|
+
var paletteCell = El('div').addClass('label-split-cell color-scheme-palette-cell').appendTo(paletteRow);
|
|
22185
|
+
var countCell = El('div').addClass('label-split-cell').appendTo(paletteRow);
|
|
22186
|
+
El('span').appendTo(paletteCell).text('Palette');
|
|
22187
|
+
paletteBtn = El('div').addClass('color-scheme-palette-btn').attr('role', 'button')
|
|
22188
|
+
.attr('aria-label', 'Palette').appendTo(paletteCell).on('click', togglePaletteMenu);
|
|
22189
|
+
paletteMenu = El('div').addClass('color-scheme-palette-menu').appendTo(paletteCell).hide();
|
|
22190
|
+
countLabel = El('span').appendTo(countCell).text('Colors');
|
|
22191
|
+
// the scheme sets the limits (see getMinSchemeColors(), getMaxSchemeColors())
|
|
22192
|
+
countField = new SizeField(countCell, {
|
|
22193
|
+
min: 1,
|
|
22194
|
+
max: maxCategoricalColors,
|
|
22195
|
+
step: 1,
|
|
22196
|
+
bigStep: 2,
|
|
22197
|
+
decimals: 0,
|
|
22198
|
+
title: 'Number of classes',
|
|
22199
|
+
onSet: function(n) {
|
|
22200
|
+
changeScheme(setTileCount(scheme, n, getCategoryCount()));
|
|
22201
|
+
},
|
|
22202
|
+
onStep: function(delta) {
|
|
22203
|
+
changeScheme(setTileCount(scheme, scheme.n + (delta > 0 ? 1 : -1), getCategoryCount()));
|
|
22204
|
+
},
|
|
22205
|
+
onDone: releaseFocus
|
|
22206
|
+
});
|
|
22207
|
+
|
|
22208
|
+
tileRow = El('div').addClass('color-scheme-tile-row').appendTo(controlsEl);
|
|
22209
|
+
gradientEl = El('div').addClass('color-scheme-gradient').appendTo(tileRow);
|
|
22210
|
+
tilesEl = El('div').addClass('color-scheme-tiles').appendTo(tileRow);
|
|
22211
|
+
picker = new ColorPicker(tileRow, {
|
|
22212
|
+
presetRows: [],
|
|
22213
|
+
onPreview: function(hex) {
|
|
22214
|
+
var tile = getTileCells()[selectedTile];
|
|
22215
|
+
if (tile) tile.querySelector('.color-scheme-tile').style.backgroundColor = hex;
|
|
22216
|
+
},
|
|
22217
|
+
onChange: function(hex) {
|
|
22218
|
+
if (selectedTile > -1) changeScheme(setTileColor(scheme, selectedTile, hex));
|
|
22219
|
+
},
|
|
22220
|
+
// the highlight says which tile the picker is editing
|
|
22221
|
+
onHide: function() {
|
|
22222
|
+
selectedTile = -1;
|
|
22223
|
+
render();
|
|
22224
|
+
}
|
|
22225
|
+
});
|
|
22226
|
+
|
|
22227
|
+
// the slider works in thousandths of OKLCH chroma
|
|
22228
|
+
vibranceRow = addSliderRow('Vibrance', 'Makes all the colors more vivid',
|
|
22229
|
+
0, Math.round(maxVibrance * 1000), 5, function(val) {
|
|
22230
|
+
changeScheme(setSchemeVibrance(scheme, val / 1000));
|
|
22231
|
+
});
|
|
22232
|
+
vibranceInput = vibranceRow.findChild('input');
|
|
22233
|
+
initRangeControl(vibranceRow);
|
|
22234
|
+
longHueBtn = addIconButton(vibranceRow, 'Go the long way around the color wheel', longHueIcon, function() {
|
|
22235
|
+
changeScheme(setSchemeLongHue(scheme, !scheme.longHue));
|
|
22236
|
+
}).addClass('long-hue-btn');
|
|
22237
|
+
continuousBtn = addIconButton(vibranceRow, 'Continuous colors (unclassed)', continuousIcon, function() {
|
|
22238
|
+
changeScheme(setSchemeContinuous(scheme, !scheme.continuous));
|
|
22239
|
+
}).addClass('continuous-btn');
|
|
22240
|
+
reverseBtn = addIconButton(vibranceRow, 'Reverse the colors', reverseIcon, function() {
|
|
22241
|
+
changeScheme(reverseScheme(scheme));
|
|
22242
|
+
});
|
|
22243
|
+
shuffleBtn = addIconButton(vibranceRow, 'Shuffle the colors', shuffleIcon, function() {
|
|
22244
|
+
changeScheme(shuffleScheme(scheme));
|
|
22245
|
+
});
|
|
22246
|
+
// the color of features with no data, and how many there are
|
|
22247
|
+
nullControl = makeColorRow(controlsEl, {
|
|
22248
|
+
label: 'No data',
|
|
22249
|
+
noOpacity: true,
|
|
22250
|
+
onColor: function(color) {
|
|
22251
|
+
changeScheme(setSchemeNullColor(scheme, color));
|
|
22252
|
+
}
|
|
22253
|
+
});
|
|
22254
|
+
nullControl.row.addClass('color-scheme-null-row');
|
|
22255
|
+
nullControl.chit.on('click', function() {
|
|
22256
|
+
picker.hide();
|
|
22257
|
+
});
|
|
22258
|
+
nullCount = El('span').addClass('color-scheme-note color-scheme-null-count').appendTo(nullControl.aside);
|
|
22259
|
+
|
|
22260
|
+
selectionNote = El('div').addClass('label-style-row color-scheme-note').appendTo(controlsEl)
|
|
22261
|
+
.text('Colors apply to every feature in the layer, not only the selected ones.');
|
|
22262
|
+
|
|
22263
|
+
document.addEventListener('mousedown', function(e) {
|
|
22264
|
+
if (paletteMenu.visible() && !paletteMenu.node().contains(e.target) &&
|
|
22265
|
+
!paletteBtn.node().contains(e.target)) {
|
|
22266
|
+
hidePaletteMenu();
|
|
22267
|
+
}
|
|
22268
|
+
});
|
|
22269
|
+
}
|
|
22270
|
+
|
|
22271
|
+
// The pivot, the pivot class and how the classes are split between the
|
|
22272
|
+
// sides
|
|
22273
|
+
function initDivergingControls() {
|
|
22274
|
+
divergingEl = El('div').appendTo(controlsEl);
|
|
22275
|
+
var pivotRow = El('div').addClass('label-style-row label-split-row').appendTo(divergingEl);
|
|
22276
|
+
var pivotCell = El('div').addClass('label-split-cell color-scheme-select-row').appendTo(pivotRow);
|
|
22277
|
+
var valueCell = El('div').addClass('label-split-cell color-scheme-select-row').appendTo(pivotRow);
|
|
22278
|
+
El('span').appendTo(pivotCell).text('Pivot');
|
|
22279
|
+
pivotSelect = El('select').attr('aria-label', 'Pivot').appendTo(pivotCell).on('change', function() {
|
|
22280
|
+
var name = pivotSelect.node().value;
|
|
22281
|
+
var current = scheme.layout ? scheme.layout.pivot : 0;
|
|
22282
|
+
changeScheme(setSchemePivot(scheme, name == 'value' ? current : name));
|
|
22283
|
+
});
|
|
22284
|
+
pivotOptions.forEach(function(item) {
|
|
22285
|
+
El('option').attr('value', item.name).text(item.label).appendTo(pivotSelect);
|
|
22286
|
+
});
|
|
22287
|
+
El('span').appendTo(valueCell).text('Value');
|
|
22288
|
+
pivotInput = El('input').attr('type', 'text').attr('aria-label', 'Pivot value')
|
|
22289
|
+
.addClass('color-scheme-pivot-input').appendTo(valueCell)
|
|
22290
|
+
.on('change', function() {
|
|
22291
|
+
var val = parseFloat(pivotInput.node().value);
|
|
22292
|
+
if (isFinite(val)) {
|
|
22293
|
+
changeScheme(setSchemePivot(scheme, val));
|
|
22294
|
+
} else {
|
|
22295
|
+
render();
|
|
22296
|
+
}
|
|
22297
|
+
})
|
|
22298
|
+
.on('keydown', function(e) {
|
|
22299
|
+
if (e.key == 'Enter') pivotInput.node().blur();
|
|
22300
|
+
});
|
|
22301
|
+
pivotNote = El('div').addClass('label-style-row color-scheme-note').appendTo(divergingEl);
|
|
22302
|
+
|
|
22303
|
+
var optionRow = El('div').addClass('label-style-row label-split-row').appendTo(divergingEl);
|
|
22304
|
+
var splitCell = El('div').addClass('label-split-cell color-scheme-select-row').appendTo(optionRow);
|
|
22305
|
+
var neutralCell = El('div').addClass('label-split-cell color-scheme-select-row').appendTo(optionRow);
|
|
22306
|
+
var neutralLabel = El('span').addClass('color-scheme-hint-label').appendTo(neutralCell).text('Pivot class')
|
|
22307
|
+
.attr('data-tooltip', 'A class around the pivot, in the center color')
|
|
22308
|
+
.on('mouseenter', function() { keepTooltipInWindow(neutralLabel.node()); });
|
|
22309
|
+
var neutralLine = El('div').addClass('color-scheme-toggle-line').appendTo(neutralCell);
|
|
22310
|
+
neutralToggle = makePanelToggle(neutralLine, {
|
|
22311
|
+
title: 'Pivot class',
|
|
22312
|
+
onChange: function(on) {
|
|
22313
|
+
changeScheme(setSchemeNeutral(scheme, on));
|
|
22314
|
+
}
|
|
22315
|
+
});
|
|
22316
|
+
El('span').appendTo(splitCell).text('Sides');
|
|
22317
|
+
splitSelect = El('select').attr('aria-label', 'Classes on each side').appendTo(splitCell).on('change', function() {
|
|
22318
|
+
changeScheme(setSchemeSplit(scheme, splitSelect.node().value));
|
|
22319
|
+
});
|
|
22320
|
+
divergingSplits.forEach(function(item) {
|
|
22321
|
+
El('option').attr('value', item.name).text(item.label).appendTo(splitSelect);
|
|
22322
|
+
});
|
|
22323
|
+
}
|
|
22324
|
+
|
|
22325
|
+
function renderDivergingControls() {
|
|
22326
|
+
var diverging = scheme.type == 'diverging';
|
|
22327
|
+
var pivot = scheme.pivot;
|
|
22328
|
+
var option = typeof pivot == 'number' ? 'value' : pivot;
|
|
22329
|
+
var summary;
|
|
22330
|
+
divergingEl.classed('hidden', !diverging);
|
|
22331
|
+
if (!diverging) return;
|
|
22332
|
+
pivotSelect.node().value = option;
|
|
22333
|
+
pivotInput.node().disabled = option != 'value';
|
|
22334
|
+
if (document.activeElement != pivotInput.node()) {
|
|
22335
|
+
pivotInput.node().value = scheme.layout ? formatNumber(scheme.layout.pivot) : '';
|
|
22336
|
+
}
|
|
22337
|
+
summary = getPivotSummary(scheme, targetLayer);
|
|
22338
|
+
pivotNote.text(summary ? formatCount(summary.below) + ' below the pivot, ' +
|
|
22339
|
+
summary.above + ' at or above' : 'No numeric data to classify.');
|
|
22340
|
+
neutralToggle.setState(getSchemeNeutral(scheme) ? 'on' : 'off');
|
|
22341
|
+
splitSelect.node().value = scheme.split;
|
|
22342
|
+
}
|
|
22343
|
+
|
|
22344
|
+
function formatCount(n) {
|
|
22345
|
+
return n + (n == 1 ? ' feature' : ' features');
|
|
22346
|
+
}
|
|
22347
|
+
|
|
22348
|
+
function formatNumber(val) {
|
|
22349
|
+
return String(+val.toPrecision(6));
|
|
22350
|
+
}
|
|
22351
|
+
|
|
22352
|
+
function render() {
|
|
22353
|
+
var categorical, fields, colors, tiles;
|
|
22354
|
+
if (!targetLayer || !scheme) return;
|
|
22355
|
+
categorical = scheme.type == 'categorical';
|
|
22356
|
+
Object.keys(tabs).forEach(function(type) {
|
|
22357
|
+
tabs[type].classed('selected', type == scheme.type)
|
|
22358
|
+
.attr('aria-selected', type == scheme.type ? 'true' : 'false');
|
|
22359
|
+
});
|
|
22360
|
+
fields = categorical ? getCategoryFields(targetLayer) : getNumericFields(targetLayer);
|
|
22361
|
+
noFieldsNote.classed('hidden', categorical || fields.length > 0);
|
|
22362
|
+
controlsEl.classed('hidden', !categorical && fields.length === 0);
|
|
22363
|
+
if (!categorical && fields.length === 0) return;
|
|
22364
|
+
renderFieldOptions(fields);
|
|
22365
|
+
fieldRow.classed('hidden', scheme.method == 'non-adjacent');
|
|
22366
|
+
renderMethodOptions(fields);
|
|
22367
|
+
renderDivergingControls();
|
|
22368
|
+
countLabel.text(scheme.type != 'diverging' ? 'Colors' : scheme.split == 'count' ? 'Per side' : 'Classes');
|
|
22369
|
+
countField.setValue(String(scheme.n));
|
|
22370
|
+
tiles = getSchemeTiles(scheme);
|
|
22371
|
+
colors = tiles.map(function(tile) { return tile.color; });
|
|
22372
|
+
renderPaletteButton(colors);
|
|
22373
|
+
renderTiles(tiles);
|
|
22374
|
+
renderVibrance();
|
|
22375
|
+
renderNullColor();
|
|
22376
|
+
selectionNote.classed('hidden', !(opts.getSelectionCount() > 0));
|
|
22377
|
+
}
|
|
22378
|
+
|
|
22379
|
+
function renderNullColor() {
|
|
22380
|
+
var count = getNoDataCount(targetLayer, scheme);
|
|
22381
|
+
nullControl.row.classed('hidden', scheme.method == 'non-adjacent');
|
|
22382
|
+
if (scheme.method == 'non-adjacent') nullControl.picker.hide();
|
|
22383
|
+
nullControl.showColor(getSchemeNullColor(scheme));
|
|
22384
|
+
nullCount.text(count === 0 ? 'None in this layer' : formatCount(count));
|
|
22385
|
+
}
|
|
22386
|
+
|
|
22387
|
+
function renderMethodOptions(fields) {
|
|
22388
|
+
methodSelect.empty();
|
|
22389
|
+
getSchemeMethods(scheme).forEach(function(item) {
|
|
22390
|
+
var opt = El('option').attr('value', item.name).text(item.label).appendTo(methodSelect);
|
|
22391
|
+
// classifying by category needs a field
|
|
22392
|
+
if (item.name == 'categorical' && fields.length === 0) opt.attr('disabled', true);
|
|
22393
|
+
});
|
|
22394
|
+
methodSelect.node().value = scheme.method;
|
|
22395
|
+
}
|
|
22396
|
+
|
|
22397
|
+
// Each tab keeps its own scheme while the panel is open, and starts from a
|
|
22398
|
+
// default the first time
|
|
22399
|
+
function switchType(type) {
|
|
22400
|
+
var next;
|
|
22401
|
+
if (type == scheme.type) return;
|
|
22402
|
+
tabSchemes[scheme.type] = scheme;
|
|
22403
|
+
next = tabSchemes[type] || getDefaultSchemeOfType(type, targetLayer);
|
|
22404
|
+
// one no-data color for both tabs
|
|
22405
|
+
next = updateDivergingLayout(Object.assign({}, next, {nullColor: scheme.nullColor}), targetLayer);
|
|
22406
|
+
picker.hide();
|
|
22407
|
+
selectedTile = -1;
|
|
22408
|
+
scheme = next;
|
|
22409
|
+
if (canApply(scheme)) apply();
|
|
22410
|
+
render();
|
|
22411
|
+
}
|
|
22412
|
+
|
|
22413
|
+
// a diverging scheme needs data that its classes can divide
|
|
22414
|
+
function canApply(s) {
|
|
22415
|
+
if (s.type == 'diverging' && !s.layout) return false;
|
|
22416
|
+
return s.method == 'non-adjacent' || !!s.field;
|
|
22417
|
+
}
|
|
22418
|
+
|
|
22419
|
+
// The number of categories in a field (the scheme's, by default), for a
|
|
22420
|
+
// categorical scheme
|
|
22421
|
+
function getCategoryCount(field) {
|
|
22422
|
+
if (scheme.type != 'categorical') return -1;
|
|
22423
|
+
return getCategories(targetLayer, field || scheme.field).length;
|
|
22424
|
+
}
|
|
22425
|
+
|
|
22426
|
+
function addSliderRow(label, tooltip, min, max, step, onInput) {
|
|
22427
|
+
var row = El('div').addClass('label-style-row color-scheme-slider-row').appendTo(controlsEl);
|
|
22428
|
+
var labelEl = El('span').addClass('color-scheme-slider-label').attr('data-tooltip', tooltip)
|
|
22429
|
+
.appendTo(row).text(label)
|
|
22430
|
+
.on('mouseenter', function() { keepTooltipInWindow(labelEl.node()); });
|
|
22431
|
+
var input = El('input').attr('type', 'range').attr('min', min).attr('max', max).attr('step', step)
|
|
22432
|
+
.attr('aria-label', label).appendTo(row)
|
|
22433
|
+
.on('input', function() {
|
|
22434
|
+
showSliderFill(input);
|
|
22435
|
+
onInput(+input.node().value);
|
|
22436
|
+
})
|
|
22437
|
+
.on('change', releaseFocus);
|
|
22438
|
+
return row;
|
|
22439
|
+
}
|
|
22440
|
+
|
|
22441
|
+
function addIconButton(parent, label, icon, onClick) {
|
|
22442
|
+
var btn = El('button').addClass('color-scheme-icon-btn').attr('type', 'button')
|
|
22443
|
+
.attr('aria-label', label).attr('data-tooltip', label).html(icon).appendTo(parent)
|
|
22444
|
+
.on('click', onClick)
|
|
22445
|
+
.on('mouseenter', function() { keepTooltipInWindow(btn.node()); });
|
|
22446
|
+
return btn;
|
|
22447
|
+
}
|
|
22448
|
+
|
|
22449
|
+
// The part of a preset that the tiles are taken from: a strip of the whole
|
|
22450
|
+
// ramp, shaded outside the part in use, with a handle at each end of it
|
|
22451
|
+
function initRangeControl(row) {
|
|
22452
|
+
var label = El('span').addClass('color-scheme-hint-label color-scheme-range-part')
|
|
22453
|
+
.attr('data-tooltip', 'Use part of the color ramp\n(double-click the strip to use all of it)')
|
|
22454
|
+
.appendTo(row).text('Range')
|
|
22455
|
+
.on('mouseenter', function() { keepTooltipInWindow(label.node()); });
|
|
22456
|
+
rangeEl = El('div').addClass('color-scheme-range color-scheme-range-part').appendTo(row)
|
|
22457
|
+
.on('pointerdown', startRangeDrag)
|
|
22458
|
+
.on('dblclick', function() {
|
|
22459
|
+
changeScheme(resetSchemeRange(scheme));
|
|
22460
|
+
});
|
|
22461
|
+
rangeStrip = El('div').addClass('color-scheme-range-strip').appendTo(rangeEl);
|
|
22462
|
+
rangeShades = ['left', 'right'].map(function(side) {
|
|
22463
|
+
return El('div').addClass('color-scheme-range-shade ' + side).appendTo(rangeEl);
|
|
22464
|
+
});
|
|
22465
|
+
rangeHandles = ['left', 'right'].map(function(side) {
|
|
22466
|
+
return El('div').addClass('color-scheme-range-handle').attr('role', 'slider').attr('tabindex', '0')
|
|
22467
|
+
.attr('aria-label', side == 'left' ? 'Start of range' : 'End of range')
|
|
22468
|
+
.attr('aria-valuemin', '0').attr('aria-valuemax', '100')
|
|
22469
|
+
.appendTo(rangeEl)
|
|
22470
|
+
.on('keydown', function(e) {
|
|
22471
|
+
onRangeKey(e, side);
|
|
22472
|
+
});
|
|
22473
|
+
});
|
|
22474
|
+
}
|
|
22475
|
+
|
|
22476
|
+
function renderRange() {
|
|
22477
|
+
var display, colors;
|
|
22478
|
+
if (!hasSchemeRange(scheme)) return;
|
|
22479
|
+
display = getDisplayRange(scheme);
|
|
22480
|
+
colors = getRangeStripColors(scheme, 33);
|
|
22481
|
+
rangeStrip.css('background-image', 'linear-gradient(to right, ' + colors.join(', ') + ')');
|
|
22482
|
+
rangeShades[0].css('width', pct(display[0]));
|
|
22483
|
+
rangeShades[1].css('width', pct(1 - display[1]));
|
|
22484
|
+
rangeHandles.forEach(function(handle, i) {
|
|
22485
|
+
handle.css('left', pct(display[i])).attr('aria-valuenow', String(Math.round(display[i] * 100)));
|
|
22486
|
+
});
|
|
22487
|
+
}
|
|
22488
|
+
|
|
22489
|
+
function pct(val) {
|
|
22490
|
+
return round(val * 100) + '%';
|
|
22491
|
+
}
|
|
22492
|
+
|
|
22493
|
+
// Dragging a handle (or pressing the strip, which takes the nearer handle
|
|
22494
|
+
// there) moves that end of the range
|
|
22495
|
+
function startRangeDrag(e) {
|
|
22496
|
+
var box = rangeStrip.node().getBoundingClientRect();
|
|
22497
|
+
var display = getDisplayRange(scheme);
|
|
22498
|
+
var pos = getPos(e);
|
|
22499
|
+
var side = e.target == rangeHandles[0].node() ? 'left' :
|
|
22500
|
+
e.target == rangeHandles[1].node() ? 'right' :
|
|
22501
|
+
Math.abs(pos - display[0]) <= Math.abs(pos - display[1]) ? 'left' : 'right';
|
|
22502
|
+
if (e.button !== 0 || !(box.width > 0)) return;
|
|
22503
|
+
e.preventDefault();
|
|
22504
|
+
picker.hide();
|
|
22505
|
+
move(e);
|
|
22506
|
+
document.addEventListener('pointermove', move);
|
|
22507
|
+
document.addEventListener('pointerup', finish);
|
|
22508
|
+
document.addEventListener('pointercancel', finish);
|
|
22509
|
+
|
|
22510
|
+
// in steps of half a percent, so that a drag runs fewer commands
|
|
22511
|
+
function getPos(e) {
|
|
22512
|
+
return Math.round((e.clientX - box.left) / box.width * 200) / 200;
|
|
22513
|
+
}
|
|
22514
|
+
|
|
22515
|
+
function move(e) {
|
|
22516
|
+
var next = setSchemeRangeEnd(scheme, side, getPos(e));
|
|
22517
|
+
if (getDisplayRange(next).join() != getDisplayRange(scheme).join()) changeScheme(next);
|
|
22518
|
+
}
|
|
22519
|
+
|
|
22520
|
+
function finish() {
|
|
22521
|
+
document.removeEventListener('pointermove', move);
|
|
22522
|
+
document.removeEventListener('pointerup', finish);
|
|
22523
|
+
document.removeEventListener('pointercancel', finish);
|
|
22524
|
+
}
|
|
22525
|
+
}
|
|
22526
|
+
|
|
22527
|
+
function onRangeKey(e, side) {
|
|
22528
|
+
var step = e.shiftKey ? 0.05 : 0.01;
|
|
22529
|
+
var display = getDisplayRange(scheme);
|
|
22530
|
+
var pos = side == 'left' ? display[0] : display[1];
|
|
22531
|
+
if (e.key == 'ArrowLeft' || e.key == 'ArrowDown') {
|
|
22532
|
+
pos -= step;
|
|
22533
|
+
} else if (e.key == 'ArrowRight' || e.key == 'ArrowUp') {
|
|
22534
|
+
pos += step;
|
|
22535
|
+
} else {
|
|
22536
|
+
return;
|
|
22537
|
+
}
|
|
22538
|
+
e.preventDefault();
|
|
22539
|
+
e.stopPropagation();
|
|
22540
|
+
changeScheme(setSchemeRangeEnd(scheme, side, pos));
|
|
22541
|
+
}
|
|
22542
|
+
|
|
22543
|
+
// Presets and categorical schemes have no interpolated tiles for vibrance
|
|
22544
|
+
// or the hue path to change; the row keeps only the reverse button for them,
|
|
22545
|
+
// and for sequential and diverging presets, the range.
|
|
22546
|
+
function renderVibrance() {
|
|
22547
|
+
var vibrance = getSchemeVibrance(scheme);
|
|
22548
|
+
setSliderValue(vibranceInput, Math.round(vibrance * 1000));
|
|
22549
|
+
vibranceRow.classed('preset', !!scheme.preset || scheme.type == 'categorical');
|
|
22550
|
+
vibranceRow.classed('ranged', hasSchemeRange(scheme));
|
|
22551
|
+
renderRange();
|
|
22552
|
+
reverseBtn.classed('hidden', scheme.type == 'categorical');
|
|
22553
|
+
shuffleBtn.classed('hidden', scheme.type != 'categorical');
|
|
22554
|
+
// each half of a diverging ramp runs from a low-chroma center, where the
|
|
22555
|
+
// hue path makes little difference
|
|
22556
|
+
longHueBtn.classed('hidden', scheme.type == 'diverging')
|
|
22557
|
+
.classed('selected', !!scheme.longHue)
|
|
22558
|
+
.attr('aria-pressed', scheme.longHue ? 'true' : 'false');
|
|
22559
|
+
continuousBtn.classed('hidden', scheme.type == 'categorical')
|
|
22560
|
+
.classed('selected', isContinuousScheme(scheme))
|
|
22561
|
+
.attr('aria-pressed', isContinuousScheme(scheme) ? 'true' : 'false');
|
|
22562
|
+
}
|
|
22563
|
+
|
|
22564
|
+
function setSliderValue(input, val) {
|
|
22565
|
+
if (input.node().value != val) input.node().value = val;
|
|
22566
|
+
showSliderFill(input);
|
|
22567
|
+
}
|
|
22568
|
+
|
|
22569
|
+
// the track is filled up to the center of the handle, which travels the
|
|
22570
|
+
// track's width less its 12px box (see page.css)
|
|
22571
|
+
function showSliderFill(input) {
|
|
22572
|
+
var el = input.node();
|
|
22573
|
+
var min = +el.min, max = +el.max;
|
|
22574
|
+
var fill = max > min ? (el.value - min) / (max - min) : 0;
|
|
22575
|
+
el.style.setProperty('--fill', 'calc(6px + (100% - 12px) * ' + round(fill * 1000) / 1000 + ')');
|
|
22576
|
+
}
|
|
22577
|
+
|
|
22578
|
+
function renderFieldOptions(fields) {
|
|
22579
|
+
fieldSelect.empty();
|
|
22580
|
+
fields.forEach(function(field) {
|
|
22581
|
+
El('option').attr('value', field).text(field).appendTo(fieldSelect);
|
|
22582
|
+
});
|
|
22583
|
+
fieldSelect.node().value = scheme.field || '';
|
|
22584
|
+
}
|
|
22585
|
+
|
|
22586
|
+
function renderPaletteButton(colors) {
|
|
22587
|
+
paletteBtn.empty();
|
|
22588
|
+
El('div').addClass('color-scheme-strip').appendTo(paletteBtn)
|
|
22589
|
+
.css('background-image', isContinuousScheme(scheme) ?
|
|
22590
|
+
getGradientStripBackground(scheme) : getStripBackground(colors));
|
|
22591
|
+
El('span').addClass('color-scheme-palette-name').appendTo(paletteBtn)
|
|
22592
|
+
.text(scheme.preset || 'Custom');
|
|
22593
|
+
}
|
|
22594
|
+
|
|
22595
|
+
function renderTiles(tiles) {
|
|
22596
|
+
var categorical = scheme.type == 'categorical';
|
|
22597
|
+
var diverging = scheme.type == 'diverging';
|
|
22598
|
+
var continuous = isContinuousScheme(scheme);
|
|
22599
|
+
var used = tiles.length;
|
|
22600
|
+
var tileUse = diverging ? getDivergingTileUse(scheme) : null;
|
|
22601
|
+
var center = getCenterTile(scheme);
|
|
22602
|
+
var classLabels = categorical ? null : continuous ? getStopLabels() : getClassLabels(tileUse);
|
|
22603
|
+
var groups = categorical && scheme.method == 'categorical' ?
|
|
22604
|
+
getSwatchCategories(getCategories(targetLayer, scheme.field), used) : null;
|
|
22605
|
+
// the end tiles of a sequential preset can be taken away
|
|
22606
|
+
var removable = canRemoveEndTile(scheme);
|
|
22607
|
+
var n, perRow;
|
|
22608
|
+
// a categorical scheme shows its whole palette, and marks the swatches
|
|
22609
|
+
// in use
|
|
22610
|
+
if (categorical) {
|
|
22611
|
+
tiles = getCategoricalPalette(scheme).map(function(color) {
|
|
22612
|
+
return {color: color, pinned: false, adjusted: false};
|
|
22613
|
+
});
|
|
22614
|
+
}
|
|
22615
|
+
n = tiles.length;
|
|
22616
|
+
// the stops of a continuous ramp stay in one row, under the gradient
|
|
22617
|
+
perRow = n > 12 && !continuous ? Math.ceil(n / 2) : n;
|
|
22618
|
+
tilesEl.empty();
|
|
22619
|
+
tilesEl.classed('two-rows', perRow < n);
|
|
22620
|
+
tilesEl.classed('continuous', continuous);
|
|
22621
|
+
tilesEl.node().style.setProperty('--tiles-per-row', perRow);
|
|
22622
|
+
dropMarker = El('div').addClass('color-scheme-drop-marker').appendTo(tilesEl).hide();
|
|
22623
|
+
tiles.forEach(function(tile, i) {
|
|
22624
|
+
var color = tile.color;
|
|
22625
|
+
var inUse = tileUse ? tileUse[i] : !categorical || i < used;
|
|
22626
|
+
var cell = El('div').addClass('color-scheme-tile-cell').appendTo(tilesEl);
|
|
22627
|
+
var tileEl = El('div').addClass('color-scheme-tile').attr('role', 'button')
|
|
22628
|
+
.attr('aria-label', 'Color ' + (i + 1) + ': ' + color + (inUse ? '' : ' (not used)'))
|
|
22629
|
+
.classed('selected', i == selectedTile)
|
|
22630
|
+
.css('background-color', color)
|
|
22631
|
+
.appendTo(cell)
|
|
22632
|
+
.on('click', function() {
|
|
22633
|
+
if (tileDrag && tileDrag.moved) return;
|
|
22634
|
+
selectTile(i, color);
|
|
22635
|
+
});
|
|
22636
|
+
if (categorical) {
|
|
22637
|
+
tileEl.on('pointerdown', function(e) {
|
|
22638
|
+
startTileDrag(e, i);
|
|
22639
|
+
});
|
|
22640
|
+
}
|
|
22641
|
+
if (removable && (i === 0 || i == n - 1)) {
|
|
22642
|
+
El('button').addClass('color-scheme-tile-remove').attr('type', 'button')
|
|
22643
|
+
.attr('aria-label', 'Remove this color').text('×').appendTo(tileEl)
|
|
22644
|
+
.on('click', function(e) {
|
|
22645
|
+
e.stopPropagation();
|
|
22646
|
+
changeScheme(removeEndTile(scheme, i === 0 ? 'left' : 'right'));
|
|
22647
|
+
});
|
|
22648
|
+
}
|
|
22649
|
+
if (groups && inUse || classLabels && classLabels[i]) {
|
|
22650
|
+
tileEl.attr('data-tooltip', groups ? formatCategories(groups[i]) : classLabels[i])
|
|
22651
|
+
.on('mouseenter', function() { keepTooltipInWindow(tileEl.node()); });
|
|
22652
|
+
}
|
|
22653
|
+
if (diverging && !continuous) {
|
|
22654
|
+
// the bar under the tiles that classes use; a gap at the center
|
|
22655
|
+
// when there is no pivot class (continuous ramps have the gradient)
|
|
22656
|
+
El('div').addClass('color-scheme-bar').appendTo(cell)
|
|
22657
|
+
.classed('used', inUse)
|
|
22658
|
+
.classed('joined', inUse && i < n - 1 && tileUse[i + 1]);
|
|
22659
|
+
}
|
|
22660
|
+
var pin = El('div').addClass('color-scheme-pin').appendTo(cell)
|
|
22661
|
+
.on('mouseenter', function() { keepTooltipInWindow(pin.node()); });
|
|
22662
|
+
var end = i === 0 || i == n - 1 || i == center;
|
|
22663
|
+
if (categorical) {
|
|
22664
|
+
// a bar under the swatches in use, unbroken to the end of each row
|
|
22665
|
+
pin.classed('used', inUse)
|
|
22666
|
+
.classed('joined', inUse && i < used - 1 && (i + 1) % perRow > 0);
|
|
22667
|
+
// a pinned tile between the ends needs its dot for unpinning, even if
|
|
22668
|
+
// vibrance was clipped
|
|
22669
|
+
} else if (tile.pinned && !end) {
|
|
22670
|
+
pin.addClass('pinned').attr('role', 'button').attr('aria-label', 'Unpin this color')
|
|
22671
|
+
.attr('data-tooltip', 'Unpin this color')
|
|
22672
|
+
.on('click', function() {
|
|
22673
|
+
changeScheme(clearTileColor(scheme, i));
|
|
22674
|
+
});
|
|
22675
|
+
} else if (tile.adjusted) {
|
|
22676
|
+
pin.addClass('adjusted').attr('data-tooltip', describeAdjustment(tile));
|
|
22677
|
+
}
|
|
22678
|
+
});
|
|
22679
|
+
renderGradient(continuous ? getContinuousSegments(scheme) : null, n);
|
|
22680
|
+
}
|
|
22681
|
+
|
|
22682
|
+
// The gradient over a continuous ramp's tiles, each stop over the center
|
|
22683
|
+
// of its tile; one bar per side of a diverging ramp, and one for a pivot
|
|
22684
|
+
// class
|
|
22685
|
+
function renderGradient(segments, n) {
|
|
22686
|
+
var width = tilesEl.node().clientWidth;
|
|
22687
|
+
var gap = 3;
|
|
22688
|
+
var tileWidth = (width - (n - 1) * gap) / n;
|
|
22689
|
+
var center = function(pos) { return pos * (tileWidth + gap) + tileWidth / 2; };
|
|
22690
|
+
gradientEl.empty();
|
|
22691
|
+
gradientEl.classed('hidden', !segments);
|
|
22692
|
+
if (!segments || !(width > 0)) return;
|
|
22693
|
+
segments.forEach(function(seg) {
|
|
22694
|
+
var left = seg.flatStart ? center(seg.start) - tileWidth / 2 : center(seg.start);
|
|
22695
|
+
var right = seg.flatEnd ? center(seg.end) + tileWidth / 2 : center(seg.end);
|
|
22696
|
+
var stops = seg.samples.map(function(sample) {
|
|
22697
|
+
return sample.color + ' ' + round(center(sample.pos) - left) + 'px';
|
|
22698
|
+
});
|
|
22699
|
+
if (stops.length == 1) stops.push(stops[0]);
|
|
22700
|
+
El('div').addClass('color-scheme-gradient-segment').appendTo(gradientEl).css({
|
|
22701
|
+
left: round(left) + 'px',
|
|
22702
|
+
width: round(right - left) + 'px',
|
|
22703
|
+
'background-image': 'linear-gradient(to right, ' + stops.join(', ') + ')'
|
|
22704
|
+
});
|
|
22705
|
+
});
|
|
22706
|
+
}
|
|
22707
|
+
|
|
22708
|
+
function round(val) {
|
|
22709
|
+
return Math.round(val * 10) / 10;
|
|
22710
|
+
}
|
|
22711
|
+
|
|
22712
|
+
// The data value at each stop of a continuous ramp, and the range of a
|
|
22713
|
+
// pivot class
|
|
22714
|
+
function getStopLabels() {
|
|
22715
|
+
var stops = getContinuousTileStops(scheme, targetLayer) || [];
|
|
22716
|
+
return stops.map(function(stop) {
|
|
22717
|
+
if (!stop) return null;
|
|
22718
|
+
if (stop.range) return formatClassRange(stop.range);
|
|
22719
|
+
return formatNumber(stop.value) + (stop.min ? ' (min)' : stop.max ? ' (max)' : '');
|
|
22720
|
+
});
|
|
22721
|
+
}
|
|
22722
|
+
|
|
22723
|
+
// The data range of each tile's class, for the tiles that classes use
|
|
22724
|
+
// (sequential tiles are all used, one class each)
|
|
22725
|
+
function getClassLabels(tileUse) {
|
|
22726
|
+
var ranges, j = 0;
|
|
22727
|
+
if (scheme.type == 'sequential') {
|
|
22728
|
+
ranges = getSequentialClassRanges(scheme, targetLayer) || [];
|
|
22729
|
+
return ranges.map(formatClassRange);
|
|
22730
|
+
}
|
|
22731
|
+
ranges = getDivergingClassRanges(scheme.layout);
|
|
22732
|
+
return tileUse.map(function(used) {
|
|
22733
|
+
return used ? formatClassRange(ranges[j++]) : null;
|
|
22734
|
+
});
|
|
22735
|
+
}
|
|
22736
|
+
|
|
22737
|
+
function formatClassRange(range) {
|
|
22738
|
+
if (!range) return null;
|
|
22739
|
+
if (range[0] == -Infinity && range[1] == Infinity) return 'All values';
|
|
22740
|
+
if (range[0] == -Infinity) return 'Below ' + formatNumber(range[1]);
|
|
22741
|
+
if (range[1] == Infinity) return formatNumber(range[0]) + ' and above';
|
|
22742
|
+
return formatNumber(range[0]) + ' to ' + formatNumber(range[1]);
|
|
22743
|
+
}
|
|
22744
|
+
|
|
22745
|
+
// Dragging a categorical swatch moves it to where it's dropped, which may
|
|
22746
|
+
// be into or out of the swatches in use
|
|
22747
|
+
function startTileDrag(e, from) {
|
|
22748
|
+
var x0 = e.clientX, y0 = e.clientY;
|
|
22749
|
+
var drag = tileDrag = {from: from, moved: false, before: -1};
|
|
22750
|
+
if (e.button !== 0) return;
|
|
22751
|
+
document.addEventListener('pointermove', onMove);
|
|
22752
|
+
document.addEventListener('pointerup', onUp);
|
|
22753
|
+
document.addEventListener('pointercancel', onCancel);
|
|
22754
|
+
|
|
22755
|
+
function onMove(e) {
|
|
22756
|
+
if (!drag.moved && Math.abs(e.clientX - x0) + Math.abs(e.clientY - y0) < 5) return;
|
|
22757
|
+
if (!drag.moved) {
|
|
22758
|
+
drag.moved = true;
|
|
22759
|
+
tilesEl.addClass('dragging');
|
|
22760
|
+
getTileCells()[from].classList.add('drag-source');
|
|
22761
|
+
}
|
|
22762
|
+
drag.before = findDropPlace(e.clientX, e.clientY);
|
|
22763
|
+
showDropMarker(drag.before);
|
|
22764
|
+
}
|
|
22765
|
+
|
|
22766
|
+
function onUp() {
|
|
22767
|
+
var to = drag.before > from ? drag.before - 1 : drag.before;
|
|
22768
|
+
finish();
|
|
22769
|
+
if (drag.moved && drag.before > -1 && to != from) {
|
|
22770
|
+
picker.hide();
|
|
22771
|
+
selectedTile = -1;
|
|
22772
|
+
changeScheme(moveSwatch(scheme, from, to));
|
|
22773
|
+
}
|
|
22774
|
+
// the click that follows a drag is ignored, then dragging is over
|
|
22775
|
+
setTimeout(function() {
|
|
22776
|
+
if (tileDrag == drag) tileDrag = null;
|
|
22777
|
+
}, 0);
|
|
22778
|
+
}
|
|
22779
|
+
|
|
22780
|
+
function onCancel() {
|
|
22781
|
+
finish();
|
|
22782
|
+
tileDrag = null;
|
|
22783
|
+
}
|
|
22784
|
+
|
|
22785
|
+
function finish() {
|
|
22786
|
+
document.removeEventListener('pointermove', onMove);
|
|
22787
|
+
document.removeEventListener('pointerup', onUp);
|
|
22788
|
+
document.removeEventListener('pointercancel', onCancel);
|
|
22789
|
+
tilesEl.removeClass('dragging');
|
|
22790
|
+
getTileCells().forEach(function(cell) { cell.classList.remove('drag-source'); });
|
|
22791
|
+
dropMarker.hide();
|
|
22792
|
+
}
|
|
22793
|
+
}
|
|
22794
|
+
|
|
22795
|
+
function getTileCells() {
|
|
22796
|
+
return Array.from(tilesEl.node().querySelectorAll('.color-scheme-tile-cell'));
|
|
22797
|
+
}
|
|
22798
|
+
|
|
22799
|
+
// The index of the tile that a swatch dropped at (x, y) goes in front of
|
|
22800
|
+
// (the number of tiles, after the last), on the row nearest the pointer
|
|
22801
|
+
function findDropPlace(x, y) {
|
|
22802
|
+
var rects = getTileCells().map(function(cell) {
|
|
22803
|
+
return cell.querySelector('.color-scheme-tile').getBoundingClientRect();
|
|
22804
|
+
});
|
|
22805
|
+
var best = -1, bestDist = Infinity;
|
|
22806
|
+
rects.forEach(function(rect, i) {
|
|
22807
|
+
var dy = y < rect.top ? rect.top - y : y > rect.bottom ? y - rect.bottom : 0;
|
|
22808
|
+
var dx = x < rect.left ? rect.left - x : x > rect.right ? x - rect.right : 0;
|
|
22809
|
+
var dist = dy * 1000 + dx;
|
|
22810
|
+
if (dist < bestDist) {
|
|
22811
|
+
bestDist = dist;
|
|
22812
|
+
best = x < (rect.left + rect.right) / 2 ? i : i + 1;
|
|
22813
|
+
}
|
|
22814
|
+
});
|
|
22815
|
+
return best;
|
|
22816
|
+
}
|
|
22817
|
+
|
|
22818
|
+
// A line in the gap where the swatch would go
|
|
22819
|
+
function showDropMarker(before) {
|
|
22820
|
+
var cells = getTileCells();
|
|
22821
|
+
var box = tilesEl.node().getBoundingClientRect();
|
|
22822
|
+
var ref = cells[Math.min(before, cells.length - 1)].querySelector('.color-scheme-tile');
|
|
22823
|
+
var rect = ref.getBoundingClientRect();
|
|
22824
|
+
var left = before < cells.length ? rect.left - 2 : rect.right + 1;
|
|
22825
|
+
dropMarker.css({
|
|
22826
|
+
left: (left - box.left) + 'px',
|
|
22827
|
+
top: (rect.top - box.top) + 'px',
|
|
22828
|
+
height: rect.height + 'px'
|
|
22829
|
+
}).show();
|
|
22830
|
+
}
|
|
22831
|
+
|
|
22832
|
+
// A tooltip is centered under its pin or button, unless that would run it past the
|
|
22833
|
+
// edge of the window
|
|
22834
|
+
function keepTooltipInWindow(el) {
|
|
22835
|
+
var margin = 4;
|
|
22836
|
+
el.style.removeProperty('--tooltip-shift');
|
|
22837
|
+
if (!el.hasAttribute('data-tooltip')) return;
|
|
22838
|
+
var width = parseFloat(getComputedStyle(el, '::after').width) || 0;
|
|
22839
|
+
var rect = el.getBoundingClientRect();
|
|
22840
|
+
var center = rect.left + rect.width / 2;
|
|
22841
|
+
var left = center - width / 2;
|
|
22842
|
+
var right = center + width / 2;
|
|
22843
|
+
var maxRight = document.documentElement.clientWidth - margin;
|
|
22844
|
+
var shift = 0;
|
|
22845
|
+
if (right > maxRight) shift = maxRight - right;
|
|
22846
|
+
else if (left < margin) shift = margin - left;
|
|
22847
|
+
if (shift) el.style.setProperty('--tooltip-shift', shift + 'px');
|
|
22848
|
+
}
|
|
22849
|
+
|
|
22850
|
+
function formatCategories(values) {
|
|
22851
|
+
// "and 1 more" would take a line that the value itself can have
|
|
22852
|
+
var max = values.length == 5 ? 5 : 4;
|
|
22853
|
+
var labels = values.slice(0, max).map(function(val) {
|
|
22854
|
+
return val === '' || val === null || val === undefined ? '(no value)' : String(val);
|
|
22855
|
+
});
|
|
22856
|
+
if (values.length > max) labels.push('and ' + (values.length - max) + ' more');
|
|
22857
|
+
return labels.join('\n');
|
|
22858
|
+
}
|
|
22859
|
+
|
|
22860
|
+
function describeAdjustment(tile) {
|
|
22861
|
+
var parts = [];
|
|
22862
|
+
var dl = tile.l - tile.ideal.l;
|
|
22863
|
+
var dc = tile.c - tile.ideal.c;
|
|
22864
|
+
if (Math.abs(dl) >= 0.0005) parts.push('lightness ' + formatShift(dl));
|
|
22865
|
+
if (Math.abs(dc) >= 0.0005) parts.push('chroma ' + formatShift(dc));
|
|
22866
|
+
return 'Shifted to fit the sRGB gamut:\n' + parts.join(', ');
|
|
22867
|
+
}
|
|
22868
|
+
|
|
22869
|
+
function formatShift(val) {
|
|
22870
|
+
return (val > 0 ? '+' : '') + val.toFixed(3);
|
|
22871
|
+
}
|
|
22872
|
+
|
|
22873
|
+
function selectTile(i, color) {
|
|
22874
|
+
nullControl.picker.hide();
|
|
22875
|
+
if (selectedTile == i && picker.visible()) {
|
|
22876
|
+
picker.hide();
|
|
22877
|
+
} else {
|
|
22878
|
+
selectedTile = i;
|
|
22879
|
+
picker.setColor(getTileEditColor(scheme, i) || color);
|
|
22880
|
+
if (!picker.visible()) picker.toggle();
|
|
22881
|
+
}
|
|
22882
|
+
render();
|
|
22883
|
+
}
|
|
22884
|
+
|
|
22885
|
+
function togglePaletteMenu() {
|
|
22886
|
+
if (paletteMenu.visible()) {
|
|
22887
|
+
hidePaletteMenu();
|
|
22888
|
+
} else {
|
|
22889
|
+
renderPaletteMenu();
|
|
22890
|
+
paletteMenu.show();
|
|
22891
|
+
}
|
|
22892
|
+
}
|
|
22893
|
+
|
|
22894
|
+
function hidePaletteMenu() {
|
|
22895
|
+
paletteMenu.hide();
|
|
22896
|
+
}
|
|
22897
|
+
|
|
22898
|
+
function renderPaletteMenu() {
|
|
22899
|
+
var categorical = scheme.type == 'categorical';
|
|
22900
|
+
paletteMenu.empty();
|
|
22901
|
+
addPaletteItem('Custom', getSchemeColors(makeSchemeCustom(scheme)), !scheme.preset, function() {
|
|
22902
|
+
changeScheme(makeSchemeCustom(scheme));
|
|
22903
|
+
});
|
|
22904
|
+
getPresetGroups(scheme.type).forEach(function(group) {
|
|
22905
|
+
El('div').addClass('color-scheme-palette-heading').appendTo(paletteMenu).text(group.source);
|
|
22906
|
+
group.names.forEach(function(name) {
|
|
22907
|
+
var colors = categorical ? getCategoricalPresetColors(name) : getPresetColors(name, 7);
|
|
22908
|
+
addPaletteItem(name, colors, scheme.preset == name, function() {
|
|
22909
|
+
changeScheme(choosePreset(scheme, name, getCategoryCount()));
|
|
22910
|
+
});
|
|
22911
|
+
});
|
|
22912
|
+
});
|
|
22913
|
+
}
|
|
22914
|
+
|
|
22915
|
+
function addPaletteItem(name, colors, selected, onSelect) {
|
|
22916
|
+
var item = El('div').addClass('color-scheme-palette-item').attr('role', 'button')
|
|
22917
|
+
.classed('selected', selected).appendTo(paletteMenu)
|
|
22918
|
+
.on('click', function() {
|
|
22919
|
+
hidePaletteMenu();
|
|
22920
|
+
onSelect();
|
|
22921
|
+
});
|
|
22922
|
+
El('div').addClass('color-scheme-strip').appendTo(item)
|
|
22923
|
+
.css('background-image', getStripBackground(colors));
|
|
22924
|
+
El('span').appendTo(item).text(name);
|
|
22925
|
+
}
|
|
22926
|
+
|
|
22927
|
+
function changeScheme(next) {
|
|
22928
|
+
var prevSize = getSchemeTiles(scheme).length;
|
|
22929
|
+
scheme = updateDivergingLayout(next, targetLayer);
|
|
22930
|
+
if (getSchemeTiles(scheme).length != prevSize) {
|
|
22931
|
+
picker.hide();
|
|
22932
|
+
selectedTile = -1;
|
|
22933
|
+
}
|
|
22934
|
+
apply();
|
|
22935
|
+
render();
|
|
22936
|
+
}
|
|
22937
|
+
|
|
22938
|
+
function apply() {
|
|
22939
|
+
var lyr = targetLayer;
|
|
22940
|
+
var applying = scheme;
|
|
22941
|
+
var colors = getAppliedColors(scheme, getCategoryCount());
|
|
22942
|
+
var cmd, extra;
|
|
22943
|
+
if (!lyr || !canApply(scheme) || !gui.console) return;
|
|
22944
|
+
getSession();
|
|
22945
|
+
if (!session || !session.isPending()) {
|
|
22946
|
+
sessionLayer = lyr;
|
|
22947
|
+
baseScheme = getLayerScheme(lyr);
|
|
22948
|
+
}
|
|
22949
|
+
cmd = formatSchemeCommand(scheme, colors, {target: getTargetOption(lyr)});
|
|
22950
|
+
extra = opts.getExtraCommands ? opts.getExtraCommands() : '';
|
|
22951
|
+
if (extra) cmd += ' ' + extra;
|
|
22952
|
+
runGuiEditCommand(gui, cmd, {
|
|
22953
|
+
title: 'Color scheme',
|
|
22954
|
+
session: session,
|
|
22955
|
+
onDone: function(err) {
|
|
22956
|
+
if (!err) setLayerScheme(lyr, getAppliedScheme(applying, colors, lyr));
|
|
22957
|
+
opts.onUpdate();
|
|
22958
|
+
}
|
|
22959
|
+
});
|
|
22960
|
+
}
|
|
22961
|
+
|
|
22962
|
+
// The scheme on each side of the session's undo state, to go with the fills
|
|
22963
|
+
// that undo and redo put back.
|
|
22964
|
+
function onCommit() {
|
|
22965
|
+
var lyr = sessionLayer;
|
|
22966
|
+
var before = baseScheme;
|
|
22967
|
+
var after = lyr ? getLayerScheme(lyr) : null;
|
|
22968
|
+
if (!lyr) return null;
|
|
22969
|
+
return {
|
|
22970
|
+
onUndo: function() {
|
|
22971
|
+
setLayerScheme(lyr, before);
|
|
22972
|
+
},
|
|
22973
|
+
onRedo: function() {
|
|
22974
|
+
setLayerScheme(lyr, after);
|
|
22975
|
+
}
|
|
22976
|
+
};
|
|
22977
|
+
}
|
|
22978
|
+
|
|
22979
|
+
function getTargetOption(lyr) {
|
|
22980
|
+
var active = gui.model.getActiveLayer();
|
|
22981
|
+
if (active && active.layer == lyr) return '';
|
|
22982
|
+
return internal.formatOptionValue(internal.getLayerTargetId(gui.model, lyr));
|
|
22983
|
+
}
|
|
22984
|
+
|
|
22985
|
+
function releaseFocus() {
|
|
22986
|
+
releasePanelFocus(panel.node());
|
|
22987
|
+
}
|
|
22988
|
+
}
|
|
22989
|
+
|
|
22990
|
+
// A scheme's colors as a CSS background: blocks, or for a continuous scheme,
|
|
22991
|
+
// its gradients, side by side
|
|
22992
|
+
function getSchemeStripBackground(scheme) {
|
|
22993
|
+
return isContinuousScheme(scheme) ? getGradientStripBackground(scheme) : getStripBackground(scheme.colors);
|
|
22994
|
+
}
|
|
22995
|
+
|
|
22996
|
+
// Each gradient takes room for its length, from its first stop to its last
|
|
22997
|
+
// (see getContinuousSegments()), without the flat ends the panel's bar has
|
|
22998
|
+
// over its end tiles; a pivot class takes a tile's width. Hard edges between
|
|
22999
|
+
// them.
|
|
23000
|
+
function getGradientStripBackground(scheme) {
|
|
23001
|
+
var segments = getContinuousSegments(scheme);
|
|
23002
|
+
var lengths = segments.map(function(seg) {
|
|
23003
|
+
return seg.end > seg.start ? seg.end - seg.start : 1;
|
|
23004
|
+
});
|
|
23005
|
+
var total = lengths.reduce(function(memo, len) { return memo + len; }, 0);
|
|
23006
|
+
var offset = 0;
|
|
23007
|
+
var stops = [];
|
|
23008
|
+
var pct = function(pos) { return (Math.round(pos / total * 10000) / 100) + '%'; };
|
|
23009
|
+
if (!(total > 0)) return 'none';
|
|
23010
|
+
segments.forEach(function(seg, i) {
|
|
23011
|
+
var first = seg.samples[0].color;
|
|
23012
|
+
var last = seg.samples[seg.samples.length - 1].color;
|
|
23013
|
+
stops.push(first + ' ' + pct(offset));
|
|
23014
|
+
seg.samples.forEach(function(sample) {
|
|
23015
|
+
stops.push(sample.color + ' ' + pct(offset + sample.pos - seg.start));
|
|
23016
|
+
});
|
|
23017
|
+
offset += lengths[i];
|
|
23018
|
+
stops.push(last + ' ' + pct(offset));
|
|
23019
|
+
});
|
|
23020
|
+
return 'linear-gradient(to right, ' + stops.join(', ') + ')';
|
|
23021
|
+
}
|
|
23022
|
+
|
|
23023
|
+
// A row of hard-edged color blocks, as a CSS background
|
|
23024
|
+
function getStripBackground(colors) {
|
|
23025
|
+
var n = colors.length;
|
|
23026
|
+
var stops = colors.map(function(color, i) {
|
|
23027
|
+
return color + ' ' + (i / n * 100) + '%, ' + color + ' ' + ((i + 1) / n * 100) + '%';
|
|
23028
|
+
});
|
|
23029
|
+
return 'linear-gradient(to right, ' + stops.join(', ') + ')';
|
|
23030
|
+
}
|
|
23031
|
+
|
|
20684
23032
|
var savedStylesKey = 'layer_style_presets';
|
|
20685
|
-
var styleFields = ['stroke', 'stroke-width', 'stroke-opacity', 'stroke-dasharray', 'line-start', 'line-end', 'line-end-size', 'fill', 'fill-opacity', 'fill-pattern'].concat(internal.svg.glowFields);
|
|
23033
|
+
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);
|
|
20686
23034
|
var arrowShapes = [{
|
|
20687
23035
|
name: 'arrow',
|
|
20688
23036
|
title: 'solid arrowheads'
|
|
@@ -20713,14 +23061,28 @@
|
|
|
20713
23061
|
label: 'Both',
|
|
20714
23062
|
title: 'arrowheads at both ends of the line'
|
|
20715
23063
|
}];
|
|
23064
|
+
var lineCaps = [{
|
|
23065
|
+
name: 'round',
|
|
23066
|
+
label: 'Round',
|
|
23067
|
+
title: 'round line caps'
|
|
23068
|
+
}, {
|
|
23069
|
+
name: 'butt',
|
|
23070
|
+
label: 'Butt',
|
|
23071
|
+
title: 'flat line caps that end at the line\'s endpoints'
|
|
23072
|
+
}, {
|
|
23073
|
+
name: 'square',
|
|
23074
|
+
label: 'Square',
|
|
23075
|
+
title: 'flat line caps that extend past the line\'s endpoints'
|
|
23076
|
+
}];
|
|
20716
23077
|
|
|
20717
23078
|
function LayerStyleTool(gui) {
|
|
20718
23079
|
var parent = gui.container.findChild('.mshp-main-map');
|
|
20719
23080
|
var panel = El('div').addClass('label-style-panel layer-style-panel rollover').appendTo(parent).hide();
|
|
20720
|
-
var title, editingStatus, clearLink, strokeControl, fillControl, strokeWidthField, dashControl, arrowControl,
|
|
23081
|
+
var title, editingStatus, clearLink, strokeControl, fillControl, strokeWidthField, capControl, dashControl, arrowControl, presetControl, patternControl, glowControl, hit;
|
|
23082
|
+
var schemeBtn, schemeStrip, schemePanel;
|
|
20721
23083
|
var targetLayer = null;
|
|
20722
23084
|
// What the arrowhead switch turns on, for lines that have no heads
|
|
20723
|
-
var lastArrow = {shape: 'arrow', position: 'end'};
|
|
23085
|
+
var lastArrow = {shape: 'arrow', position: 'end', fade: 0};
|
|
20724
23086
|
|
|
20725
23087
|
initPanel();
|
|
20726
23088
|
hit = gui.map.getHitControl && gui.map.getHitControl();
|
|
@@ -20771,6 +23133,7 @@
|
|
|
20771
23133
|
}
|
|
20772
23134
|
|
|
20773
23135
|
function turnOff() {
|
|
23136
|
+
schemePanel.close();
|
|
20774
23137
|
panel.hide();
|
|
20775
23138
|
strokeControl.picker.hide();
|
|
20776
23139
|
fillControl.picker.hide();
|
|
@@ -20809,9 +23172,11 @@
|
|
|
20809
23172
|
strokeControl = addColorControl(panel, 'Stroke', 'stroke', '#000000');
|
|
20810
23173
|
strokeWidthField = addStrokeWidthControl(strokeControl.aside);
|
|
20811
23174
|
fillControl = addColorControl(panel, 'Fill', 'fill', '');
|
|
20812
|
-
|
|
20813
|
-
|
|
20814
|
-
|
|
23175
|
+
addColorSchemeControl();
|
|
23176
|
+
var lineRow = El('div').addClass('label-style-row label-split-row layer-line-row').appendTo(panel);
|
|
23177
|
+
capControl = addLineCapControl(lineRow);
|
|
23178
|
+
dashControl = addDashArrayControl(lineRow);
|
|
23179
|
+
dashControl.row = lineRow;
|
|
20815
23180
|
arrowControl = addArrowControl(panel);
|
|
20816
23181
|
|
|
20817
23182
|
var buttonRow = El('div').addClass('label-style-row label-panel-button-row').appendTo(panel);
|
|
@@ -20870,6 +23235,74 @@
|
|
|
20870
23235
|
return control;
|
|
20871
23236
|
}
|
|
20872
23237
|
|
|
23238
|
+
// Fills colored by a data field. While a scheme is applied, its colors take
|
|
23239
|
+
// the place of the fill's swatch and hex value, which have no one value to
|
|
23240
|
+
// show; the opacity still applies. Clicking them reopens the scheme.
|
|
23241
|
+
function addColorSchemeControl() {
|
|
23242
|
+
var box = fillControl.input.parent();
|
|
23243
|
+
schemeStrip = El('div').addClass('layer-scheme-strip').attr('role', 'button')
|
|
23244
|
+
.attr('aria-label', 'Edit color scheme');
|
|
23245
|
+
box.node().insertBefore(schemeStrip.node(), fillControl.input.node());
|
|
23246
|
+
schemeStrip.on('click', openColorSchemePanel);
|
|
23247
|
+
El('div').addClass('layer-scheme-colors').appendTo(schemeStrip);
|
|
23248
|
+
El('div').addClass('layer-scheme-remove').attr('role', 'button')
|
|
23249
|
+
.attr('aria-label', 'Remove color scheme').attr('title', 'Remove color scheme')
|
|
23250
|
+
.text('×').appendTo(schemeStrip)
|
|
23251
|
+
.on('click', function(e) {
|
|
23252
|
+
e.stopPropagation();
|
|
23253
|
+
removeColorScheme();
|
|
23254
|
+
});
|
|
23255
|
+
schemeBtn = makePanelActionButton(fillControl.aside, 'Palettes', openColorSchemePanel)
|
|
23256
|
+
.addClass('layer-palettes-btn');
|
|
23257
|
+
schemePanel = new ColorSchemePanel(gui, {
|
|
23258
|
+
getExtraCommands: function() {
|
|
23259
|
+
if (!targetLayer) return '';
|
|
23260
|
+
return formatStyleEditCommands(patternControl.getRefillExpressionEdits(getAllFeatureIds(targetLayer)));
|
|
23261
|
+
},
|
|
23262
|
+
getSelectionCount: function() {
|
|
23263
|
+
return getSelectionIds().length;
|
|
23264
|
+
},
|
|
23265
|
+
onUpdate: function() {
|
|
23266
|
+
if (panel.visible()) updateControls();
|
|
23267
|
+
},
|
|
23268
|
+
onClose: function() {
|
|
23269
|
+
panel.removeClass('replaced');
|
|
23270
|
+
}
|
|
23271
|
+
});
|
|
23272
|
+
}
|
|
23273
|
+
|
|
23274
|
+
function openColorSchemePanel() {
|
|
23275
|
+
syncTargetLayer();
|
|
23276
|
+
if (!targetLayer || targetLayer.geometry_type != 'polygon') return;
|
|
23277
|
+
fillControl.picker.hide();
|
|
23278
|
+
strokeControl.picker.hide();
|
|
23279
|
+
patternControl.hidePicker();
|
|
23280
|
+
// The scheme panel takes this panel's place until it closes
|
|
23281
|
+
panel.addClass('replaced');
|
|
23282
|
+
schemePanel.open(targetLayer);
|
|
23283
|
+
}
|
|
23284
|
+
|
|
23285
|
+
// Back to a single fill, unset, as when the fill field is emptied
|
|
23286
|
+
function removeColorScheme() {
|
|
23287
|
+
schemePanel.close();
|
|
23288
|
+
applyColorControlStyle(fillControl, '');
|
|
23289
|
+
}
|
|
23290
|
+
|
|
23291
|
+
function updateColorSchemeControl() {
|
|
23292
|
+
var isPolygon = targetLayer && targetLayer.geometry_type == 'polygon';
|
|
23293
|
+
var scheme = isPolygon ? getLayerScheme(targetLayer) : null;
|
|
23294
|
+
schemeBtn.classed('hidden', !isPolygon);
|
|
23295
|
+
fillControl.input.parent().classed('has-scheme', !!scheme);
|
|
23296
|
+
schemeBtn.classed('selected', schemePanel.isOpen());
|
|
23297
|
+
if (scheme) {
|
|
23298
|
+
schemeStrip.findChild('.layer-scheme-colors')
|
|
23299
|
+
.css('background-image', getSchemeStripBackground(scheme));
|
|
23300
|
+
schemeStrip.attr('title', scheme.method == 'non-adjacent' ?
|
|
23301
|
+
'Neighbors colored differently' : 'Colored by ' + scheme.field);
|
|
23302
|
+
fillControl.picker.hide();
|
|
23303
|
+
}
|
|
23304
|
+
}
|
|
23305
|
+
|
|
20873
23306
|
// In the narrow column beside the stroke's colour. Stepping runs up a ladder
|
|
20874
23307
|
// of widths rather than by a fixed amount, because the useful ones are close
|
|
20875
23308
|
// together at the hairline end and far apart above 2px.
|
|
@@ -20889,12 +23322,60 @@
|
|
|
20889
23322
|
});
|
|
20890
23323
|
}
|
|
20891
23324
|
|
|
20892
|
-
//
|
|
20893
|
-
//
|
|
20894
|
-
function
|
|
20895
|
-
var
|
|
20896
|
-
var
|
|
20897
|
-
El('
|
|
23325
|
+
// Round is the default, except on a dashed line, which the renderers give
|
|
23326
|
+
// butt caps unless it has a cap of its own.
|
|
23327
|
+
function addLineCapControl(row) {
|
|
23328
|
+
var cell = El('div').addClass('label-split-cell label-align-row').appendTo(row);
|
|
23329
|
+
var control = {btns: {}};
|
|
23330
|
+
El('span').appendTo(cell).text('Caps');
|
|
23331
|
+
var group = El('div').addClass('label-btn-group layer-cap-buttons').appendTo(cell);
|
|
23332
|
+
lineCaps.forEach(function(item) {
|
|
23333
|
+
control.btns[item.name] = makePanelButton(group, item.label, function() {
|
|
23334
|
+
applyLineCap(item.name);
|
|
23335
|
+
}).attr('data-line-cap', item.name).attr('aria-label', item.title);
|
|
23336
|
+
});
|
|
23337
|
+
return control;
|
|
23338
|
+
}
|
|
23339
|
+
|
|
23340
|
+
function getDefaultLineCap(rec) {
|
|
23341
|
+
return rec && rec['stroke-dasharray'] ? 'butt' : 'round';
|
|
23342
|
+
}
|
|
23343
|
+
|
|
23344
|
+
function getLineCap(rec) {
|
|
23345
|
+
return rec && rec['stroke-linecap'] || getDefaultLineCap(rec);
|
|
23346
|
+
}
|
|
23347
|
+
|
|
23348
|
+
// A cap that is the line's default is unset rather than stored, so that a
|
|
23349
|
+
// layer left at Round gets no stroke-linecap column.
|
|
23350
|
+
function applyLineCap(cap) {
|
|
23351
|
+
var records = getTargetRecords();
|
|
23352
|
+
var edits = [];
|
|
23353
|
+
getTargetIds().forEach(function(id) {
|
|
23354
|
+
var rec = records[id] || {};
|
|
23355
|
+
var value = cap == getDefaultLineCap(rec) ? '' : cap;
|
|
23356
|
+
if (value != (rec['stroke-linecap'] || '')) {
|
|
23357
|
+
edits.push({id: id, styles: [['stroke-linecap', value]]});
|
|
23358
|
+
}
|
|
23359
|
+
});
|
|
23360
|
+
runStyleEdits(edits);
|
|
23361
|
+
}
|
|
23362
|
+
|
|
23363
|
+
function updateLineCapControl() {
|
|
23364
|
+
var records = getTargetRecords();
|
|
23365
|
+
var cap;
|
|
23366
|
+
getTargetIds().forEach(function(id, i) {
|
|
23367
|
+
var val = getLineCap(records[id]);
|
|
23368
|
+
cap = i === 0 || val == cap ? val : '';
|
|
23369
|
+
});
|
|
23370
|
+
lineCaps.forEach(function(item) {
|
|
23371
|
+
capControl.btns[item.name].classed('selected', item.name == cap);
|
|
23372
|
+
});
|
|
23373
|
+
}
|
|
23374
|
+
|
|
23375
|
+
// A literal stroke-dasharray, in the narrow column beside the caps. A blank
|
|
23376
|
+
// field makes the line solid again.
|
|
23377
|
+
function addDashArrayControl(row) {
|
|
23378
|
+
var cell = El('div').addClass('label-split-cell layer-dash-cell').appendTo(row);
|
|
20898
23379
|
var caption = El('div').addClass('label-style-row-label').appendTo(cell).text('Dashes');
|
|
20899
23380
|
makeFieldTip(caption,
|
|
20900
23381
|
'Dash and gap lengths in pixels, separated\n' +
|
|
@@ -20909,13 +23390,14 @@
|
|
|
20909
23390
|
}
|
|
20910
23391
|
applyDashArrayStyle(value);
|
|
20911
23392
|
});
|
|
20912
|
-
return {
|
|
23393
|
+
return {input: input};
|
|
20913
23394
|
}
|
|
20914
23395
|
|
|
20915
23396
|
// A switch in the heading says whether the lines have arrowheads; the rows
|
|
20916
23397
|
// under it are the head's shape with its size beside it, then which ends
|
|
20917
|
-
// get it. A line has one shape for both
|
|
20918
|
-
// though -style can give the two ends
|
|
23398
|
+
// get it with the line's fade beside that. A line has one shape for both
|
|
23399
|
+
// ends, which is all the panel sets, though -style can give the two ends
|
|
23400
|
+
// different ones.
|
|
20919
23401
|
function addArrowControl(parent) {
|
|
20920
23402
|
var section = makePanelSection(parent, 'Arrowheads');
|
|
20921
23403
|
var toggle = makePanelToggle(section.findChild('.label-style-section-title'), {
|
|
@@ -20928,8 +23410,8 @@
|
|
|
20928
23410
|
var sizeCell = El('div').addClass('label-split-cell label-spacing-row layer-arrow-size-row').appendTo(shapeRow);
|
|
20929
23411
|
var posRow = El('div').addClass('label-style-row label-split-row').appendTo(section);
|
|
20930
23412
|
var posCell = El('div').addClass('label-split-cell label-align-row').appendTo(posRow);
|
|
23413
|
+
var fadeCell = El('div').addClass('label-split-cell layer-arrow-fade-cell').appendTo(posRow);
|
|
20931
23414
|
var control = {section: section, toggle: toggle, shapeBtns: {}, posBtns: {}};
|
|
20932
|
-
El('div').addClass('label-split-cell').appendTo(posRow);
|
|
20933
23415
|
El('span').appendTo(shapeCell).text('Shape');
|
|
20934
23416
|
var shapeGroup = El('div').addClass('label-btn-group label-callout-buttons layer-arrow-shape-buttons').appendTo(shapeCell);
|
|
20935
23417
|
arrowShapes.forEach(function(item) {
|
|
@@ -20958,6 +23440,22 @@
|
|
|
20958
23440
|
onStep: nudgeArrowSize,
|
|
20959
23441
|
onDone: releaseFocus
|
|
20960
23442
|
});
|
|
23443
|
+
var fadeCaption = El('div').addClass('label-style-row-label').appendTo(fadeCell).text('Fade %');
|
|
23444
|
+
makeFieldTip(fadeCaption,
|
|
23445
|
+
'How much of the line fades in from its\n' +
|
|
23446
|
+
'tail, as a percent of its length. The fade\n' +
|
|
23447
|
+
'is a straight gradient, so it follows\n' +
|
|
23448
|
+
'straight and gently curved lines best.');
|
|
23449
|
+
control.fadeField = new SizeField(fadeCell, {
|
|
23450
|
+
title: 'Percent of the line that fades in from its tail',
|
|
23451
|
+
min: 0,
|
|
23452
|
+
max: 100,
|
|
23453
|
+
step: 5,
|
|
23454
|
+
bigStep: 25,
|
|
23455
|
+
onSet: applyArrowFade,
|
|
23456
|
+
onStep: nudgeArrowFade,
|
|
23457
|
+
onDone: releaseFocus
|
|
23458
|
+
});
|
|
20961
23459
|
return control;
|
|
20962
23460
|
}
|
|
20963
23461
|
|
|
@@ -20972,10 +23470,11 @@
|
|
|
20972
23470
|
return {shape: shape, position: position};
|
|
20973
23471
|
}
|
|
20974
23472
|
|
|
20975
|
-
// Switching on gives the lines that have no heads the last shape and
|
|
20976
|
-
// the section showed, so that off and on again is a round trip; the
|
|
20977
|
-
// that have heads keep theirs. Switching off leaves line-end-size
|
|
20978
|
-
// for the same reason
|
|
23473
|
+
// Switching on gives the lines that have no heads the last shape, ends and
|
|
23474
|
+
// fade the section showed, so that off and on again is a round trip; the
|
|
23475
|
+
// lines that have heads keep theirs. Switching off leaves line-end-size
|
|
23476
|
+
// alone, for the same reason, but removes the fade, which would otherwise
|
|
23477
|
+
// go on showing with its control hidden.
|
|
20979
23478
|
function setArrowsOn(on) {
|
|
20980
23479
|
applyArrowEdits(function(info) {
|
|
20981
23480
|
return on ? fillArrowInfo(info) : {shape: 'none', position: ''};
|
|
@@ -21007,7 +23506,8 @@
|
|
|
21007
23506
|
var edits = [];
|
|
21008
23507
|
getTargetIds().forEach(function(id) {
|
|
21009
23508
|
var rec = records[id] || {};
|
|
21010
|
-
var
|
|
23509
|
+
var info = getArrowInfo(rec);
|
|
23510
|
+
var next = getNext(info);
|
|
21011
23511
|
var on = next.shape != 'none';
|
|
21012
23512
|
var styles = [
|
|
21013
23513
|
['line-start', on && next.position != 'end' ? next.shape : ''],
|
|
@@ -21016,6 +23516,11 @@
|
|
|
21016
23516
|
// no need to remove what is not there
|
|
21017
23517
|
return style[1] || rec[style[0]];
|
|
21018
23518
|
});
|
|
23519
|
+
if (!on && rec['line-fade']) {
|
|
23520
|
+
styles.push(['line-fade', '']);
|
|
23521
|
+
} else if (on && info.shape == 'none' && lastArrow.fade > 0 && !rec['line-fade']) {
|
|
23522
|
+
styles.push(['line-fade', lastArrow.fade]);
|
|
23523
|
+
}
|
|
21019
23524
|
if (on && addStroke) styles.push(['stroke', strokeControl.defaultColor]);
|
|
21020
23525
|
if (styles.length > 0) edits.push({id: id, styles: styles});
|
|
21021
23526
|
});
|
|
@@ -21037,6 +23542,34 @@
|
|
|
21037
23542
|
applyArrowSize(Math.max(1, Math.round(size + delta)));
|
|
21038
23543
|
}
|
|
21039
23544
|
|
|
23545
|
+
// The field is a percent; line-fade is a share of the line, 0-1. No fade
|
|
23546
|
+
// is unset rather than stored as 0.
|
|
23547
|
+
function applyArrowFade(pct) {
|
|
23548
|
+
var ids = getArrowTargetIds();
|
|
23549
|
+
var value = pct > 0 ? Math.round(pct * 10) / 1000 : '';
|
|
23550
|
+
if (ids.length === 0) return;
|
|
23551
|
+
runStyleEdits(ids.map(function(id) {
|
|
23552
|
+
return {id: id, styles: [['line-fade', value]]};
|
|
23553
|
+
}));
|
|
23554
|
+
}
|
|
23555
|
+
|
|
23556
|
+
function nudgeArrowFade(delta) {
|
|
23557
|
+
var shown = getArrowFadeShown(getArrowTargetIds());
|
|
23558
|
+
if (shown.value === '') return;
|
|
23559
|
+
applyArrowFade(Math.max(0, Math.min(100, Math.round(shown.value + delta))));
|
|
23560
|
+
}
|
|
23561
|
+
|
|
23562
|
+
function getArrowFadeShown(ids) {
|
|
23563
|
+
var records = getTargetRecords();
|
|
23564
|
+
var value = '', pct;
|
|
23565
|
+
for (var i=0; i<ids.length; i++) {
|
|
23566
|
+
pct = Math.round(internal.svg.parseLineFade(records[ids[i]] && records[ids[i]]['line-fade']) * 1000) / 10;
|
|
23567
|
+
if (i > 0 && pct !== value) return {value: '', mixed: true};
|
|
23568
|
+
value = pct;
|
|
23569
|
+
}
|
|
23570
|
+
return {value: value, mixed: false};
|
|
23571
|
+
}
|
|
23572
|
+
|
|
21040
23573
|
function getArrowTargetIds() {
|
|
21041
23574
|
var records = getTargetRecords();
|
|
21042
23575
|
return getTargetIds().filter(function(id) {
|
|
@@ -21097,6 +23630,11 @@
|
|
|
21097
23630
|
arrowControl.sizeField.setValue(shown.value);
|
|
21098
23631
|
arrowControl.sizeField.setPlaceholder(shown.mixed ? 'mixed' : '');
|
|
21099
23632
|
arrowControl.sizeField.setDisabled(arrowIds.length === 0);
|
|
23633
|
+
var fade = arrowIds.length > 0 ? getArrowFadeShown(arrowIds) : {value: '', mixed: false};
|
|
23634
|
+
if (fade.value !== '') lastArrow.fade = fade.value / 100;
|
|
23635
|
+
arrowControl.fadeField.setValue(fade.value);
|
|
23636
|
+
arrowControl.fadeField.setPlaceholder(fade.mixed ? 'mixed' : '');
|
|
23637
|
+
arrowControl.fadeField.setDisabled(arrowIds.length === 0);
|
|
21100
23638
|
}
|
|
21101
23639
|
|
|
21102
23640
|
function getTargetRecords() {
|
|
@@ -21122,8 +23660,11 @@
|
|
|
21122
23660
|
updateColorControl(fillControl);
|
|
21123
23661
|
updateStrokeWidthControl();
|
|
21124
23662
|
updateDashArrayControl();
|
|
21125
|
-
if (geom == 'polyline')
|
|
21126
|
-
|
|
23663
|
+
if (geom == 'polyline') {
|
|
23664
|
+
updateLineCapControl();
|
|
23665
|
+
updateArrowControl();
|
|
23666
|
+
}
|
|
23667
|
+
updateColorSchemeControl();
|
|
21127
23668
|
patternControl.section.classed('hidden', geom != 'polygon');
|
|
21128
23669
|
if (geom == 'polygon') patternControl.update();
|
|
21129
23670
|
glowControl.section.classed('hidden', geom != 'polygon');
|
|
@@ -21260,6 +23801,8 @@
|
|
|
21260
23801
|
}
|
|
21261
23802
|
}
|
|
21262
23803
|
|
|
23804
|
+
// No button runs this for now; the Random fill button is waiting for a new
|
|
23805
|
+
// place in the panel.
|
|
21263
23806
|
// Patterns keep their backgrounds in step with the fills, and the new fills
|
|
21264
23807
|
// are not known until -classify has chosen them, so the patterns are
|
|
21265
23808
|
// rewritten by an expression that reads each feature's fill when it runs.
|
|
@@ -21304,6 +23847,7 @@
|
|
|
21304
23847
|
addStyleValue(style, 'stroke-opacity', parseOpacityValue(strokeControl.opacity.node().value));
|
|
21305
23848
|
if (targetLayer && targetLayer.geometry_type == 'polyline') {
|
|
21306
23849
|
addStyleValue(style, 'stroke-dasharray', getControlValue(dashControl.input));
|
|
23850
|
+
addStyleValue(style, 'stroke-linecap', getCommonStyleValue('stroke-linecap'));
|
|
21307
23851
|
addArrowStyleValues(style);
|
|
21308
23852
|
}
|
|
21309
23853
|
if (targetLayer && targetLayer.geometry_type == 'polygon') {
|
|
@@ -21318,7 +23862,7 @@
|
|
|
21318
23862
|
// Only when every target agrees, as with the other controls; a size is
|
|
21319
23863
|
// saved only if the lines have their own.
|
|
21320
23864
|
function addArrowStyleValues(style) {
|
|
21321
|
-
['line-start', 'line-end', 'line-end-size'].forEach(function(field) {
|
|
23865
|
+
['line-start', 'line-end', 'line-end-size', 'line-fade'].forEach(function(field) {
|
|
21322
23866
|
addStyleValue(style, field, getCommonStyleValue(field));
|
|
21323
23867
|
});
|
|
21324
23868
|
}
|
|
@@ -21439,6 +23983,7 @@
|
|
|
21439
23983
|
function syncTargetLayer() {
|
|
21440
23984
|
var lyr = getActiveLayer();
|
|
21441
23985
|
if (lyr == targetLayer) return;
|
|
23986
|
+
schemePanel.close();
|
|
21442
23987
|
if (layerCanBeStyled(lyr)) {
|
|
21443
23988
|
targetLayer = lyr;
|
|
21444
23989
|
if (hit) hit.clearSelection();
|
|
@@ -23972,9 +26517,20 @@
|
|
|
23972
26517
|
_self.dispatchEvent('leave');
|
|
23973
26518
|
}
|
|
23974
26519
|
|
|
26520
|
+
// mouseover and mouseout can arrive after a quick move has already pressed
|
|
26521
|
+
// and released the button somewhere else -- a click on a panel over the map
|
|
26522
|
+
// then looked like a click on the map. A press or release is over the area
|
|
26523
|
+
// if it lands on the area itself.
|
|
26524
|
+
function updateHover(e) {
|
|
26525
|
+
var over = !!e.target && element.contains(e.target);
|
|
26526
|
+
if (over && !_isOver) onAreaEnter();
|
|
26527
|
+
else if (!over && _isOver) onAreaOut();
|
|
26528
|
+
}
|
|
26529
|
+
|
|
23975
26530
|
function onMouseUp(e) {
|
|
23976
|
-
var evt
|
|
23977
|
-
|
|
26531
|
+
var evt, elapsed, dx, dy;
|
|
26532
|
+
updateHover(e);
|
|
26533
|
+
evt = procMouseEvent(e);
|
|
23978
26534
|
_self.dispatchEvent('mouseup', evt);
|
|
23979
26535
|
if (_dragging) {
|
|
23980
26536
|
stopDragging(evt);
|
|
@@ -23996,6 +26552,7 @@
|
|
|
23996
26552
|
}
|
|
23997
26553
|
|
|
23998
26554
|
function onMouseDown(e) {
|
|
26555
|
+
updateHover(e);
|
|
23999
26556
|
if (e.button != 2 && e.which != 3) { // ignore right-click
|
|
24000
26557
|
_downEvt = procMouseEvent(e);
|
|
24001
26558
|
}
|
|
@@ -31455,6 +34012,7 @@
|
|
|
31455
34012
|
style.lineStart = rec && rec['line-start'];
|
|
31456
34013
|
style.lineEnd = rec && rec['line-end'];
|
|
31457
34014
|
style.lineEndSize = rec && rec['line-end-size'];
|
|
34015
|
+
style.lineFade = rec && rec['line-fade'];
|
|
31458
34016
|
}
|
|
31459
34017
|
if (hasGlows) {
|
|
31460
34018
|
style.outerGlow = layerOuterGlow ? null : internal.svg.getPolygonGlow(rec, 'outer');
|
|
@@ -32819,9 +35377,10 @@
|
|
|
32819
35377
|
continue;
|
|
32820
35378
|
}
|
|
32821
35379
|
// A line with arrowheads is drawn on its own, trimmed to meet its heads;
|
|
32822
|
-
// see svg-line-arrows.mjs. Only lines styled with line-start
|
|
32823
|
-
// get here, so the batch below is untouched for
|
|
32824
|
-
|
|
35380
|
+
// see svg-line-arrows.mjs. Only lines styled with line-start, line-end
|
|
35381
|
+
// or line-fade get here, so the batch below is untouched for
|
|
35382
|
+
// everything else.
|
|
35383
|
+
if (drawStyle.lineStart || drawStyle.lineEnd || drawStyle.lineFade) {
|
|
32825
35384
|
if (!arrowPencil) arrowPencil = getArrowLinePencil(arcs, _ext, lineScale);
|
|
32826
35385
|
if (arrowPencil(shp, _ctx, drawStyle, startPath, draw)) continue;
|
|
32827
35386
|
}
|
|
@@ -33269,14 +35828,17 @@
|
|
|
33269
35828
|
};
|
|
33270
35829
|
}
|
|
33271
35830
|
|
|
33272
|
-
// Returns a function that draws one line feature with its arrowheads
|
|
33273
|
-
// returns false if the feature has
|
|
33274
|
-
// stroke), for the caller to draw it the usual way.
|
|
35831
|
+
// Returns a function that draws one line feature with its arrowheads and fade,
|
|
35832
|
+
// and returns false if the feature has neither to draw (a style of 'none', or
|
|
35833
|
+
// no stroke), for the caller to draw it the usual way.
|
|
33275
35834
|
//
|
|
33276
35835
|
// Heads are sized by the same factor as the line width, so that a preview
|
|
33277
35836
|
// matches the exported SVG. Far enough in that paths are clipped to the view
|
|
33278
35837
|
// (see protectIterForDrawing()), the line is drawn clipped and untrimmed and
|
|
33279
|
-
// only the heads are added, since a clipped path's ends are not its ends.
|
|
35838
|
+
// only the heads are added, since a clipped path's ends are not its ends. The
|
|
35839
|
+
// fade is placed by the whole line, so it is unaffected by the clipping.
|
|
35840
|
+
//
|
|
35841
|
+
// A faded part is stroked on its own, with its own gradient, as in SVG.
|
|
33280
35842
|
function getArrowLinePencil(arcs, ext, lineScale) {
|
|
33281
35843
|
var t = getScaledTransform(ext);
|
|
33282
35844
|
var iter = new internal.ShapeIter(arcs);
|
|
@@ -33284,10 +35846,13 @@
|
|
|
33284
35846
|
var clipped = ext.scale() > 100;
|
|
33285
35847
|
return function(shp, ctx, style, startPath, draw) {
|
|
33286
35848
|
var opts = internal.svg.makeLineArrowOpts(style.lineStart, style.lineEnd,
|
|
33287
|
-
style.lineEndSize, style.strokeWidth, strokeScale);
|
|
35849
|
+
style.lineEndSize, style.strokeWidth, strokeScale, style.lineFade);
|
|
35850
|
+
var fadeColors = opts.fade > 0 ? internal.svg.getLineFadeColors(style.strokeColor) : null;
|
|
33288
35851
|
var heads = [];
|
|
35852
|
+
var plain = [];
|
|
35853
|
+
var faded = [];
|
|
33289
35854
|
var i, coords, shape;
|
|
33290
|
-
if (opts.start == 'none' && opts.end == 'none' || !(style.strokeWidth > 0)) {
|
|
35855
|
+
if (opts.start == 'none' && opts.end == 'none' && !fadeColors || !(style.strokeWidth > 0)) {
|
|
33291
35856
|
return false;
|
|
33292
35857
|
}
|
|
33293
35858
|
startPath(ctx, style);
|
|
@@ -33296,15 +35861,38 @@
|
|
|
33296
35861
|
if (coords.length < 2) continue;
|
|
33297
35862
|
shape = internal.svg.getLineArrowShape(coords, opts);
|
|
33298
35863
|
heads = heads.concat(shape.heads);
|
|
33299
|
-
if (
|
|
35864
|
+
if (shape.fade && fadeColors) {
|
|
35865
|
+
faded.push({path: shp[i], shape: shape});
|
|
35866
|
+
} else if (clipped) {
|
|
35867
|
+
plain.push(shp[i]);
|
|
35868
|
+
} else {
|
|
35869
|
+
traceCoords(shape.coords, ctx);
|
|
35870
|
+
}
|
|
33300
35871
|
}
|
|
33301
|
-
if (clipped) draw(
|
|
35872
|
+
if (clipped && plain.length > 0) draw(plain, ctx, style);
|
|
33302
35873
|
endPath(ctx, style);
|
|
35874
|
+
for (i=0; i<faded.length; i++) {
|
|
35875
|
+
startPath(ctx, style);
|
|
35876
|
+
ctx.strokeStyle = getLineFadeGradient(ctx, faded[i].shape.fade, fadeColors);
|
|
35877
|
+
if (clipped) {
|
|
35878
|
+
draw([faded[i].path], ctx, style);
|
|
35879
|
+
} else {
|
|
35880
|
+
traceCoords(faded[i].shape.coords, ctx);
|
|
35881
|
+
}
|
|
35882
|
+
endPath(ctx, style);
|
|
35883
|
+
}
|
|
33303
35884
|
drawArrowHeads(heads, ctx, style, opts.width);
|
|
33304
35885
|
return true;
|
|
33305
35886
|
};
|
|
33306
35887
|
}
|
|
33307
35888
|
|
|
35889
|
+
function getLineFadeGradient(ctx, axis, colors) {
|
|
35890
|
+
var gradient = ctx.createLinearGradient(axis.from[0], axis.from[1], axis.to[0], axis.to[1]);
|
|
35891
|
+
gradient.addColorStop(0, colors.transparent);
|
|
35892
|
+
gradient.addColorStop(1, colors.solid);
|
|
35893
|
+
return gradient;
|
|
35894
|
+
}
|
|
35895
|
+
|
|
33308
35896
|
function getPixelCoords(iter, path, t) {
|
|
33309
35897
|
var coords = [];
|
|
33310
35898
|
var x, y, prev;
|
|
@@ -33437,7 +36025,7 @@
|
|
|
33437
36025
|
ctx.lineWidth = strokeWidth * strokeScale;
|
|
33438
36026
|
ctx.strokeStyle = style.strokeColor;
|
|
33439
36027
|
if (style.lineDash){
|
|
33440
|
-
ctx.lineCap = 'butt';
|
|
36028
|
+
ctx.lineCap = style.lineCap || 'butt';
|
|
33441
36029
|
ctx.setLineDash(scaleCanvasLineDash(style.lineDash, strokeScale));
|
|
33442
36030
|
}
|
|
33443
36031
|
if (style.miterLimit) {
|
|
@@ -36034,8 +38622,9 @@
|
|
|
36034
38622
|
var marginField = toolbar.addTextField('Margin', {
|
|
36035
38623
|
classname: 'frame-toolbar-margin-input',
|
|
36036
38624
|
placeholder: '0',
|
|
36037
|
-
tooltip: 'Padding added when fitting: 20px, 1cm, ' +
|
|
36038
|
-
|
|
38625
|
+
tooltip: 'Padding added when fitting: 2%, 20px, 1cm, or ' +
|
|
38626
|
+
'one to four values in CSS order (top right bottom left). ' +
|
|
38627
|
+
"A percentage is of the frame's width on every side."
|
|
36039
38628
|
});
|
|
36040
38629
|
toolbar.addSeparator();
|
|
36041
38630
|
toolbar.addTextButton('Done', {
|
|
@@ -36285,7 +38874,7 @@
|
|
|
36285
38874
|
return internal.getLayerTargetId(gui.model, o.layer);
|
|
36286
38875
|
}).join(','))
|
|
36287
38876
|
];
|
|
36288
|
-
if (margin) parts.push('
|
|
38877
|
+
if (margin) parts.push('margin=' + quoteCommandValue(margin));
|
|
36289
38878
|
if (!lockSize) parts.push('fix-scale');
|
|
36290
38879
|
parts.push(getTargetOption(target));
|
|
36291
38880
|
runGuiEditCommand(gui, parts.join(' '), {
|
|
@@ -36298,7 +38887,7 @@
|
|
|
36298
38887
|
// cleared so the fit that just ran matches what the toolbar shows.
|
|
36299
38888
|
function getMargin() {
|
|
36300
38889
|
var value = marginField.getValue();
|
|
36301
|
-
if (
|
|
38890
|
+
if (hasNonZeroMargin(value)) return value;
|
|
36302
38891
|
if (value) marginField.setValue('');
|
|
36303
38892
|
return '';
|
|
36304
38893
|
}
|
|
@@ -36349,8 +38938,10 @@
|
|
|
36349
38938
|
marginInput.node().value = '2%';
|
|
36350
38939
|
makeFieldTip(fitRow,
|
|
36351
38940
|
'Space around the layers: 2%, 20px, 1cm.\n' +
|
|
36352
|
-
"A percentage is of the frame's width on
|
|
36353
|
-
'side
|
|
38941
|
+
"A percentage is of the frame's width, on\n" +
|
|
38942
|
+
'every side. Use two to four values for\n' +
|
|
38943
|
+
'uneven margins, in CSS order: 4% 2%, or\n' +
|
|
38944
|
+
'8% 2% 2% 2% (top right bottom left).');
|
|
36354
38945
|
|
|
36355
38946
|
var drawRow = El('div').addClass('frame-create-option-row').appendTo(section);
|
|
36356
38947
|
addButton(drawRow, 'Draw on the map', function() {
|
|
@@ -36382,7 +38973,7 @@
|
|
|
36382
38973
|
|
|
36383
38974
|
function getMargin() {
|
|
36384
38975
|
var value = marginInput.node().value.trim();
|
|
36385
|
-
return
|
|
38976
|
+
return hasNonZeroMargin(value) ? value : '';
|
|
36386
38977
|
}
|
|
36387
38978
|
}
|
|
36388
38979
|
|
|
@@ -36462,7 +39053,7 @@
|
|
|
36462
39053
|
});
|
|
36463
39054
|
var parts = ['-frame', 'width=' + quoteCommandValue(width)];
|
|
36464
39055
|
if (aspect) parts.push('aspect-ratio=' + aspect);
|
|
36465
|
-
if (margin) parts.push('
|
|
39056
|
+
if (margin) parts.push('margin=' + quoteCommandValue(margin));
|
|
36466
39057
|
parts.push('name=frame');
|
|
36467
39058
|
parts.push('target=' + internal.formatOptionValue(ids.join(',')));
|
|
36468
39059
|
runCreateCommand(parts.join(' '));
|
|
@@ -36541,6 +39132,14 @@
|
|
|
36541
39132
|
}
|
|
36542
39133
|
}
|
|
36543
39134
|
|
|
39135
|
+
// A margin field's value is passed to the command when any of its one to four
|
|
39136
|
+
// values is positive, e.g. '0 2%'; a blank or all-zero margin is dropped.
|
|
39137
|
+
function hasNonZeroMargin(value) {
|
|
39138
|
+
return String(value || '').split(/[\s,]+/).some(function(str) {
|
|
39139
|
+
return parseFloat(str) > 0;
|
|
39140
|
+
});
|
|
39141
|
+
}
|
|
39142
|
+
|
|
36544
39143
|
function addButton(parent, label, action) {
|
|
36545
39144
|
El('div').addClass('btn dialog-btn').appendTo(parent)
|
|
36546
39145
|
.text(label).on('click', action);
|