/*global require*/
"use strict";
var dateFormat = require("dateformat");
var ClockRange = require("terriajs-cesium/Source/Core/ClockRange").default;
var ClockStep = require("terriajs-cesium/Source/Core/ClockStep").default;
var DataSourceClock = require("terriajs-cesium/Source/DataSources/DataSourceClock")
.default;
var defaultValue = require("terriajs-cesium/Source/Core/defaultValue").default;
var defined = require("terriajs-cesium/Source/Core/defined").default;
var destroyObject = require("terriajs-cesium/Source/Core/destroyObject")
.default;
var DeveloperError = require("terriajs-cesium/Source/Core/DeveloperError")
.default;
var Iso8601 = require("terriajs-cesium/Source/Core/Iso8601").default;
var JulianDate = require("terriajs-cesium/Source/Core/JulianDate").default;
var knockout = require("terriajs-cesium/Source/ThirdParty/knockout").default;
var TimeInterval = require("terriajs-cesium/Source/Core/TimeInterval").default;
var TimeIntervalCollection = require("terriajs-cesium/Source/Core/TimeIntervalCollection")
.default;
var csv = require("../ThirdParty/csv");
var DataUri = require("../Core/DataUri");
var DisplayVariablesConcept = require("../Map/DisplayVariablesConcept");
var inherit = require("../Core/inherit");
var TableColumn = require("./TableColumn");
var VarType = require("../Map/VarType");
var setClockCurrentTime = require("../Models/setClockCurrentTime");
var defaultDisplayVariableTypes = [VarType.ENUM, VarType.SCALAR, VarType.ALT];
var defaultFinalDurationSeconds = 3600 * 24 - 1; // one day less a second, if there is only one date.
var defaultShaveSeconds = 0;
/**
* TableStructure provides an abstraction of a data table, ie. a structure with rows and columns.
* Its primary responsibility is to load and parse the data, from csvs or other.
* It stores each column as a TableColumn, and saves the rows too if conversion to rows is requested.
* Columns are also sorted by type for easier access.
*
* @alias TableStructure
* @constructor
* @extends {DisplayVariablesConcept}
* @param {String} [name] Name to use in the NowViewing tab, defaults to 'Display Variable'.
* @param {Object} [options] Options:
* @param {Array} [options.displayVariableTypes] Which variable types to show in the NowViewing tab. Defaults to ENUM, SCALAR, and ALT (not LAT, LON or TIME).
* @param {VarType[]} [options.unallowedTypes] An array of types which should not be guessed. If not present, all types are allowed. Cannot include VarType.SCALAR.
* @param {String} [options.initialTimeSource] A string specifiying the value of the animation timeline at start. Valid options are:
* ("present": closest to today's date,
* "start": start of time range of animation,
* "end": end of time range of animation,
* An ISO8601 date e.g. "2015-08-08": specified date or nearest if date is outside range).
* @param {Number} [options.displayDuration] Passed on to TableColumn, unless overridden by options.columnOptions.
* @param {String[]} [options.replaceWithNullValues] Passed on to TableColumn, unless overridden by options.columnOptions.
* @param {String[]} [options.replaceWithZeroValues] Passed on to TableColumn, unless overridden by options.columnOptions.
* @param {Object} [options.columnOptions] An object with keys identifying columns (column names or indices),
* and per-column properties displayDuration, replaceWithNullValues, replaceWithZeroValues, name, active, units and/or type.
* For type, converts strings, which are case-insensitive keys of VarType, to their VarType integer.
* @param {Function} [options.getColorCallback] Passed to DisplayVariableConcept.
* @param {Entity} [options.sourceFeature] The feature to which this table applies, if any; not used internally by TableStructure or TableColumn.
* @param {Array} [options.idColumnNames] An array of column names/indexes/ids which identify unique features across rows
* (see CsvCatalogItem.idColumns).
* @param {Boolean} [options.isSampled] Does this data correspond to "sampled" data?
* See CsvCatalogItem.isSampled for an explanation.
* @param {Number} [options.shaveSeconds] How many seconds to shave off each time period so periods do not overlap. Defaults to 1 second.
* @param {JulianDate} [options.finalEndJulianDate] If present, use this as the final end date for all points.
* @param {Boolean} [options.requireSomeActive=false] Set to true if at least one column must be selected at all times.
*/
var TableStructure = function(name, options) {
options = defaultValue(options, defaultValue.EMPTY_OBJECT);
DisplayVariablesConcept.call(this, name, {
getColorCallback: options.getColorCallback,
requireSomeActive: defaultValue(options.requireSomeActive, false)
});
this.displayVariableTypes = defaultValue(
options.displayVariableTypes,
defaultDisplayVariableTypes
);
this.shaveSeconds = defaultValue(options.shaveSeconds, defaultShaveSeconds);
this.finalEndJulianDate = options.finalEndJulianDate;
this.unallowedTypes = options.unallowedTypes;
this.initialTimeSource = options.initialTimeSource;
this.displayDuration = options.displayDuration;
this.replaceWithNullValues = options.replaceWithNullValues;
this.replaceWithZeroValues = options.replaceWithZeroValues;
this.columnOptions = options.columnOptions;
this.sourceFeature = options.sourceFeature;
this.idColumnNames = options.idColumnNames; // Actually names, ids or indexes.
this.isSampled = options.isSampled;
/**
* Gets or sets the active time column name, id or index.
* If you pass an array of two such, eg. [0, 1], treats these as the start and end date column identifiers.
* @type {String|Number|String[]|Number[]|undefined}
*/
this._activeTimeColumnNameIdOrIndex = undefined;
// Track sourceFeature as it is shown on the NowViewing panel.
// Track items so that charts can update live. (Already done by DisplayVariableConcept.)
knockout.track(this, ["sourceFeature", "_activeTimeColumnNameIdOrIndex"]);
/**
* Gets the columnsByType for this structure,
* an object whose keys are VarTypes, and whose values are arrays of TableColumn with matching type.
* Only existing types are present (eg. columnsByType[VarType.ALT] may be undefined).
* @memberOf TableStructure.prototype
* @type {Object}
*/
knockout.defineProperty(this, "columnsByType", {
get: function() {
return getColumnsByType(this.items);
}
});
};
inherit(DisplayVariablesConcept, TableStructure);
Object.defineProperties(TableStructure.prototype, {
/**
* Gets or sets the columns for this structure.
* @memberOf TableStructure.prototype
* @type {TableColumn[]}
*/
columns: {
get: function() {
return this.items;
},
set: function(value) {
if (areColumnsEqualLength(value)) {
this.items = value;
} else {
var msg = "Badly formed data table - columns have different lengths.";
throw new DeveloperError(msg);
}
}
},
/**
* Gets a flag which states whether this data has latitude and longitude data.
* @type {Boolean}
*/
hasLatitudeAndLongitude: {
get: function() {
var longitudeColumn = this.columnsByType[VarType.LON][0];
var latitudeColumn = this.columnsByType[VarType.LAT][0];
return defined(longitudeColumn) && defined(latitudeColumn);
}
},
/**
* Gets a flag which states whether this data has address data.
* @type {Boolean}
*/
hasAddress: {
get: function() {
var address = this.columnsByType[VarType.ADDR][0];
return defined(address);
}
},
/**
* Gets or sets the active time column name, id or index.
* If you pass an array of two such, eg. [0, 1], treats these as the start and end date column identifiers.
* @type {String|Integer|String[]|Integer[]|undefined}
*/
activeTimeColumnNameIdOrIndex: {
get: function() {
return this._activeTimeColumnNameIdOrIndex;
},
set: function(nameIdOrIndex) {
this._activeTimeColumnNameIdOrIndex = nameIdOrIndex;
if (defined(nameIdOrIndex)) {
// Sort by the newly active time column, if available (so charts and derived charts don't double-back on themselves).
// Don't replace the table structure's columns until we have finished all our finish date calculations.
var sortedColumns = getSortedColumns(
this,
this.getColumnWithNameIdOrIndex(valueOrFirstValue(nameIdOrIndex))
);
// sortBy changes all the columns, so get the new time column.
var timeColumnToActivate = getColumnWithNameIdOrIndex(
valueOrFirstValue(nameIdOrIndex),
sortedColumns
);
if (
defined(timeColumnToActivate) &&
!defined(timeColumnToActivate.finishJulianDates)
) {
// Calculate default end dates and timeIntervals, and define a clock on the active time column.
timeColumnToActivate.finishJulianDates = calculateFinishDates(
sortedColumns,
nameIdOrIndex,
this
);
timeColumnToActivate._timeIntervals = calculateTimeIntervals(
timeColumnToActivate
);
timeColumnToActivate._clock = createClock(timeColumnToActivate, this);
var intervals = timeColumnToActivate._timeIntervals;
var stopTime;
if (intervals.length > 0) {
var lastInterval;
for (
var i = intervals.length - 1;
!defined(lastInterval) && i >= 0;
--i
) {
lastInterval = intervals[i];
}
if (defined(lastInterval)) {
stopTime = lastInterval.start;
}
}
setClockCurrentTime(
timeColumnToActivate._clock,
this.initialTimeSource,
stopTime
);
this.columns = sortedColumns;
} else {
this._activeTimeColumnNameIdOrIndex = undefined;
}
}
}
},
/**
* Gets the active time column for this structure. If two were provided (for the start and end times), return only the start date column.
* @memberOf TableStructure.prototype
* @type {TableColumn}
*/
activeTimeColumn: {
get: function() {
var nameIdOrIndex = this._activeTimeColumnNameIdOrIndex;
if (defined(nameIdOrIndex)) {
return this.getColumnWithNameIdOrIndex(
valueOrFirstValue(nameIdOrIndex)
);
}
return undefined;
}
},
/**
* Returns an array describing when each row is visible. Only defined if there is an active time column.
* @memberOf TableStructure.prototype
* @type {TimeIntervalCollection[]}
*/
timeIntervals: {
get: function() {
var activeTimeColumn = this.activeTimeColumn;
if (!defined(this.activeTimeColumn)) {
return undefined;
}
return activeTimeColumn._timeIntervals;
}
},
/**
* Returns an array of the finish Julian dates for each row. Only defined if there is an active time column.
* @memberOf TableStructure.prototype
* @type {JulianDate[]}
*/
finishJulianDates: {
get: function() {
var activeTimeColumn = this.activeTimeColumn;
if (!defined(this.activeTimeColumn)) {
return undefined;
}
return activeTimeColumn.finishJulianDates;
}
},
/**
* Returns a clock whose start and stop times correspond to the first and last visible row.
* Only defined if type == VarType.TIME.
* @memberOf TableColumn.prototype
* @type {DataSourceClock}
*/
clock: {
get: function() {
var activeTimeColumn = this.activeTimeColumn;
if (!defined(this.activeTimeColumn)) {
return undefined;
}
return activeTimeColumn._clock;
}
}
});
function getVarTypeFromString(typeString) {
if (!defined(typeString)) {
return;
}
var typeNumber = parseInt(typeString, 10);
if (typeNumber === typeNumber) {
// parseInt returns NaN for non-numeric strings, and NaN !== NaN.
return typeNumber;
}
for (var varTypeName in VarType) {
if (typeString.toLowerCase() === varTypeName.toLowerCase()) {
return VarType[varTypeName];
}
}
}
/**
* Expose the default display variable types.
* @type {Array}
*/
TableStructure.defaultDisplayVariableTypes = defaultDisplayVariableTypes;
/**
* Create a TableStructure from a JSON object, eg. [['x', 'y'], [1, 5], [3, 8], [4, -3]].
*
* @param {Object} json Table data as an object (in json format).
* @param {TableStructure} [result] A pre-existing TableStructure object; if not present, creates a new one.
*/
TableStructure.fromJson = function(json, result) {
if (!defined(json) || json.length === 0 || json[0].length === 0) {
return;
}
if (!defined(result)) {
result = new TableStructure();
}
// Build up the columns (=== items) and then replace them all in one go, so that knockout's tracking doesn't see every change.
var columns = [];
var columnNames = json[0];
var rowNumber, name, values;
for (
var columnNumber = 0;
columnNumber < columnNames.length;
columnNumber++
) {
name = isString(columnNames[columnNumber])
? columnNames[columnNumber].trim()
: "_Column" + String(columnNumber);
values = [];
for (rowNumber = 1; rowNumber < json.length; rowNumber++) {
values.push(json[rowNumber][columnNumber]);
}
var nameAndcolumnOptions = getColumnOptions(name, result, columnNumber);
columns.push(
new TableColumn(nameAndcolumnOptions[0], values, nameAndcolumnOptions[1])
);
}
result.items = columns;
return result;
};
/**
* Create a TableStructure from a string in csv format.
* Understands \r\n, \r and \n as newlines.
*
* @param {String} csvString String in csv format.
* @param {TableStructure} [result] A pre-existing TableStructure object; if not present, creates a new one.
*/
TableStructure.fromCsv = function(csvString, result) {
// Originally from jquery-csv plugin. Modified to avoid stripping leading zeros.
function castToScalar(value, state) {
if (state.rowNum === 1) {
// Don't cast column names
return value;
} else {
var hasDot = /\./;
var leadingZero = /^0[0-9]/;
var numberWithThousands = /^[1-9]\d?\d?(,\d\d\d)+(\.\d+)?$/;
if (numberWithThousands.test(value)) {
value = value.replace(/,/g, "");
}
if (isNaN(value)) {
return value;
}
if (leadingZero.test(value)) {
return value;
}
if (hasDot.test(value)) {
return parseFloat(value);
}
var integer = parseInt(value, 10);
if (isNaN(integer)) {
return null;
}
return integer;
}
}
//normalize line breaks
csvString = csvString.replace(/\r\n|\r|\n/g, "\r\n");
// Handle CSVs missing a final linefeed
if (csvString[csvString.length - 1] !== "\n") {
csvString += "\r\n";
}
var json = csv.toArrays(csvString, {
onParseValue: castToScalar
});
// Remove any blank lines. Completely blank lines come back as [null]; lines with no entries as [null, null, ..., null].
// So remove all lines that consist only of nulls.
json = json.filter(function(jsonLine) {
return !jsonLine.every(function(c) {
return c === null;
});
});
return TableStructure.fromJson(json, result);
};
/**
* Load a JSON object into an existing TableStructure.
*
* @param {Object} json Table data as an object (in json format).
*/
TableStructure.prototype.loadFromJson = function(json) {
return TableStructure.fromJson(json, this);
};
/**
* Load a string in csv format into an existing TableStructure.
*
* @param {String} csvString String in csv format.
*/
TableStructure.prototype.loadFromCsv = function(csvString) {
return TableStructure.fromCsv(csvString, this);
};
/**
* Returns an array of active columns.
* @returns {TableColumn[]} An array of active columns.
*/
TableStructure.prototype.getActiveColumns = function() {
return this.columns.filter(function(column) {
return column.isActive;
});
};
// Returns indices such that sortedUniqueDates[inverseIndices[k]] = originalDates[k].
// Eg. var data = ['c', 'a', 'b', 'd'];
// var sortedData = data.slice().sort();
// var inverseIndices = data.map(function(datum) { return sortedData.indexOf(datum); });
// expect(inverseIndices).toEqual([2, 0, 1, 3]);
// However this works by converting the dates to strings first.
function calculateInverseIndicies(originalDates, sortedUniqueDates) {
var originalDateStrings = originalDates.map(function(date) {
return date && JulianDate.toIso8601(date);
});
var sortedUniqueDateStrings = sortedUniqueDates.map(function(date) {
return date && JulianDate.toIso8601(date);
});
return originalDateStrings.map(function(s) {
return sortedUniqueDateStrings.indexOf(s);
});
}
function calculateUniqueJulianDates(originalDates) {
var uniqueJulianDates = originalDates.filter(function(d) {
return defined(d);
});
// uniqueJulianDates.sort(JulianDate.compare); // We now assume they are sorted.
uniqueJulianDates = uniqueJulianDates.filter(function(element, index, array) {
return index === 0 || !JulianDate.equals(array[index - 1], element);
});
return uniqueJulianDates;
}
/**
* @param {JulianDate[]} startJulianDates An array of start dates.
* @param {Number} [localDefaultFinalDurationSeconds] The duration to use if there is only one date in the list. Defaults to defaultFinalDurationSeconds.
* @param {Number} [shaveSeconds] Subtract this many seconds from the end dates so they don't overlap (defaults to zero). If duration < 20 * shaveSeconds, use 5% of duration.
* @param {JulianDate} [finalEndJulianDate] If present, use this for the final end date.
* @return {JulianDate[]} An array of end dates which correspond to the array of start dates.
*/
function calculateFinishDatesFromStartDates(
startJulianDates,
localDefaultFinalDurationSeconds,
shaveSeconds,
finalEndJulianDate
) {
// First calculate a set of unique dates. Assume they are pre-sorted.
var sortedUniqueJulianDates = calculateUniqueJulianDates(startJulianDates);
// indices[k] has the property that startJulianDates[indices[k]] = sortedUniqueJulianDates[k].
var indices = calculateInverseIndicies(
startJulianDates,
sortedUniqueJulianDates
);
// Calculate end dates corresponding to each revised date (which are start dates).
// Typically just shave a second off the next start date, unless the difference is < 20 seconds,
// in which case shave off 5% of the difference.
var endDates;
if (shaveSeconds > 0) {
endDates = sortedUniqueJulianDates
.slice(1)
.map(function(rawEndDate, index) {
var secondsDifference = JulianDate.secondsDifference(
rawEndDate,
sortedUniqueJulianDates[index]
);
if (secondsDifference < 20) {
return JulianDate.addSeconds(
rawEndDate,
-secondsDifference / 20,
new JulianDate()
);
} else {
return JulianDate.addSeconds(rawEndDate, -1, new JulianDate());
}
});
} else {
endDates = sortedUniqueJulianDates.slice(1);
}
// For the final end date, if there is a finalEndJulianDate, use it.
// Otherwise, use the average spacing of the unique dates.
// If there is only one date, use defaultFinalDurationSeconds.
if (defined(finalEndJulianDate)) {
endDates.push(finalEndJulianDate);
} else {
var finalDurationSeconds = defined(localDefaultFinalDurationSeconds)
? localDefaultFinalDurationSeconds
: defaultFinalDurationSeconds;
var n = sortedUniqueJulianDates.length;
if (n > 1) {
finalDurationSeconds =
JulianDate.secondsDifference(
sortedUniqueJulianDates[n - 1],
sortedUniqueJulianDates[0]
) /
(n - 1);
endDates.push(
JulianDate.addSeconds(
sortedUniqueJulianDates[n - 1],
finalDurationSeconds,
new JulianDate()
)
);
} else {
endDates.push(undefined);
}
}
var result = indices.map(function(sortedIndex) {
return endDates[sortedIndex];
});
return result;
}
// For each row, find the next different date (minus 1 second).
// Restrict to only those rows with this value of the idColumnNames, if present.
// Return an array of these finish dates, one per row.
// Assume the rows are already sorted by date.
// For the final date, use the average spacing of the unique dates as the final duration.
// (If there is only one date, use a default value.)
function calculateFinishDates(columns, nameIdOrIndex, tableStructure) {
// This is the start column.
var timeColumn = getColumnWithNameIdOrIndex(
valueOrFirstValue(nameIdOrIndex),
columns
);
// If there is an end column as well, just use it.
if (Array.isArray(nameIdOrIndex) && nameIdOrIndex.length > 1) {
var endColumn = getColumnWithNameIdOrIndex(nameIdOrIndex[1], columns);
if (defined(endColumn) && defined(endColumn.julianDates)) {
return endColumn.julianDates;
}
}
var startJulianDates = timeColumn.julianDates;
if (
!defined(tableStructure.idColumnNames) ||
tableStructure.idColumnNames.length === 0
) {
return calculateFinishDatesFromStartDates(
startJulianDates,
defaultFinalDurationSeconds,
tableStructure.shaveSeconds,
tableStructure.finalEndJulianDate
);
}
// If the table has valid id columns, then take account of these by calculating feature-specific end dates.
// First calculate the default duration for any rows with only one observation; this should match the average
var finalDurationSeconds;
var idMapping = getIdMapping(tableStructure.idColumnNames, columns);
// Find a mapping with more than one row to estimate an average duration. We'll need this for any ids with only one row.
for (var featureIdString in idMapping) {
if (idMapping.hasOwnProperty(featureIdString)) {
var rowNumbersWithThisId = idMapping[featureIdString];
if (rowNumbersWithThisId.length > 1) {
var theseStartDates = rowNumbersWithThisId.map(
rowNumber => timeColumn.julianDates[rowNumber]
);
var sortedUniqueJulianDates = calculateUniqueJulianDates(
theseStartDates
);
var n = sortedUniqueJulianDates.length;
if (n > 1) {
finalDurationSeconds =
JulianDate.secondsDifference(
sortedUniqueJulianDates[n - 1],
sortedUniqueJulianDates[0]
) /
(n - 1);
break;
}
}
}
}
// Build the end dates, one id at a time.
var endDates = [];
for (featureIdString in idMapping) {
if (idMapping.hasOwnProperty(featureIdString)) {
rowNumbersWithThisId = idMapping[featureIdString];
theseStartDates = rowNumbersWithThisId.map(
rowNumber => timeColumn.julianDates[rowNumber]
);
var theseEndDates = calculateFinishDatesFromStartDates(
theseStartDates,
finalDurationSeconds,
tableStructure.shaveSeconds,
tableStructure.finalEndJulianDate
);
for (var i = 0; i < theseEndDates.length; i++) {
endDates[rowNumbersWithThisId[i]] = theseEndDates[i];
}
}
}
return endDates;
}
var endScratch = new JulianDate();
/**
* Gets the finish time for the specified index.
* @private
* @param {TableColumn} timeColumn The time column that applies to this data.
* @param {Integer} index The index into the time column.
* @return {JulianDate} The finnish time that corresponds to the index.
*/
function finishFromIndex(timeColumn, index) {
if (!defined(timeColumn.displayDuration)) {
return timeColumn.finishJulianDates[index];
} else {
return JulianDate.addMinutes(
timeColumn.julianDates[index],
timeColumn.displayDuration,
endScratch
);
}
}
/**
* Calculate and return the availability interval for the index'th entry in timeColumn.
* If the entry has no valid time, returns undefined.
* @private
* @param {TableColumn} timeColumn The time column that applies to this data.
* @param {Integer} index The index into the time column.
* @param {JulianDate} endTime The last time for all intervals.
* @return {TimeInterval} The time interval over which this entry is visible.
*/
function calculateAvailability(timeColumn, index, endTime) {
var startJulianDate = timeColumn.julianDates[index];
if (defined(startJulianDate)) {
var finishJulianDate = finishFromIndex(timeColumn, index);
return new TimeInterval({
start: timeColumn.julianDates[index],
stop: finishJulianDate,
isStopIncluded: JulianDate.equals(finishJulianDate, endTime),
data: timeColumn.julianDates[index] // Stop overlapping intervals being collapsed into one interval unless they start at the same time
});
}
}
/**
* Calculates and returns TimeInterval array, whose elements say when to display each row.
* @private
*/
function calculateTimeIntervals(timeColumn) {
// First we find the last time for all of the data (this is an optomisation for the calculateAvailability operation.
const endTime = timeColumn.values.reduce(function(latest, value, index) {
const current = finishFromIndex(timeColumn, index);
if (
!defined(latest) ||
(defined(current) && JulianDate.greaterThan(current, latest))
) {
return current;
}
return latest;
}, finishFromIndex(timeColumn, 0));
return timeColumn.values.map(function(value, index) {
return calculateAvailability(timeColumn, index, endTime);
});
}
/**
* Returns a DataSourceClock out of this column. Only call if this is a time column.
* @private
*/
function createClock(timeColumn, tableStructure) {
var availabilityCollection = new TimeIntervalCollection();
timeColumn._timeIntervals
.filter(function(availability) {
return defined(availability && availability.start);
})
.forEach(function(availability) {
availabilityCollection.addInterval(availability);
});
if (!defined(timeColumn._clock)) {
if (
availabilityCollection.length > 0 &&
!availabilityCollection.start.equals(Iso8601.MINIMUM_VALUE)
) {
var startTime = availabilityCollection.start;
var stopTime = availabilityCollection.stop;
var totalSeconds = JulianDate.secondsDifference(stopTime, startTime);
var multiplier = Math.round(totalSeconds / 120.0);
if (
defined(tableStructure.idColumnNames) &&
tableStructure.idColumnNames.length > 0
) {
stopTime = timeColumn.julianDates.reduce((d1, d2) =>
JulianDate.greaterThan(d1, d2) ? d1 : d2
);
}
var clock = new DataSourceClock();
clock.startTime = JulianDate.clone(startTime);
clock.stopTime = JulianDate.clone(stopTime);
clock.clockRange = ClockRange.LOOP_STOP;
clock.multiplier = multiplier;
clock.currentTime = JulianDate.clone(startTime);
clock.clockStep = ClockStep.SYSTEM_CLOCK_MULTIPLIER;
return clock;
}
}
return timeColumn._clock;
}
/**
* Return data as an array of columns, eg. [ ['x', 1, 2, 3], ['y', 10, 20, 5] ].
* @returns {Object} An array of column arrays, each beginning with the column name.
*/
TableStructure.prototype.toArrayOfColumns = function() {
var result = [];
var column;
for (var i = 0; i < this.columns.length; i++) {
column = this.columns[i];
result.push(column.toArrayWithName());
}
return result;
};
/**
* Return data as an array of rows of formatted data, eg. [ ['x', 'y'], ['1', '12,345'], ['2.1', '20'] ].
* @param {String} [dateFormatString] If present, override the standard date format with a string (see https://www.npmjs.com/package/dateformat)
* Eg. "isoDateTime" or "dd mmm yyyy HH:MM:ss".
* "source" is a special override which uses the original source date format.
* @param {Integer[]} [rowNumbers] An array of row numbers to return. Defaults to all rows.
* @param {Boolean} [formatScalars] True by default; if false, leave numbers as they are.
* @param {Boolean} [quoteStringsIfNeeded] False by default; if true, any strings which contain commas will be quoted (including column names).
* @returns {Object} An array of rows of formatted data, the first of which is the column names. If they contain commas, they are quoted.
*/
TableStructure.prototype.toArrayOfRows = function(
dateFormatString,
rowNumbers,
formatScalars,
quoteStringsIfNeeded
) {
if (this.columns.length < 1) {
return [];
}
if (!defined(formatScalars)) {
formatScalars = true;
}
var that = this;
function updatedForQuotes(s) {
// Following https://tools.ietf.org/html/rfc4180 .
var hasQuotes = s.indexOf('"') >= 0;
if (hasQuotes) {
s = s.replace(/"/g, '""');
}
if (hasQuotes || s.indexOf(",") >= 0) {
s = '"' + s + '"';
}
return s;
}
function getRow(rowNumber) {
return that.columns.map(column => {
if (dateFormatString && column.type === VarType.TIME) {
if (dateFormatString === "source") {
return column.values[rowNumber];
}
return dateFormat(column.dates[rowNumber], dateFormatString);
}
if (!formatScalars && column.type === VarType.SCALAR) {
return column.values[rowNumber];
}
var formattedValue = column._formattedValues[rowNumber];
if (quoteStringsIfNeeded) {
// Put quotes around any value that contains commas or quotes, so csv format doesn't go nuts.
return formattedValue && updatedForQuotes(formattedValue.toString());
} else {
return formattedValue;
}
});
}
var rows;
if (defined(rowNumbers)) {
rows = rowNumbers.map(getRow);
} else {
rows = that.columns[0].values.map((_, rowNumber) => getRow(rowNumber));
}
var columnNames = that.getColumnNames();
if (quoteStringsIfNeeded) {
columnNames = columnNames.map(s => updatedForQuotes(s));
}
rows.unshift(columnNames);
return rows;
};
/**
* Return data as a csv string with formatted values, eg. 'x,y\n1,"12,345"\n2.1,20'.
* @param {String} [dateFormatString] If present, override the standard date format with a string (see https://www.npmjs.com/package/dateformat)
* Eg. "isoDateTime" or "dd mmm yyyy HH:MM:ss".
* "source" is a special override which uses the original source date format.
* @param {Integer[]} [rowNumbers] An array of row numbers to return. Defaults to all rows.
* @param {Boolean} [formatScalars] True by default; if false, leave numbers as they are.
* @returns {String} Returns the data as a csv string, including the header row.
*/
TableStructure.prototype.toCsvString = function(
dateFormatString,
rowNumbers,
formatScalars
) {
var arraysOfRows = this.toArrayOfRows(
dateFormatString,
rowNumbers,
formatScalars,
true
); // true => quote strings with commas.
var joinedRows = arraysOfRows.map(row => row.join(","));
return joinedRows.join("\n");
};
/**
* Return data as an array of rows of objects, eg. [{'x': 1, 'y': 10}, {'x': 2, 'y': 20}, ...].
* Note this won't work if a column name is a javascript reserved word.
* Has the same arguments as TableStructure.prototype.toArrayOfRows.
* @returns {Object[]} Array of objects containing a property for each column of the row. If the table has no data, returns [].
*/
TableStructure.prototype.toRowObjects = function(
dateFormatString,
rowNumbers,
formatScalars,
quoteStringsWithCommas
) {
var asRows = this.toArrayOfRows(
dateFormatString,
rowNumbers,
formatScalars,
quoteStringsWithCommas
);
if (!defined(asRows) || asRows.length < 1) {
return [];
}
var columnNames = asRows[0];
var result = [];
for (var i = 1; i < asRows.length; i++) {
var rowObject = {};
for (var j = 0; j < columnNames.length; j++) {
rowObject[columnNames[j]] = asRows[i][j];
}
result.push(rowObject);
}
return result;
};
/**
* Return data as an array of rows of objects with string and number values, eg.
* [{'string': {'x': '12,345', 'y': '10'}, 'number': {'x': 12345, 'y': 10}}, {'string': {'x':...}, ...}].
* @param {String} [dateFormatString] If present, override the standard date format with a string (see https://www.npmjs.com/package/dateformat)
* Eg. "isoDateTime" or "dd mmm yyyy HH:MM:ss".
* "source" is a special override which uses the original source date format.
* @return {Object[]} Array of objects with "string" and "number" properties, whose properties are the column names.
*/
TableStructure.prototype.toStringAndNumberRowObjects = function(
dateFormatString
) {
var stringRows = this.toArrayOfRows(dateFormatString, undefined, true);
if (!defined(stringRows) || stringRows.length < 1) {
return [];
}
var numberRows = this.toArrayOfRows(dateFormatString, undefined, false);
var columnNames = stringRows[0];
var result = [];
for (var i = 1; i < stringRows.length; i++) {
var rowObject = { string: {}, number: {} };
for (var j = 0; j < columnNames.length; j++) {
rowObject.string[columnNames[j]] = stringRows[i][j];
rowObject.number[columnNames[j]] = numberRows[i][j];
}
result.push(rowObject);
}
return result;
};
TableStructure.prototype.toDataUri = function() {
return DataUri.make("csv", this.toCsvString("source"));
};
/**
* Provide an array which maps ids to names, if they differ.
* @return {Object[]} An array of objects with 'id' and 'name' properties; only where the id and name differ.
*/
TableStructure.prototype.getColumnAliases = function() {
return this.columns
.filter(function(column) {
return column.id !== column.name;
})
.map(function(column) {
return { id: column.id, name: column.name };
});
};
function describeRow(tableStructure, rowObject, index, infoFields) {
// Note this passes any html straight through, including tags.
// We do not escape the keys or values because they could contain custom tags, eg. <chart>.
var html = '<table class="cesium-infoBox-defaultTable">';
for (var key in infoFields) {
if (infoFields.hasOwnProperty(key)) {
var value = rowObject[key];
if (defined(value)) {
// Skip keys starting with double underscore
if (key.substring(0, 2) === "__") {
continue;
}
html +=
"<tr><td>" + infoFields[key] + "</td><td>" + value + "</td></tr>";
}
}
}
html += "</table>";
return html;
}
/**
* Returns data as an array of html for each row.
* @param {Array|Object} [featureInfoFields] Either an array of keys from the row objects, or an object that maps keys to names of keys.
* If not provided, defaults to using all keys unaltered.
* @return {String[]} Array of html for each row.
*/
TableStructure.prototype.toRowDescriptions = function(featureInfoFields) {
var infoFields = defined(featureInfoFields)
? featureInfoFields
: this.getColumnNames();
if (infoFields instanceof Array) {
// Allow [ "FIELD1", "FIELD2" ] as a shorthand for { "FIELD1": "FIELD1", "FIELD2": "FIELD2" }
var o = {};
infoFields.forEach(function(key) {
o[key] = key;
});
infoFields = o;
}
var that = this;
return this.toRowObjects().map(function(rowObject, index) {
return describeRow(that, rowObject, index, infoFields);
});
};
/**
* Returns the active columns as an array of arrays of objects with x and y properties, using js dates for x values if available.
* Useful for plotting the data.
* Eg. "a,b,c\n1,2,3\n4,5,6" => [[{x: 1, y: 2}, {x: 4, y: 5}], [{x: 1, y: 3}, {x: 4, y: 6}]].
* @param {TableColumn} [xColumn] Which column to use for the x values. Defaults to the first column.
* @param {TableColumn[]} [yColumns] Which columns to use for the y values. Defaults to all columns excluding xColumn.
* @return {Array[]} The data as arrays of objects.
*/
TableStructure.prototype.toPointArrays = function(xColumn, yColumns) {
var result = [];
if (!defined(xColumn)) {
xColumn = this.columns[0];
}
var xColumnValues =
xColumn.type === VarType.TIME ? xColumn.dates : xColumn.values;
if (!defined(yColumns)) {
yColumns = this.columns.filter(column => column !== xColumn);
}
var getXYFunction = function(j) {
return (x, index) => {
return { x: x, y: yColumns[j].values[index] };
};
};
for (var j = 0; j < yColumns.length; j++) {
result.push(xColumnValues.map(getXYFunction(j)));
}
return result;
};
/**
* Get the column names.
*
* @returns {String[]} Array of column names.
*/
TableStructure.prototype.getColumnNames = function() {
var result = [];
for (var i = 0; i < this.columns.length; i++) {
result.push(this.columns[i].name);
}
return result;
};
/**
* Returns the first column with the given name, or undefined if none match.
*
* @param {String} name The column name.
* @param {TableColumn[]} [columns] If provided, test on these columns instead of this.columns.
* @returns {TableColumn} The matching column.
*/
TableStructure.prototype.getColumnWithName = function(name, columns) {
if (!defined(columns)) {
columns = this.columns;
}
for (var i = 0; i < columns.length; i++) {
if (columns[i].name === name) {
return columns[i];
}
}
};
/**
* Returns the index of the given column, or undefined if none match.
* @param {TableStructure} tableStructure the table structure.
* @param {TableColumn} column The column.
* @returns {integer} The index of the column.
* @private
*/
function getIndexOfColumn(tableStructure, column) {
for (var i = 0; i < tableStructure.columns.length; i++) {
if (tableStructure.columns[i] === column) {
return i;
}
}
}
/**
* Returns the first column with the given name or id, or undefined if none match.
* @param {String} nameOrId The column name or id.
* @param {TableColumn[]} columns Test on these columns.
* @returns {TableColumn} The matching column.
* @private
*/
function getColumnWithNameOrId(nameOrId, columns) {
for (var i = 0; i < columns.length; i++) {
if (columns[i].name === nameOrId || columns[i].id === nameOrId) {
return columns[i];
}
}
}
/**
* Returns the first column with the given name, id or index, or undefined if none match (or null is passed in).
* @param {String|Integer|null} nameIdOrIndex The column name, id or index.
* @param {TableColumn[]} columns Test on these columns.
* @returns {TableColumn} The matching column.
*/
function getColumnWithNameIdOrIndex(nameIdOrIndex, columns) {
if (nameIdOrIndex === null) {
return undefined;
}
if (isInteger(nameIdOrIndex)) {
return columns[nameIdOrIndex];
}
return getColumnWithNameOrId(nameIdOrIndex, columns);
}
/**
* Returns the first column with the given name or id, or undefined if none match.
* @param {String} nameOrId The column name or id.
* @returns {TableColumn} The matching column.
*/
TableStructure.prototype.getColumnWithNameOrId = function(nameOrId) {
return getColumnWithNameOrId(nameOrId, this.columns);
};
/**
* Returns the first column with the given name, id or index, or undefined if none match (or null is passed in).
* @param {String|Integer|null} nameIdOrIndex The column name, id or index.
* @returns {TableColumn} The matching column.
*/
TableStructure.prototype.getColumnWithNameIdOrIndex = function(nameIdOrIndex) {
return getColumnWithNameIdOrIndex(nameIdOrIndex, this.columns);
};
/**
* Add column to tableStructure.
*
* @param {String} name Name of column (column header).
* @param {Number[]} values Values of column to add to table.
*/
TableStructure.prototype.addColumn = function(name, values) {
var nameAndColumnOptions = getColumnOptions(name, this, 0);
var newCol = [new TableColumn(name, values, nameAndColumnOptions[1])];
var newCols = newCol.concat(this.columns);
this.columns = newCols;
};
// columns is a required parameter.
function getIdColumns(idColumnNames, columns) {
if (!defined(idColumnNames)) {
return [];
}
return idColumnNames.map(name => getColumnWithNameIdOrIndex(name, columns));
}
function getIdStringForRowNumber(idColumns, rowNumber) {
return idColumns
.map(function(column) {
return column.values[rowNumber];
})
.join("^^");
}
/**
* Returns an id string for the given row, based on idColumns (defaulting to idColumnNames).
* Use this to index into the result of this.getIdMapping().
* @param {Integer} rowNumber The row number.
* @param {Array} [idColumnNames] An array of id column names (or indexes or ids).
* @return {Object} An id string for that row based on joining the id column values for that row such as "Newtown^^NSW".
*/
TableStructure.prototype.getIdStringForRowNumber = function(
rowNumber,
idColumnNames
) {
if (!defined(idColumnNames)) {
idColumnNames = this.idColumnNames;
}
return getIdStringForRowNumber(
getIdColumns(idColumnNames, this.columns),
rowNumber
);
};
// Both arguments are required.
function getIdMapping(idColumnNames, columns) {
var idColumns = getIdColumns(idColumnNames, columns);
if (idColumns.length === 0) {
return {};
}
return idColumns[0].values.reduce(function(result, value, rowNumber) {
var idString = getIdStringForRowNumber(idColumns, rowNumber);
if (!defined(result[idString])) {
result[idString] = [];
}
result[idString].push(rowNumber);
return result;
}, {});
}
/**
* Returns a mapping from the idColumnNames to all the rows in the table with that id.
* If no columnIdNames are defined, returns undefined.
* @param {Array} [idColumnNames] Provide if you wish to override this table's own idColumnNames.
* This is supplied to getColumnWithNameIdOrIndex, so the "names" could be ids or indexes too.
* @return {Object} An object with keys equal to idStrings (use tableStructure.getIdStringForRowNumber(i) to get this)
* and values equal to an array of rowNumbers.
*/
TableStructure.prototype.getIdMapping = function(idColumnNames) {
if (!defined(idColumnNames)) {
idColumnNames = this.idColumnNames;
}
return getIdMapping(idColumnNames, this.columns);
};
/**
* Updates this table's columns with new ones, using the existing columns' metadata, and replacing the column values.
* If a time column is present, reset it, which can involve sorting the columns.
* @param {Array[]} updatedColumnValuesArrays Array of values arrays.
*/
TableStructure.prototype.getUpdatedColumns = function(
updatedColumnValuesArrays
) {
return this.columns.reduce((updatedColumns, column, columnNumber) => {
updatedColumns.push(
new TableColumn(
column.name,
updatedColumnValuesArrays[columnNumber],
column.getFullOptions()
)
);
return updatedColumns;
}, []);
};
/**
* Appends table2 to this table. If rowNumbers are provided, only takes those
* row numbers from table2.
* Changes all the columns in one go, to avoid partial updates from tracked values.
* @param {TableStructure} table2 The table to add to this one.
* @param {Integer[]} [rowNumbers] The row numbers from table2 to add (defaults to all).
*/
TableStructure.prototype.append = function(table2, rowNumbers) {
if (this.columns.length !== table2.columns.length) {
throw new DeveloperError(
"Cannot add tables with different numbers of columns."
);
}
var updatedColumnValuesArrays = [];
function mapRowNumberToValue(valuesToAdd) {
return rowNumber => valuesToAdd[rowNumber];
}
for (
var columnNumber = 0;
columnNumber < table2.columns.length;
columnNumber++
) {
var valuesToAdd;
if (defined(rowNumbers)) {
valuesToAdd = rowNumbers.map(
mapRowNumberToValue(table2.columns[columnNumber].values)
);
// Could also do: valuesToAdd = valuesToAdd.filter((_, rowNumber) => rowNumbers.indexOf(rowNumber) >= 0);
} else {
valuesToAdd = table2.columns[columnNumber].values;
}
updatedColumnValuesArrays.push(
this.columns[columnNumber].values.concat(valuesToAdd)
);
}
this.columns = this.getUpdatedColumns(updatedColumnValuesArrays);
};
/**
* Replace specific rows in this table with rows in table2.
* Changes all the columns in one go, to avoid partial updates from tracked values.
* @param {TableStructure} table2 The table whose rows should replace this table's rows.
* @param {Object} replacementMap An object whose properties are {table 1 row number: table 2 row number}.
*/
TableStructure.prototype.replaceRows = function(table2, replacementMap) {
var updatedColumnValuesArrays = [];
for (
var columnNumber = 0;
columnNumber < table2.columns.length;
columnNumber++
) {
updatedColumnValuesArrays.push(this.columns[columnNumber].values);
for (var table1RowNumber in replacementMap) {
if (replacementMap.hasOwnProperty(table1RowNumber)) {
var table2RowNumber = replacementMap[table1RowNumber];
updatedColumnValuesArrays[columnNumber][table1RowNumber] =
table2.columns[columnNumber].values[table2RowNumber];
}
}
}
var updatedColumns = this.columns.map(
(column, columnNumber) =>
new TableColumn(
column.name,
updatedColumnValuesArrays[columnNumber],
column.getFullOptions()
)
);
this.columns = updatedColumns;
};
function getColumnWithSameId(column1, columns) {
if (defined(column1)) {
var matchingColumns = columns.filter(column => column.id === column1.id);
if (matchingColumns.length !== 1) {
throw new DeveloperError("Ambiguous column: " + column1.name);
}
return matchingColumns[0];
}
}
/**
* Merges the rows of table2 into the rows of this table.
* Uses this.idColumnNames (and this.activeTimeColumn, if present) to identify matching rows.
* The columns must be in the same order in the two tables.
* Changes all the columns in one go, to avoid partial updates from tracked values.
* @param {TableStructure} table2 The table to merge into this one.
*/
TableStructure.prototype.merge = function(table2) {
if (!defined(this.idColumnNames) || this.idColumnNames.length === 0) {
throw new DeveloperError("Cannot merge tables without id columns.");
}
if (this.columns.length !== table2.columns.length) {
throw new DeveloperError(
"Cannot merge tables with different numbers of columns."
);
}
var table1RowNumbersMap = this.getIdMapping();
var table2RowNumbersMap = table2.getIdMapping(this.idColumnNames);
var rowsFromTable2ToAppend = []; // An array of row numbers.
var rowsToReplace = {}; // Properties are {table 1 row number: table 2 row number}.
var table2ActiveTimeColumn = getColumnWithSameId(
this.activeTimeColumn,
table2.columns
);
for (var featureIdString in table2RowNumbersMap) {
if (table2RowNumbersMap.hasOwnProperty(featureIdString)) {
var table2RowNumbersForThisFeature = table2RowNumbersMap[featureIdString];
var table1RowNumbersForThisFeature = table1RowNumbersMap[featureIdString];
if (!defined(table1RowNumbersForThisFeature)) {
// This feature appears in table 2, but not in table 1.
// Add all these rows to table 1.
rowsFromTable2ToAppend = rowsFromTable2ToAppend.concat(
table2RowNumbersForThisFeature
);
} else if (!this.activeTimeColumn) {
// The feature is in both tables, and there is no time column, so just replace table 1's.
rowsToReplace[table1RowNumbersForThisFeature[0]] =
table2RowNumbersForThisFeature[0];
} else {
for (var i = 0; i < table2RowNumbersForThisFeature.length; i++) {
var table2RowNumber = table2RowNumbersForThisFeature[i];
// Is there a row with this feature and this datetime already?
var table1Dates = table1RowNumbersForThisFeature.map(rowNumber =>
this.activeTimeColumn.dates[rowNumber].toString()
);
var table1DatesIndex = table1Dates.indexOf(
table2ActiveTimeColumn.dates[table2RowNumber].toString()
);
if (table1DatesIndex >= 0) {
// Yes, so replace it. (Noting table1DatesIndex is an index into table1RowNumbersForThisFeature.)
rowsToReplace[
table1RowNumbersForThisFeature[table1DatesIndex]
] = table2RowNumber;
} else {
// This is a new datetime, so append the row.
rowsFromTable2ToAppend.push(table2RowNumber);
}
}
}
}
}
// Replace existing rows from Table 2.
this.replaceRows(table2, rowsToReplace);
// Append new rows from Table 2.
this.append(table2, rowsFromTable2ToAppend);
};
/**
* Sets the relevant active time column on the table structure, defaulting to the first time column present
* unless the tableStyle has a 'timeColumn' property. A null timeColumn should explicitly not have a time column, even if one is present.
* @param {String|Number|undefined} nameIdOrIndex A way to identify the column, eg. from tableStyle.timeColumn.
*/
TableStructure.prototype.setActiveTimeColumn = function(nameIdOrIndex) {
function getIndexOfFirstTimeColumnOrStartAndEnd(columns) {
var startIndex, endIndex;
for (var i = columns.length - 1; i >= 0; i--) {
if (columns[i].type === VarType.TIME) {
if (columns[i]._isEndDate) {
endIndex = i;
} else {
startIndex = i;
}
}
}
if (defined(endIndex)) {
return [startIndex, endIndex];
} else {
return startIndex;
}
}
// undefined should default to the first time column, null should explicitly have no time column.
if (typeof nameIdOrIndex !== "undefined") {
this.activeTimeColumnNameIdOrIndex = nameIdOrIndex;
if (
defined(this.activeTimeColumn) &&
this.activeTimeColumn.type !== VarType.TIME
) {
this.activeTimeColumnNameIdOrIndex = getIndexOfFirstTimeColumnOrStartAndEnd(
this.columns
);
throw new DeveloperError(
'"' + nameIdOrIndex + '" is not a valid time column.'
);
}
} else {
this.activeTimeColumnNameIdOrIndex = getIndexOfFirstTimeColumnOrStartAndEnd(
this.columns
);
}
};
// Returns new columns sorted in sortColumn order.
function getSortedColumns(tableStructure, sortColumn, compareFunction) {
// With help from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
var mappedArray = sortColumn.julianDatesOrValues.map(function(value, i) {
return { index: i, value: value };
});
if (!defined(compareFunction)) {
if (sortColumn.type === VarType.TIME) {
compareFunction = function(a, b) {
if (defined(a) && defined(b)) {
return JulianDate.compare(a, b);
}
return defined(a) ? -1 : defined(b) ? 1 : 0; // so that undefined > defined, ie. all undefined dates go to the end.
};
} else {
compareFunction = function(a, b) {
return +(a > b) || +(a === b) - 1;
};
}
}
mappedArray.sort(function(a, b) {
return compareFunction(a.value, b.value);
});
return tableStructure.columns.map(column => {
var sortedValues = mappedArray.map(element => column.values[element.index]);
return new TableColumn(column.name, sortedValues, column.getFullOptions());
});
}
/**
* Sorts the rows of the TableStructure by the provided column's values.
* If the sortColumn is a date/time column, uses its julianDates to sort; otherwise, the values.
* The tableStructure is given new TableColumns.
* @param {TableColumn} sortColumn Column whose values should be sorted.
* @param {Function} [compareFunction] The compare function passed to Array.prototype.sort().
*/
TableStructure.prototype.sortBy = function(sortColumn, compareFunction) {
this.columns = getSortedColumns(this, sortColumn, compareFunction);
};
/**
* Given an id, find the index of the column and return that.
* Will return undefined if column with matching id cannot be found in tableStructure.
* @param {Number} id Id of column to find index of.
* @return {Number} index of column.
*/
TableStructure.prototype.getColumnIndex = function(id) {
var index;
for (var i = 0; i < this.columns.length; i++) {
if (id === this.columns[i].id) {
index = i;
}
}
return index;
};
/**
* Given an optional array of row numbers of the table which you would like to make into a chart,
* returns the key information for that chart, ie. the data (in csv string format), units, and x and y labels.
* @param {Number[]} [rowNumbers] The row numbers.
* @return {Object} An object with xName, yName, csvData (Strings) and units (String[]) properties.
*/
TableStructure.prototype.getChartDetailsForRowNumbers = function(rowNumbers) {
var csvData = this.toCsvString("source", rowNumbers, false);
const yColumn = this.getActiveColumns()[0];
if (defined(yColumn) && yColumn.type === VarType.SCALAR) {
return {
xName: this.activeTimeColumn.name,
yName: yColumn.name,
csvData: csvData,
units: this.columns.map(column => column.units || "")
};
}
};
/**
* Returns new columns for this table structure that include rows for all features at the full table's start and end dates,
* if they do not already exist. The data for all columns is copied from the feature's start and end date row,
* except for the provided valueColumn, which is set to null.
* Pass the time column explicitly if desired, to override this.activeTimeColumn. (Useful if you want to call this before setting it.)
* It is recommended if you use this, to also set the table's finalEndJulianDate beforehand, so the new feature rows don't blow out the end dates.
* @param {String|Integer} timeColumnNameIdOrIndex Name, id or index of the time column.
* @param {String|Integer} valueColumnNameIdOrIndex Name, id or index of the column which should be set to null at the table's start and end dates.
*/
TableStructure.prototype.getColumnsWithFeatureRowsAtStartAndEndDates = function(
timeColumnNameIdOrIndex,
valueColumnNameIdOrIndex
) {
// Get the min and max dates, both as a Number (which can be turned into a js date with new Date(number)),
// and in the same format as the original.
var tableStructure = this;
var valueColumnIndex = getIndexOfColumn(
tableStructure,
tableStructure.getColumnWithNameIdOrIndex(valueColumnNameIdOrIndex)
);
if (!defined(timeColumnNameIdOrIndex)) {
timeColumnNameIdOrIndex = tableStructure.activeTimeColumnNameIdOrIndex;
}
var timeColumn = tableStructure.getColumnWithNameIdOrIndex(
timeColumnNameIdOrIndex
);
var timeColumnIndex = getIndexOfColumn(tableStructure, timeColumn);
var dates = timeColumn.dates;
var minDateAsNumber = Math.min.apply(null, dates);
var maxDateAsNumber = Math.max.apply(null, dates);
var minDateString =
timeColumn.values[dates.map(d => Number(d)).indexOf(minDateAsNumber)];
var maxDateString =
timeColumn.values[dates.map(d => Number(d)).indexOf(maxDateAsNumber)];
// For each separate feature, as defined by this.idColumnNames, decide if we need to add missing-valued entry
// for the min and max dates.
var idMapping = tableStructure.getIdMapping();
var copiedColumnValues = tableStructure.columns.map(c => c.values.slice());
function addRowToCopiedColumnValues(newDateValue, rowNumberToCopy) {
// Appends a row to all the values from rowNumberToCopy, updates the date to newDateValue, and sets valueColumn to null.
var newRowNumber = copiedColumnValues[0].length;
for (var i = 0; i < copiedColumnValues.length; i++) {
copiedColumnValues[i].push(
tableStructure.columns[i].values[rowNumberToCopy]
);
}
copiedColumnValues[timeColumnIndex][newRowNumber] = newDateValue;
copiedColumnValues[valueColumnIndex][newRowNumber] = null;
}
Object.keys(idMapping).forEach(idString => {
var rowNumbers = idMapping[idString];
if (rowNumbers.length > 0) {
if (Number(timeColumn.dates[rowNumbers[0]]) > minDateAsNumber) {
addRowToCopiedColumnValues(minDateString, rowNumbers[0]);
}
var lastRowNumber = rowNumbers[rowNumbers.length - 1];
if (Number(timeColumn.dates[lastRowNumber]) < maxDateAsNumber) {
addRowToCopiedColumnValues(maxDateString, lastRowNumber);
}
}
});
return this.getUpdatedColumns(copiedColumnValues);
};
/**
* Destroy the object and release resources. Is this necessary?
*/
TableStructure.prototype.destroy = function() {
return destroyObject(this);
};
/**
* Return column options object, using defaults where appropriate.
*
* @param {String} name Name of column
* @param {TableStructure} tableStructure TableStructure to use to calculate values.
* @param {Int} columnNumber Which column should be used as template for default column options
* @return {Object} Column options that TableColumn's constructor understands
*/
function getColumnOptions(name, tableStructure, columnNumber) {
var columnOptions = defaultValue.EMPTY_OBJECT;
if (defined(tableStructure.columnOptions)) {
columnOptions = defaultValue(
tableStructure.columnOptions[name],
defaultValue(
tableStructure.columnOptions[columnNumber],
defaultValue.EMPTY_OBJECT
)
);
}
var niceName = defaultValue(columnOptions.name, name);
var type = getVarTypeFromString(columnOptions.type);
var format = defaultValue(columnOptions.format, format);
var displayDuration = defaultValue(
columnOptions.displayDuration,
tableStructure.displayDuration
);
var replaceWithNullValues = defaultValue(
columnOptions.replaceWithNullValues,
tableStructure.replaceWithNullValues
);
var replaceWithZeroValues = defaultValue(
columnOptions.replaceWithZeroValues,
tableStructure.replaceWithZeroValues
);
var colOptions = {
tableStructure: tableStructure,
displayVariableTypes: tableStructure.displayVariableTypes,
unallowedTypes: tableStructure.unallowedTypes,
displayDuration: displayDuration,
replaceWithNullValues: replaceWithNullValues,
replaceWithZeroValues: replaceWithZeroValues,
id: name,
type: type,
units: columnOptions.units,
format: columnOptions.format,
active: columnOptions.active,
chartLineColor: columnOptions.chartLineColor,
yAxisMin: columnOptions.yAxisMin,
yAxisMax: columnOptions.yAxisMax
};
return [niceName, colOptions];
}
/**
* Normally a TableStructure is generated from a csvString, using loadFromCsv, or via loadFromJson.
* However, if its columns are set directly, we should check the columns are all the same length.
* @private
* @param {Concept[]} columns Array of columns to check.
* @return {Boolean} True if the columns are all the same length, false otherwise.
*/
function areColumnsEqualLength(columns) {
if (columns.length <= 1) {
return true;
}
var firstLength = columns[0].values.length;
var columnsWithTheSameLength = columns.slice(1).filter(function(column) {
return column.values.length === firstLength;
});
return columnsWithTheSameLength.length === columns.length - 1;
}
/**
* Given columns, returns columnsByType, which is an object whose keys are elements of VarType,
* and whose values are arrays of TableColumn objects of that type.
* All types are present (eg. structure.columnsByType[VarType.ALT] always exists), possibly [].
* @private
*/
function getColumnsByType(columns) {
var columnsByType = {};
for (var varType in VarType) {
if (VarType.hasOwnProperty(varType)) {
var v = VarType[varType]; // we don't want the keys to be LAT, LON, ..., but 0, 1, ...
columnsByType[v] = [];
}
}
for (var i = 0; i < columns.length; i++) {
var column = columns[i];
if (defined(columnsByType[column.type])) {
columnsByType[column.type].push(column);
}
}
return columnsByType;
}
function isInteger(value) {
return (
!isNaN(value) &&
parseInt(Number(value), 10) === +value &&
!isNaN(parseInt(value, 10))
);
}
function isString(param) {
return typeof param === "string" || param instanceof String;
}
// Return the value, or value[0] if it is an array.
function valueOrFirstValue(valueOrArray) {
if (Array.isArray(valueOrArray)) {
return valueOrArray[0];
}
return valueOrArray;
}
module.exports = TableStructure;