Skip to content

Commit

Permalink
feat(isValid3DBoundingBox): add new matcher
Browse files Browse the repository at this point in the history
Verifies a three dimensional bounding box meets WGS-84 and GeoJSON validity requirements.

Resolves: #7
  • Loading branch information
M-Scott-Lassiter committed May 25, 2022
1 parent 3ee9e3e commit 6ee8cc6
Show file tree
Hide file tree
Showing 9 changed files with 337 additions and 2 deletions.
1 change: 1 addition & 0 deletions .cz-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
const coordinateMatchers = [
{ name: 'isValid2DBoundingBox' },
{ name: 'isValid2DCoordinate' },
{ name: 'isValid3DBoundingBox' },
{ name: 'isValid3DCoordinate' },
{ name: 'isValidCoordinate' }
]
Expand Down
3 changes: 2 additions & 1 deletion src/core.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/* eslint-disable global-require */

exports.boundingBoxes = {
valid2DBoundingBox: require('./core/boundingBoxes/valid2DBoundingBox')
valid2DBoundingBox: require('./core/boundingBoxes/valid2DBoundingBox'),
valid3DBoundingBox: require('./core/boundingBoxes/valid3DBoundingBox')
}

exports.coordinates = {
Expand Down
48 changes: 48 additions & 0 deletions src/core/boundingBoxes/valid3DBoundingBox.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
const { valid2DBoundingBox } = require('./valid2DBoundingBox')
/**
* Verifies a three dimensional bounding box meets WGS-84 and GeoJSON validity requirements.
*
* @memberof Core.BoundingBoxes
* @see https://github.com/M-Scott-Lassiter/jest-geojson/issues/7
* @param {GeoJSON-bbox} bboxArray A six element WGS-84 array of numbers in format [west, south, depth, east, north, altitude].
* @returns {boolean} True if a valid 3D GeoJSON coordinate. If invalid, it will throw an error.
* @throws {Error} Input must be an array of only four elements of type number
* @throws {Error} Depth and altitude must be numeric.
* @throws {RangeError} Longitude must be a number between -180 and 180
* @throws {RangeError} Latitude must be a number between -90 and 90
* @throws {RangeError} North must be greater than or equal to south
* @throws {RangeError} Altitude must be greater than or equal to depth
* @example
* const goodBBox = valid3DBoundingBox([-10, -20, -100, 20, 10, 0]) // true
* const crossesAntimeridian = valid3DBoundingBox([170, -20, -22.5, 20, -170, 12345.678]) // true
* @example
* const badExample1 valid3DBoundingBox([-10, -91, 0, 10, 20, 0]) // throws error for south being out of range
* const badExample2 valid3DBoundingBox([-10, -10, "0", 10, 20, 0]) // throws error for non-numeric value
*/
function valid3DBoundingBox(bboxArray) {
if (!Array.isArray(bboxArray) || bboxArray.length !== 6) {
throw new Error('Bounding box must be an array of only six elments.')
}

// Reuse functionality from 2D bounding box. The lat/lon values must satisfy the same criteria
valid2DBoundingBox([bboxArray[0], bboxArray[1], bboxArray[3], bboxArray[4]])

if (
typeof bboxArray[2] !== 'number' ||
typeof bboxArray[5] !== 'number' ||
// eslint-disable-next-line no-self-compare
bboxArray[2] !== bboxArray[2] || // Accounts for NaN
// eslint-disable-next-line no-self-compare
bboxArray[5] !== bboxArray[5] // Accounts for NaN
) {
throw new Error('Northern value must be a number between -90 and 90.')
}

if (bboxArray[5] < bboxArray[2]) {
throw new Error('Altitude value must be greater than depth.')
}

return true
}

exports.valid3DBoundingBox = valid3DBoundingBox
4 changes: 3 additions & 1 deletion src/matchers.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
// Bounding Boxes
exports.boundingBoxes = {
isValid2DBoundingBox: require('./matchers/boundingBoxes/isValid2DBoundingBox')
.isValid2DBoundingBox
.isValid2DBoundingBox,
isValid3DBoundingBox: require('./matchers/boundingBoxes/isValid3DBoundingBox')
.isValid3DBoundingBox
}
// Coordinates
exports.coordinates = {
Expand Down
67 changes: 67 additions & 0 deletions src/matchers/boundingBoxes/isValid3DBoundingBox.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
const { valid3DBoundingBox } = require('../../core/boundingBoxes/valid3DBoundingBox')

// eslint-disable-next-line jsdoc/require-returns
/**
* Verifies a three dimensional bounding box meets WGS-84 and GeoJSON validity requirements.
*
* @memberof BoundingBoxes
* @see https://github.com/M-Scott-Lassiter/jest-geojson/issues/7
* @param {number[]} bboxArray A six element WGS-84 array of numbers in format [west, south, depth, east, north, altitude].
* Longitude must be between -180 to 180.
* Latitude must be between -90 to 90.
* North must be greater than or equal to south.
* Altitude must be greater than or equal to depth.
* Bounding boxes that cross the antimeridian will have an eastern value less than the western value.
* @example
* expect([-10, -20, -100, 20, 10, 0]).isValid3DBoundingBox()
* expect([170, -20, -22.5, 20, -170, 12345.678]).isValid3DBoundingBox() // Crosses antimeridian
* @example
* expect([-10, -91, 0, 10, 20, 0]).not.isValid3DBoundingBox() // south out of range
* expect([-20, 10, -10, 20]).not.isValid3DBoundingBox() //2D bounding box
* expect([-10, -10, "0", 10, 20, 0]).not.isValid2DBoundingBox() // Non-numeric value
*/
function isValid3DBoundingBox(bboxArray) {
const { printReceived, matcherHint } = this.utils
const passMessage =
// eslint-disable-next-line prefer-template
matcherHint(
'.not.isValid2DBoundingBox',
'[west, south, depth, east, north, altitude]',
''
) +
'\n\n' +
`Expected input to not be a six element array of numbers with longitude between (-90 to 90),
latitude between (-180 to 180), northern boundary greater than southern boundary,
and altitude greater than depth.\n\n` +
`Received: ${printReceived(bboxArray)}`

/**
* Combines a custom error message with built in Jest tools to provide a more descriptive error
* meessage to the end user.
*
* @param {string} errorMessage Error message text to return to the user
* @returns {string} Concatenated Jest test result string
*/
function failMessage(errorMessage) {
return (
// eslint-disable-next-line prefer-template, no-unused-expressions
matcherHint(
'.isValid2DBoundingBox',
'[west, south, depth, east, north, altitude]',
''
) +
'\n\n' +
`${errorMessage}\n\n` +
`Received: ${printReceived(bboxArray)}`
)
}

try {
valid3DBoundingBox(bboxArray)
} catch (err) {
return { pass: false, message: () => failMessage(err.message) }
}
return { pass: true, message: () => passMessage }
}

exports.isValid3DBoundingBox = isValid3DBoundingBox
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Error Snapshot Testing. Throws error: expect([10, 10, 0, 20, 20, 0]).not.isValid3DBoundingBox 1`] = `
"expect([west, south, depth, east, north, altitude]).not.isValid2DBoundingBox()
Expected input to not be a six element array of numbers with longitude between (-90 to 90),
latitude between (-180 to 180), northern boundary greater than southern boundary,
and altitude greater than depth.
Received: [10, 10, 0, 20, 20, 0]"
`;

exports[`Error Snapshot Testing. Throws error: expect(false).isValid3DBoundingBox() 1`] = `
"expect([west, south, depth, east, north, altitude]).isValid2DBoundingBox()
Bounding box must be an array of only six elments.
Received: false"
`;
189 changes: 189 additions & 0 deletions tests/boundingBoxes/isValid3DBoundingBox.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
const goodBBoxes = [
[[-20, 10, 0, -10, 20, 0]],
[[10, 10, 0, 20, 20, 0]],
[[-20, -20, 0, -10, -10, 0]],
[[10, -20, 0, 20, -10, 0]],
[[-10, -20, 0, 20, -10, 0]],
[[-10, -20, 0, 20, 10, 0]],
[[170, -20, 0, -170, 20, 0]],
[[-10, -20, -100, 20, 10, 0]],
[[-10, -20, -500, 20, 10, -50]],
[[170, -20, 0, -170, 20, 100]],
[[-10, -20, 50, 20, 10, 500]],
[[-10, -20, -22.5, 20, 10, 12345.678]]
]
const goodBoundaryCoordinates = [
[[-180, 10, 0, 20, 20, 0]],
[[10, 10, 0, 180, 20, 0]],
[[-180, 80, 0, 180, 90, 0]],
[[-180, -90, 0, 180, -80, 0]],
[[10, 80, 0, 20, 90, 0]],
[[-45, -90, 0, -80, -80, 0]],
[[-180, 10, 0, 180, 20, 0]],
[[-10, -90, 0, 10, 90, 0]],
[[-180, -90, 0, 180, 90, 0]],
[[-180, -90, -100000, 180, 90, 100000]],
[[-10, -20, 0, 10, -20, 0]],
[[-10, 20, 0, 10, 20, 0]],
[[-10, -20, 0, -10, 20, 0]],
[[10, -20, 0, 10, 20, 0]],
[[-10, -20, -220, -10, 20, -220]],
[[-10, -20, 330, -10, 20, 330]],
[[0, 0, 0, 0, 0, 0]]
]
const coordinatesOutOfRange = [
[[-10, -90.0000001, 0, 10, 0, 0]],
[[-10, 0, 0, 10, 90.0000001, 0]],
[[-10, -90000, 0, 10, 0, 0]],
[[-10, 0, 0, 10, 90000, 0]],
[[-180.0000001, -10, 0, -160, 10, 0]],
[[160, -10, 0, 180.0000001, 10, 0]],
[[-1800000, -10, 0, -160, 10, 0]],
[[160, -10, 0, 1800000, 10, 0]],
[[-181, -10, 0, 181, 10, 0]],
[[-10, -91, 0, 10, 91, 0]],
[[-181, -91, 0, 10, 10, 0]],
[[-10, -10, 0, 181, 91, 0]],
[[-181, -91, 0, 181, 91, 0]]
]
const invalidInputValues = [
undefined,
null,
true,
false,
200,
-200,
Infinity,
-Infinity,
NaN,
{ coordinates: [0, 0] },
'',
'Random Coordinate',
'[10, 10, 0, 20, 20, 0]'
]
const invalidAltitudeValues = [
undefined,
null,
true,
false,
NaN,
{ coordinates: [0, 0] },
'',
'Random Coordinate',
'[10, 10, 0, 20, 20, 0]'
]

describe('Valid Use Cases', () => {
describe('Expect to pass with good coordinates:', () => {
test.each([...goodBBoxes])('expect(%p)', (bboxArray) => {
expect(bboxArray).isValid3DBoundingBox()
})
})

describe('Expect to pass with good boundary coordinates:', () => {
test.each([...goodBoundaryCoordinates])('expect(%p)', (bboxArray) => {
expect(bboxArray).isValid3DBoundingBox()
})
})
})

describe('Inalid Use Cases', () => {
describe('Expect to fail with bad inputs:', () => {
test.each([...invalidInputValues])('expect(%p)', (badInput) => {
expect(badInput).not.isValid3DBoundingBox()
})
})

describe('Expect to fail with incorrect number of array elements:', () => {
test.each([
[[]],
[[20]],
[[20, 10]],
[[20, 30, 0]],
[[-10, 30, -5, 40]],
[[20, 30, 0, 20, 30]],
[[20, 30, 0, 20, 30, 0, 2]],
[[20, 30, 0, 20, 30, 0, 20, 30, 0]]
])('expect(%p)', (badInput) => {
expect(badInput).not.isValid3DBoundingBox()
})
})

describe('Expect to fail with out of range coordinate:', () => {
test.each([...coordinatesOutOfRange])('expect(%p)', (coordinate) => {
expect(coordinate).not.isValid3DBoundingBox()
})
})

describe('Expect to fail with illogical BBox:', () => {
test('Northern boundary less than southern: expect([-10, 20, 0, 10, -20, 0])', () => {
expect([-10, 20, 0, 10, -20, 0]).not.isValid3DBoundingBox()
})

test('Altitude less than depth: expect([-10, -20, 200, 20, 10, 150])', () => {
expect([-10, -20, 200, 20, 10, 150]).not.isValid3DBoundingBox()
})
})

describe('Passing Bad Individual Coordinate Values', () => {
describe('Expect to fail with bad western value:', () => {
test.each([...invalidInputValues])('expect([%p, -10, 0, 10, 10, 0])', (input) => {
expect([input, -10, 0, 10, 10, 0]).not.isValid3DBoundingBox()
})
})

describe('Expect to fail with bad southern value:', () => {
test.each([...invalidInputValues])('expect([-10, %p, 0, 10, 10, 0])', (input) => {
expect([-10, input, 0, 10, 10, 0]).not.isValid3DBoundingBox()
})
})

describe('Expect to fail with bad depth value:', () => {
test.each([...invalidAltitudeValues])('expect([-10, -10, %p, 10, 10, 0])', (input) => {
expect([-10, -10, input, 10, 10, 0]).not.isValid3DBoundingBox()
})
})

describe('Expect to fail with bad eastern value:', () => {
test.each([...invalidInputValues])('expect([-10, -10, 0, %p, 10, 0])', (input) => {
expect([-10, -10, 0, input, 10, 0]).not.isValid3DBoundingBox()
})
})

describe('Expect to fail with bad northern value:', () => {
test.each([...invalidInputValues])('expect([-10, -10, 0, 10, %p, 0])', (input) => {
expect([-10, -10, 0, 10, input, 0]).not.isValid3DBoundingBox()
})
})

describe('Expect to fail with bad altitude value:', () => {
test.each([...invalidAltitudeValues])('expect([-10, -10, 0, 10, 10, %p])', (input) => {
expect([-10, -10, 0, 10, 10, input]).not.isValid3DBoundingBox()
})
})

describe('Expect to fail with bad inputs for all values:', () => {
test.each([...invalidInputValues])('expect([%p, %p, %p, %p, %p, %p])', (input) => {
expect([input, input, input, input, input, input]).not.isValid3DBoundingBox()
})
})
})

describe('Expect to fail when BBox values are arrays of otherwise valid numbers:', () => {
test('expect([[-20], [10], [0], [-10], [20], [0]])', () => {
expect([[-20], [10], [0], [-10], [20], [0]]).not.isValid3DBoundingBox()
})
})
})

describe('Error Snapshot Testing. Throws error:', () => {
test('expect([10, 10, 0, 20, 20, 0]).not.isValid3DBoundingBox', () => {
expect(() =>
expect([10, 10, 0, 20, 20, 0]).not.isValid3DBoundingBox()
).toThrowErrorMatchingSnapshot()
})

test('expect(false).isValid3DBoundingBox()', () => {
expect(() => expect(false).isValid3DBoundingBox()).toThrowErrorMatchingSnapshot()
})
})
4 changes: 4 additions & 0 deletions tests/core.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,8 @@ describe('Bounding Box Functions Exported', () => {
test('valid2DBoundingBox', () => {
expect('valid2DBoundingBox' in core.boundingBoxes).toBeTruthy()
})

test('valid3DBoundingBox', () => {
expect('valid3DBoundingBox' in core.boundingBoxes).toBeTruthy()
})
})
4 changes: 4 additions & 0 deletions tests/matchers.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,8 @@ describe('Bounding Box Matchers Exported', () => {
test('isValid2DBoundingBox', () => {
expect('isValid2DBoundingBox' in matchers.boundingBoxes).toBeTruthy()
})

test('isValid3DBoundingBox', () => {
expect('isValid2DBoundingBox' in matchers.boundingBoxes).toBeTruthy()
})
})

0 comments on commit 6ee8cc6

Please sign in to comment.