UNPKG

terriajs

Version:

Geospatial data visualization platform.

990 lines (890 loc) 72.2 kB
'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