terriajs
Version:
Geospatial data visualization platform.
990 lines (890 loc) • 72.2 kB
JavaScript
'use strict';
/*global require,describe,it,expect,beforeEach,fail*/
var clone = require('terriajs-cesium/Source/Core/clone');
var Color = require('terriajs-cesium/Source/Core/Color');
var JulianDate = require('terriajs-cesium/Source/Core/JulianDate');
var Rectangle = require('terriajs-cesium/Source/Core/Rectangle');
var CatalogItem = require('../../lib/Models/CatalogItem');
var CsvCatalogItem = require('../../lib/Models/CsvCatalogItem');
var ImageryLayerCatalogItem = require('../../lib/Models/ImageryLayerCatalogItem');
var ImageryProviderHooks = require('../../lib/Map/ImageryProviderHooks');
var loadAndStubTextResources = require('../Utility/loadAndStubTextResources');
var TableStyle = require('../../lib/Models/TableStyle');
var Terria = require('../../lib/Models/Terria');
var TimeInterval = require('terriajs-cesium/Source/Core/TimeInterval');
var VarType = require('../../lib/Map/VarType');
var greenTableStyle = new TableStyle({
'colorMap': [
{
'offset': 0,
'color': 'rgba(0, 64, 0, 1.00)'
},
{
'offset': 1,
'color': 'rgba(0, 255, 0, 1.00)'
}
]
});
function featureColor(csvItem, i) {
return csvItem.dataSource.entities.values[i]._point._color._value;
}
describe('CsvCatalogItem with lat and lon', function() {
var terria;
var csvItem;
beforeEach(function() {
terria = new Terria({
baseUrl: './'
});
csvItem = new CsvCatalogItem(terria);
});
it('has sensible type and typeName', function() {
expect(csvItem.type).toBe('csv');
expect(csvItem.typeName).toBe('Comma-Separated Values (CSV)');
});
it('throws if constructed without a Terria instance', function() {
expect(function() {
var viewModel = new CsvCatalogItem(); // eslint-disable-line no-unused-vars
}).toThrow();
});
it('can be constructed', function() {
expect(csvItem).toBeDefined();
});
it('is derived from CatalogItem', function() {
expect(csvItem instanceof CatalogItem).toBe(true);
});
it('can update from json', function() {
var dataStr = 'col1, col2\ntest, 0';
csvItem.updateFromJson({
name: 'Name',
description: 'Description',
rectangle: [-10, 10, -20, 20],
url: 'http://my.csv.com/test.csv',
data: dataStr,
dataSourceUrl: 'none',
dataCustodian: 'Data Custodian',
});
expect(csvItem.name).toBe('Name');
expect(csvItem.description).toBe('Description');
expect(csvItem.rectangle).toEqual(Rectangle.fromDegrees(-10, 10, -20, 20));
expect(csvItem.type).toBe('csv');
expect(csvItem.url.indexOf('http://my.csv.com/test.csv')).toBe(0);
expect(csvItem.data.indexOf(dataStr)).toBe(0);
expect(csvItem.dataCustodian).toBe('Data Custodian');
expect(csvItem.dataSourceUrl).toBe('none');
});
it('uses reasonable defaults for updateFromJson', function() {
csvItem.updateFromJson({});
expect(csvItem.name).toBe('Unnamed Item');
expect(csvItem.description).toBe('');
expect(csvItem.rectangle).toBeUndefined();
expect(csvItem.type).toBe('csv');
expect(csvItem.url).toBeUndefined();
expect(csvItem.data).toBeUndefined();
expect(csvItem.dataSourceUrl).toBeUndefined();
expect(csvItem.dataCustodian).toBeUndefined();
});
it('can be round-tripped with serializeToJson and updateFromJson', function() {
var dataStr = 'col1, col2\ntest, 0';
csvItem.updateFromJson({
name: 'Name',
id: 'Id',
description: 'Description',
rectangle: [-10, 10, -20, 20],
url: 'http://my.csv.com/test.csv',
data: dataStr,
dataSourceUrl: 'none',
dataCustodian: 'Data Custodian',
dataUrl: 'http://my.csv.com/test.csv',
dataUrlType: 'direct'
});
var json = csvItem.serializeToJson();
var reconstructed = new CsvCatalogItem(terria);
reconstructed.updateFromJson(json);
expect(reconstructed.name).toEqual(csvItem.name);
expect(reconstructed.id).toEqual(csvItem.id);
expect(reconstructed.description).toEqual(csvItem.description);
expect(reconstructed.rectangle).toEqual(csvItem.rectangle);
expect(reconstructed.url).toEqual(csvItem.url);
expect(reconstructed.data).toEqual(csvItem.data);
expect(reconstructed.dataSourceUrl).toEqual(csvItem.dataSourceUrl);
expect(reconstructed.dataCustodian).toEqual(csvItem.dataCustodian);
expect(reconstructed.dataUrl).toEqual(csvItem.dataUrl);
expect(reconstructed.dataUrlType).toEqual(csvItem.dataUrlType);
});
it('is correctly loading csv data from a file', function(done) {
csvItem.url = 'test/csv/minimal.csv';
csvItem.load().then(function() {
expect(csvItem.dataSource).toBeDefined();
expect(csvItem.dataSource.tableStructure).toBeDefined();
expect(csvItem.dataSource.tableStructure.columns.length).toEqual(5);
}).otherwise(fail).then(done);
});
it('is able to generate a Legend', function(done) {
csvItem.url = 'test/csv/minimal.csv';
csvItem.load().then(function() {
expect(csvItem.legendUrl).toBeDefined();
expect(csvItem.legendUrl.mimeType).toBeDefined();
expect(csvItem.legendUrl.url).toBeDefined();
}).otherwise(fail).then(done);
});
it('identifies "lat" and "lon" fields', function(done) {
csvItem.updateFromJson( { data: 'lat,lon,value\n-37,145,10' });
csvItem.load().then(function() {
expect(csvItem.dataSource.tableStructure.hasLatitudeAndLongitude).toBe(true);
}).otherwise(fail).then(done);
});
it('identifies "latitude" and "longitude" fields', function(done) {
csvItem.updateFromJson( { data: 'latitude,longitude,value\n-37,145,10' });
csvItem.load().then(function() {
expect(csvItem.dataSource.tableStructure.hasLatitudeAndLongitude).toBe(true);
}).otherwise(fail).then(done);
});
it('does not mistakenly identify "latvian" and "lone_person" fields', function(done) {
csvItem.updateFromJson( { data: 'latvian,lone_person,lat,lon,value\n-37,145,-37,145,10' });
csvItem.load().then(function() {
expect(csvItem.dataSource.tableStructure.columnsByType[VarType.LON][0].name).toEqual('lon');
expect(csvItem.dataSource.tableStructure.columnsByType[VarType.LAT][0].name).toEqual('lat');
}).otherwise(fail).then(done);
});
it('handles one line with enum', function(done) {
csvItem.updateFromJson({data: 'lat,lon,org\n-37,145,test'});
csvItem.load().then(function() {
expect(csvItem.dataSource.tableStructure.hasLatitudeAndLongitude).toBe(true);
expect(csvItem.legendUrl).toBeDefined();
}).otherwise(fail).then(done);
});
it('handles numeric fields containing (quoted) thousands commas', function(done) {
csvItem.updateFromJson({data: 'lat,lon,value\n-37,145,"1,000"\n-38,145,"234,567.89"'});
csvItem.load().then(function() {
var tableStructure = csvItem.dataSource.tableStructure;
expect(tableStructure.hasLatitudeAndLongitude).toBe(true);
expect(tableStructure.columns[2].values[0]).toEqual(1000);
expect(tableStructure.columns[2].values[1]).toBeCloseTo(234567.89, 2);
}).otherwise(fail).then(done);
});
it('handles missing lines', function(done) {
csvItem.url = 'test/csv/blank_line.csv';
csvItem.load().then(function() {
var tableStructure = csvItem.dataSource.tableStructure;
var latColumn = tableStructure.columnsByType[VarType.LAT][0];
var lonColumn = tableStructure.columnsByType[VarType.LON][0];
// There are 7 lines after the header, but only 4 are non-blank.
expect(tableStructure.columns[0].values.length).toBe(4);
expect(latColumn.minimumValue).toBeLessThan(-30);
expect(lonColumn.minimumValue).toBeGreaterThan(150);
}).otherwise(fail).then(done);
});
it('handles enum fields', function(done) {
csvItem.url = 'test/csv/lat_lon_enum.csv';
csvItem.load().then(function() {
expect(csvItem.dataSource.tableStructure.activeItems[0].name).toBe('enum');
}).otherwise(fail).then(done);
});
it('sets active variable to dataVariable if provided', function(done) {
csvItem.url = 'test/csv/lat_lon_enum_val.csv';
csvItem._tableStyle = new TableStyle({
dataVariable: 'val'
});
csvItem.load().then(function() {
expect(csvItem.dataSource.tableStructure.activeItems[0].name).toBe('val');
}).otherwise(fail).then(done);
});
it('does not set an active variable to dataVariable if null', function(done) {
csvItem.url = 'test/csv/lat_lon_enum_val.csv';
csvItem._tableStyle = new TableStyle({
dataVariable: null
});
csvItem.load().then(function() {
expect(csvItem.dataSource.tableStructure.activeItems.length).toEqual(0);
}).otherwise(fail).then(done);
});
it('colors enum fields the same (only) when the value is the same', function(done) {
csvItem.url = 'test/csv/lat_lon_enum.csv';
csvItem.load().then(function() {
expect(featureColor(csvItem, 0)).not.toEqual(featureColor(csvItem, 1));
expect(featureColor(csvItem, 0)).not.toEqual(featureColor(csvItem, 2));
expect(featureColor(csvItem, 0)).not.toEqual(featureColor(csvItem, 3));
expect(featureColor(csvItem, 0)).toEqual(featureColor(csvItem, 4));
expect(featureColor(csvItem, 1)).toEqual(featureColor(csvItem, 3));
}).otherwise(fail).then(done);
});
it('handles no data variable', function(done) {
csvItem.url = 'test/csv/lat_lon_novals.csv';
csvItem.load().then(function() {
expect(csvItem.dataSource.tableStructure.activeItems.length).toEqual(0);
expect(csvItem.dataSource.tableStructure.columns.length).toEqual(2);
expect(csvItem.dataSource.tableStructure.columns[0].values.length).toEqual(5);
}).otherwise(fail).then(done);
});
it('supports dates', function(done) {
csvItem.url = 'test/csv/lat_long_enum_moving_date.csv';
csvItem.load().then(function() {
var source = csvItem.dataSource;
expect(source.tableStructure.activeTimeColumn.name).toEqual('date');
expect(source.tableStructure.columns[0].values.length).toEqual(13);
expect(source.tableStructure.columnsByType[VarType.TIME].length).toEqual(1);
expect(source.tableStructure.columnsByType[VarType.TIME][0].julianDates[0]).toEqual(JulianDate.fromIso8601('2015-08-01'));
// Test that an entity exists at the expected dates.
var features = source.entities.values;
var featureDates = features.map(getPropertiesDate);
expect(featureDates.indexOf('2015-07-31')).toBe(-1); // no such dates in the input file
expect(featureDates.indexOf('2015-08-07')).toBe(-1);
var earlyFeature = features[featureDates.indexOf('2015-08-01')];
// The date '2015-08-01' appears to be interpreted as starting at midnight in the local time zone (at least on Chrome).
// Eg. in Sydney summer, JulianDate.toIso8601(earlyFeature.availability.start) returns "2015-07-31T14:00:00Z".
expect(TimeInterval.contains(earlyFeature.availability, JulianDate.fromIso8601('2015-08-01'))).toBe(true);
// Also test the duration of the interval is one day (the time between input rows).
var durationInSeconds = JulianDate.secondsDifference(earlyFeature.availability.stop, earlyFeature.availability.start);
expect(durationInSeconds).toBe(24 * 3600); // 24 hours
}).otherwise(fail).then(done);
});
it('supports dates and very long displayDuration', function(done) {
var sevenDaysInMinutes = 60 * 24 * 7;
csvItem.url = 'test/csv/lat_long_enum_moving_date.csv';
csvItem._tableStyle = new TableStyle({displayDuration: sevenDaysInMinutes});
csvItem.load().then(function() {
// Now, the features' availabilities should persist for 7 days, not just under 1 day.
var features = csvItem.dataSource.entities.values;
var featureDates = features.map(getPropertiesDate);
var earlyFeature = features[featureDates.indexOf('2015-08-01')];
expect(TimeInterval.contains(earlyFeature.availability, JulianDate.fromIso8601('2015-08-01T12:00:00Z'))).toBe(true);
var durationInSeconds = JulianDate.secondsDifference(earlyFeature.availability.stop, earlyFeature.availability.start);
expect(durationInSeconds).toEqual(sevenDaysInMinutes * 60);
}).otherwise(fail).then(done);
});
it('supports dates sorted randomly', function(done) {
// Now that we use availability to establish when entities exist, this is not much of a test.
// Could delete, or change it to test something more useful.
csvItem.url = 'test/csv/lat_lon_enum_moving_date_unsorted.csv';
csvItem.load().then(function() {
var source = csvItem.dataSource;
expect(source.tableStructure.columns[0].values.length).toEqual(13);
expect(source.tableStructure.columnsByType[VarType.TIME].length).toEqual(1);
// expect(source.tableStructure.columnsByType[VarType.TIME][0].julianDates[0]).toEqual(JulianDate.fromIso8601('2015-08-05'));
// Test that an entity exists at the expected dates.
var features = source.entities.values;
var featureDates = features.map(getPropertiesDate);
expect(featureDates.indexOf('2015-07-31')).toBe(-1); // no such dates in the input file
expect(featureDates.indexOf('2015-08-07')).toBe(-1);
var earlyFeature = features[featureDates.indexOf('2015-08-01')];
// The date '2015-08-01' appears to be interpreted as starting at midnight in the local time zone (at least on Chrome).
// Eg. in Sydney summer, JulianDate.toIso8601(earlyFeature.availability.start) returns "2015-07-31T14:00:00Z".
expect(TimeInterval.contains(earlyFeature.availability, JulianDate.fromIso8601('2015-08-01'))).toBe(true);
// Also test the duration of the interval is one day (the time between input rows).
var durationInSeconds = JulianDate.secondsDifference(earlyFeature.availability.stop, earlyFeature.availability.start);
expect(durationInSeconds).toBe(24 * 3600); // 24 hours
}).otherwise(fail).then(done);
});
it('supports moving-point csvs with id column by default', function(done) {
csvItem.url = 'test/csv/lat_lon_enum_date_id.csv';
csvItem.load().then(function() {
var features = csvItem.dataSource.entities.values;
expect(features.length).toEqual(4); // There are 4 features A, B, C and D; does not equal the 13 rows in the file.
var featureA = features.filter(function(feature) { return feature.name === 'feature A'; })[0];
// FeatureA has rows for 1,2,4,5,6th of August. But it should still be available on the 3rd.
expect(TimeInterval.contains(featureA.availability, JulianDate.fromIso8601('2015-08-02'))).toBe(true);
expect(TimeInterval.contains(featureA.availability, JulianDate.fromIso8601('2015-08-03'))).toBe(true);
expect(TimeInterval.contains(featureA.availability, JulianDate.fromIso8601('2015-08-04'))).toBe(true);
expect(TimeInterval.contains(featureA.availability, JulianDate.fromIso8601('2015-08-08'))).toBe(false);
}).otherwise(fail).then(done);
});
it('supports overriding moving-point csvs with id column using null', function(done) {
csvItem.url = 'test/csv/lat_lon_enum_date_id.csv';
csvItem.idColumns = null;
csvItem.load().then(function() {
var features = csvItem.dataSource.entities.values;
expect(features.length).toEqual(13); // There are 13 rows in the file.
}).otherwise(fail).then(done);
});
it('supports overriding moving-point csvs with id column using []', function(done) {
csvItem.url = 'test/csv/lat_lon_enum_date_id.csv';
csvItem.idColumns = [];
csvItem.load().then(function() {
var features = csvItem.dataSource.entities.values;
expect(features.length).toEqual(13); // There are 13 rows in the file.
}).otherwise(fail).then(done);
});
it('ignores dates if tableStyle.timeColumn is null', function(done) {
csvItem.url = 'test/csv/lat_long_enum_moving_date.csv';
csvItem._tableStyle = new TableStyle({timeColumn: null});
csvItem.load().then(function() {
var source = csvItem.dataSource;
expect(source.tableStructure.activeTimeColumn).toBeUndefined();
expect(csvItem.clock).toBeUndefined();
expect(source.clock).toBeUndefined();
}).otherwise(fail).then(done);
});
it('ignores dates if tableStyle.timeColumn is set to null from json', function(done) {
// The test above did not pick up a problem in updateFromJson when the meaning of Cesium's defined was changed to also mean notNull (Cesium 1.19).
csvItem.url = 'test/csv/lat_long_enum_moving_date.csv';
csvItem._tableStyle = new TableStyle();
csvItem._tableStyle.updateFromJson({timeColumn: null});
csvItem.load().then(function() {
var source = csvItem.dataSource;
expect(source.tableStructure.activeTimeColumn).toBeUndefined();
expect(csvItem.clock).toBeUndefined();
expect(source.clock).toBeUndefined();
}).otherwise(fail).then(done);
});
it('uses a second date column with tableStyle.timeColumn name', function(done) {
csvItem.url = 'test/csv/lat_lon_enum_date_year.csv';
csvItem._tableStyle = new TableStyle({timeColumn: 'year'});
csvItem.load().then(function() {
var source = csvItem.dataSource;
expect(source.tableStructure.activeTimeColumn.name).toEqual('year');
}).otherwise(fail).then(done);
});
it('uses a second date column with tableStyle.timeColumn index', function(done) {
csvItem.url = 'test/csv/lat_lon_enum_date_year.csv';
csvItem._tableStyle = new TableStyle({timeColumn: 4});
csvItem.load().then(function() {
var source = csvItem.dataSource;
expect(source.tableStructure.activeTimeColumn.name).toEqual('year');
}).otherwise(fail).then(done);
});
it('returns valid values for intervals', function(done) {
csvItem.url = 'test/csv/lat_long_enum_moving_date.csv';
csvItem._tableStyle = new TableStyle({displayDuration: 60});
csvItem.load().then(function() {
var intervals = csvItem.intervals;
expect(intervals.length).toBe(6); // 13 rows over 6 days
// interval length is 1 hour
expect(intervals.get(0).start).toEqual(JulianDate.fromIso8601('2015-08-01'));
expect(intervals.get(0).stop).toEqual(JulianDate.fromIso8601('2015-08-01T01:00Z'));
expect(intervals.start).toEqual(JulianDate.fromIso8601('2015-08-01'));
expect(intervals.stop).toEqual(JulianDate.fromIso8601('2015-08-06T01:00Z'));
}).otherwise(fail).then(done);
});
it('has the right values in descriptions for feature picking', function(done) {
csvItem.url = 'test/csv/lat_lon_enum.csv';
csvItem.load().then(function() {
function desc(i) { return csvItem.dataSource.entities.values[i].description._value; }
expect(desc(0)).toContain('hello');
expect(desc(1)).toContain('boots');
}).otherwise(fail).then(done);
});
it('has a blank in the description table for a missing number', function(done) {
csvItem.url = 'test/csv/missingNumberFormatting.csv';
return csvItem.load().then(function() {
var entities = csvItem.dataSource.entities.values;
expect(entities.length).toBe(2);
expect(entities[0].description.getValue()).toMatch('<td>Vals</td><td[^>]*>10</td>');
expect(entities[1].description.getValue()).toMatch('<td>Vals</td><td[^>]*>-</td>');
}).otherwise(fail).then(done);
});
it('scales points to a size ratio of 300% if scaleByValue true and respects scale value', function(done) {
csvItem.url = 'test/csv/lat_lon_val.csv';
csvItem._tableStyle = new TableStyle({scale: 5, scaleByValue: true });
return csvItem.load().then(function() {
var pixelSizes = csvItem.dataSource.entities.values.map(function(e) { return e.point._pixelSize._value; });
csvItem._minPix = Math.min.apply(null, pixelSizes);
csvItem._maxPix = Math.max.apply(null, pixelSizes);
// we don't want to be too prescriptive, but by default the largest object should be 150% normal, smallest is 50%, so 3x difference.
expect(csvItem._maxPix).toEqual(csvItem._minPix * 3);
}).then(function() {
var csvItem2 = new CsvCatalogItem(terria);
csvItem2._tableStyle = new TableStyle({scale: 10, scaleByValue: true });
csvItem2.url = 'test/csv/lat_lon_val.csv';
return csvItem2.load().yield(csvItem2);
}).then(function(csvItem2) {
var pixelSizes = csvItem2.dataSource.entities.values.map(function(e) { return e.point._pixelSize._value; });
var minPix = Math.min.apply(null, pixelSizes);
var maxPix = Math.max.apply(null, pixelSizes);
// again, we don't specify the base size, but x10 things should be twice as big as x5 things.
expect(maxPix).toEqual(csvItem._maxPix * 2);
expect(minPix).toEqual(csvItem._minPix * 2);
}).otherwise(fail).then(done);
});
it('does not make a feature if it is missing longitude', function(done) {
csvItem.url = 'test/csv/lat_lon-missing_val.csv';
return csvItem.load().then(function() {
expect(csvItem.tableStructure.columns[0].values.length).toEqual(5);
expect(csvItem.dataSource.entities.values.length).toEqual(4); // one line is missing longitude.
}).otherwise(fail).then(done);
});
it('makes features even if no value column', function(done) {
csvItem.url = 'test/csv/lat_lon.csv';
return csvItem.load().then(function() {
expect(csvItem.dataSource.entities.values.length).toBeGreaterThan(1);
}).otherwise(fail).then(done);
});
it('supports replaceWithNullValues', function(done) {
csvItem.url = 'test/csv/lat_lon_badvalue.csv';
csvItem._tableStyle = new TableStyle({replaceWithNullValues: ['bad']});
csvItem.load().then(function() {
var valueColumn = csvItem.tableStructure.columns[2];
expect(valueColumn.values[0]).toEqual(5);
expect(valueColumn.values[1]).toEqual(null);
expect(valueColumn.values[2]).toEqual(0);
}).otherwise(fail).then(done);
});
it('supports replaceWithZeroValues', function(done) {
csvItem.url = 'test/csv/lat_lon_badvalue.csv';
csvItem._tableStyle = new TableStyle({replaceWithZeroValues: ['bad']});
csvItem.load().then(function() {
var valueColumn = csvItem.tableStructure.columns[2];
expect(valueColumn.values[0]).toEqual(5);
expect(valueColumn.values[1]).toEqual(0);
expect(valueColumn.values[2]).toEqual(0);
}).otherwise(fail).then(done);
});
it('defaults to blanks in numeric columns being null', function(done) {
csvItem.url = 'test/csv/lat_lon_blankvalue.csv';
csvItem.load().then(function() {
var valueColumn = csvItem.tableStructure.columns[2];
expect(valueColumn.values[0]).toEqual(5);
expect(valueColumn.values[1]).toEqual(null);
expect(valueColumn.values[2]).toEqual(0);
}).otherwise(fail).then(done);
});
it('does not color null the same as zero', function(done) {
csvItem.url = 'test/csv/lat_lon_badvalue.csv';
csvItem._tableStyle = new TableStyle({replaceWithNullValues: ['bad']});
csvItem.load().then(function() {
expect(featureColor(csvItem, 1)).not.toEqual(featureColor(csvItem, 2));
}).otherwise(fail).then(done);
});
it('supports nullColor', function(done) {
csvItem.url = 'test/csv/lat_lon_badvalue.csv';
csvItem._tableStyle = new TableStyle({
replaceWithNullValues: ['bad'],
nullColor: '#A0B0C0'
});
var nullColor = new Color(160/255, 176/255, 192/255, 1);
csvItem.load().then(function() {
expect(featureColor(csvItem, 1)).toEqual(nullColor);
// This next expectation checks that zeros and null values are differently colored, and that
// null values do not lead to coloring getting out of sync with values.
expect(featureColor(csvItem, 2)).not.toEqual(nullColor);
}).otherwise(fail).then(done);
});
it('when no column selected, colors with non-null color', function(done) {
csvItem.url = 'test/csv/lat_lon_enum_val.csv';
csvItem._tableStyle = new TableStyle({
dataVariable: null,
nullColor: '#000000'
});
var nullColor = new Color(0, 0, 0, 1);
csvItem.load().then(function() {
expect(featureColor(csvItem, 1)).not.toEqual(nullColor);
}).otherwise(fail).then(done);
});
it('replaces enum tail with "X other values" in the legend', function(done) {
csvItem.url = 'test/csv/lat_lon_enum_lots.csv';
csvItem._tableStyle = new TableStyle({colorBins: 9});
csvItem.load().then(function() {
expect(csvItem.legendUrl).toBeDefined();
var url = csvItem.legendUrl.url;
expect(url).toContain('2 other values');
expect(url).not.toContain('unicorns');
expect(url).toContain('guinea pigs');
}).otherwise(fail).then(done);
});
it('does not replace enum tail with "other values" if it fits', function(done) {
csvItem.url = 'test/csv/lat_lon_enum_lots2.csv';
csvItem._tableStyle = new TableStyle({colorBins: 9});
csvItem.load().then(function() {
expect(csvItem.legendUrl).toBeDefined();
expect(csvItem.legendUrl.url).not.toContain('other values');
expect(csvItem.legendUrl.url).toContain('turtles');
}).otherwise(fail).then(done);
});
it('honors colorBins property when it is less than the number of colors in the palette', function(done) {
csvItem.url = 'test/csv/lat_lon_enum_lots.csv';
csvItem._tableStyle = new TableStyle({colorBins: 3});
csvItem.load().then(function() {
expect(csvItem.legendUrl).toBeDefined();
var url = csvItem.legendUrl.url;
expect(url).toContain('8 other values');
expect(url).toContain('cats');
expect(url).toContain('dogs');
}).otherwise(fail).then(done);
});
it('displays a "XX values" legend when colorBinMethod=cycle and there are more unique values than color bins', function(done) {
csvItem.url = 'test/csv/lat_lon_enum_lots.csv';
csvItem._tableStyle = new TableStyle({colorBins: 9, colorBinMethod: 'cycle'});
csvItem.load().then(function() {
expect(csvItem.legendUrl).toBeDefined();
var url = csvItem.legendUrl.url;
expect(url).toContain('10 values');
expect(url).not.toContain('dogs');
}).otherwise(fail).then(done);
});
it('displays a normal legend when colorBinMethod=cycle but there are less unique values than color bins', function(done) {
csvItem.url = 'test/csv/lat_lon_enum_lots.csv';
csvItem._tableStyle = new TableStyle({colorBins: 15, colorBinMethod: 'cycle'});
csvItem.load().then(function() {
expect(csvItem.legendUrl).toBeDefined();
var url = csvItem.legendUrl.url;
expect(url).not.toContain('values');
expect(url).toContain('dogs');
}).otherwise(fail).then(done);
});
describe('and per-column tableStyle', function() {
it('scales by value', function(done) {
csvItem.url = 'test/csv/lat_lon_val.csv';
csvItem._tableStyle = new TableStyle({
columns: {
value: { // only scale the 'value' column
scale: 5,
scaleByValue: true
}
}
});
return csvItem.load().then(function() {
var pixelSizes = csvItem.dataSource.entities.values.map(function(e) { return e.point._pixelSize._value; });
csvItem._minPix = Math.min.apply(null, pixelSizes);
csvItem._maxPix = Math.max.apply(null, pixelSizes);
// we don't want to be too prescriptive, but by default the largest object should be 150% normal, smallest is 50%, so 3x difference.
expect(csvItem._maxPix).toEqual(csvItem._minPix * 3);
}).then(function() {
var csvItem2 = new CsvCatalogItem(terria);
csvItem2._tableStyle = new TableStyle({scale: 10, scaleByValue: true }); // this time, apply it to all columns
csvItem2.url = 'test/csv/lat_lon_val.csv';
return csvItem2.load().yield(csvItem2);
}).then(function(csvItem2) {
var pixelSizes = csvItem2.dataSource.entities.values.map(function(e) { return e.point._pixelSize._value; });
var minPix = Math.min.apply(null, pixelSizes);
var maxPix = Math.max.apply(null, pixelSizes);
// again, we don't specify the base size, but x10 things should be twice as big as x5 things.
expect(maxPix).toEqual(csvItem._maxPix * 2);
expect(minPix).toEqual(csvItem._minPix * 2);
}).otherwise(fail).then(done);
});
it('uses correct defaults', function(done) {
// nullColor is passed through to the columns as well, if not overridden explicitly.
csvItem.url = 'test/csv/lat_lon_badvalue.csv';
csvItem._tableStyle = new TableStyle({
nullColor: '#A0B0C0',
columns: {
value: {
replaceWithNullValues: ['bad']
}
}
});
var nullColor = new Color(160/255, 176/255, 192/255, 1);
csvItem.load().then(function() {
expect(featureColor(csvItem, 1)).toEqual(nullColor);
}).otherwise(fail).then(done);
});
it('supports name and nullColor with column ref by name', function(done) {
csvItem.url = 'test/csv/lat_lon_badvalue.csv';
csvItem._tableStyle = new TableStyle({
nullColor: '#123456',
columns: {
value: {
replaceWithNullValues: ['bad'],
nullColor: '#A0B0C0',
name: 'Temperature'
}
}
});
var nullColor = new Color(160/255, 176/255, 192/255, 1);
csvItem.load().then(function() {
expect(csvItem.tableStructure.columns[2].name).toEqual('Temperature');
expect(featureColor(csvItem, 1)).toEqual(nullColor);
}).otherwise(fail).then(done);
});
it('supports nullColor with column ref by number', function(done) {
csvItem.url = 'test/csv/lat_lon_badvalue.csv';
csvItem._tableStyle = new TableStyle({
columns: {
2: {
replaceWithNullValues: ['bad'],
nullColor: '#A0B0C0'
}
}
});
var nullColor = new Color(160/255, 176/255, 192/255, 1);
csvItem.load().then(function() {
expect(featureColor(csvItem, 1)).toEqual(nullColor);
}).otherwise(fail).then(done);
});
it('supports type', function(done) {
csvItem.url = 'test/csv/lat_lon_badvalue.csv';
csvItem._tableStyle = new TableStyle({
columns: {
value: {
replaceWithNullValues: ['bad'],
type: 'enum'
}
}
});
csvItem.load().then(function() {
expect(csvItem.tableStructure.columns[2].type).toEqual(VarType.ENUM);
}).otherwise(fail).then(done);
});
});
});
// eg. use as entities.map(getPropertiesDate) to just get the dates of the entities.
function getPropertiesDate(obj) {
return obj.properties.date.getValue();
}
// eg. use as regions.map(getId) to just get the ids of the regions.
function getId(obj) {
return obj.id;
}
describe('CsvCatalogItem with region mapping', function() {
var terria;
var csvItem;
beforeEach(function() {
terria = new Terria({
baseUrl: './'
});
terria.configParameters.regionMappingDefinitionsUrl = 'test/csv/regionMapping.json';
csvItem = new CsvCatalogItem(terria);
// Instead of directly inspecting the recoloring function (which is a private and inaccessible variable),
// get it from this function call.
// This unfortunately makes the test depend on an implementation detail.
spyOn(ImageryProviderHooks, 'addRecolorFunc');
// Also, for feature detection, spy on this call; the second argument is the regionImageryProvider.
// This unfortunately makes the test depend on an implementation detail.
spyOn(ImageryLayerCatalogItem, 'enableLayer');
// loadAndStubTextResources(done, [
// terria.configParameters.regionMappingDefinitionsUrl
// ]).then(done).otherwise(done.fail);
});
it('does not think a lat-lon csv has regions', function(done) {
csvItem.url = 'test/csv/lat_long_enum_moving_date.csv';
csvItem.load().then(function() {
expect(csvItem.regionMapping).toBeUndefined();
}).otherwise(fail).then(done);
});
it('does not use region mapping when regions present with lat and lon', function(done) {
csvItem.url = 'test/csv/lat_lon_enum_postcode.csv';
csvItem.load().then(function() {
expect(csvItem.regionMapping).toBeUndefined();
}).otherwise(fail).then(done);
});
it('detects LGAs by code', function(done) {
csvItem.updateFromJson({data: 'lga_code,value\n31000,1'});
csvItem.load().then(function() {
var regionDetails = csvItem.regionMapping.regionDetails;
expect(regionDetails).toBeDefined();
var regionDetail = regionDetails[0];
expect(regionDetail.columnName).toEqual('lga_code');
expect(regionDetail.regionProvider.regionType).toEqual('LGA');
expect(csvItem.legendUrl).toBeDefined();
}).otherwise(fail).then(done);
});
it('matches LGAs by code', function(done) {
csvItem.updateFromJson({data: 'lga_code,value\n31000,1'});
csvItem.load().then(function() {
csvItem.isEnabled = true; // The recolorFunction call is only made once the layer is enabled.
var regionDetails = csvItem.regionMapping.regionDetails;
expect(regionDetails).toBeDefined();
var regionDetail = regionDetails[0];
var recolorFunction = ImageryProviderHooks.addRecolorFunc.calls.argsFor(0)[1];
var indexOfThisRegion = regionDetail.regionProvider.regions.map(getId).indexOf(31000);
expect(recolorFunction(indexOfThisRegion)[0]).toBeDefined(); // Test that at least one rgba component is defined.
expect(recolorFunction(indexOfThisRegion)).not.toEqual([0, 0, 0, 0]); // And that the color is not all zeros.
}).otherwise(fail).then(done);
});
it('matches LGAs by names in various formats', function(done) {
// City of Melbourne is not actually a region, but melbourne is. Same with Sydney (S) and sydney. But test they work anyway.
csvItem.updateFromJson({data: 'lga_name,value\nCity of Melbourne,1\nGreater Geelong,2\nSydney (S),3'});
csvItem.load().then(function() {
csvItem.isEnabled = true;
var regionDetails = csvItem.regionMapping.regionDetails;
expect(regionDetails).toBeDefined();
var regionDetail = regionDetails[0];
var recolorFunction = ImageryProviderHooks.addRecolorFunc.calls.argsFor(0)[1];
var regionNames = regionDetail.regionProvider.regions.map(getId);
expect(recolorFunction(regionNames.indexOf('bogan'))).not.toBeDefined(); // Test that we didn't try to recolor other regions.
expect(recolorFunction(regionNames.indexOf('melbourne'))[0]).toBeDefined(); // Test that at least one rgba component is defined.
expect(recolorFunction(regionNames.indexOf('melbourne'))).not.toEqual([0, 0, 0, 0]); // And that the color is not all zeros.
expect(recolorFunction(regionNames.indexOf('greater geelong'))[0]).toBeDefined(); // Test that at least one rgba component is defined.
expect(recolorFunction(regionNames.indexOf('greater geelong'))).not.toEqual([0, 0, 0, 0]); // And that the color is not all zeros.
expect(recolorFunction(regionNames.indexOf('sydney'))[0]).toBeDefined(); // Test that at least one rgba component is defined.
expect(recolorFunction(regionNames.indexOf('sydney'))).not.toEqual([0, 0, 0, 0]); // And that the color is not all zeros.
}).otherwise(fail).then(done);
});
it('matches mapped region column names', function(done) {
csvItem.updateFromJson({
data: 'nothing,value\n31000,1',
tableStyle: {
columns: {
'nothing': {
name: 'lga_code'
}
}
}
});
csvItem.load().then(function() {
expect(csvItem.regionMapping.regionDetails).toBeDefined();
}).otherwise(fail).then(done);
});
it('does not match original name of mapped region column names', function(done) {
csvItem.updateFromJson({
data: 'lga_code,value\n31000,1',
tableStyle: {
columns: {
'lga_code': {
name: 'something else'
}
}
}
});
csvItem.load().then(function() {
expect(csvItem.regionMapping).not.toBeDefined();
}).otherwise(fail).then(done);
});
// TODO: What is this testing?
xit('matches numeric state IDs with regexes', function(done) {
csvItem.updateFromJson({data: 'state,value\n3,30\n4,40\n5,50,\n8,80\n9,90'});
csvItem.load().then(function() {
csvItem.isEnabled = true;
var regionDetails = csvItem.regionMapping.regionDetails;
expect(regionDetails).toBeDefined();
var regionDetail = regionDetails[0];
var regionNames = regionDetail.regionProvider.regions.map(getId);
// TODO: This is the old test, which doesn't really have an equivalent in the new csv refactor:
// expect(csvItem.dataSource.dataset.variables.state.regionCodes).toEqual(["queensland", "south australia", "western australia", "other territories"]);
// Possibly something like this? However, this fails - it includes tasmania and not queensland.
var names = csvItem.dataSource.tableStructure.columns[0].values.map(function(id) { return regionNames[id]; });
expect(names).toEqual(["queensland", "south australia", "western australia", "other territories"]);
}).otherwise(fail).then(done);
});
// I think this would be better as a test of RegionProvider?
// it('matches SA4s', function(done) {
// csvItem.updateFromJson({data: 'sa4,value\n209,correct'});
// csvItem.load().then(function() {
// csvItem.isEnabled = true;
// return csvItem.dataSource.regionPromise.then(function(regionDetails) {
// expect(regionDetails).toBeDefined();
// // There is no "rowPropertiesByCode" method any more.
// expect(csvItem.rowPropertiesByCode(209).value).toBe('correct');
// }).otherwise(fail);
// }).otherwise(fail).then(done);
// });
it('respects tableStyle color ramping for regions', function(done) {
csvItem.updateFromJson({
data: 'lga_name,value\nmelbourne,0\ngreater geelong,5\nsydney,10',
tableStyle: greenTableStyle
});
csvItem.load().then(function() {
csvItem.isEnabled = true;
var regionDetails = csvItem.regionMapping.regionDetails;
expect(regionDetails).toBeDefined();
var regionDetail = regionDetails[0];
var recolorFunction = ImageryProviderHooks.addRecolorFunc.calls.argsFor(0)[1];
var regionNames = regionDetail.regionProvider.regions.map(getId);
// Require the green value to range from 64 to 255, but do not require a linear mapping.
expect(recolorFunction(regionNames.indexOf('melbourne'))).toEqual([0, 64, 0, 255]);
expect(recolorFunction(regionNames.indexOf('greater geelong'))[1]).toBeGreaterThan(64);
expect(recolorFunction(regionNames.indexOf('greater geelong'))[1]).toBeLessThan(255);
expect(recolorFunction(regionNames.indexOf('sydney'))).toEqual([0, 255, 0, 255]);
expect(csvItem.legendUrl).toBeDefined();
}).otherwise(fail).then(done);
});
it('uses the requested region mapping column, not just the first one', function(done) {
// The column names in postcode_lga_val_enum.csv are: lga_name, val1, enum, postcode.
var revisedGreenTableStyle = clone(greenTableStyle);
revisedGreenTableStyle.regionType = 'poa';
revisedGreenTableStyle.regionVariable = 'postcode';
csvItem.updateFromJson({
url: 'test/csv/postcode_lga_val_enum.csv',
tableStyle: revisedGreenTableStyle
});
csvItem.load().then(function() {
var regionDetails = csvItem.regionMapping.regionDetails;
expect(regionDetails).toBeDefined();
expect(csvItem.tableStructure.columnsByType[VarType.REGION][0].name).toBe('postcode');
}).otherwise(fail).then(done);
});
it('can default to an enum field', function(done) {
csvItem.url = 'test/csv/postcode_enum.csv';
csvItem.load().then(function() {
var regionDetails = csvItem.regionMapping.regionDetails;
expect(regionDetails).toBeDefined();
expect(csvItem.tableStructure.activeItems[0].name).toBe('enum');
expect(csvItem.legendUrl).toBeDefined();
}).otherwise(fail).then(done);
});
it('handles one line with enum', function(done) {
csvItem.updateFromJson({data: 'state,org\nNSW,test'});
csvItem.load().then(function() {
var regionDetails = csvItem.regionMapping.regionDetails;
expect(regionDetails).toBeDefined();
expect(csvItem.legendUrl).toBeDefined();
}).otherwise(fail).then(done);
});
it('handles no data variable', function(done) {
csvItem.url = 'test/csv/postcode_novals.csv';
csvItem.load().then(function() {
var regionDetails = csvItem.regionMapping.regionDetails;
expect(regionDetails).toBeDefined();
expect(csvItem.tableStructure.activeItems.length).toEqual(0);
expect(csvItem.tableStructure.columns[0].values.length).toBeGreaterThan(1);
csvItem.isEnabled = true;
expect(csvItem.legendUrl).toBeDefined();
}).otherwise(fail).then(done);
});
it('chooses the leftmost data column when none specified', function(done) {
csvItem.url = 'test/csv/val_enum_postcode.csv';
csvItem.load().then(function() {
var regionDetails = csvItem.regionMapping.regionDetails;
expect(regionDetails).toBeDefined();
expect(csvItem.tableStructure.activeItems[0].name).toEqual('val1');
}).otherwise(fail).then(done);
});
it('handles LGA names with states for disambiguation', function(done) {
csvItem.updateFromJson({
url: 'test/csv/lga_state_disambig.csv',
tableStyle: new TableStyle({dataVariable: 'StateCapital'})
});
csvItem.load().then(function() {
var regionDetails = csvItem.regionMapping.regionDetails;
expect(regionDetails).toBeDefined();
var regionDetail = regionDetails[0];
expect(regionDetail.disambigColumnName).toEqual('State');
// The following test is much more rigorous.
}).otherwise(fail).then(done);
});
it('supports region-mapped files with dates', function(done) {
csvItem.updateFromJson({
url: 'test/csv/postcode_date_value.csv'
});
csvItem.load().then(function() {
var regionMapping = csvItem.regionMapping;
var j = JulianDate.fromIso8601;
regionMapping._catalogItem.clock.currentTime = j('2015-08-08');
csvItem.isEnabled = true;
var regionDetails = regionMapping.regionDetails;
expect(regionDetails).toBeDefined();
var regionDetail = regionDetails[0];
expect(csvItem.tableStructure.columns[0].values.length).toEqual(10);
expect(csvItem.tableStructure.columnsByType[VarType.TIME].length).toEqual(1);
expect(csvItem.tableStructure.columnsByType[VarType.TIME][0].julianDates[0]).toEqual(j('2015-08-07'));
// Test that the right regions have been colored (since the datasource doesn't expose the entities).
// On 2015-08-08, only postcodes 3121 and 3122 have values. On neighboring dates, so do 3123 and 3124.
var recolorFunction = ImageryProviderHooks.addRecolorFunc.calls.argsFor(0)[1];
var regionNames = regionDetail.regionProvider.regions.map(getId);
expect(recolorFunction(regionNames.indexOf('3121'))).toBeDefined();
expect(recolorFunction(regionNames.indexOf('3122'))).toBeDefined();
expect(recolorFunction(regionNames.indexOf('3123'))).not.toBeDefined();
expect(recolorFunction(regionNames.indexOf('3124'))).not.toBeDefined();
expect(csvItem.legendUrl).toBeDefined();
}).otherwise(fail).then(done);
});
it('supports region-mapped files with missing dates', function(done) {
csvItem.updateFromJson({
url: 'test/csv/postcode_date_value_missing_date.csv'
});
csvItem.load().then(function() {
var regionMapping = csvItem.regionMapping;
var j = JulianDate.fromIso8601;
regionMapping._catalogItem.clock.currentTime = j('2015-08-08');
csvItem.isEnabled = true;
var regionDetails = regionMapping.regionDetails;
expect(regionDetails).toBeDefined();
var regionDetail = regionDetails[0];
expect(csvItem.tableStructure.columns[0].values.length).toEqual(10);
expect(csvItem.tableStructure.columnsByType[VarType.TIME].length).toEqual(1);
expect(csvItem.tableStructure.columnsByType[VarType.TIME][0].julianDates[0]).toEqual(j('2015-08-07'));
// Test that the right regions have been colored (since the datasource doesn't expose the entities).
// On 2015-08-08, only postcodes 3121 and 3122 have values. On neighboring dates, so do 3123 and 3124.
var recolorFunction = ImageryProviderHooks.addRecolorFunc.calls.argsFor(0)[1];
var regionNames = regionDetail.regionProvider.regions.map(getId);
expect(recolorFunction(regionNames.indexOf('3121'))).toBeDefined();
expect(recolorFunction(regionNames.indexOf('3122'))).not.toBeDefined