Skip to content

Commit

Permalink
Generate points in bounding box with padding
Browse files Browse the repository at this point in the history
This is to facilitate end-to-end tests in the frontend. To ensure that
no markers appear out of the bounds of the screen when displaying a map,
the window width/height ratio can be adjusted to the be the same as the
bounding box, and a padding can be applied to ensure that markers that
would be at the very edge of the screen are well within the screen
instead.
  • Loading branch information
AlphaHydrae authored and Tazaf committed May 9, 2018
1 parent c557cc6 commit 0d8916b
Show file tree
Hide file tree
Showing 3 changed files with 186 additions and 5 deletions.
70 changes: 70 additions & 0 deletions server/spec/fixtures/geojson.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ const chance = require('chance').Chance();
* @param {object} [data.bbox] - A bounding box within which the generated point should be.
* @param {number[]} data.bbox.southWest - A longitude/latitude pair indicating the south-west corner of the bounding box.
* @param {number[]} data.bbox.northEast - A longitude/latitude pair indicating the north-east corner of the bounding box.
* @param {number[]} [data.bbox.padding] - 1 to 4 numbers indicating the padding of the bounding box
* (much like CSS padding, 1 number is all 4 directions, 2 numbers is northing/easting,
* 3 numbers is north/easting/south, 4 numbers is north/east/south/west).
* @param {number[]} [data.coordinates] - The point's coordinates (longitude & latitude).
* @returns {object} A GeoJSON point.
*/
Expand All @@ -39,6 +42,9 @@ exports.point = function(data = {}) {
* @param {object} [data.bbox] - A bounding box within which the generated coordinates should be.
* @param {number[]} data.bbox.southWest - A longitude/latitude pair indicating the south-west corner of the bounding box.
* @param {number[]} data.bbox.northEast - A longitude/latitude pair indicating the north-east corner of the bounding box.
* @param {number[]} [data.bbox.padding] - 1 to 4 numbers indicating the padding of the bounding box
* (much like CSS padding, 1 number is all 4 directions, 2 numbers is northing/easting,
* 3 numbers is north/easting/south, 4 numbers is north/east/south/west).
* @returns {number[]} A GeoJSON coordinates pair.
*/
exports.coordinates = function(data = {}) {
Expand All @@ -49,11 +55,21 @@ exports.coordinates = function(data = {}) {
let maxLongitude = 180;

if (data.bbox) {

const bbox = ensureBbox(data.bbox);
minLatitude = bbox.southWest[1];
maxLatitude = bbox.northEast[1];
minLongitude = bbox.southWest[0];
maxLongitude = bbox.northEast[0];

if (bbox.padding !== undefined) {

const padding = normalizePadding(bbox.padding);
minLatitude += padding[2];
maxLatitude -= padding[0];
minLongitude += padding[3];
maxLongitude -= padding[1];
}
}

return [
Expand All @@ -74,6 +90,30 @@ function ensureBbox(bbox) {
ensureCoordinates(bbox.southWest);
ensureCoordinates(bbox.northEast);

if (bbox.southWest[1] > bbox.northEast[1]) {
throw new Error(`Bounding box south west ${JSON.stringify(bbox.southWest)} has a greater latitude than north east ${JSON.stringify(bbox.northEast)}`);
} else if (bbox.southWest[0] > bbox.northEast[0]) {
throw new Error(`Bounding box south west ${JSON.stringify(bbox.southWest)} has a greater longitude than north east ${JSON.stringify(bbox.northEast)}`);
}

if (bbox.padding !== undefined) {
ensurePadding(bbox.padding);

const padding = normalizePadding(bbox.padding);

const minLongitude = bbox.southWest[0] + padding[3];
const maxLongitude = bbox.northEast[0] - padding[1];
if (minLongitude > maxLongitude) {
throw new Error(`Padding ${JSON.stringify(bbox.padding)} for bounding box ${JSON.stringify(bbox.southWest.concat(bbox.northEast))} would cause minimum longitude ${minLongitude} to be greater than the maximum ${maxLongitude}`);
}

const minLatitude = bbox.southWest[1] + padding[2];
const maxLatitude = bbox.northEast[1] - padding[0];
if (minLatitude > maxLatitude) {
throw new Error(`Padding ${JSON.stringify(bbox.padding)} for bounding box ${JSON.stringify(bbox.southWest.concat(bbox.northEast))} would cause minimum latitude ${minLatitude} to be greater than the maximum ${maxLatitude}`);
}
}

return bbox;
}

Expand Down Expand Up @@ -101,3 +141,33 @@ function ensureCoordinates(coordinates) {

return coordinates;
}

function ensurePadding(padding) {
if (!_.isArray(padding) && !_.isNumber(padding)) {
throw new Error(`Padding must be an array or a number, got ${typeof(padding)}`);
} else if (_.isArray(padding) && padding.length < 1 || padding.length > 4) {
throw new Error(`Padding array must have 1 to 4 elements, got ${padding.length}`);
} else if (_.isArray(padding) && padding.some(value => !_.isNumber(value))) {
throw new Error(`Padding array must contain only numbers, got [${padding.map(value => typeof(value)).join(',')}]`);
} else if (_.isArray(padding) && padding.some(value => value < 0)) {
throw new Error(`Padding array must contain only zeros or positive numbers, got ${JSON.stringify(padding)}`);
} else if (_.isNumber(padding) && padding < 0) {
throw new Error(`Padding must be zero or a positive number, got ${padding}`);
} else {
return padding;
}
}

function normalizePadding(padding) {
if (_.isNumber(padding)) {
return new Array(4).fill(padding);
} else if (padding.length === 1) {
return new Array(4).fill(padding[0]);
} else if (padding.length === 2) {
return [ padding[0], padding[1], padding[0], padding[1] ];
} else if (padding.length === 3) {
return [ ...padding, padding[1] ];
} else {
return padding;
}
}
110 changes: 109 additions & 1 deletion server/spec/fixtures/geojson.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ describe('GeoJSON fixtures', () => {
const unique = [];
_.times(100, () => {

const coordinates = generateCoordinates({ bbox: bbox });
const coordinates = generateCoordinates({ bbox });
expect(coordinates).to.be.an('array');
expect(coordinates).to.have.lengthOf(2);
expect(coordinates[0]).to.be.a('number');
Expand Down Expand Up @@ -96,6 +96,114 @@ describe('GeoJSON fixtures', () => {
expect(() => generateCoordinates({ bbox: { southWest: [ 10, 200 ], northEast: [ 20, 30 ] } })).to.throw('Latitude must be between -90 and 90, got 200');
expect(() => generateCoordinates({ bbox: { southWest: [ 10, 20 ], northEast: [ 20, -90.5 ] } })).to.throw('Latitude must be between -90 and 90, got -90.5');
});

it('should not accept a south-west point with a greater latitude than the north-east point', () => {
expect(() => generateCoordinates({ bbox: { southWest: [ 10, 20 ], northEast: [ 20, 15 ] } })).to.throw('Bounding box south west [10,20] has a greater latitude than north east [20,15]');
});

it('should not accept a south-west point with a greater longitude than the north-east point', () => {
expect(() => generateCoordinates({ bbox: { southWest: [ 10, 20 ], northEast: [ 5, 30 ] } })).to.throw('Bounding box south west [10,20] has a greater longitude than north east [5,30]');
});

describe('with a "padding" option', () => {

// Test all padding formats.
[
{ bbox: [ 10, 20, 20, 30 ], padding: [ 2, 3, 4, 3 ], effectiveBbox: [ 13, 24, 17, 28 ] },
{ bbox: [ 10, 20, 20, 30 ], padding: [ 1, 2, 3 ], effectiveBbox: [ 12, 23, 18, 29 ] },
{ bbox: [ 10, 20, 20, 30 ], padding: [ 2, 1 ], effectiveBbox: [ 11, 22, 19, 28 ] },
{ bbox: [ 10, 20, 20, 30 ], padding: [ 3 ], effectiveBbox: [ 13, 23, 17, 27 ] },
{ bbox: [ 10, 20, 20, 30 ], padding: 2.5, effectiveBbox: [ 12.5, 22.5, 17.5, 27.5 ] }
].forEach(paddingTestData => {
it(`should take a padding of ${JSON.stringify(paddingTestData.padding)} into account`, () => {

const bbox = {
southWest: paddingTestData.bbox.slice(0, 2),
northEast: paddingTestData.bbox.slice(2),
padding: paddingTestData.padding
};

const unique = [];
_.times(100, () => {

const coordinates = generateCoordinates({ bbox });
expect(coordinates).to.be.an('array');
expect(coordinates).to.have.lengthOf(2);
expect(coordinates[0]).to.be.a('number');
expect(coordinates[0]).to.be.at.least(paddingTestData.effectiveBbox[0]);
expect(coordinates[0]).to.be.at.most(paddingTestData.effectiveBbox[2]);
expect(coordinates[1]).to.be.a('number');
expect(coordinates[1]).to.be.at.least(paddingTestData.effectiveBbox[1]);
expect(coordinates[1]).to.be.at.most(paddingTestData.effectiveBbox[3]);

const fingerprint = coordinates.join(',');
if (unique.indexOf(fingerprint) < 0) {
unique.push(fingerprint);
}
});

// Check that at least 90% of the coordinates are different
// (we can't be 100% sure that they all will be).
expect(unique).to.have.lengthOf.at.least(90);
});
});

// Check that invalid padding formats are not accepted.
[
{ bbox: [ 10, 20, 20, 30 ], padding: 'foo', message: 'Padding must be an array or a number, got string' },
{ bbox: [ 10, 20, 20, 30 ], padding: -2, message: 'Padding must be zero or a positive number, got -2' },
{ bbox: [ 10, 20, 20, 30 ], padding: [], message: 'Padding array must have 1 to 4 elements, got 0' },
{ bbox: [ 10, 20, 20, 30 ], padding: [ 1, 2, 3, 4, 5 ], message: 'Padding array must have 1 to 4 elements, got 5' },
{ bbox: [ 10, 20, 20, 30 ], padding: [ 2, 3, 'bar' ], message: 'Padding array must contain only numbers, got [number,number,string]' },
{ bbox: [ 10, 20, 20, 30 ], padding: [ 2, -10, 3 ], message: 'Padding array must contain only zeros or positive numbers, got [2,-10,3]' },
].forEach(paddingTestData => {
it(`should not accept invalid padding ${JSON.stringify(paddingTestData.padding)}`, () => {

const bbox = {
southWest: paddingTestData.bbox.slice(0, 2),
northEast: paddingTestData.bbox.slice(2),
padding: paddingTestData.padding
};

expect(() => generateCoordinates({ bbox })).to.throw(paddingTestData.message);
});
});

// Check that overflow due to padding is not accepted.
[
{
bbox: [ 10, 20, 20, 30 ],
padding: [ 5, 4, 6, 3 ],
message: 'Padding [5,4,6,3] for bounding box [10,20,20,30] would cause minimum latitude 26 to be greater than the maximum 25'
},
{
bbox: [ 10, 20, 20, 30 ],
padding: [ 4, 5, 3, 6 ],
message: 'Padding [4,5,3,6] for bounding box [10,20,20,30] would cause minimum longitude 16 to be greater than the maximum 15'
},
{
bbox: [ 10, 20, 20, 30 ],
padding: [ 200, 0, 0, 0 ],
message: 'Padding [200,0,0,0] for bounding box [10,20,20,30] would cause minimum latitude 20 to be greater than the maximum -170'
},
{
bbox: [ 10, 20, 20, 30 ],
padding: [ 0, 0, 0, 100 ],
message: 'Padding [0,0,0,100] for bounding box [10,20,20,30] would cause minimum longitude 110 to be greater than the maximum 20'
}
].forEach(paddingTestData => {
it(`should not accept a padding of ${JSON.stringify(paddingTestData.padding)} for bounding box ${JSON.stringify(paddingTestData.bbox)} due to overflow`, () => {

const bbox = {
southWest: paddingTestData.bbox.slice(0, 2),
northEast: paddingTestData.bbox.slice(2),
padding: paddingTestData.padding
};

expect(() => generateCoordinates({ bbox })).to.throw(paddingTestData.message);
});
});
});
})
});
});
11 changes: 7 additions & 4 deletions server/spec/fixtures/location.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,12 @@ const geoJsonFixtures = require('./geojson');
*
* @function
* @param {object} [data={}] - Custom location data.
* @param {object} [data.bbox] - A bounding box within which the generated location should be
* @param {number[]} data.bbox.southWest - A longitude/latitude pair indicating the south-west corner of the bounding box
* @param {number[]} data.bbox.northEast - A longitude/latitude pair indicating the north-east corner of the bounding box
* @param {object} [data.bbox] - A bounding box within which the generated location should be.
* @param {number[]} data.bbox.southWest - A longitude/latitude pair indicating the south-west corner of the bounding box.
* @param {number[]} data.bbox.northEast - A longitude/latitude pair indicating the north-east corner of the bounding box.
* @param {number[]} [data.bbox.padding] - 1 to 4 numbers indicating the padding of the bounding box
* (much like CSS padding, 1 number is all 4 directions, 2 numbers is northing/easting,
* 3 numbers is north/easting/south, 4 numbers is north/east/south/west).
* @param {string} [data.name]
* @param {string} [data.shortName] - Set to `null` to create a location without a short name.
* @param {string} [data.description]
Expand All @@ -41,7 +44,7 @@ const geoJsonFixtures = require('./geojson');
* @param {object} [data.properties={}]
* @param {object} [data.address]
* @param {string} [data.address.street]
* @param {string} [data.address.number] - Set to `null` to create an address without a number
* @param {string} [data.address.number] - Set to `null` to create an address without a number.
* @param {string} [data.address.zipCode]
* @param {string} [data.address.city]
* @param {string} [data.address.state]
Expand Down

0 comments on commit 0d8916b

Please sign in to comment.