Add more automated map tests (#15507) (#15767)

Adds screenshot tests for the region maps.

This also closes https://github.com/elastic/kibana/pull/15483, https://github.com/elastic/kibana/issues/13293
This commit is contained in:
Thomas Neirynck 2017-12-26 14:01:13 -05:00 committed by GitHub
parent 91b8340a7d
commit cc57ef4feb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 587 additions and 124 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

View file

@ -0,0 +1,387 @@
import expect from 'expect.js';
import ngMock from 'ng_mock';
import _ from 'lodash';
import { RegionMapsVisualizationProvider } from '../region_map_visualization';
import ChoroplethLayer from '../choropleth_layer';
import LogstashIndexPatternStubProvider from 'fixtures/stubbed_logstash_index_pattern';
import * as visModule from 'ui/vis';
import { ImageComparator } from 'test_utils/image_comparator';
import sinon from 'sinon';
import worldJson from './world.json';
import initialPng from './initial.png';
import toiso3Png from './toiso3.png';
import afterresizePng from './afterresize.png';
import afterdatachangePng from './afterdatachange.png';
import afterdatachangeandresizePng from './afterdatachangeandresize.png';
import aftercolorchangePng from './aftercolorchange.png';
import changestartupPng from './changestartup.png';
const manifestUrl = 'https://staging-dot-catalogue-dot-elastic-layer.appspot.com/v1/manifest';
const tmsManifestUrl = `"https://tiles-maps-stage.elastic.co/v2/manifest`;
const vectorManifestUrl = `"https://staging-dot-elastic-layer.appspot.com/v1/manifest`;
const manifest = {
'services': [{
'id': 'tiles_v2',
'name': 'Elastic Tile Service',
'manifest': tmsManifestUrl,
'type': 'tms'
},
{
'id': 'geo_layers',
'name': 'Elastic Layer Service',
'manifest': vectorManifestUrl,
'type': 'file'
}
]
};
const tmsManifest = {
'services': [{
'id': 'road_map',
'url': 'https://tiles.elastic.co/v2/default/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana',
'minZoom': 0,
'maxZoom': 10,
'attribution': '© [OpenStreetMap](http://www.openstreetmap.org/copyright) © [Elastic Maps Service](https://www.elastic.co/elastic-maps-service)'
}]
};
const vectorManifest = {
'layers': [{
'attribution': '',
'name': 'US States',
'format': 'geojson',
'url': 'https://storage.googleapis.com/elastic-layer.appspot.com/L2FwcGhvc3RpbmdfcHJvZC9ibG9icy9BRW5CMlVvNGJ0aVNidFNJR2dEQl9rbTBjeXhKMU01WjRBeW1kN3JMXzM2Ry1qc3F6QjF4WE5XdHY2ODlnQkRpZFdCY2g1T2dqUGRHSFhSRTU3amlxTVFwZjNBSFhycEFwV2lYR29vTENjZjh1QTZaZnRpaHBzby5VXzZoNk1paGJYSkNPalpI?elastic_tile_service_tos=agree',
'fields': [{ 'name': 'postal', 'description': 'Two letter abbreviation' }, {
'name': 'name',
'description': 'State name'
}],
'created_at': '2017-04-26T19:45:22.377820',
'id': 5086441721823232
}, {
'attribution': '© [Elastic Tile Service](https://www.elastic.co/elastic-maps-service)',
'name': 'World Countries',
'format': 'geojson',
'url': 'https://storage.googleapis.com/elastic-layer.appspot.com/L2FwcGhvc3RpbmdfcHJvZC9ibG9icy9BRW5CMlVwWTZTWnhRRzNmUk9HUE93TENjLXNVd2IwdVNpc09SRXRyRzBVWWdqOU5qY2hldGJLOFNZSFpUMmZmZWdNZGx0NWprT1R1ZkZ0U1JEdFBtRnkwUWo0S0JuLTVYY1I5RFdSMVZ5alBIZkZuME1qVS04TS5oQTRNTl9yRUJCWk9tMk03?elastic_tile_service_tos=agree',
'fields': [{ 'name': 'iso2', 'description': 'Two letter abbreviation' }, {
'name': 'name',
'description': 'Country name'
}, { 'name': 'iso3', 'description': 'Three letter abbreviation' }],
'created_at': '2017-04-26T17:12:15.978370',
'id': 5659313586569216
}]
};
const THRESHOLD = 0.25;
const PIXEL_DIFF = 64;
describe('RegionMapsVisualizationTests', function () {
let domNode;
let RegionMapsVisualization;
let Vis;
let indexPattern;
let vis;
let imageComparator;
const _makeJsonAjaxCallOld = ChoroplethLayer.prototype._makeJsonAjaxCall;
const dummyTableGroup = {
tables: [
{
columns: [{
'aggConfig': {
'id': '2',
'enabled': true,
'type': 'terms',
'schema': 'segment',
'params': { 'field': 'geo.dest', 'size': 5, 'order': 'desc', 'orderBy': '1' }
}, 'title': 'geo.dest: Descending'
}, {
'aggConfig': { 'id': '1', 'enabled': true, 'type': 'count', 'schema': 'metric', 'params': {} },
'title': 'Count'
}],
rows: [['CN', 26], ['IN', 17], ['US', 6], ['DE', 4], ['BR', 3]]
}
]
};
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject((Private, $injector) => {
Vis = Private(visModule.VisProvider);
RegionMapsVisualization = Private(RegionMapsVisualizationProvider);
indexPattern = Private(LogstashIndexPatternStubProvider);
ChoroplethLayer.prototype._makeJsonAjaxCall = async function () {
//simulate network call
return new Promise((resolve)=> {
setTimeout(() => {
resolve(worldJson);
}, 10);
});
};
const serviceSettings = $injector.get('serviceSettings');
sinon.stub(serviceSettings, '_getManifest', function (url) {
let contents = null;
if (url.startsWith(tmsManifestUrl)) {
contents = tmsManifest;
} else if (url.startsWith(vectorManifestUrl)) {
contents = vectorManifest;
} else if (url.startsWith(manifestUrl)) {
contents = manifest;
}
return {
data: contents
};
});
}));
afterEach(function () {
ChoroplethLayer.prototype._makeJsonAjaxCall = _makeJsonAjaxCallOld;
});
describe('RegionMapVisualization - basics', function () {
beforeEach(async function () {
setupDOM('512px', '512px');
imageComparator = new ImageComparator();
vis = new Vis(indexPattern, {
type: 'region_map'
});
vis.params.selectedJoinField = { 'name': 'iso2', 'description': 'Two letter abbreviation' };
vis.params.selectedLayer = {
'attribution': '<p><a href="http://www.naturalearthdata.com/about/terms-of-use">Made with NaturalEarth</a> | <a href="https://www.elastic.co/elastic-maps-service">Elastic Maps Service</a></p>&#10;',
'name': 'World Countries',
'format': 'geojson',
'url': 'https://staging-dot-elastic-layer.appspot.com/blob/5715999101812736?elastic_tile_service_tos=agree&my_app_version=7.0.0-alpha1',
'fields': [{ 'name': 'iso2', 'description': 'Two letter abbreviation' }, {
'name': 'iso3',
'description': 'Three letter abbreviation'
}, { 'name': 'name', 'description': 'Country name' }],
'created_at': '2017-07-31T16:00:19.996450',
'id': 5715999101812736,
'layerId': 'elastic_maps_service.World Countries'
};
});
afterEach(function () {
teardownDOM();
imageComparator.destroy();
});
it('should instantiate at zoom level 2', async function () {
const regionMapsVisualization = new RegionMapsVisualization(domNode, vis);
await regionMapsVisualization.render(dummyTableGroup, {
resize: false,
params: true,
aggs: true,
data: true,
uiState: false
});
const mismatchedPixels = await compareImage(initialPng);
regionMapsVisualization.destroy();
expect(mismatchedPixels).to.be.lessThan(PIXEL_DIFF);
});
it('should update after resetting join field', async function () {
const regionMapsVisualization = new RegionMapsVisualization(domNode, vis);
await regionMapsVisualization.render(dummyTableGroup, {
resize: false,
params: true,
aggs: true,
data: true,
uiState: false
});
//this will actually create an empty image
vis.params.selectedJoinField = { 'name': 'iso3', 'description': 'Three letter abbreviation' };
vis.params.isDisplayWarning = false;//so we don't get notifications
await regionMapsVisualization.render(dummyTableGroup, {
resize: false,
params: true,
aggs: false,
data: false,
uiState: false
});
const mismatchedPixels = await compareImage(toiso3Png);
regionMapsVisualization.destroy();
expect(mismatchedPixels).to.be.lessThan(PIXEL_DIFF);
});
it('should resize', async function () {
const regionMapsVisualization = new RegionMapsVisualization(domNode, vis);
await regionMapsVisualization.render(dummyTableGroup, {
resize: false,
params: true,
aggs: true,
data: true,
uiState: false
});
domNode.style.width = '256px';
domNode.style.height = '128px';
await regionMapsVisualization.render(dummyTableGroup, {
resize: true,
params: false,
aggs: false,
data: false,
uiState: false
});
const mismatchedPixelsAfterFirstResize = await compareImage(afterresizePng);
domNode.style.width = '512px';
domNode.style.height = '512px';
await regionMapsVisualization.render(dummyTableGroup, {
resize: true,
params: false,
aggs: false,
data: false,
uiState: false
});
const mismatchedPixelsAfterSecondResize = await compareImage(initialPng);
regionMapsVisualization.destroy();
expect(mismatchedPixelsAfterFirstResize).to.be.lessThan(PIXEL_DIFF);
expect(mismatchedPixelsAfterSecondResize).to.be.lessThan(PIXEL_DIFF);
});
it('should redo data', async function () {
const regionMapsVisualization = new RegionMapsVisualization(domNode, vis);
await regionMapsVisualization.render(dummyTableGroup, {
resize: false,
params: true,
aggs: true,
data: true,
uiState: false
});
const newTableGroup = _.cloneDeep(dummyTableGroup);
newTableGroup.tables[0].rows.pop();//remove one shape
await regionMapsVisualization.render(newTableGroup, {
resize: false,
params: false,
aggs: false,
data: true,
uiState: false
});
const mismatchedPixelsAfterDataChange = await compareImage(afterdatachangePng);
const anoterTableGroup = _.cloneDeep(newTableGroup);
anoterTableGroup.tables[0].rows.pop();//remove one shape
domNode.style.width = '412px';
domNode.style.height = '112px';
await regionMapsVisualization.render(anoterTableGroup, {
resize: true,
params: false,
aggs: false,
data: true,
uiState: false
});
const mismatchedPixelsAfterDataChangeAndResize = await compareImage(afterdatachangeandresizePng);
regionMapsVisualization.destroy();
expect(mismatchedPixelsAfterDataChange).to.be.lessThan(PIXEL_DIFF);
expect(mismatchedPixelsAfterDataChangeAndResize).to.be.lessThan(PIXEL_DIFF);
});
it('should redo data and color ramp', async function () {
const regionMapsVisualization = new RegionMapsVisualization(domNode, vis);
await regionMapsVisualization.render(dummyTableGroup, {
resize: false,
params: true,
aggs: true,
data: true,
uiState: false
});
const newTableGroup = _.cloneDeep(dummyTableGroup);
newTableGroup.tables[0].rows.pop();//remove one shape
vis.params.colorSchema = 'Blues';
await regionMapsVisualization.render(newTableGroup, {
resize: false,
params: true,
aggs: false,
data: true,
uiState: false
});
const mismatchedPixelsAfterDataAndColorChange = await compareImage(aftercolorchangePng);
regionMapsVisualization.destroy();
expect(mismatchedPixelsAfterDataAndColorChange).to.be.lessThan(PIXEL_DIFF);
});
it('should zoom and center elsewhere', async function () {
vis.params.mapZoom = 4;
vis.params.mapCenter = [36, -85];
const regionMapsVisualization = new RegionMapsVisualization(domNode, vis);
await regionMapsVisualization.render(dummyTableGroup, {
resize: false,
params: true,
aggs: true,
data: true,
uiState: false
});
const mismatchedPixels = await compareImage(changestartupPng);
regionMapsVisualization.destroy();
expect(mismatchedPixels).to.be.lessThan(PIXEL_DIFF);
});
});
async function compareImage(expectedImageSource) {
const elementList = domNode.querySelectorAll('canvas');
expect(elementList.length).to.equal(1);
const firstCanvasOnMap = elementList[0];
return imageComparator.compareImage(firstCanvasOnMap, expectedImageSource, THRESHOLD);
}
function setupDOM(width, height) {
domNode = document.createElement('div');
domNode.style.top = '0';
domNode.style.left = '0';
domNode.style.width = width;
domNode.style.height = height;
domNode.style.position = 'fixed';
domNode.style.border = '1px solid blue';
domNode.style['pointer-events'] = 'none';
document.body.appendChild(domNode);
}
function teardownDOM() {
domNode.innerHTML = '';
document.body.removeChild(domNode);
}
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

File diff suppressed because one or more lines are too long

View file

@ -47,25 +47,28 @@ export default class ChoroplethLayer extends KibanaMapLayer {
this._loaded = false;
this._error = false;
this._whenDataLoaded = new Promise((resolve) => {
$.ajax({
dataType: 'json',
url: geojsonUrl,
success: (data) => {
this._leafletLayer.addData(data);
this._loaded = true;
this._setStyle();
resolve();
},
error: () => {
this._loaded = true;
this._error = true;
resolve();
}
});
this._whenDataLoaded = new Promise(async (resolve) => {
try {
const data = await this._makeJsonAjaxCall(geojsonUrl);
this._leafletLayer.addData(data);
this._loaded = true;
this._setStyle();
resolve();
} catch (e) {
this._loaded = true;
this._error = true;
resolve();
}
});
}
//This method is stubbed in the tests to avoid network request during unit tests.
async _makeJsonAjaxCall(url) {
return await $.ajax({
dataType: 'json',
url: url
});
}
_setStyle() {
if (this._error || (!this._loaded || !this._metrics || !this._joinField)) {

View file

@ -24,13 +24,13 @@ export function RegionMapsVisualizationProvider(Private, Notifier, config) {
async render(esReponse, status) {
await super.render(esReponse, status);
await this._choroplethLayer.whenDataLoaded();
if (this._choroplethLayer) {
await this._choroplethLayer.whenDataLoaded();
}
}
async _updateData(tableGroup) {
let results;
if (!tableGroup || !tableGroup.tables || !tableGroup.tables.length || tableGroup.tables[0].columns.length !== 2) {
results = [];

View file

@ -29,7 +29,7 @@ window.__KBN__ = {
layers: []
},
mapConfig: {
manifestServiceUrl: 'https://geo.elastic.co/v1/manifest'
manifestServiceUrl: 'https://staging-dot-catalogue-dot-elastic-layer.appspot.com/v1/manifest'
}
},
uiSettings: {

View file

@ -1,5 +1,4 @@
import expect from 'expect.js';
import pixelmatch from 'pixelmatch';
import { KibanaMap } from '../kibana_map';
import { GeohashLayer } from '../geohash_layer';
import { GeoHashSampleData } from './geohash_sample_data';
@ -7,12 +6,14 @@ import heatmapPng from './heatmap.png';
import scaledCircleMarkersPng from './scaledCircleMarkers.png';
import shadedCircleMarkersPng from './shadedCircleMarkers.png';
import shadedGeohashGridPng from './shadedGeohashGrid.png';
import { ImageComparator } from 'test_utils/image_comparator';
describe('kibana_map tests', function () {
let domNode;
let expectCanvas;
let kibanaMap;
let imageComparator;
function setupDOM() {
domNode = document.createElement('div');
@ -38,6 +39,7 @@ describe('kibana_map tests', function () {
beforeEach(async function () {
setupDOM();
imageComparator = new ImageComparator();
kibanaMap = new KibanaMap(domNode, {
minZoom: 1,
maxZoom: 10
@ -52,6 +54,7 @@ describe('kibana_map tests', function () {
afterEach(function () {
kibanaMap.destroy();
teardownDOM();
imageComparator.destroy();
});
[
@ -78,57 +81,18 @@ describe('kibana_map tests', function () {
}
].forEach(function (test) {
it(test.options.mapType, function (done) {
it(test.options.mapType, async function () {
const geohashGridOptions = test.options;
const geohashLayer = new GeohashLayer(GeoHashSampleData, geohashGridOptions, kibanaMap.getZoomLevel(), kibanaMap);
kibanaMap.addLayer(geohashLayer);
// Give time for canvas to render before checking output
window.setTimeout(() => {
// Extract image data from live map
const elementList = domNode.querySelectorAll('canvas');
expect(elementList.length).to.equal(1);
const canvas = elementList[0];
const ctx = canvas.getContext('2d');
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const elementList = domNode.querySelectorAll('canvas');
expect(elementList.length).to.equal(1);
const canvas = elementList[0];
// convert expect PNG into pixel data by drawing in new canvas element
expectCanvas.id = 'expectCursor';
expectCanvas.width = canvas.width;
expectCanvas.height = canvas.height;
const imageEl = new Image();
imageEl.onload = () => {
const expectCtx = expectCanvas.getContext('2d');
expectCtx.drawImage(imageEl, 0, 0, canvas.width, canvas.height); // draw reference image to size of generated image
const expectImageData = expectCtx.getImageData(0, 0, canvas.width, canvas.height);
// compare live map vs expected pixel data
const diffImage = expectCtx.createImageData(canvas.width, canvas.height);
const mismatchedPixels = pixelmatch(
imageData.data,
expectImageData.data,
diffImage.data,
canvas.width,
canvas.height,
{ threshold: 0.1 });
expect(mismatchedPixels < 16).to.equal(true);
// Display difference image for refernce
expectCtx.putImageData(diffImage, 0, 0);
done();
};
imageEl.src = test.expected;
// Instructions for creating expected image PNGs
// Comment out imageEl creation and image loading
// Comment out teardown line that removes expectCanvas from DOM
// Uncomment out below lines. Run test, right click canvas and select "Save Image As"
// const expectCtx = expectCanvas.getContext('2d');
// expectCtx.putImageData(imageData, 0, 0);
// done();
}, 200);
const mismatchedPixels = await imageComparator.compareImage(canvas, test.expected, 0.1);
expect(mismatchedPixels).to.be.lessThan(16);
});
});
@ -144,5 +108,29 @@ describe('kibana_map tests', function () {
kibanaMap.fitToData();
}).to.not.throwException();
});
it('should not throw when resizing to 0 on heatmap', function () {
const geohashGridOptions = {
mapType: 'Heatmap',
heatmap: {
heatClusterSize: '2'
}
};
const geohashLayer = new GeohashLayer(GeoHashSampleData, geohashGridOptions, kibanaMap.getZoomLevel(), kibanaMap);
kibanaMap.addLayer(geohashLayer);
domNode.style.width = 0;
domNode.style.height = 0;
expect(() => {
kibanaMap.resize();
}).to.not.throwException();
});
});
});

View file

@ -98,7 +98,7 @@ describe('kibana_map tests', function () {
});
});
describe('no map height', function () {
describe('no map height (should default to size of 1px for height)', function () {
beforeEach(async function () {
setupDOM('386px', '256px');
const noHeightNode = createDiv('386px', '0px');
@ -111,18 +111,24 @@ describe('kibana_map tests', function () {
});
});
it('should calculate map dimensions based on parent element dimensions', function () {
it('should calculate map dimensions based on enforcement of single pixel min-width CSS-rule', function () {
const bounds = kibanaMap.getUntrimmedBounds();
expect(bounds).to.have.property('bottom_right');
expect(bounds.bottom_right.lon.toFixed(2)).to.equal('0.27');
expect(bounds.bottom_right.lat.toFixed(2)).to.equal('-0.18');
expect(round(bounds.bottom_right.lon, 2)).to.equal(0.27);
expect(round(bounds.bottom_right.lat, 2)).to.equal(0);
expect(bounds).to.have.property('top_left');
expect(bounds.top_left.lon.toFixed(2)).to.equal('-0.27');
expect(bounds.top_left.lat.toFixed(2)).to.equal('0.18');
expect(round(bounds.top_left.lon, 2)).to.equal(-0.27);
expect(round(bounds.top_left.lat, 2)).to.equal(0);
});
function round(num, dig) {
return Math.round(num * Math.pow(10, dig)) / Math.pow(10, dig);
}
});
describe('no map width', function () {
describe('no map width (should default to size of 1px for width)', function () {
beforeEach(async function () {
setupDOM('386px', '256px');
const noWidthNode = createDiv('0px', '256px');
@ -135,13 +141,13 @@ describe('kibana_map tests', function () {
});
});
it('should calculate map dimensions based on parent element dimensions', function () {
it('should calculate map dimensions based on enforcement of single pixel min-width CSS-rule', function () {
const bounds = kibanaMap.getUntrimmedBounds();
expect(bounds).to.have.property('bottom_right');
expect(bounds.bottom_right.lon.toFixed(2)).to.equal('0.27');
expect(Math.round(bounds.bottom_right.lon)).to.equal(0);
expect(bounds.bottom_right.lat.toFixed(2)).to.equal('-0.18');
expect(bounds).to.have.property('top_left');
expect(bounds.top_left.lon.toFixed(2)).to.equal('-0.27');
expect(Math.round(bounds.top_left.lon)).to.equal(0);
expect(bounds.top_left.lat.toFixed(2)).to.equal('0.18');
});
});

View file

@ -24,6 +24,7 @@ export function BaseMapsVisualizationProvider(serviceSettings) {
destroy() {
if (this._kibanaMap) {
this._kibanaMap.destroy();
this._kibanaMap = null;
}
}
@ -44,6 +45,11 @@ export function BaseMapsVisualizationProvider(serviceSettings) {
*/
async render(esResponse, status) {
if (!this._kibanaMap) {
//the visualization has been destroyed;
return;
}
await this._mapIsLoaded;
if (status.resize) {
@ -78,8 +84,9 @@ export function BaseMapsVisualizationProvider(serviceSettings) {
const uiState = this.vis.getUiState();
const zoomFromUiState = parseInt(uiState.get('mapZoom'));
const centerFromUIState = uiState.get('mapCenter');
options.zoom = !isNaN(zoomFromUiState) ? zoomFromUiState : this.vis.type.visConfig.defaults.mapZoom;
options.center = centerFromUIState ? centerFromUIState : this.vis.type.visConfig.defaults.mapCenter;
options.zoom = !isNaN(zoomFromUiState) ? zoomFromUiState : this.vis.params.mapZoom;
options.center = centerFromUIState ? centerFromUIState : this.vis.params.mapCenter;
this._kibanaMap = new KibanaMap(this._container, options);
this._kibanaMap.addLegendControl();
@ -95,8 +102,6 @@ export function BaseMapsVisualizationProvider(serviceSettings) {
const mapparams = this._getMapsParams();
await this._updateBaseLayer(mapparams);
}
@ -111,7 +116,6 @@ export function BaseMapsVisualizationProvider(serviceSettings) {
this._notify.warning(e.message);
}
const { minZoom, maxZoom } = this._getMinMaxZoom();
if (mapParams.wms.enabled) {
// Switch to WMS
if (maxZoom > this._kibanaMap.getMaxZoomLevel()) {

View file

@ -396,47 +396,14 @@ export class KibanaMap extends EventEmitter {
const southEast = bounds.getSouthEast();
const northWest = bounds.getNorthWest();
let southEastLng = southEast.lng;
let northWestLng = northWest.lng;
let southEastLat = southEast.lat;
let northWestLat = northWest.lat;
const southEastLng = southEast.lng;
const northWestLng = northWest.lng;
const southEastLat = southEast.lat;
const northWestLat = northWest.lat;
// When map has not width or height, calculate map dimensions based on parent dimensions
if (southEastLat === northWestLat || southEastLng === northWestLng) {
let parent = this._containerNode.parentNode;
while (parent && (parent.clientWidth === 0 || parent.clientHeight === 0)) {
parent = parent.parentNode;
}
let width = 512;
let height = 512;
if (parent && parent.clientWidth !== 0) {
width = parent.clientWidth;
}
if (parent && parent.clientHeight !== 0) {
height = parent.clientHeight;
}
let top = 0;
let left = 0;
let bottom = height;
let right = width;
// no height - top is center of map and needs to be adjusted
if (southEastLat === northWestLat) {
top = height / 2 * -1;
bottom = height / 2;
}
// no width - left is center of map and needs to be adjusted
if (southEastLng === northWestLng) {
left = width / 2 * -1;
right = width / 2;
}
const containerSouthEast = this._leafletMap.layerPointToLatLng(L.point(right, bottom));
const containerNorthWest = this._leafletMap.layerPointToLatLng(L.point(left, top));
southEastLng = containerSouthEast.lng;
northWestLng = containerNorthWest.lng;
southEastLat = containerSouthEast.lat;
northWestLat = containerNorthWest.lat;
}
// When map has not width or height, the map has no dimensions.
// These dimensions are enforced due to CSS style rules that enforce min-width/height of 0
// that enforcement also resolves errors with the heatmap layer plugin.
return {
bottom_right: {

View file

@ -97,6 +97,10 @@
.leaflet-container {
background: @tilemap-leaflet-container-bg !important;
outline: 0 !important;
//the heatmap layer plugin logs an error to the console when the map is in a 0-sized container
min-width: 1px !important;
min-height: 1px !important;
}
.leaflet-popup-content-wrapper {

View file

@ -15,8 +15,6 @@ const TimelionRequestHandlerProvider = function (Private, Notifier, $http, $root
handler: function (vis /*, appState, uiState, queryFilter*/) {
return new Promise((resolve, reject) => {
console.log('[timelion] get');
const expression = vis.params.expression;
if (!expression) return;

View file

@ -0,0 +1,103 @@
import pixelmatch from 'pixelmatch';
/**
* Utility to compare pixels of two images
* Adds the snapshots and comparison to the corners of the HTML-body to help with human inspection.
*/
export class ImageComparator {
constructor() {
this._expectCanvas = document.createElement('canvas');
this._expectCanvas.style.position = 'fixed';
this._expectCanvas.style.right = 0;
this._expectCanvas.style.top = 0;
this._expectCanvas.style.border = '1px solid green';
document.body.appendChild(this._expectCanvas);
this._diffCanvas = document.createElement('canvas');
this._diffCanvas.style.position = 'fixed';
this._diffCanvas.style.right = 0;
this._diffCanvas.style.bottom = 0;
this._diffCanvas.style.border = '1px solid red';
document.body.appendChild(this._diffCanvas);
this._actualCanvas = document.createElement('canvas');
this._actualCanvas.style.position = 'fixed';
this._actualCanvas.style.left = 0;
this._actualCanvas.style.bottom = 0;
this._actualCanvas.style.border = '1px solid yellow';
document.body.appendChild(this._actualCanvas);
}
/**
* Do pixel-comparison of two images
* @param actualCanvasFromUser HTMl5 canvas
* @param expectedImageSourcePng Img to compare to
* @param threshold number between 0-1. A lower number indicates a lower tolerance for pixel-differences.
* @return number
*/
async compareImage(actualCanvasFromUser, expectedImageSourcePng, threshold) {
console.log('compare image:actual ', actualCanvasFromUser);
console.log('compare image:expected ', expectedImageSourcePng);
return new Promise((resolve) => {
window.setTimeout(() => {
const actualContextFromUser = actualCanvasFromUser.getContext('2d');
const actualImageDataFromUser = actualContextFromUser.getImageData(0, 0, actualCanvasFromUser.width, actualCanvasFromUser.height);
const actualContext = this._actualCanvas.getContext('2d');
this._actualCanvas.width = actualCanvasFromUser.width;
this._actualCanvas.height = actualCanvasFromUser.height;
actualContext.putImageData(actualImageDataFromUser, 0, 0);
// convert expect PNG into pixel data by drawing in new canvas element
this._expectCanvas.width = this._actualCanvas.width;
this._expectCanvas.height = this._actualCanvas.height;
const expectedImage = new Image();
expectedImage.onload = () => {
const expectCtx = this._expectCanvas.getContext('2d');
expectCtx.drawImage(expectedImage, 0, 0, this._actualCanvas.width, this._actualCanvas.height); // draw reference image to size of generated image
const expectImageData = expectCtx.getImageData(0, 0, this._actualCanvas.width, this._actualCanvas.height);
// compare live map vs expected pixel data
const diffImage = expectCtx.createImageData(this._actualCanvas.width, this._actualCanvas.height);
const mismatchedPixels = pixelmatch(
actualImageDataFromUser.data,
expectImageData.data,
diffImage.data,
this._actualCanvas.width,
this._actualCanvas.height,
{ threshold: threshold });
const diffContext = this._diffCanvas.getContext('2d');
this._diffCanvas.width = this._actualCanvas.width;
this._diffCanvas.height = this._actualCanvas.height;
diffContext.putImageData(diffImage, 0, 0);
resolve(mismatchedPixels);
};
expectedImage.src = expectedImageSourcePng;
});
});
}
destroy() {
document.body.removeChild(this._expectCanvas);
document.body.removeChild(this._diffCanvas);
document.body.removeChild(this._actualCanvas);
}
}

View file

@ -87,7 +87,6 @@ uiModules.get('kibana')
const tmsService = catalogue.services.filter((service) => service.type === 'tms')[0];
const manifest = await this._getManifest(tmsService.manifest, this._queryParams);
const services = manifest.data.services;
const firstService = _.cloneDeep(services[0]);
if (!firstService) {
throw new Error('Manifest response does not include sufficient service data.');
@ -108,6 +107,9 @@ uiModules.get('kibana')
}));
}
/**
* this internal method is overridden by the tests to simulate custom manifest.
*/
async _getManifest(manifestUrl) {
return $http({
url: extendUrl(manifestUrl, { query: this._queryParams }),