@cesium/widgets
Version:
A widgets library for use with CesiumJS. CesiumJS is a JavaScript library for creating 3D globes and 2D maps in a web browser without a plugin.
667 lines (594 loc) • 19 kB
JavaScript
import {
addAllToArray,
binarySearch,
ClockRange,
ClockStep,
defined,
DeveloperError,
JulianDate,
} from "@cesium/engine";
import knockout from "../ThirdParty/knockout.js";
import createCommand from "../createCommand.js";
import ToggleButtonViewModel from "../ToggleButtonViewModel.js";
const monthNames = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
];
const realtimeShuttleRingAngle = 15;
const maxShuttleRingAngle = 105;
function numberComparator(left, right) {
return left - right;
}
function getTypicalMultiplierIndex(multiplier, shuttleRingTicks) {
const index = binarySearch(shuttleRingTicks, multiplier, numberComparator);
return index < 0 ? ~index : index;
}
function angleToMultiplier(angle, shuttleRingTicks) {
//Use a linear scale for -1 to 1 between -15 < angle < 15 degrees
if (Math.abs(angle) <= realtimeShuttleRingAngle) {
return angle / realtimeShuttleRingAngle;
}
const minp = realtimeShuttleRingAngle;
const maxp = maxShuttleRingAngle;
let maxv;
const minv = 0;
let scale;
if (angle > 0) {
maxv = Math.log(shuttleRingTicks[shuttleRingTicks.length - 1]);
scale = (maxv - minv) / (maxp - minp);
return Math.exp(minv + scale * (angle - minp));
}
maxv = Math.log(-shuttleRingTicks[0]);
scale = (maxv - minv) / (maxp - minp);
return -Math.exp(minv + scale * (Math.abs(angle) - minp));
}
function multiplierToAngle(multiplier, shuttleRingTicks, clockViewModel) {
if (clockViewModel.clockStep === ClockStep.SYSTEM_CLOCK) {
return realtimeShuttleRingAngle;
}
if (Math.abs(multiplier) <= 1) {
return multiplier * realtimeShuttleRingAngle;
}
const fastedMultipler = shuttleRingTicks[shuttleRingTicks.length - 1];
if (multiplier > fastedMultipler) {
multiplier = fastedMultipler;
} else if (multiplier < -fastedMultipler) {
multiplier = -fastedMultipler;
}
const minp = realtimeShuttleRingAngle;
const maxp = maxShuttleRingAngle;
let maxv;
const minv = 0;
let scale;
if (multiplier > 0) {
maxv = Math.log(fastedMultipler);
scale = (maxv - minv) / (maxp - minp);
return (Math.log(multiplier) - minv) / scale + minp;
}
maxv = Math.log(-shuttleRingTicks[0]);
scale = (maxv - minv) / (maxp - minp);
return -((Math.log(Math.abs(multiplier)) - minv) / scale + minp);
}
/**
* The view model for the {@link Animation} widget.
* @alias AnimationViewModel
* @constructor
*
* @param {ClockViewModel} clockViewModel The ClockViewModel instance to use.
*
* @see Animation
*/
function AnimationViewModel(clockViewModel) {
//>>includeStart('debug', pragmas.debug);
if (!defined(clockViewModel)) {
throw new DeveloperError("clockViewModel is required.");
}
//>>includeEnd('debug');
const that = this;
this._clockViewModel = clockViewModel;
this._allShuttleRingTicks = [];
this._dateFormatter = AnimationViewModel.defaultDateFormatter;
this._timeFormatter = AnimationViewModel.defaultTimeFormatter;
/**
* Gets or sets whether the shuttle ring is currently being dragged. This property is observable.
* @type {boolean}
* @default false
*/
this.shuttleRingDragging = false;
/**
* Gets or sets whether dragging the shuttle ring should cause the multiplier
* to snap to the defined tick values rather than interpolating between them.
* This property is observable.
* @type {boolean}
* @default false
*/
this.snapToTicks = false;
knockout.track(this, [
"_allShuttleRingTicks",
"_dateFormatter",
"_timeFormatter",
"shuttleRingDragging",
"snapToTicks",
]);
this._sortedFilteredPositiveTicks = [];
this.setShuttleRingTicks(AnimationViewModel.defaultTicks);
/**
* Gets the string representation of the current time. This property is observable.
* @type {string}
*/
this.timeLabel = undefined;
knockout.defineProperty(this, "timeLabel", function () {
return that._timeFormatter(that._clockViewModel.currentTime, that);
});
/**
* Gets the string representation of the current date. This property is observable.
* @type {string}
*/
this.dateLabel = undefined;
knockout.defineProperty(this, "dateLabel", function () {
return that._dateFormatter(that._clockViewModel.currentTime, that);
});
/**
* Gets the string representation of the current multiplier. This property is observable.
* @type {string}
*/
this.multiplierLabel = undefined;
knockout.defineProperty(this, "multiplierLabel", function () {
const clockViewModel = that._clockViewModel;
if (clockViewModel.clockStep === ClockStep.SYSTEM_CLOCK) {
return "Today";
}
const multiplier = clockViewModel.multiplier;
//If it's a whole number, just return it.
if (multiplier % 1 === 0) {
return `${multiplier.toFixed(0)}x`;
}
//Convert to decimal string and remove any trailing zeroes
return `${multiplier.toFixed(3).replace(/0{0,3}$/, "")}x`;
});
/**
* Gets or sets the current shuttle ring angle. This property is observable.
* @type {number}
*/
this.shuttleRingAngle = undefined;
knockout.defineProperty(this, "shuttleRingAngle", {
get: function () {
return multiplierToAngle(
clockViewModel.multiplier,
that._allShuttleRingTicks,
clockViewModel,
);
},
set: function (angle) {
angle = Math.max(
Math.min(angle, maxShuttleRingAngle),
-maxShuttleRingAngle,
);
const ticks = that._allShuttleRingTicks;
const clockViewModel = that._clockViewModel;
clockViewModel.clockStep = ClockStep.SYSTEM_CLOCK_MULTIPLIER;
//If we are at the max angle, simply return the max value in either direction.
if (Math.abs(angle) === maxShuttleRingAngle) {
clockViewModel.multiplier =
angle > 0 ? ticks[ticks.length - 1] : ticks[0];
return;
}
let multiplier = angleToMultiplier(angle, ticks);
if (that.snapToTicks) {
multiplier = ticks[getTypicalMultiplierIndex(multiplier, ticks)];
} else if (multiplier !== 0) {
const positiveMultiplier = Math.abs(multiplier);
if (positiveMultiplier > 100) {
const numDigits = positiveMultiplier.toFixed(0).length - 2;
const divisor = Math.pow(10, numDigits);
multiplier = (Math.round(multiplier / divisor) * divisor) | 0;
} else if (positiveMultiplier > realtimeShuttleRingAngle) {
multiplier = Math.round(multiplier);
} else if (positiveMultiplier > 1) {
multiplier = +multiplier.toFixed(1);
} else if (positiveMultiplier > 0) {
multiplier = +multiplier.toFixed(2);
}
}
clockViewModel.multiplier = multiplier;
},
});
this._canAnimate = undefined;
knockout.defineProperty(this, "_canAnimate", function () {
const clockViewModel = that._clockViewModel;
const clockRange = clockViewModel.clockRange;
if (that.shuttleRingDragging || clockRange === ClockRange.UNBOUNDED) {
return true;
}
const multiplier = clockViewModel.multiplier;
const currentTime = clockViewModel.currentTime;
const startTime = clockViewModel.startTime;
let result = false;
if (clockRange === ClockRange.LOOP_STOP) {
result =
JulianDate.greaterThan(currentTime, startTime) ||
(currentTime.equals(startTime) && multiplier > 0);
} else {
const stopTime = clockViewModel.stopTime;
result =
(JulianDate.greaterThan(currentTime, startTime) &&
JulianDate.lessThan(currentTime, stopTime)) || //
(currentTime.equals(startTime) && multiplier > 0) || //
(currentTime.equals(stopTime) && multiplier < 0);
}
if (!result) {
clockViewModel.shouldAnimate = false;
}
return result;
});
this._isSystemTimeAvailable = undefined;
knockout.defineProperty(this, "_isSystemTimeAvailable", function () {
const clockViewModel = that._clockViewModel;
const clockRange = clockViewModel.clockRange;
if (clockRange === ClockRange.UNBOUNDED) {
return true;
}
const systemTime = clockViewModel.systemTime;
return (
JulianDate.greaterThanOrEquals(systemTime, clockViewModel.startTime) &&
JulianDate.lessThanOrEquals(systemTime, clockViewModel.stopTime)
);
});
this._isAnimating = undefined;
knockout.defineProperty(this, "_isAnimating", function () {
return (
that._clockViewModel.shouldAnimate &&
(that._canAnimate || that.shuttleRingDragging)
);
});
const pauseCommand = createCommand(function () {
const clockViewModel = that._clockViewModel;
if (clockViewModel.shouldAnimate) {
clockViewModel.shouldAnimate = false;
} else if (that._canAnimate) {
clockViewModel.shouldAnimate = true;
}
});
this._pauseViewModel = new ToggleButtonViewModel(pauseCommand, {
toggled: knockout.computed(function () {
return !that._isAnimating;
}),
tooltip: "Pause",
});
const playReverseCommand = createCommand(function () {
const clockViewModel = that._clockViewModel;
const multiplier = clockViewModel.multiplier;
if (multiplier > 0) {
clockViewModel.multiplier = -multiplier;
}
clockViewModel.shouldAnimate = true;
});
this._playReverseViewModel = new ToggleButtonViewModel(playReverseCommand, {
toggled: knockout.computed(function () {
return that._isAnimating && clockViewModel.multiplier < 0;
}),
tooltip: "Play Reverse",
});
const playForwardCommand = createCommand(function () {
const clockViewModel = that._clockViewModel;
const multiplier = clockViewModel.multiplier;
if (multiplier < 0) {
clockViewModel.multiplier = -multiplier;
}
clockViewModel.shouldAnimate = true;
});
this._playForwardViewModel = new ToggleButtonViewModel(playForwardCommand, {
toggled: knockout.computed(function () {
return (
that._isAnimating &&
clockViewModel.multiplier > 0 &&
clockViewModel.clockStep !== ClockStep.SYSTEM_CLOCK
);
}),
tooltip: "Play Forward",
});
const playRealtimeCommand = createCommand(
function () {
that._clockViewModel.clockStep = ClockStep.SYSTEM_CLOCK;
},
knockout.getObservable(this, "_isSystemTimeAvailable"),
);
this._playRealtimeViewModel = new ToggleButtonViewModel(playRealtimeCommand, {
toggled: knockout.computed(function () {
return clockViewModel.clockStep === ClockStep.SYSTEM_CLOCK;
}),
tooltip: knockout.computed(function () {
return that._isSystemTimeAvailable
? "Today (real-time)"
: "Current time not in range";
}),
});
this._slower = createCommand(function () {
const clockViewModel = that._clockViewModel;
const shuttleRingTicks = that._allShuttleRingTicks;
const multiplier = clockViewModel.multiplier;
const index = getTypicalMultiplierIndex(multiplier, shuttleRingTicks) - 1;
if (index >= 0) {
clockViewModel.multiplier = shuttleRingTicks[index];
}
});
this._faster = createCommand(function () {
const clockViewModel = that._clockViewModel;
const shuttleRingTicks = that._allShuttleRingTicks;
const multiplier = clockViewModel.multiplier;
const index = getTypicalMultiplierIndex(multiplier, shuttleRingTicks) + 1;
if (index < shuttleRingTicks.length) {
clockViewModel.multiplier = shuttleRingTicks[index];
}
});
}
/**
* Gets or sets the default date formatter used by new instances.
*
* @member
* @type {AnimationViewModel.DateFormatter}
*/
AnimationViewModel.defaultDateFormatter = function (date, viewModel) {
const gregorianDate = JulianDate.toGregorianDate(date);
return `${monthNames[gregorianDate.month - 1]} ${gregorianDate.day} ${
gregorianDate.year
}`;
};
/**
* Gets or sets the default array of known clock multipliers associated with new instances of the shuttle ring.
* @type {number[]}
*/
AnimationViewModel.defaultTicks = [
//
0.001,
0.002,
0.005,
0.01,
0.02,
0.05,
0.1,
0.25,
0.5,
1.0,
2.0,
5.0,
10.0, //
15.0,
30.0,
60.0,
120.0,
300.0,
600.0,
900.0,
1800.0,
3600.0,
7200.0,
14400.0, //
21600.0,
43200.0,
86400.0,
172800.0,
345600.0,
604800.0,
];
/**
* Gets or sets the default time formatter used by new instances.
*
* @member
* @type {AnimationViewModel.TimeFormatter}
*/
AnimationViewModel.defaultTimeFormatter = function (date, viewModel) {
const gregorianDate = JulianDate.toGregorianDate(date);
const millisecond = Math.round(gregorianDate.millisecond);
if (Math.abs(viewModel._clockViewModel.multiplier) < 1) {
return `${gregorianDate.hour
.toString()
.padStart(2, "0")}:${gregorianDate.minute
.toString()
.padStart(2, "0")}:${gregorianDate.second
.toString()
.padStart(2, "0")}.${millisecond.toString().padStart(3, "0")}`;
}
return `${gregorianDate.hour
.toString()
.padStart(2, "0")}:${gregorianDate.minute
.toString()
.padStart(2, "0")}:${gregorianDate.second.toString().padStart(2, "0")} UTC`;
};
/**
* Gets a copy of the array of positive known clock multipliers to associate with the shuttle ring.
*
* @returns {number[]} The array of known clock multipliers associated with the shuttle ring.
*/
AnimationViewModel.prototype.getShuttleRingTicks = function () {
return this._sortedFilteredPositiveTicks.slice(0);
};
/**
* Sets the array of positive known clock multipliers to associate with the shuttle ring.
* These values will have negative equivalents created for them and sets both the minimum
* and maximum range of values for the shuttle ring as well as the values that are snapped
* to when a single click is made. The values need not be in order, as they will be sorted
* automatically, and duplicate values will be removed.
*
* @param {number[]} positiveTicks The list of known positive clock multipliers to associate with the shuttle ring.
*/
AnimationViewModel.prototype.setShuttleRingTicks = function (positiveTicks) {
//>>includeStart('debug', pragmas.debug);
if (!defined(positiveTicks)) {
throw new DeveloperError("positiveTicks is required.");
}
//>>includeEnd('debug');
let i;
let len;
let tick;
const hash = {};
const sortedFilteredPositiveTicks = this._sortedFilteredPositiveTicks;
sortedFilteredPositiveTicks.length = 0;
for (i = 0, len = positiveTicks.length; i < len; ++i) {
tick = positiveTicks[i];
//filter duplicates
if (!hash.hasOwnProperty(tick)) {
hash[tick] = true;
sortedFilteredPositiveTicks.push(tick);
}
}
sortedFilteredPositiveTicks.sort(numberComparator);
const allTicks = [];
for (len = sortedFilteredPositiveTicks.length, i = len - 1; i >= 0; --i) {
tick = sortedFilteredPositiveTicks[i];
if (tick !== 0) {
allTicks.push(-tick);
}
}
addAllToArray(allTicks, sortedFilteredPositiveTicks);
this._allShuttleRingTicks = allTicks;
};
Object.defineProperties(AnimationViewModel.prototype, {
/**
* Gets a command that decreases the speed of animation.
* @memberof AnimationViewModel.prototype
* @type {Command}
*/
slower: {
get: function () {
return this._slower;
},
},
/**
* Gets a command that increases the speed of animation.
* @memberof AnimationViewModel.prototype
* @type {Command}
*/
faster: {
get: function () {
return this._faster;
},
},
/**
* Gets the clock view model.
* @memberof AnimationViewModel.prototype
*
* @type {ClockViewModel}
*/
clockViewModel: {
get: function () {
return this._clockViewModel;
},
},
/**
* Gets the pause toggle button view model.
* @memberof AnimationViewModel.prototype
*
* @type {ToggleButtonViewModel}
*/
pauseViewModel: {
get: function () {
return this._pauseViewModel;
},
},
/**
* Gets the reverse toggle button view model.
* @memberof AnimationViewModel.prototype
*
* @type {ToggleButtonViewModel}
*/
playReverseViewModel: {
get: function () {
return this._playReverseViewModel;
},
},
/**
* Gets the play toggle button view model.
* @memberof AnimationViewModel.prototype
*
* @type {ToggleButtonViewModel}
*/
playForwardViewModel: {
get: function () {
return this._playForwardViewModel;
},
},
/**
* Gets the realtime toggle button view model.
* @memberof AnimationViewModel.prototype
*
* @type {ToggleButtonViewModel}
*/
playRealtimeViewModel: {
get: function () {
return this._playRealtimeViewModel;
},
},
/**
* Gets or sets the function which formats a date for display.
* @memberof AnimationViewModel.prototype
*
* @type {AnimationViewModel.DateFormatter}
* @default AnimationViewModel.defaultDateFormatter
*/
dateFormatter: {
//TODO:@exception {DeveloperError} dateFormatter must be a function.
get: function () {
return this._dateFormatter;
},
set: function (dateFormatter) {
//>>includeStart('debug', pragmas.debug);
if (typeof dateFormatter !== "function") {
throw new DeveloperError("dateFormatter must be a function");
}
//>>includeEnd('debug');
this._dateFormatter = dateFormatter;
},
},
/**
* Gets or sets the function which formats a time for display.
* @memberof AnimationViewModel.prototype
*
* @type {AnimationViewModel.TimeFormatter}
* @default AnimationViewModel.defaultTimeFormatter
*/
timeFormatter: {
//TODO:@exception {DeveloperError} timeFormatter must be a function.
get: function () {
return this._timeFormatter;
},
set: function (timeFormatter) {
//>>includeStart('debug', pragmas.debug);
if (typeof timeFormatter !== "function") {
throw new DeveloperError("timeFormatter must be a function");
}
//>>includeEnd('debug');
this._timeFormatter = timeFormatter;
},
},
});
//Currently exposed for tests.
AnimationViewModel._maxShuttleRingAngle = maxShuttleRingAngle;
AnimationViewModel._realtimeShuttleRingAngle = realtimeShuttleRingAngle;
/**
* A function that formats a date for display.
* @callback AnimationViewModel.DateFormatter
*
* @param {JulianDate} date The date to be formatted
* @param {AnimationViewModel} viewModel The AnimationViewModel instance requesting formatting.
* @returns {string} The string representation of the calendar date portion of the provided date.
*/
/**
* A function that formats a time for display.
* @callback AnimationViewModel.TimeFormatter
*
* @param {JulianDate} date The date to be formatted
* @param {AnimationViewModel} viewModel The AnimationViewModel instance requesting formatting.
* @returns {string} The string representation of the time portion of the provided date.
*/
export default AnimationViewModel;