diff --git a/.cz-config.js b/.cz-config.js index a8f9cf6..fe3b726 100644 --- a/.cz-config.js +++ b/.cz-config.js @@ -8,6 +8,7 @@ const coordinateMatchers = [ { name: 'isValidBoundingBox' }, { name: 'isValidCoordinate' }, { name: 'toBeLineStringGeometry' }, + { name: 'toBeMultiLineStringGeometry' }, { name: 'toBeMultiPointGeometry' }, { name: 'toBePointGeometry' } ] diff --git a/README.md b/README.md index 5acf41d..57b8130 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,7 @@ _1.0.0_ - [x] toBePointGeometry - [x] toBeMultiPointGeometry - [x] toBeLineStringGeometry -- [ ] toBeMultiLineStringGeometry +- [x] toBeMultiLineStringGeometry - [ ] toBePolygonGeometry - [ ] toBeMultiPolygonGeometry - [ ] toBeAnyGeometry diff --git a/src/core.js b/src/core.js index 22ecb9c..e6aee4d 100644 --- a/src/core.js +++ b/src/core.js @@ -14,6 +14,7 @@ exports.coordinates = { exports.geometries = { lineStringGeometry: require('./core/geometries/lineStringGeometry'), + multiLineStringGeometry: require('./core/geometries/multiLineStringGeometry'), multiPointGeometry: require('./core/geometries/multiPointGeometry'), pointGeometry: require('./core/geometries/pointGeometry') } diff --git a/src/core/geometries/lineStringGeometry.js b/src/core/geometries/lineStringGeometry.js index 24dbb96..bf602bc 100644 --- a/src/core/geometries/lineStringGeometry.js +++ b/src/core/geometries/lineStringGeometry.js @@ -3,15 +3,15 @@ const { validCoordinate } = require('../coordinates/validCoordinate') /** * Verifies an object is a valid GeoJSON LineString Geometry. This geometry requires a * 'type' property that must equal "LineString", and a 'coordinates' property that contains - * an array of two or more valid WGS-84 GeoJSON coordinate(s). The coordinates may be an empty array, + * an array of two or more valid WGS-84 GeoJSON coordinates. The coordinates may be an empty array, * but may not be an array of empty arrays. * * Foreign members are allowed with the exceptions thrown below. * * @memberof Core.Geometries * @see https://github.com/M-Scott-Lassiter/jest-geojson/issues/11 - * @param {object} geometryObject a GeoJSON Multi Point Geometry object - * @returns {boolean} True if a valid GeoJSON Multi Point Geometry. If invalid, it will throw an error. + * @param {object} geometryObject a GeoJSON LineString Geometry object + * @returns {boolean} True if a valid GeoJSON LineString Geometry. If invalid, it will throw an error. * @throws {Error} Argument not an object * @throws {Error} Must have a type property with value 'LineString' * @throws {Error} forbidden from having a property 'geometry', 'properties', or 'features' diff --git a/src/core/geometries/multiLineStringGeometry.js b/src/core/geometries/multiLineStringGeometry.js new file mode 100644 index 0000000..1ad84c8 --- /dev/null +++ b/src/core/geometries/multiLineStringGeometry.js @@ -0,0 +1,104 @@ +const { validCoordinate } = require('../coordinates/validCoordinate') + +/** + * Verifies an object is a valid GeoJSON MultiLineString Geometry. This geometry requires a + * 'type' property that must equal "MultiLineString", and a 'coordinates' property that contains + * an array of linestring arrays (i.e. each linestring array containing at least two or more valid + * WGS-84 GeoJSON coordinates). + * + * The coordinates may be an empty array, but may not be an array of empty arrays. + * + * Foreign members are allowed with the exceptions thrown below. + * + * @memberof Core.Geometries + * @see https://github.com/M-Scott-Lassiter/jest-geojson/issues/12 + * @param {object} geometryObject a GeoJSON MultiLineString Geometry object + * @returns {boolean} True if a valid GeoJSON MultiLineString Geometry. If invalid, it will throw an error. + * @throws {Error} Argument not an object + * @throws {Error} Must have a type property with value 'MultiLineString' + * @throws {Error} forbidden from having a property 'geometry', 'properties', or 'features' + * @example + * const multiLineString = { + "type": "MultiLineString", + "coordinates": [ + [ + [100.0, 0.0], + [101.0, 1.0] + ], + [ + [102.0, 2.0], + [103.0, 3.0] + ] + ] + } + const multiLineStringOneCoordinate = { + "type": "MultiLineString", + "coordinates": [ + [ + [100.0, 0.0] + ] + ] + } + const point = { + type: "Point", + coordinates: [100.0, 0.0] + } + + console.log(multiLineStringGeometry(multiLineString)) // true + console.log(multiLineStringGeometry(point)) // throws error + console.log(multiLineStringGeometry(multiLineStringOneCoordinate)) // throws error + */ +function multiLineStringGeometry(geometryObject) { + if ( + typeof geometryObject !== 'object' || + Array.isArray(geometryObject) || + geometryObject === null + ) { + throw new Error('Argument must be a GeoJSON MultiLineString Geometry object.') + } + + if (!('coordinates' in geometryObject)) { + throw new Error(`GeoJSON MultiLineString Geometry must contain a 'coordinates' property.`) + } + + if (geometryObject.type !== 'MultiLineString') { + throw new Error(`Must have a type property with value 'MultiLineString'`) + } + + if ('geometry' in geometryObject) { + throw new Error( + `GeoJSON MultiLineString Geometry objects are forbidden from having a property 'geometry'.` + ) + } + + if ('properties' in geometryObject) { + throw new Error( + `GeoJSON MultiLineString Geometry objects are forbidden from having a property 'properties'.` + ) + } + + if ('features' in geometryObject) { + throw new Error( + `GeoJSON MultiLineString Geometry objects are forbidden from having a property 'features'.` + ) + } + + // // // Geometry objects are allowed to have empty arrays as coordinates, however validCoordinate may not. + // // If coordinates is an empty array, we're done. Otherwise, check for coordinate validity. + // if (!Array.isArray(geometryObject.coordinates) && geometryObject.coordinates.length !== 1) { + // throw new Error('Coordinates property must be an array of valid GeoJSON coordinates') + // } + + for (let i = 0; i < geometryObject.coordinates.length; i++) { + if (geometryObject.coordinates[i].length === 1) { + throw new Error('Coordinates array must contain two or more valid GeoJSON coordinates') + } + for (let j = 0; j < geometryObject.coordinates[i].length; j++) { + validCoordinate(geometryObject.coordinates[i][j]) + } + } + + return true +} + +exports.multiLineStringGeometry = multiLineStringGeometry diff --git a/src/core/geometries/multiPointGeometry.js b/src/core/geometries/multiPointGeometry.js index edb61ed..706cfa8 100644 --- a/src/core/geometries/multiPointGeometry.js +++ b/src/core/geometries/multiPointGeometry.js @@ -3,14 +3,15 @@ const { validCoordinate } = require('../coordinates/validCoordinate') /** * Verifies an object is a valid GeoJSON MultiPoint Geometry. This geometry requires a * 'type' property that must equal "MultiPoint", and a 'coordinates' property that contains - * an array of valid WGS-84 GeoJSON coordinate(s). The coordinates may be an empty array. + * a single coordinate or an array of valid WGS-84 GeoJSON coordinates. + * The coordinates may be an empty array. * * Foreign members are allowed with the exceptions thrown below. * * @memberof Core.Geometries * @see https://github.com/M-Scott-Lassiter/jest-geojson/issues/10 - * @param {object} geometryObject a GeoJSON Multi Point Geometry object - * @returns {boolean} True if a valid GeoJSON Multi Point Geometry. If invalid, it will throw an error. + * @param {object} geometryObject a GeoJSON MultiPoint Geometry object + * @returns {boolean} True if a valid GeoJSON MultiPoint Geometry. If invalid, it will throw an error. * @throws {Error} Argument not an object * @throws {Error} Must have a type property with value 'MultiPoint' * @throws {Error} forbidden from having a property 'geometry', 'properties', or 'features' diff --git a/src/matchers.js b/src/matchers.js index 9c738e2..c69a11a 100644 --- a/src/matchers.js +++ b/src/matchers.js @@ -24,6 +24,8 @@ exports.coordinates = { exports.geometries = { toBeLineStringGeometry: require('./matchers/geometries/toBeLineStringGeometry') .toBeLineStringGeometry, + toBeMultiLineStringGeometry: require('./matchers/geometries/toBeMultiLineStringGeometry') + .toBeMultiLineStringGeometry, toBeMultiPointGeometry: require('./matchers/geometries/toBeMultiPointGeometry') .toBeMultiPointGeometry, toBePointGeometry: require('./matchers/geometries/toBePointGeometry').toBePointGeometry diff --git a/src/matchers/geometries/toBeLineStringGeometry.js b/src/matchers/geometries/toBeLineStringGeometry.js index 02e3c85..8b5186b 100644 --- a/src/matchers/geometries/toBeLineStringGeometry.js +++ b/src/matchers/geometries/toBeLineStringGeometry.js @@ -11,7 +11,7 @@ const { lineStringGeometry } = require('../../core/geometries/lineStringGeometry * * @memberof Matchers.Geometries * @see https://github.com/M-Scott-Lassiter/jest-geojson/issues/11 - * @param {object} geometryObject a GeoJSON MultiPoint Geometry object + * @param {object} geometryObject a GeoJSON LineString Geometry object * @example const linestring = { "type": "LineString", diff --git a/src/matchers/geometries/toBeMultiLineStringGeometry.js b/src/matchers/geometries/toBeMultiLineStringGeometry.js new file mode 100644 index 0000000..95707dd --- /dev/null +++ b/src/matchers/geometries/toBeMultiLineStringGeometry.js @@ -0,0 +1,83 @@ +const { multiLineStringGeometry } = require('../../core/geometries/multiLineStringGeometry') + +// eslint-disable-next-line jsdoc/require-returns +/** + * Verifies an object is a valid GeoJSON MultiLineString Geometry. This geometry requires a + * 'type' property that must equal "MultiLineString", and a 'coordinates' property that contains + * an array of linestring arrays (i.e. each linestring array containing at least two or more valid + * WGS-84 GeoJSON coordinates). + * + * The coordinates may be an empty array, but may not be an array of empty arrays. + * + * Foreign members are allowed with the exception of 'geometry', 'properties', or 'features'. + * + * @memberof Matchers.Geometries + * @see https://github.com/M-Scott-Lassiter/jest-geojson/issues/12 + * @param {object} geometryObject a GeoJSON MultiLineString Geometry object + * @example + const multiLineString = { + "type": "MultiLineString", + "coordinates": [ + [ + [100.0, 0.0], + [101.0, 1.0] + ], + [ + [102.0, 2.0], + [103.0, 3.0] + ] + ] + } + const multiLineStringOneCoordinate = { + "type": "MultiLineString", + "coordinates": [ + [ + [100.0, 0.0] + ] + ] + } + const point = { + type: "Point", + coordinates: [100.0, 0.0] + } + + expect(multiLineString).toBeMultiLineStringGeometry() + + expect(point).not.toBeMultiLineStringGeometry() + expect(multiLineStringOneCoordinate).not.toBeMultiLineStringGeometry() + */ +function toBeMultiLineStringGeometry(geometryObject) { + const { printReceived, matcherHint } = this.utils + const passMessage = + // eslint-disable-next-line prefer-template + matcherHint('.not.toBeMultiLineStringGeometry', 'GeometryObject', '') + + '\n\n' + + `Expected input to not be a valid GeoJSON MultiLineString geometry.\n\n` + + `Received: ${printReceived(geometryObject)}` + + /** + * 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('.toBeMultiLineStringGeometry', 'GeometryObject', '') + + '\n\n' + + `${errorMessage}\n\n` + + `Received: ${printReceived(geometryObject)}` + ) + } + + try { + multiLineStringGeometry(geometryObject) + } catch (err) { + return { pass: false, message: () => failMessage(err.message) } + } + return { pass: true, message: () => passMessage } +} + +exports.toBeMultiLineStringGeometry = toBeMultiLineStringGeometry diff --git a/tests/core.test.js b/tests/core.test.js index 388953d..776fd76 100644 --- a/tests/core.test.js +++ b/tests/core.test.js @@ -35,6 +35,10 @@ describe('Geometry Functions Exported', () => { expect('lineStringGeometry' in core.geometries).toBeTruthy() }) + test('multiLineStringGeometry', () => { + expect('multiLineStringGeometry' in core.geometries).toBeTruthy() + }) + test('multiPointGeometry', () => { expect('multiPointGeometry' in core.geometries).toBeTruthy() }) diff --git a/tests/geometries/__snapshots__/toBeMultiLineStringGeometry.test.js.snap b/tests/geometries/__snapshots__/toBeMultiLineStringGeometry.test.js.snap new file mode 100644 index 0000000..7c7c83e --- /dev/null +++ b/tests/geometries/__snapshots__/toBeMultiLineStringGeometry.test.js.snap @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Error Snapshot Testing. Throws error: expect({type: 'MultiLineString', coordinates: [[[0, 0], [1, 1]]]}).not.toBeMultiLineStringGeometry 1`] = ` +"expect(GeometryObject).not.toBeMultiLineStringGeometry() + +Expected input to not be a valid GeoJSON MultiLineString geometry. + +Received: {\\"coordinates\\": [[[0, 0], [1, 1]]], \\"type\\": \\"MultiLineString\\"}" +`; + +exports[`Error Snapshot Testing. Throws error: expect(false).toBeMultiLineStringGeometry() 1`] = ` +"expect(GeometryObject).toBeMultiLineStringGeometry() + +Argument must be a GeoJSON MultiLineString Geometry object. + +Received: false" +`; diff --git a/tests/geometries/toBeLineStringGeometry.test.js b/tests/geometries/toBeLineStringGeometry.test.js index c6709d1..33b61cb 100644 --- a/tests/geometries/toBeLineStringGeometry.test.js +++ b/tests/geometries/toBeLineStringGeometry.test.js @@ -21,7 +21,7 @@ const invalidInputValues = [ '[0, 0]', '[[0, 0], [0, 0]]', JSON.stringify({ - type: 'Point', + type: 'LineString', coordinates: [ [25, 90], [2, 2] diff --git a/tests/geometries/toBeMultiLineStringGeometry.test.js b/tests/geometries/toBeMultiLineStringGeometry.test.js new file mode 100644 index 0000000..7ae76d7 --- /dev/null +++ b/tests/geometries/toBeMultiLineStringGeometry.test.js @@ -0,0 +1,404 @@ +const invalidInputValues = [ + undefined, + null, + true, + false, + 200, + -200, + Infinity, + -Infinity, + NaN, + [ + { + coordinates: [ + [ + [0, 0], + [1, 1] + ] + ] + } + ], + '', + 'Random Geometry', + '[0, 0]', + '[[[0, 0], [0, 0]]]', + JSON.stringify({ + type: 'MultiLineString', + coordinates: [ + [25, 90], + [2, 2] + ] + }) +] +const coordinatesInRange = [ + [ + [ + [ + [0, 1], + [0, 2] + ], + [ + [1, 0], + [2, 0], + [3, 0] + ] + ] + ], + [ + [ + [ + [2, 20, 0], + [4, 10, 0] + ], + [ + [3, 0.0, 0], + [6, -10, 0], + [9, -20, 0] + ] + ] + ], + [ + [ + [ + [100.0, 0.0], + [90, 0.0, 0] + ], + [ + [100.0, 0.0, 0], + [110, 5], + [100.0, 11.33, 259] + ] + ] + ], + [ + [ + [ + [180.0, 40.0], + [180.0, 50.0], + [170.0, 50.0], + [170.0, 40.0], + [180.0, 40.0] + ] + ] + ], + [ + [ + [ + [175, 0], + [-175, 0] + ], + [ + [-175, 0], + [175, 0] + ] + ] + ], + [ + [ + [ + [0, 0], + [0, 0], + [0, 0] + ] + ] + ] +] +const coordinatesOutOfRange = [ + [ + [ + [ + [0, 0], + [181, 91] + ], + [ + [0, 0], + [181, -91] + ] + ] + ], + [ + [ + [ + [0, 0], + [-181, 91, 0] + ], + [ + [0, 0], + [-181, -91, 200] + ] + ] + ], + [[[[0, 0, 0, 0]]]] +] +const emptyArrays = [[[[[]]]], [[[[], []]]], [[[[], [], []]]]] +const incorrectTypeValues = [ + ...invalidInputValues, + 'Point', + 'MultiPoint', + 'LineString', + 'Polygon', + 'MultiPolygon', + 'GeometryCollection', + 'MULTILINESTRING', + 'multilinestring' +] + +describe('Valid Use Cases', () => { + describe('Basic Formatting, Values in Range:', () => { + test.each([...coordinatesInRange])('Good in range coordinates: %p', (coordinateArray) => { + const testMultiPoint = { + type: 'MultiLineString', + coordinates: coordinateArray + } + expect(testMultiPoint).toBeMultiLineStringGeometry() + }) + + test('Empty coordinate', () => { + const testMultiPoint = { + type: 'MultiLineString', + coordinates: [] + } + expect(testMultiPoint).toBeMultiLineStringGeometry() + }) + + test('Stress test with many points in many linestrings', () => { + const testMultiPoint = { + type: 'MultiLineString', + coordinates: [[]] + } + for (let i = 0; i < 30; i++) { + testMultiPoint.coordinates.push([]) + for (let j = 0; j < 30; j++) { + testMultiPoint.coordinates[i].push([j, j]) + } + } + expect(testMultiPoint).toBeMultiLineStringGeometry() + }) + }) + + describe('Foreign Properties Allowed:', () => { + const testMultiLineString1 = { + type: 'MultiLineString', + id: null, + coordinates: [ + [ + [25, 90], + [-180, 0] + ] + ] + } + const testMultiLineString2 = { + type: 'MultiLineString', + geometries: testMultiLineString1, + coordinates: [ + [ + [-100.0, -15.0, 2000], + [0, 0] + ] + ] + } + const testMultiLineString3 = { + type: 'MultiLineString', + someRandomProp: true, + geometries: testMultiLineString2, + coordinates: [ + [ + [180, 10.2, -125], + [-180, -90] + ] + ] + } + + test.each(['Test 1', 1])('ID: %p', (input) => { + const testMultiLineString = { + type: 'MultiLineString', + id: input, + coordinates: [ + [ + [25, 90], + [-180, 0] + ] + ] + } + expect(testMultiLineString).toBeMultiLineStringGeometry() + }) + + test.each([testMultiLineString1, testMultiLineString2, testMultiLineString3])( + 'Non-alphanumeric ID', + (testMultiLineString) => { + expect(testMultiLineString).toBeMultiLineStringGeometry() + } + ) + }) +}) + +describe('Inalid Use Cases', () => { + describe('Expect to fail with bad inputs:', () => { + test.each([...invalidInputValues])( + 'expect(%p).not.toBeMultiLineStringGeometry()', + (badInput) => { + expect(badInput).not.toBeMultiLineStringGeometry() + } + ) + }) + + describe('Expect to fail with out of range or bad coordinate:', () => { + test.each([...coordinatesOutOfRange, ...invalidInputValues])( + 'coordinates: %p', + (coordinate) => { + const testMultiLineString = { + type: 'LineString', + coordinates: coordinate + } + expect(testMultiLineString).not.toBeMultiLineStringGeometry() + } + ) + }) + + describe('Expect to fail with one bad coordinate:', () => { + // Add the extra empty array here. "coordinates" can be an empty array, but individual elements within cannot. + test.each([...invalidInputValues, []])('coordinates: [[[0, 0], %p]]]', (coordinate) => { + const testMultiLineString = { + type: 'MultiLineString', + coordinates: [[[0, 0], coordinate]] + } + expect(testMultiLineString).not.toBeMultiLineStringGeometry() + }) + }) + + describe('Expect to fail with only a single coordinate:', () => { + test('coordinates: [[0, 0]]', () => { + const testMultiLineString = { + type: 'MultiLineString', + coordinates: [[[0, 0]]] + } + expect(testMultiLineString).not.toBeMultiLineStringGeometry() + }) + }) + + describe('Expect to fail with coordinates array of empty arrays:', () => { + test.each([...emptyArrays])('coordinates: %p', (coordinate) => { + const testMultiLineString = { + type: 'MultiLineString', + coordinates: [coordinate] + } + expect(testMultiLineString).not.toBeMultiLineStringGeometry() + }) + }) + + describe('Expect to fail with bad type value:', () => { + test.each([...incorrectTypeValues, ...invalidInputValues])('type: %p', (input) => { + const testMultiLineString = { + type: input, + coordinates: [ + [ + [0, 0], + [1, 1, 0] + ] + ] + } + expect(testMultiLineString).not.toBeMultiLineStringGeometry() + }) + }) + + describe('Expect to fail when contains prohibited properties:', () => { + test(`Contains: 'geometry'`, () => { + const testMultiLineString = { + type: 'MultiLineString', + coordinates: [ + [ + [0, 0], + [1, 1, 0] + ] + ], + geometry: { + type: 'Point', + coordinates: [102.0, 0.5] + } + } + expect(testMultiLineString).not.toBeMultiLineStringGeometry() + }) + + test(`Contains: 'properties'`, () => { + const testMultiLineString = { + type: 'MultiLineString', + coordinates: [ + [ + [0, 0], + [1, 1, 0] + ] + ], + properties: { + prop1: true + } + } + expect(testMultiLineString).not.toBeMultiLineStringGeometry() + }) + + test(`Contains: 'features'`, () => { + const testMultiLineString = { + type: 'MultiLineString', + coordinates: [ + [ + [0, 0], + [1, 1, 0] + ] + ], + features: [ + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [102.0, 0.5] + }, + properties: { + prop0: 'value0' + } + } + ] + } + expect(testMultiLineString).not.toBeMultiLineStringGeometry() + }) + }) + + describe('Expect to fail when missing required properties:', () => { + test(`Missing: 'type'`, () => { + const testMultiLineString = { + coordinates: [ + [ + [0, 0], + [1, 1, 0] + ] + ] + } + expect(testMultiLineString).not.toBeMultiLineStringGeometry() + }) + + test(`Missing: 'coordinates'`, () => { + const testMultiLineString = { + type: 'MultiLineString' + } + expect(testMultiLineString).not.toBeMultiLineStringGeometry() + }) + }) +}) + +describe('Error Snapshot Testing. Throws error:', () => { + test(`expect({type: 'MultiLineString', coordinates: [[[0, 0], [1, 1]]]}).not.toBeMultiLineStringGeometry`, () => { + expect(() => + expect({ + type: 'MultiLineString', + coordinates: [ + [ + [0, 0], + [1, 1] + ] + ] + }).not.toBeMultiLineStringGeometry() + ).toThrowErrorMatchingSnapshot() + }) + + test('expect(false).toBeMultiLineStringGeometry()', () => { + expect(() => expect(false).toBeMultiLineStringGeometry()).toThrowErrorMatchingSnapshot() + }) +}) diff --git a/tests/matchers.test.js b/tests/matchers.test.js index d206294..e0aaf05 100644 --- a/tests/matchers.test.js +++ b/tests/matchers.test.js @@ -35,6 +35,10 @@ describe('Geometry Matchers Exported', () => { expect('toBeLineStringGeometry' in matchers.geometries).toBeTruthy() }) + test('toBeMultiLineStringGeometry', () => { + expect('toBeMultiLineStringGeometry' in matchers.geometries).toBeTruthy() + }) + test('toBeMultiPointGeometry', () => { expect('toBeMultiPointGeometry' in matchers.geometries).toBeTruthy() })