diff --git a/.changeset/soft-pans-deny.md b/.changeset/soft-pans-deny.md new file mode 100644 index 000000000..ae23bddd0 --- /dev/null +++ b/.changeset/soft-pans-deny.md @@ -0,0 +1,5 @@ +--- +"@koopjs/winnow": patch +--- + +- fix behavior of intersects, contains, and within diff --git a/package-lock.json b/package-lock.json index 2a638d483..7b75c20fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5197,6 +5197,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@turf/bbox": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/bbox/-/bbox-6.5.0.tgz", + "integrity": "sha512-RBbLaao5hXTYyyg577iuMtDB8ehxMlUqHEJiMs8jT1GHkFhr6sYre3lmLsPeYEi/ZKj5TP5tt7fkzNdJ4GIVyw==", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/meta": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, "node_modules/@turf/bbox-polygon": { "version": "6.5.0", "license": "MIT", @@ -5207,6 +5219,102 @@ "url": "https://opencollective.com/turf" } }, + "node_modules/@turf/boolean-contains": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-contains/-/boolean-contains-6.5.0.tgz", + "integrity": "sha512-4m8cJpbw+YQcKVGi8y0cHhBUnYT+QRfx6wzM4GI1IdtYH3p4oh/DOBJKrepQyiDzFDaNIjxuWXBh0ai1zVwOQQ==", + "dependencies": { + "@turf/bbox": "^6.5.0", + "@turf/boolean-point-in-polygon": "^6.5.0", + "@turf/boolean-point-on-line": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-disjoint": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-disjoint/-/boolean-disjoint-6.5.0.tgz", + "integrity": "sha512-rZ2ozlrRLIAGo2bjQ/ZUu4oZ/+ZjGvLkN5CKXSKBcu6xFO6k2bgqeM8a1836tAW+Pqp/ZFsTA5fZHsJZvP2D5g==", + "dependencies": { + "@turf/boolean-point-in-polygon": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/line-intersect": "^6.5.0", + "@turf/meta": "^6.5.0", + "@turf/polygon-to-line": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-equal": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-equal/-/boolean-equal-6.5.0.tgz", + "integrity": "sha512-cY0M3yoLC26mhAnjv1gyYNQjn7wxIXmL2hBmI/qs8g5uKuC2hRWi13ydufE3k4x0aNRjFGlg41fjoYLwaVF+9Q==", + "dependencies": { + "@turf/clean-coords": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "geojson-equality": "0.1.6" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-intersects": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-intersects/-/boolean-intersects-6.5.0.tgz", + "integrity": "sha512-nIxkizjRdjKCYFQMnml6cjPsDOBCThrt+nkqtSEcxkKMhAQj5OO7o2CecioNTaX8EayqwMGVKcsz27oP4mKPTw==", + "dependencies": { + "@turf/boolean-disjoint": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/meta": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-point-in-polygon": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-point-in-polygon/-/boolean-point-in-polygon-6.5.0.tgz", + "integrity": "sha512-DtSuVFB26SI+hj0SjrvXowGTUCHlgevPAIsukssW6BG5MlNSBQAo70wpICBNJL6RjukXg8d2eXaAWuD/CqL00A==", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-point-on-line": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-point-on-line/-/boolean-point-on-line-6.5.0.tgz", + "integrity": "sha512-A1BbuQ0LceLHvq7F/P7w3QvfpmZqbmViIUPHdNLvZimFNLo4e6IQunmzbe+8aSStH9QRZm3VOflyvNeXvvpZEQ==", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-within": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-within/-/boolean-within-6.5.0.tgz", + "integrity": "sha512-YQB3oU18Inx35C/LU930D36RAVe7LDXk1kWsQ8mLmuqYn9YdPsDQTMTkLJMhoQ8EbN7QTdy333xRQ4MYgToteQ==", + "dependencies": { + "@turf/bbox": "^6.5.0", + "@turf/boolean-point-in-polygon": "^6.5.0", + "@turf/boolean-point-on-line": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, "node_modules/@turf/centroid": { "version": "6.5.0", "license": "MIT", @@ -5218,9 +5326,86 @@ "url": "https://opencollective.com/turf" } }, + "node_modules/@turf/clean-coords": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/clean-coords/-/clean-coords-6.5.0.tgz", + "integrity": "sha512-EMX7gyZz0WTH/ET7xV8MyrExywfm9qUi0/MY89yNffzGIEHuFfqwhcCqZ8O00rZIPZHUTxpmsxQSTfzJJA1CPw==", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/envelope": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/envelope/-/envelope-6.5.0.tgz", + "integrity": "sha512-9Z+FnBWvOGOU4X+fMZxYFs1HjFlkKqsddLuMknRaqcJd6t+NIv5DWvPtDL8ATD2GEExYDiFLwMdckfr1yqJgHA==", + "dependencies": { + "@turf/bbox": "^6.5.0", + "@turf/bbox-polygon": "^6.5.0", + "@turf/helpers": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/flatten": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/flatten/-/flatten-6.5.0.tgz", + "integrity": "sha512-IBZVwoNLVNT6U/bcUUllubgElzpMsNoCw8tLqBw6dfYg9ObGmpEjf9BIYLr7a2Yn5ZR4l7YIj2T7kD5uJjZADQ==", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/meta": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, "node_modules/@turf/helpers": { "version": "6.5.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-6.5.0.tgz", + "integrity": "sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw==", + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/invariant": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-6.5.0.tgz", + "integrity": "sha512-Wv8PRNCtPD31UVbdJE/KVAWKe7l6US+lJItRR/HOEW3eh+U/JwRCSUl/KZ7bmjM/C+zLNoreM2TU6OoLACs4eg==", + "dependencies": { + "@turf/helpers": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/line-intersect": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/line-intersect/-/line-intersect-6.5.0.tgz", + "integrity": "sha512-CS6R1tZvVQD390G9Ea4pmpM6mJGPWoL82jD46y0q1KSor9s6HupMIo1kY4Ny+AEYQl9jd21V3Scz20eldpbTVA==", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "@turf/line-segment": "^6.5.0", + "@turf/meta": "^6.5.0", + "geojson-rbush": "3.x" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/line-segment": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/line-segment/-/line-segment-6.5.0.tgz", + "integrity": "sha512-jI625Ho4jSuJESNq66Mmi290ZJ5pPZiQZruPVpmHkUw257Pew0alMmb6YrqYNnLUuiVVONxAAKXUVeeUGtycfw==", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "@turf/meta": "^6.5.0" + }, "funding": { "url": "https://opencollective.com/turf" } @@ -5235,6 +5420,18 @@ "url": "https://opencollective.com/turf" } }, + "node_modules/@turf/polygon-to-line": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/polygon-to-line/-/polygon-to-line-6.5.0.tgz", + "integrity": "sha512-5p4n/ij97EIttAq+ewSnKt0ruvuM+LIDzuczSzuHTpq4oS7Oq8yqg5TQ4nzMVuK41r/tALCk7nAoBuw3Su4Gcw==", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, "node_modules/@types/babel__core": { "version": "7.20.1", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.1.tgz", @@ -7874,7 +8071,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", @@ -9482,7 +9678,6 @@ }, "node_modules/functions-have-names": { "version": "1.2.3", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -9515,6 +9710,50 @@ "node": ">=6.9.0" } }, + "node_modules/geojson-equality": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/geojson-equality/-/geojson-equality-0.1.6.tgz", + "integrity": "sha512-TqG8YbqizP3EfwP5Uw4aLu6pKkg6JQK9uq/XZ1lXQntvTHD1BBKJWhNpJ2M0ax6TuWMP3oyx6Oq7FCIfznrgpQ==", + "dependencies": { + "deep-equal": "^1.0.0" + } + }, + "node_modules/geojson-equality/node_modules/deep-equal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz", + "integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==", + "dependencies": { + "is-arguments": "^1.1.1", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.5.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/geojson-rbush": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/geojson-rbush/-/geojson-rbush-3.2.0.tgz", + "integrity": "sha512-oVltQTXolxvsz1sZnutlSuLDEcQAKYC/uXt9zDzJJ6bu0W+baTI8LZBaTup5afzibEH4N3jlq2p+a152wlBJ7w==", + "dependencies": { + "@turf/bbox": "*", + "@turf/helpers": "6.x", + "@turf/meta": "6.x", + "@types/geojson": "7946.0.8", + "rbush": "^3.0.1" + } + }, + "node_modules/geojson-rbush/node_modules/@types/geojson": { + "version": "7946.0.8", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.8.tgz", + "integrity": "sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA==" + }, "node_modules/geojson-validation": { "version": "1.0.2", "license": "LGPL-3", @@ -10142,7 +10381,6 @@ }, "node_modules/has-tostringtag": { "version": "1.0.0", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.2" @@ -10719,7 +10957,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -10823,7 +11060,6 @@ }, "node_modules/is-date-object": { "version": "1.0.5", - "dev": true, "license": "MIT", "dependencies": { "has-tostringtag": "^1.0.0" @@ -10988,7 +11224,6 @@ }, "node_modules/is-regex": { "version": "1.1.4", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -16984,7 +17219,6 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.3" @@ -16998,7 +17232,6 @@ }, "node_modules/object-keys": { "version": "1.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -18415,6 +18648,11 @@ "node": ">=8" } }, + "node_modules/quickselect": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", + "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==" + }, "node_modules/randombytes": { "version": "2.1.0", "dev": true, @@ -18443,6 +18681,14 @@ "node": ">= 0.8" } }, + "node_modules/rbush": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/rbush/-/rbush-3.0.1.tgz", + "integrity": "sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w==", + "dependencies": { + "quickselect": "^2.0.0" + } + }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -18994,7 +19240,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -19413,7 +19658,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", - "dev": true, "dependencies": { "define-data-property": "^1.0.1", "functions-have-names": "^1.2.3", @@ -22171,7 +22415,13 @@ "@terraformer/arcgis": "^2.1.2", "@terraformer/spatial": "^2.1.2", "@turf/bbox-polygon": "^6.5.0", + "@turf/boolean-contains": "^6.5.0", + "@turf/boolean-equal": "^6.5.0", + "@turf/boolean-intersects": "^6.5.0", + "@turf/boolean-within": "^6.5.0", "@turf/centroid": "^6.5.0", + "@turf/envelope": "^6.5.0", + "@turf/flatten": "^6.5.0", "@types/geojson": "^7946.0.14", "alasql": "^4.3.3", "classybrew": "0.0.3", @@ -22186,6 +22436,7 @@ "wkt-parser": "^1.3.3" }, "devDependencies": { + "@turf/helpers": "^6.5.0", "benchmark": "^2.1.4", "fs-extra": "^11.2.0", "proxyquire": "^2.1.3", @@ -24623,7 +24874,14 @@ "@terraformer/arcgis": "^2.1.2", "@terraformer/spatial": "^2.1.2", "@turf/bbox-polygon": "^6.5.0", + "@turf/boolean-contains": "^6.5.0", + "@turf/boolean-equal": "^6.5.0", + "@turf/boolean-intersects": "^6.5.0", + "@turf/boolean-within": "^6.5.0", "@turf/centroid": "^6.5.0", + "@turf/envelope": "^6.5.0", + "@turf/flatten": "^6.5.0", + "@turf/helpers": "^6.5.0", "@types/geojson": "^7946.0.14", "alasql": "^4.3.3", "benchmark": "^2.1.4", @@ -26053,12 +26311,96 @@ } } }, + "@turf/bbox": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/bbox/-/bbox-6.5.0.tgz", + "integrity": "sha512-RBbLaao5hXTYyyg577iuMtDB8ehxMlUqHEJiMs8jT1GHkFhr6sYre3lmLsPeYEi/ZKj5TP5tt7fkzNdJ4GIVyw==", + "requires": { + "@turf/helpers": "^6.5.0", + "@turf/meta": "^6.5.0" + } + }, "@turf/bbox-polygon": { "version": "6.5.0", "requires": { "@turf/helpers": "^6.5.0" } }, + "@turf/boolean-contains": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-contains/-/boolean-contains-6.5.0.tgz", + "integrity": "sha512-4m8cJpbw+YQcKVGi8y0cHhBUnYT+QRfx6wzM4GI1IdtYH3p4oh/DOBJKrepQyiDzFDaNIjxuWXBh0ai1zVwOQQ==", + "requires": { + "@turf/bbox": "^6.5.0", + "@turf/boolean-point-in-polygon": "^6.5.0", + "@turf/boolean-point-on-line": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + } + }, + "@turf/boolean-disjoint": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-disjoint/-/boolean-disjoint-6.5.0.tgz", + "integrity": "sha512-rZ2ozlrRLIAGo2bjQ/ZUu4oZ/+ZjGvLkN5CKXSKBcu6xFO6k2bgqeM8a1836tAW+Pqp/ZFsTA5fZHsJZvP2D5g==", + "requires": { + "@turf/boolean-point-in-polygon": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/line-intersect": "^6.5.0", + "@turf/meta": "^6.5.0", + "@turf/polygon-to-line": "^6.5.0" + } + }, + "@turf/boolean-equal": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-equal/-/boolean-equal-6.5.0.tgz", + "integrity": "sha512-cY0M3yoLC26mhAnjv1gyYNQjn7wxIXmL2hBmI/qs8g5uKuC2hRWi13ydufE3k4x0aNRjFGlg41fjoYLwaVF+9Q==", + "requires": { + "@turf/clean-coords": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "geojson-equality": "0.1.6" + } + }, + "@turf/boolean-intersects": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-intersects/-/boolean-intersects-6.5.0.tgz", + "integrity": "sha512-nIxkizjRdjKCYFQMnml6cjPsDOBCThrt+nkqtSEcxkKMhAQj5OO7o2CecioNTaX8EayqwMGVKcsz27oP4mKPTw==", + "requires": { + "@turf/boolean-disjoint": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/meta": "^6.5.0" + } + }, + "@turf/boolean-point-in-polygon": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-point-in-polygon/-/boolean-point-in-polygon-6.5.0.tgz", + "integrity": "sha512-DtSuVFB26SI+hj0SjrvXowGTUCHlgevPAIsukssW6BG5MlNSBQAo70wpICBNJL6RjukXg8d2eXaAWuD/CqL00A==", + "requires": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + } + }, + "@turf/boolean-point-on-line": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-point-on-line/-/boolean-point-on-line-6.5.0.tgz", + "integrity": "sha512-A1BbuQ0LceLHvq7F/P7w3QvfpmZqbmViIUPHdNLvZimFNLo4e6IQunmzbe+8aSStH9QRZm3VOflyvNeXvvpZEQ==", + "requires": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + } + }, + "@turf/boolean-within": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-within/-/boolean-within-6.5.0.tgz", + "integrity": "sha512-YQB3oU18Inx35C/LU930D36RAVe7LDXk1kWsQ8mLmuqYn9YdPsDQTMTkLJMhoQ8EbN7QTdy333xRQ4MYgToteQ==", + "requires": { + "@turf/bbox": "^6.5.0", + "@turf/boolean-point-in-polygon": "^6.5.0", + "@turf/boolean-point-on-line": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + } + }, "@turf/centroid": { "version": "6.5.0", "requires": { @@ -26066,8 +26408,68 @@ "@turf/meta": "^6.5.0" } }, + "@turf/clean-coords": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/clean-coords/-/clean-coords-6.5.0.tgz", + "integrity": "sha512-EMX7gyZz0WTH/ET7xV8MyrExywfm9qUi0/MY89yNffzGIEHuFfqwhcCqZ8O00rZIPZHUTxpmsxQSTfzJJA1CPw==", + "requires": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + } + }, + "@turf/envelope": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/envelope/-/envelope-6.5.0.tgz", + "integrity": "sha512-9Z+FnBWvOGOU4X+fMZxYFs1HjFlkKqsddLuMknRaqcJd6t+NIv5DWvPtDL8ATD2GEExYDiFLwMdckfr1yqJgHA==", + "requires": { + "@turf/bbox": "^6.5.0", + "@turf/bbox-polygon": "^6.5.0", + "@turf/helpers": "^6.5.0" + } + }, + "@turf/flatten": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/flatten/-/flatten-6.5.0.tgz", + "integrity": "sha512-IBZVwoNLVNT6U/bcUUllubgElzpMsNoCw8tLqBw6dfYg9ObGmpEjf9BIYLr7a2Yn5ZR4l7YIj2T7kD5uJjZADQ==", + "requires": { + "@turf/helpers": "^6.5.0", + "@turf/meta": "^6.5.0" + } + }, "@turf/helpers": { - "version": "6.5.0" + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-6.5.0.tgz", + "integrity": "sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw==" + }, + "@turf/invariant": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-6.5.0.tgz", + "integrity": "sha512-Wv8PRNCtPD31UVbdJE/KVAWKe7l6US+lJItRR/HOEW3eh+U/JwRCSUl/KZ7bmjM/C+zLNoreM2TU6OoLACs4eg==", + "requires": { + "@turf/helpers": "^6.5.0" + } + }, + "@turf/line-intersect": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/line-intersect/-/line-intersect-6.5.0.tgz", + "integrity": "sha512-CS6R1tZvVQD390G9Ea4pmpM6mJGPWoL82jD46y0q1KSor9s6HupMIo1kY4Ny+AEYQl9jd21V3Scz20eldpbTVA==", + "requires": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "@turf/line-segment": "^6.5.0", + "@turf/meta": "^6.5.0", + "geojson-rbush": "3.x" + } + }, + "@turf/line-segment": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/line-segment/-/line-segment-6.5.0.tgz", + "integrity": "sha512-jI625Ho4jSuJESNq66Mmi290ZJ5pPZiQZruPVpmHkUw257Pew0alMmb6YrqYNnLUuiVVONxAAKXUVeeUGtycfw==", + "requires": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "@turf/meta": "^6.5.0" + } }, "@turf/meta": { "version": "6.5.0", @@ -26075,6 +26477,15 @@ "@turf/helpers": "^6.5.0" } }, + "@turf/polygon-to-line": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/polygon-to-line/-/polygon-to-line-6.5.0.tgz", + "integrity": "sha512-5p4n/ij97EIttAq+ewSnKt0ruvuM+LIDzuczSzuHTpq4oS7Oq8yqg5TQ4nzMVuK41r/tALCk7nAoBuw3Su4Gcw==", + "requires": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + } + }, "@types/babel__core": { "version": "7.20.1", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.1.tgz", @@ -27920,7 +28331,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, "requires": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", @@ -29050,8 +29460,7 @@ } }, "functions-have-names": { - "version": "1.2.3", - "dev": true + "version": "1.2.3" }, "gauge": { "version": "4.0.4", @@ -29073,6 +29482,48 @@ "version": "1.0.0-beta.2", "dev": true }, + "geojson-equality": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/geojson-equality/-/geojson-equality-0.1.6.tgz", + "integrity": "sha512-TqG8YbqizP3EfwP5Uw4aLu6pKkg6JQK9uq/XZ1lXQntvTHD1BBKJWhNpJ2M0ax6TuWMP3oyx6Oq7FCIfznrgpQ==", + "requires": { + "deep-equal": "^1.0.0" + }, + "dependencies": { + "deep-equal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz", + "integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==", + "requires": { + "is-arguments": "^1.1.1", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.5.1" + } + } + } + }, + "geojson-rbush": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/geojson-rbush/-/geojson-rbush-3.2.0.tgz", + "integrity": "sha512-oVltQTXolxvsz1sZnutlSuLDEcQAKYC/uXt9zDzJJ6bu0W+baTI8LZBaTup5afzibEH4N3jlq2p+a152wlBJ7w==", + "requires": { + "@turf/bbox": "*", + "@turf/helpers": "6.x", + "@turf/meta": "6.x", + "@types/geojson": "7946.0.8", + "rbush": "^3.0.1" + }, + "dependencies": { + "@types/geojson": { + "version": "7946.0.8", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.8.tgz", + "integrity": "sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA==" + } + } + }, "geojson-validation": { "version": "1.0.2" }, @@ -29501,7 +29952,6 @@ }, "has-tostringtag": { "version": "1.0.0", - "dev": true, "requires": { "has-symbols": "^1.0.2" } @@ -29888,7 +30338,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "dev": true, "requires": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -29953,7 +30402,6 @@ }, "is-date-object": { "version": "1.0.5", - "dev": true, "requires": { "has-tostringtag": "^1.0.0" } @@ -30040,7 +30488,6 @@ }, "is-regex": { "version": "1.1.4", - "dev": true, "requires": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -34264,15 +34711,13 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", - "dev": true, "requires": { "call-bind": "^1.0.2", "define-properties": "^1.1.3" } }, "object-keys": { - "version": "1.1.1", - "dev": true + "version": "1.1.1" }, "object.assign": { "version": "4.1.5", @@ -35232,6 +35677,11 @@ "version": "4.0.1", "dev": true }, + "quickselect": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", + "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==" + }, "randombytes": { "version": "2.1.0", "dev": true, @@ -35251,6 +35701,14 @@ "unpipe": "1.0.0" } }, + "rbush": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/rbush/-/rbush-3.0.1.tgz", + "integrity": "sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w==", + "requires": { + "quickselect": "^2.0.0" + } + }, "rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -35670,7 +36128,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", - "dev": true, "requires": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -35944,7 +36401,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", - "dev": true, "requires": { "define-data-property": "^1.0.1", "functions-have-names": "^1.2.3", diff --git a/packages/winnow/coverage-unit.svg b/packages/winnow/coverage-unit.svg index 5faf7c602..fb3d4915b 100644 --- a/packages/winnow/coverage-unit.svg +++ b/packages/winnow/coverage-unit.svg @@ -1,5 +1,5 @@ - - coverage: 96.69% + + coverage: 96.89% @@ -13,8 +13,8 @@ \ No newline at end of file diff --git a/packages/winnow/coverage.svg b/packages/winnow/coverage.svg index 75fc1335a..eca5d594b 100644 --- a/packages/winnow/coverage.svg +++ b/packages/winnow/coverage.svg @@ -1,5 +1,5 @@ - - coverage: 98.34% + + coverage: 98.52% @@ -13,8 +13,8 @@ \ No newline at end of file diff --git a/packages/winnow/package.json b/packages/winnow/package.json index d35794d45..c137150fb 100644 --- a/packages/winnow/package.json +++ b/packages/winnow/package.json @@ -49,7 +49,13 @@ "@terraformer/arcgis": "^2.1.2", "@terraformer/spatial": "^2.1.2", "@turf/bbox-polygon": "^6.5.0", + "@turf/boolean-contains": "^6.5.0", + "@turf/boolean-equal": "^6.5.0", + "@turf/boolean-intersects": "^6.5.0", + "@turf/boolean-within": "^6.5.0", "@turf/centroid": "^6.5.0", + "@turf/envelope": "^6.5.0", + "@turf/flatten": "^6.5.0", "@types/geojson": "^7946.0.14", "alasql": "^4.3.3", "classybrew": "0.0.3", @@ -64,6 +70,7 @@ "wkt-parser": "^1.3.3" }, "devDependencies": { + "@turf/helpers": "^6.5.0", "benchmark": "^2.1.4", "fs-extra": "^11.2.0", "proxyquire": "^2.1.3", diff --git a/packages/winnow/src/filter-and-transform/filter-and-transform.spec.js b/packages/winnow/src/filter-and-transform/filter-and-transform.spec.js index c468f222c..3c2bb80a0 100644 --- a/packages/winnow/src/filter-and-transform/filter-and-transform.spec.js +++ b/packages/winnow/src/filter-and-transform/filter-and-transform.spec.js @@ -1,67 +1,12 @@ const test = require('tape'); const { filterAndTransform } = require('./'); -const polygonFilter = { - type: 'Polygon', - coordinates: [ - [ - [-56.9, -61.8], - [-52.9, -61.8], - [-52.9, -60.5], - [-56.9, -60.5], - [-56.9, -61.8], - ], - ], -}; -const pointFeature = { - type: 'Point', - coordinates: [-55.1, -61.1], -}; -const lineFeature = { - type: 'LineString', - coordinates: [ - [-54.5, -60.7], - [-54.7, -60.8], - ], -}; -const polygonFeature = { - type: 'Polygon', - coordinates: [ - [ - [-54.3, -61.3], - [-53.9, -61.3], - [-53.9, -60.7], - [-54.3, -61.3], - ], - ], -}; -test('sql.fn.ST_Within - geometries fully within a target polygon should return true', (t) => { - t.plan(3); - t.ok(filterAndTransform.fn.ST_Within(pointFeature, polygonFilter), 'point within filter geom'); - t.ok(filterAndTransform.fn.ST_Within(lineFeature, polygonFilter), 'line within filter geom'); - t.ok( - filterAndTransform.fn.ST_Within(polygonFeature, polygonFilter), - 'polygon within filter geom', - ); -}); - -test('sql.fn.ST_Within - falsey feature geometries should return false', (t) => { - const falseyFeatures = [ - undefined, - null, - {}, - { coordinates: [] }, - { coordinates: [0, 0] }, - { type: 'Point' }, - ]; +// Within - the search geometry MUST BE WITHIN the FEATURE GEOM - falseyFeatures.forEach((falsey) => { - t.notOk( - filterAndTransform.fn.ST_Within(falsey, polygonFilter), - 'falsey feature should return false', - ); - }); +test('sql.fn.ST_Within - search geometry fully within feature geometry', (t) => { + t.equals(typeof filterAndTransform.fn.ST_Within, 'function'); + t.equals(typeof filterAndTransform.fn.ST_Contains, 'function'); + t.equals(typeof filterAndTransform.fn.ST_Intersects, 'function'); + t.equals(typeof filterAndTransform.fn.ST_EnvelopeIntersects, 'function'); t.end(); }); - -// TODO: put other method decorators under test diff --git a/packages/winnow/src/filter-and-transform/filters/contains.js b/packages/winnow/src/filter-and-transform/filters/contains.js index 0cfb2ae71..ca98d3460 100644 --- a/packages/winnow/src/filter-and-transform/filters/contains.js +++ b/packages/winnow/src/filter-and-transform/filters/contains.js @@ -1,8 +1,68 @@ -const _ = require('lodash'); -const { contains } = require('@terraformer/spatial'); -module.exports = function (featureGeometry = {}, filterGeometry = {}) { - if (_.isEmpty(featureGeometry)) return false; - const { type, coordinates = [] } = featureGeometry; - if (!type || !coordinates || coordinates.length === 0) return false; - return contains(filterGeometry, featureGeometry); +const flatten = require('@turf/flatten').default; +const contains = require('@turf/boolean-contains').default; +const { + createOperationLookup, + POINT, + MULTIPOINT, + LINESTRING, + POLYGON, + MULTILINESTRING, + MULTIPOLYGON, + normalizeGeometry, + isValidGeometry, +} = require('./helpers'); + +const CONTAINS = 'contains'; +const MULTI_CONTAINS = 'multiContains'; + +const operationsTree = { + [CONTAINS]: [ + [POINT, [POINT]], + [MULTIPOINT, [POINT, MULTIPOINT]], + [LINESTRING, [POINT, MULTIPOINT, LINESTRING]], + [POLYGON, [POINT, MULTIPOINT, LINESTRING, POLYGON]], + ], + [MULTI_CONTAINS]: [ + [POINT, [MULTIPOINT]], + [MULTILINESTRING, [POINT, MULTIPOINT, LINESTRING, MULTILINESTRING]], + [POLYGON, [MULTILINESTRING, MULTIPOLYGON]], + [MULTIPOLYGON, [POINT, MULTIPOINT, LINESTRING, MULTILINESTRING, MULTIPOLYGON]], + ], }; + +const operationLookup = createOperationLookup(operationsTree); + +module.exports = function (searchGeometry, geometry) { + if (!geometry) { + return false; + } + + const featureGeometry = normalizeGeometry(geometry); + + if (!isValidGeometry(featureGeometry)) { + return false; + } + + const searchOperation = operationLookup.get(`${searchGeometry.type}::${featureGeometry.type}`); + + if (searchOperation === CONTAINS) { + return contains(searchGeometry, featureGeometry); + } + + if (searchOperation === MULTI_CONTAINS) { + return someMultiSearchGeometriesContains(searchGeometry, featureGeometry); + } + + return false; +}; + +function someMultiSearchGeometriesContains(searchGeometry, featureGeometry) { + const searchCollection = flatten(searchGeometry); + const featureCollection = flatten(featureGeometry); + + return featureCollection.features.every((feature) => { + return searchCollection.features.some((searchFeature) => { + return contains(searchFeature, feature); + }); + }); +} diff --git a/packages/winnow/src/filter-and-transform/filters/contains.spec.js b/packages/winnow/src/filter-and-transform/filters/contains.spec.js index 9b1cdcc54..e6e125401 100644 --- a/packages/winnow/src/filter-and-transform/filters/contains.spec.js +++ b/packages/winnow/src/filter-and-transform/filters/contains.spec.js @@ -1,90 +1,232 @@ const test = require('tape'); const contains = require('./contains'); +const turf = require('@turf/helpers'); + +const point = turf.point([-130, 37]).geometry; +const multiPoint = turf.multiPoint([ + [-130, 37], + [-130, 37], +]).geometry; + +const line = turf.lineString([ + [-130, 38], + [-130, 37], + [-130, 36], +]).geometry; + +const multiLine = turf.multiLineString([ + [ + [-130, 38], + [-130, 37], + [-130, 36], + ], + [ + [-130, 38], + [-130, 37], + [-130, 36], + ], +]).geometry; + +const poly = turf.polygon([ + [ + [-131, 39], + [-131, 35], + [-113, 35], + [-131, 39], + ], +]).geometry; + +const multiPoly = turf.multiPolygon([ + [ + [ + [-131, 39], + [-131, 35], + [-113, 35], + [-131, 39], + ], + ], + [ + [ + [-131, 39], + [-131, 35], + [-113, 35], + [-131, 39], + ], + ], +]).geometry; + +const multiPoly2 = turf.multiPolygon([ + [ + [ + [-130, 39], + [-130, 35], + [-113, 35], + [-130, 39], + ], + ], + [ + [ + [-130, 39], + [-130, 35], + [-113, 35], + [-130, 39], + ], + ], +]).geometry; +const searchGeometry = { + coordinates: [ + [ + [-130, 38], + [-130, 35], + [-113, 35], + [-113, 38], + [-130, 38], + ], + ], + type: 'Polygon', +}; test('contains: empty input', (t) => { - const result = contains(); + const result = contains(searchGeometry); t.equals(result, false); t.end(); }); test('contains: empty object input', (t) => { - const result = contains({}, {}); + const result = contains(searchGeometry, {}); t.equals(result, false); t.end(); }); test('contains: null input', (t) => { - const result = contains(null, {}); - t.equals(result, false); - t.end(); -}); - -test('contains: null input', (t) => { - const result = contains({}, null); + const result = contains(searchGeometry, null); t.equals(result, false); t.end(); }); test('contains: missing geometry type', (t) => { - const result = contains({ coordinates: [44, 84] }, {}); + const result = contains(searchGeometry, { coordinates: [44, 84] }); t.equals(result, false); t.end(); }); test('contains: missing coordinates', (t) => { - const result = contains({ type: 'Point' }, {}); + const result = contains(searchGeometry, { type: 'Point' }); t.equals(result, false); t.end(); }); test('contains: missing empty coordinates', (t) => { - const result = contains({ type: 'Point', coordinates: [] }, {}); + const result = contains(searchGeometry, { type: 'Point', coordinates: [] }); t.equals(result, false); t.end(); }); -test('contains: missing filter geometry', (t) => { - const result = contains({ type: 'Point', coordinates: [44, -84.5] }); +test('contains: feature on search geometry edge is false', (t) => { + const result = contains( + searchGeometry, + { type: 'Point', coordinates: [-130, 38] }, // feature + ); t.equals(result, false); t.end(); }); -test('contains: true', (t) => { +test('contains: feature inside search geometry is true', (t) => { const result = contains( - { type: 'Point', coordinates: [44, -84.5] }, - { - type: 'Polygon', - coordinates: [ - [ - [44, -85], - [45, -85], - [45, -84], - [44, -84], - [44, -85], - ], - ], - }, + searchGeometry, + { type: 'Point', coordinates: [-129, 37] }, // feature ); t.equals(result, true); t.end(); }); -test('contains: false', (t) => { - const result = contains( - { type: 'Point', coordinates: [0, 0] }, - { - type: 'Polygon', - coordinates: [ - [ - [44, -85], - [45, -85], - [45, -84], - [44, -84], - [44, -85], - ], - ], - }, - ); +test('contains: point outside the search geometry is false', (t) => { + const result = contains(searchGeometry, { type: 'Point', coordinates: [0, 0] }); t.equals(result, false); t.end(); }); + +test('contains: search geometry contains a feature, true', (t) => { + t.ok(contains(point, point)); + t.ok(contains(multiPoint, point)); + t.ok(contains(multiPoint, multiPoint)); + t.ok(contains(line, point)); + t.ok(contains(line, multiPoint)); + t.ok(contains(line, line)); + + t.ok(contains(poly, point)); + t.ok(contains(poly, multiPoint)); + t.ok(contains(poly, line)); + t.ok(contains(poly, poly)); + + t.ok(contains(multiLine, point)); + t.ok(contains(multiLine, line)); + t.ok(contains(multiLine, multiLine)); + t.ok(contains(multiLine, multiPoint)); + + t.ok(contains(poly, multiLine)); + t.ok(contains(poly, multiPoly)); + + t.ok(contains(multiPoly, point)); + t.ok(contains(multiPoly, line)); + t.ok(contains(multiPoly, multiLine)); + t.ok(contains(multiPoly, multiPoint)); + t.ok(contains(multiPoly, multiPoly)); + t.end(); +}); + +test('contains: search geometry within a feature, false', (t) => { + const pointOut = turf.point([0, 0]).geometry; + t.notOk(contains(pointOut, point)); + t.notOk(contains(pointOut, multiPoint)); + t.notOk(contains(pointOut, line)); + t.notOk(contains(pointOut, multiLine)); + t.notOk(contains(pointOut, multiPoly)); + + const multiPointOut = turf.multiPoint([ + [0, 0], + [0, 0], + ]).geometry; + t.notOk(contains(multiPointOut, multiPoint)); + t.notOk(contains(multiPointOut, line)); + t.notOk(contains(multiPointOut, multiLine)); + t.notOk(contains(multiPointOut, poly)); + t.notOk(contains(multiPointOut, multiPoly)); + + const lineOut = turf.lineString([ + [0, 0], + [0, 1], + ]).geometry; + t.notOk(contains(lineOut, line)); + t.notOk(contains(lineOut, multiLine)); + t.notOk(contains(lineOut, poly)); + t.notOk(contains(lineOut, multiPoly)); + + const multiLineOut = turf.multiLineString([ + [ + [0, 0], + [0, 1], + ], + [ + [0, 0], + [0, 1], + ], + ]).geometry; + t.notOk(contains(multiLineOut, multiLine)); + t.notOk(contains(multiLineOut, poly)); + t.notOk(contains(multiLineOut, multiPoly)); + + const polyOut = turf.polygon([ + [ + [0, 0], + [0, 1], + [1, 1], + [0, 0], + ], + ]).geometry; + t.notOk(contains(polyOut, poly)); + t.notOk(contains(polyOut, multiPoly)); + + t.notOk(contains(multiPoly2, multiPoly)); + t.end(); +}); diff --git a/packages/winnow/src/filter-and-transform/filters/envelope-intersects.js b/packages/winnow/src/filter-and-transform/filters/envelope-intersects.js index 4755a5c88..cd7b219fc 100644 --- a/packages/winnow/src/filter-and-transform/filters/envelope-intersects.js +++ b/packages/winnow/src/filter-and-transform/filters/envelope-intersects.js @@ -1,33 +1,19 @@ -const _ = require('lodash'); -const { calculateBounds, intersects, contains } = require('@terraformer/spatial'); -const bboxPolygon = require('@turf/bbox-polygon').default; -const { arcgisToGeoJSON } = require('@terraformer/arcgis'); +const intersects = require('@turf/boolean-intersects').default; +const envelope = require('@turf/envelope').default; +const { normalizeGeometry, isValidGeometry } = require('./helpers'); -module.exports = function (featureGeometry = {}, filterGeometry = {}) { - if (_.isEmpty(featureGeometry) || _.isEmpty(filterGeometry)) return false; +module.exports = function (searchGeometry, geometry) { + if (!geometry) { + return false; + } - const normalizedFeatureGeometry = isGeoJsonGeometry(featureGeometry) - ? featureGeometry - : arcgisToGeoJSON(featureGeometry); + const featureGeometry = normalizeGeometry(geometry); - const { type, coordinates = [] } = normalizedFeatureGeometry; + if (!isValidGeometry(featureGeometry)) { + return false; + } - if (!type || coordinates.length === 0) return false; + const geometryFilterEnvelope = envelope(searchGeometry); - const geometryFilterEnvelope = convertGeometryToEnvelopePolygon(filterGeometry); - - if (type === 'Point') return contains(geometryFilterEnvelope, normalizedFeatureGeometry); - - const featureEnvelope = convertGeometryToEnvelopePolygon(normalizedFeatureGeometry); - return intersects(geometryFilterEnvelope, featureEnvelope); + return intersects(geometryFilterEnvelope, featureGeometry); }; - -function convertGeometryToEnvelopePolygon(geometry) { - const bounds = calculateBounds(geometry); - const { geometry: envelopePolygon } = bboxPolygon(bounds); - return envelopePolygon; -} - -function isGeoJsonGeometry({ type, coordinates }) { - return type && coordinates; -} diff --git a/packages/winnow/src/filter-and-transform/filters/envelope-intersects.spec.js b/packages/winnow/src/filter-and-transform/filters/envelope-intersects.spec.js index d71844d40..01aadfa90 100644 --- a/packages/winnow/src/filter-and-transform/filters/envelope-intersects.spec.js +++ b/packages/winnow/src/filter-and-transform/filters/envelope-intersects.spec.js @@ -1,161 +1,75 @@ const test = require('tape'); -const envelopeIntersects = require('./envelope-intersects'); +const intersects = require('./envelope-intersects'); -test('envelopeIntersects: empty input', (t) => { - const result = envelopeIntersects(); - t.equals(result, false); - t.end(); -}); +const searchGeometry = { + coordinates: [ + [ + [-130, 38], + [-130, 35], + [-113, 35], + [-113, 38], + [-130, 38], + ], + ], + type: 'Polygon', +}; -test('envelopeIntersects: empty object input', (t) => { - const result = envelopeIntersects({}, {}); +test('intersects: empty input', (t) => { + const result = intersects(searchGeometry); t.equals(result, false); t.end(); }); -test('envelopeIntersects: null input', (t) => { - const result = envelopeIntersects(null, {}); +test('intersects: empty object input', (t) => { + const result = intersects(searchGeometry, {}); t.equals(result, false); t.end(); }); -test('envelopeIntersects: null input', (t) => { - const result = envelopeIntersects({}, null); +test('intersects: null input', (t) => { + const result = intersects(searchGeometry, null); t.equals(result, false); t.end(); }); -test('envelopeIntersects: missing geometry type', (t) => { - const result = envelopeIntersects({ coordinates: [44, 84] }, {}); +test('intersects: missing geometry type', (t) => { + const result = intersects(searchGeometry, { coordinates: [44, 84] }); t.equals(result, false); t.end(); }); -test('envelopeIntersects: missing coordinates', (t) => { - const result = envelopeIntersects({ type: 'Point' }, {}); +test('intersects: missing coordinates', (t) => { + const result = intersects(searchGeometry, { type: 'Point' }); t.equals(result, false); t.end(); }); -test('envelopeIntersects: missing empty coordinates', (t) => { - const result = envelopeIntersects({ type: 'Point', coordinates: [] }, {}); +test('intersects: missing empty coordinates', (t) => { + const result = intersects(searchGeometry, { type: 'Point', coordinates: [] }); t.equals(result, false); t.end(); }); -test('envelopeIntersects: missing filter geometry', (t) => { - const result = envelopeIntersects({ - type: 'Point', - coordinates: [44, -84.5], - }); - t.equals(result, false); - t.end(); -}); - -test('envelopeIntersects: Point inside polygon, true', (t) => { - const result = envelopeIntersects( - { type: 'Point', coordinates: [44, -84.5] }, - { - type: 'Polygon', - coordinates: [ - [ - [44, -85], - [45, -85], - [45, -84], - [44, -84], - [44, -85], - ], - ], - }, - ); +test('intersects: Point inside polygon, true', (t) => { + const result = intersects(searchGeometry, { type: 'Point', coordinates: [-115, 37] }); t.equals(result, true); t.end(); }); -test('envelopeIntersects: LineString outside polygon, false', (t) => { - const result = envelopeIntersects( - { - type: 'LineString', - coordinates: [ - [17.41, 52.22], - [17.42, 52.22], - ], - }, - { - type: 'Polygon', - coordinates: [ - [ - [17.2, 52.2], - [17.4, 52.2], - [17.4, 52.3], - [17.2, 52.3], - [17.2, 52.2], - ], - ], - }, - ); +test('intersects: LineString outside polygon, false', (t) => { + const result = intersects(searchGeometry, { + type: 'LineString', + coordinates: [ + [17.41, 52.22], + [17.42, 52.22], + ], + }); t.equals(result, false); t.end(); }); -test('envelopeIntersects: Point inside polygon, Esri Geometry, true', (t) => { - const result = envelopeIntersects( - { x: 44, y: -84.5 }, - { - type: 'Polygon', - coordinates: [ - [ - [44, -85], - [45, -85], - [45, -84], - [44, -84], - [44, -85], - ], - ], - }, - ); +test('intersects: Point inside polygon, Esri Geometry, true', (t) => { + const result = intersects(searchGeometry, { x: -115, y: 37 }); t.equals(result, true); t.end(); }); - -test('envelopeIntersects: Point inside polygon envelope, true', (t) => { - const result = envelopeIntersects( - { type: 'Point', coordinates: [17.505, 52.029] }, - { - type: 'Polygon', - coordinates: [ - [ - [17.52, 52.037], - [17.5, 52.02], - [17.55134582519531, 52.01], - [17.56988525390625, 52.02], - [17.538986206054688, 52.03], - [17.52, 52.037], - ], - ], - }, - ); - t.equals(result, true); - t.end(); -}); - -test('envelopeIntersects: Point outside polygon envelope, false', (t) => { - const result = envelopeIntersects( - { type: 'Point', coordinates: [20, 53] }, - { - type: 'Polygon', - coordinates: [ - [ - [17.52, 52.037], - [17.5, 52.02], - [17.55134582519531, 52.01], - [17.56988525390625, 52.02], - [17.538986206054688, 52.03], - [17.52, 52.037], - ], - ], - }, - ); - t.equals(result, false); - t.end(); -}); diff --git a/packages/winnow/src/filter-and-transform/filters/helpers.js b/packages/winnow/src/filter-and-transform/filters/helpers.js new file mode 100644 index 000000000..1ac5902ec --- /dev/null +++ b/packages/winnow/src/filter-and-transform/filters/helpers.js @@ -0,0 +1,45 @@ +const { arcgisToGeoJSON } = require('@terraformer/arcgis'); + +const POINT = 'Point'; +const MULTIPOINT = 'MultiPoint'; +const LINESTRING = 'LineString'; +const MULTILINESTRING = 'MultiLineString'; +const POLYGON = 'Polygon'; +const MULTIPOLYGON = 'MultiPolygon'; + +function normalizeGeometry(geometry) { + return isGeoJsonGeometry(geometry) ? geometry : arcgisToGeoJSON(geometry); +} + +function isGeoJsonGeometry({ type, coordinates }) { + return type && coordinates; +} + +function isValidGeometry(geometry) { + return isGeoJsonGeometry(geometry) && geometry.coordinates.length > 0; +} + +function createOperationLookup(operationsTree) { + const entries = Object.entries(operationsTree); + return entries.reduce((map, [operation, combinations]) => { + combinations.forEach(([searchType, featureTypes]) => { + featureTypes.forEach((featureType) => { + map.set(`${searchType}::${featureType}`, operation); + }); + }); + + return map; + }, new Map()); +} + +module.exports = { + POINT, + MULTIPOINT, + LINESTRING, + MULTILINESTRING, + POLYGON, + MULTIPOLYGON, + normalizeGeometry, + isValidGeometry, + createOperationLookup, +}; diff --git a/packages/winnow/src/filter-and-transform/filters/intersects.js b/packages/winnow/src/filter-and-transform/filters/intersects.js index 779b3b430..6283917a1 100644 --- a/packages/winnow/src/filter-and-transform/filters/intersects.js +++ b/packages/winnow/src/filter-and-transform/filters/intersects.js @@ -1,18 +1,17 @@ -const _ = require('lodash'); -const { intersects, contains } = require('@terraformer/spatial'); -const { arcgisToGeoJSON } = require('@terraformer/arcgis'); +const { normalizeGeometry, isValidGeometry } = require('./helpers'); -module.exports = function (featureGeometry = {}, filterGeometry = {}) { - if (_.isEmpty(featureGeometry)) return false; - const geometry = isGeoJsonGeometry(featureGeometry) - ? featureGeometry - : arcgisToGeoJSON(featureGeometry); - const { type, coordinates = [] } = geometry; - if (!type || !coordinates || coordinates.length === 0) return false; - if (type === 'Point') return contains(filterGeometry, geometry); - return intersects(filterGeometry, geometry); -}; +const intersects = require('@turf/boolean-intersects').default; + +module.exports = function (searchGeometry, geometry) { + if (!geometry) { + return false; + } + + const featureGeometry = normalizeGeometry(geometry); -function isGeoJsonGeometry({ type, coordinates }) { - return type && coordinates; -} + if (!isValidGeometry(featureGeometry)) { + return false; + } + + return intersects(searchGeometry, featureGeometry); +}; diff --git a/packages/winnow/src/filter-and-transform/filters/intersects.spec.js b/packages/winnow/src/filter-and-transform/filters/intersects.spec.js index af1d88a5c..7e6f185e7 100644 --- a/packages/winnow/src/filter-and-transform/filters/intersects.spec.js +++ b/packages/winnow/src/filter-and-transform/filters/intersects.spec.js @@ -1,116 +1,75 @@ const test = require('tape'); const intersects = require('./intersects'); +const searchGeometry = { + coordinates: [ + [ + [-130, 38], + [-130, 35], + [-113, 35], + [-113, 38], + [-130, 38], + ], + ], + type: 'Polygon', +}; + test('intersects: empty input', (t) => { - const result = intersects(); + const result = intersects(searchGeometry); t.equals(result, false); t.end(); }); test('intersects: empty object input', (t) => { - const result = intersects({}, {}); - t.equals(result, false); - t.end(); -}); - -test('intersects: null input', (t) => { - const result = intersects(null, {}); + const result = intersects(searchGeometry, {}); t.equals(result, false); t.end(); }); test('intersects: null input', (t) => { - const result = intersects({}, null); + const result = intersects(searchGeometry, null); t.equals(result, false); t.end(); }); test('intersects: missing geometry type', (t) => { - const result = intersects({ coordinates: [44, 84] }, {}); + const result = intersects(searchGeometry, { coordinates: [44, 84] }); t.equals(result, false); t.end(); }); test('intersects: missing coordinates', (t) => { - const result = intersects({ type: 'Point' }, {}); + const result = intersects(searchGeometry, { type: 'Point' }); t.equals(result, false); t.end(); }); test('intersects: missing empty coordinates', (t) => { - const result = intersects({ type: 'Point', coordinates: [] }, {}); - t.equals(result, false); - t.end(); -}); - -test('intersects: missing filter geometry', (t) => { - const result = intersects({ type: 'Point', coordinates: [44, -84.5] }); + const result = intersects(searchGeometry, { type: 'Point', coordinates: [] }); t.equals(result, false); t.end(); }); test('intersects: Point inside polygon, true', (t) => { - const result = intersects( - { type: 'Point', coordinates: [44, -84.5] }, - { - type: 'Polygon', - coordinates: [ - [ - [44, -85], - [45, -85], - [45, -84], - [44, -84], - [44, -85], - ], - ], - }, - ); + const result = intersects(searchGeometry, { type: 'Point', coordinates: [-115, 37] }); t.equals(result, true); t.end(); }); test('intersects: LineString outside polygon, false', (t) => { - const result = intersects( - { - type: 'LineString', - coordinates: [ - [17.41, 52.22], - [17.42, 52.22], - ], - }, - { - type: 'Polygon', - coordinates: [ - [ - [17.2, 52.2], - [17.4, 52.2], - [17.4, 52.3], - [17.2, 52.3], - [17.2, 52.2], - ], - ], - }, - ); + const result = intersects(searchGeometry, { + type: 'LineString', + coordinates: [ + [17.41, 52.22], + [17.42, 52.22], + ], + }); t.equals(result, false); t.end(); }); test('intersects: Point inside polygon, Esri Geometry, true', (t) => { - const result = intersects( - { x: 44, y: -84.5 }, - { - type: 'Polygon', - coordinates: [ - [ - [44, -85], - [45, -85], - [45, -84], - [44, -84], - [44, -85], - ], - ], - }, - ); + const result = intersects(searchGeometry, { x: -115, y: 37 }); t.equals(result, true); t.end(); }); diff --git a/packages/winnow/src/filter-and-transform/filters/within.js b/packages/winnow/src/filter-and-transform/filters/within.js index 484e3e796..7df13a11d 100644 --- a/packages/winnow/src/filter-and-transform/filters/within.js +++ b/packages/winnow/src/filter-and-transform/filters/within.js @@ -1,9 +1,75 @@ -const _ = require('lodash'); -const { within } = require('@terraformer/spatial'); - -module.exports = function (featureGeometry, filterGeometry = {}) { - if (_.isEmpty(featureGeometry)) return false; - const { type, coordinates = [] } = featureGeometry; - if (!type || !coordinates || coordinates.length === 0) return false; - return within(featureGeometry, filterGeometry); +const within = require('@turf/boolean-within').default; +const equals = require('@turf/boolean-equal').default; +const flatten = require('@turf/flatten').default; +const { + createOperationLookup, + POINT, + MULTIPOINT, + LINESTRING, + POLYGON, + MULTILINESTRING, + MULTIPOLYGON, + normalizeGeometry, + isValidGeometry, +} = require('./helpers'); + +const EQUALS = 'equals'; +const WITHIN = 'within'; +const EACH_WITHIN = 'eachWithin'; + +const operationTree = { + [EQUALS]: [[POINT, [POINT]]], + [WITHIN]: [ + [POINT, [MULTIPOINT, LINESTRING, POLYGON, MULTIPOLYGON]], + [MULTIPOINT, [MULTIPOINT, LINESTRING, POLYGON, MULTIPOLYGON]], + [LINESTRING, [LINESTRING, POLYGON, MULTIPOLYGON]], + [POLYGON, [POLYGON, MULTIPOLYGON]], + ], + [EACH_WITHIN]: [ + [POINT, [MULTILINESTRING]], + [MULTIPOINT, [MULTILINESTRING]], + [LINESTRING, [MULTILINESTRING]], + [MULTILINESTRING, [MULTILINESTRING, POLYGON, MULTIPOLYGON]], + ], +}; + +const operationLookup = createOperationLookup(operationTree); + +module.exports = function (searchGeometry, geometry) { + if (!geometry) { + return false; + } + + const featureGeometry = normalizeGeometry(geometry); + + if (!isValidGeometry(featureGeometry)) { + return false; + } + + const operation = operationLookup.get(`${searchGeometry.type}::${featureGeometry.type}`); + + if (operation === EQUALS) { + return equals(searchGeometry, featureGeometry); + } + + if (operation === WITHIN) { + return within(searchGeometry, featureGeometry); + } + + if (operation === EACH_WITHIN) { + return allMultiSearchGeometriesWithin(searchGeometry, featureGeometry); + } + + return false; }; + +function allMultiSearchGeometriesWithin(searchGeometry, featureGeometry) { + const searchCollection = flatten(searchGeometry); + const featureCollection = flatten(featureGeometry); + + return searchCollection.features.every((searchFeature) => { + return featureCollection.features.some((feature) => { + return within(searchFeature, feature); + }); + }); +} diff --git a/packages/winnow/src/filter-and-transform/filters/within.spec.js b/packages/winnow/src/filter-and-transform/filters/within.spec.js index 4e4f9a329..d183bb769 100644 --- a/packages/winnow/src/filter-and-transform/filters/within.spec.js +++ b/packages/winnow/src/filter-and-transform/filters/within.spec.js @@ -1,90 +1,178 @@ const test = require('tape'); const within = require('./within'); +const turf = require('@turf/helpers'); + +const point = turf.point([-130, 37]).geometry; +const multiPoint = turf.multiPoint([ + [-130, 37], + [-130, 37], +]).geometry; + +const line = turf.lineString([ + [-130, 38], + [-130, 37], + [-130, 36], +]).geometry; + +const multiLine = turf.multiLineString([ + [ + [-130, 38], + [-130, 37], + [-130, 36], + ], + [ + [-130, 38], + [-130, 37], + [-130, 36], + ], +]).geometry; + +const poly = turf.polygon([ + [ + [-131, 39], + [-131, 35], + [-113, 35], + [-131, 39], + ], +]).geometry; + +const multiPoly = turf.multiPolygon([ + [ + [ + [-131, 39], + [-131, 35], + [-113, 35], + [-131, 39], + ], + ], + [ + [ + [-131, 39], + [-131, 35], + [-113, 35], + [-131, 39], + ], + ], +]).geometry; test('within: empty input', (t) => { - const result = within(); + const result = within(poly); t.equals(result, false); t.end(); }); test('within: empty object input', (t) => { - const result = within({}, {}); - t.equals(result, false); - t.end(); -}); - -test('within: null input', (t) => { - const result = within(null, {}); + const result = within(poly, {}); t.equals(result, false); t.end(); }); test('within: null input', (t) => { - const result = within({}, null); + const result = within(poly, null); t.equals(result, false); t.end(); }); test('within: missing geometry type', (t) => { - const result = within({ coordinates: [44, 84] }, {}); + const result = within(poly, { coordinates: [44, 84] }); t.equals(result, false); t.end(); }); test('within: missing coordinates', (t) => { - const result = within({ type: 'Point' }, {}); + const result = within(poly, { type: 'Point' }); t.equals(result, false); t.end(); }); test('within: missing empty coordinates', (t) => { - const result = within({ type: 'Point', coordinates: [] }, {}); + const result = within(poly, { type: 'Point', coordinates: [] }); t.equals(result, false); t.end(); }); -test('within: missing filter geometry', (t) => { - const result = within({ type: 'Point', coordinates: [44, -84.5] }); - t.equals(result, false); - t.end(); -}); +test('within: search geometry within a feature, true', (t) => { + t.ok(within(point, point)); + t.ok(within(point, multiPoint)); + t.ok(within(point, line)); + t.ok(within(point, multiLine)); + t.ok(within(point, poly)); + t.ok(within(point, multiPoly)); + + t.ok(within(multiPoint, multiPoint)); + t.ok(within(multiPoint, line)); + t.ok(within(multiPoint, multiLine)); + t.ok(within(multiPoint, poly)); + t.ok(within(multiPoint, multiPoly)); + + t.ok(within(line, line)); + t.ok(within(line, multiLine)); + t.ok(within(line, poly)); + t.ok(within(line, multiPoly)); -test('within: true', (t) => { - const result = within( - { type: 'Point', coordinates: [44, -84.5] }, - { - type: 'Polygon', - coordinates: [ - [ - [44, -85], - [45, -85], - [45, -84], - [44, -84], - [44, -85], - ], - ], - }, - ); - t.equals(result, true); + t.ok(within(multiLine, multiLine)); + t.ok(within(multiLine, poly)); + t.ok(within(multiLine, multiPoly)); + + t.ok(within(poly, poly)); + t.ok(within(poly, multiPoly)); t.end(); }); -test('within: false', (t) => { - const result = within( - { type: 'Point', coordinates: [0, 0] }, - { - type: 'Polygon', - coordinates: [ - [ - [44, -85], - [45, -85], - [45, -84], - [44, -84], - [44, -85], - ], - ], - }, - ); - t.equals(result, false); +test('within: search geometry within a feature, false', (t) => { + const pointOut = turf.point([0, 0]).geometry; + t.notOk(within(pointOut, point)); + t.notOk(within(pointOut, multiPoint)); + t.notOk(within(pointOut, line)); + t.notOk(within(pointOut, multiLine)); + t.notOk(within(pointOut, multiPoly)); + + t.notOk(within(multiPoint, point)); + + const multiPointOut = turf.multiPoint([ + [0, 0], + [0, 0], + ]).geometry; + t.notOk(within(multiPointOut, multiPoint)); + t.notOk(within(multiPointOut, line)); + t.notOk(within(multiPointOut, multiLine)); + t.notOk(within(multiPointOut, poly)); + t.notOk(within(multiPointOut, multiPoly)); + + const lineOut = turf.lineString([ + [0, 0], + [0, 1], + ]).geometry; + t.notOk(within(lineOut, line)); + t.notOk(within(lineOut, multiLine)); + t.notOk(within(lineOut, poly)); + t.notOk(within(lineOut, multiPoly)); + + const multiLineOut = turf.multiLineString([ + [ + [0, 0], + [0, 1], + ], + [ + [0, 0], + [0, 1], + ], + ]).geometry; + t.notOk(within(multiLineOut, multiLine)); + t.notOk(within(multiLineOut, poly)); + t.notOk(within(multiLineOut, multiPoly)); + + const polyOut = turf.polygon([ + [ + [0, 0], + [0, 1], + [1, 1], + [0, 0], + ], + ]).geometry; + t.notOk(within(polyOut, poly)); + t.notOk(within(polyOut, multiPoly)); + + t.notOk(within(multiPoly, multiPoly)); t.end(); }); diff --git a/packages/winnow/src/normalize-query-options/geometry-filter.js b/packages/winnow/src/normalize-query-options/geometry-filter.js index fb1251129..9dfacbed5 100644 --- a/packages/winnow/src/normalize-query-options/geometry-filter.js +++ b/packages/winnow/src/normalize-query-options/geometry-filter.js @@ -9,7 +9,9 @@ const normalizeSourceSR = require('./source-data-spatial-reference'); function normalizeGeometryFilter(options = {}) { const geometry = options.geometry || options.bbox; - if (!geometry) return; + if (!geometry) { + return; + } const geometryFilterSpatialReference = normalizeGeometryFilterSpatialReference(options); const fromSR = getCrsString(geometryFilterSpatialReference); @@ -73,7 +75,7 @@ function transformEsriEnvelopeToPolygon({ xmin, ymin, xmax, ymax }) { }; } -function getCrsString({ wkt, wkid } = {}) { +function getCrsString({ wkt, wkid }) { return wkt || `EPSG:${wkid}`; } diff --git a/packages/winnow/src/sql-query-builder/where-builder/index.js b/packages/winnow/src/sql-query-builder/where-builder/index.js index 4aa851b32..f86d6fb1f 100644 --- a/packages/winnow/src/sql-query-builder/where-builder/index.js +++ b/packages/winnow/src/sql-query-builder/where-builder/index.js @@ -56,8 +56,8 @@ class WhereBuilder { const spatialPredicate = this.#options.spatialPredicate || 'ST_Intersects'; // The "?" in the string below is a SQL query parameter. When it is executed, - // a supplied value is used in its place - this.#geometryPredicate = `${spatialPredicate}(geometry, ?)`; + // a supplied search-geometry is used in its place + this.#geometryPredicate = `${spatialPredicate}(?, geometry)`; return this; } diff --git a/packages/winnow/src/sql-query-builder/where-builder/index.spec.js b/packages/winnow/src/sql-query-builder/where-builder/index.spec.js index 190b1a1af..5008afe00 100644 --- a/packages/winnow/src/sql-query-builder/where-builder/index.spec.js +++ b/packages/winnow/src/sql-query-builder/where-builder/index.spec.js @@ -54,7 +54,7 @@ test('WhereBuilder.create: returns where clause with geometry predicate', (t) => const whereClause = WhereBuilder.create({ geometry: [0, 0, 0, 0], }); - t.equals(whereClause, 'ST_Intersects(geometry, ?)'); + t.equals(whereClause, 'ST_Intersects(?, geometry)'); }); test('returns where clause with translated sql-where and geometry predicate', (t) => { @@ -63,7 +63,7 @@ test('returns where clause with translated sql-where and geometry predicate', (t geometry: [0, 0, 0, 0], where: "color='red'", }); - t.equals(whereClause, "properties->`color` = 'red' AND ST_Intersects(geometry, ?)"); + t.equals(whereClause, "properties->`color` = 'red' AND ST_Intersects(?, geometry)"); }); test('predicate with OBJECTID and no metadata fields to user-defined function', (t) => { diff --git a/packages/winnow/test/integration/filter.spec.js b/packages/winnow/test/integration/filter.spec.js index f70e20dd5..12277c34a 100644 --- a/packages/winnow/test/integration/filter.spec.js +++ b/packages/winnow/test/integration/filter.spec.js @@ -306,44 +306,6 @@ test('With a point geometry filter', (t) => { run('trees', options, 1, t); }); -test('With a ST_Contains geometry predicate', (t) => { - const options = { - geometry: { - type: 'Polygon', - coordinates: [ - [ - [-118.163, 34.162], - [-118.108, 34.162], - [-118.108, 34.173], - [-118.163, 34.173], - [-118.163, 34.162], - ], - ], - }, - spatialPredicate: 'ST_Contains', - }; - run('trees', options, 12, t); -}); - -test('With a ST_Within geometry predicate', (t) => { - const options = { - geometry: { - type: 'Polygon', - coordinates: [ - [ - [-118.163, 34.162], - [-118.108, 34.162], - [-118.108, 34.173], - [-118.163, 34.173], - [-118.163, 34.162], - ], - ], - }, - spatialPredicate: 'ST_Within', - }; - run('trees', options, 12, t); -}); - test('With a ST_EnvelopeIntersects geometry predicate', (t) => { const options = { geometry: { @@ -363,20 +325,6 @@ test('With a ST_EnvelopeIntersects geometry predicate', (t) => { run('states', options, 2, t); }); -test('With a ST_Intersects geometry predicate', (t) => { - const options = { - geometry: { - type: 'LineString', - coordinates: [ - [-85.983201784023521, 34.515410848143297, 204.5451898127248], - [-121.278821256198796, 39.823566607727578, 1173.189682061974963], - ], - }, - spatialPredicate: 'ST_Intersects', - }; - run('states', options, 1, t); -}); - test('With a where and a geometry option', (t) => { const options = { where: "Genus like '%Quercus%'", @@ -448,32 +396,6 @@ test('With an envelope, an inSR and an outSR', (t) => { run('trees', options, 6, t); }); -test('With a multi-ring geometry and an inSR', (t) => { - const options = { - geometry: { - rings: [ - [ - [19930537.269606635, -1018885.7633881811], - [19930537.269606635, 13148258.807095852], - [20037508.342788905, 13148258.807095852], - [20037508.342788905, -1018885.7633881811], - [19930537.269606635, -1018885.7633881811], - ], - [ - [-20037508.342788905, -1018885.7633881811], - [-20037508.342788905, 13148258.807095852], - [-4568447.54013514, 13148258.807095852], - [-4568447.54013514, -1018885.7633881811], - [-20037508.342788905, -1018885.7633881811], - ], - ], - }, - geometryType: 'esriGeometryPolygon', - inSR: 102100, - }; - run('ringbug', options, 30, t); -}); - test('with a coded value domain', (t) => { const options = { where: "State = '1'", diff --git a/test/geoservice-query.spec.js b/test/geoservice-query.spec.js index f5e495a7b..6bbdefef7 100644 --- a/test/geoservice-query.spec.js +++ b/test/geoservice-query.spec.js @@ -9,239 +9,359 @@ const mockLogger = { error: () => {}, }; -describe('koop', () => { +describe('Feature Server Output - query', () => { const koop = new Koop({ logLevel: 'error', logger: mockLogger }); koop.register(provider, { dataDir: './test/provider-data' }); - describe('Feature Server', () => { - describe('/query', () => { - describe('objectIds', () => { - test('handles empty value', async () => { + describe('objectIds', () => { + describe('using OBJECTID field', () => { + test('handles empty value', async () => { + try { + const response = await request(koop.server).get( + '/file-geojson/rest/services/points-w-objectid/FeatureServer/0/query?objectIds=', + ); + expect(response.status).toBe(200); + const { features } = response.body; + expect(features.length).toBe(3); + } catch (error) { + console.error(error); + throw error; + } + }); + + test('handles single value', async () => { + try { + const response = await request(koop.server).get( + '/file-geojson/rest/services/points-w-objectid/FeatureServer/0/query?objectIds=2', + ); + expect(response.status).toBe(200); + const { features } = response.body; + expect(features.length).toBe(1); + expect(features[0].attributes.OBJECTID).toBe(2); + } catch (error) { + console.error(error); + throw error; + } + }); + + test('handles delimited values', async () => { + try { + const response = await request(koop.server).get( + '/file-geojson/rest/services/points-w-objectid/FeatureServer/0/query?objectIds=2,3', + ); + expect(response.status).toBe(200); + const { features } = response.body; + expect(features.length).toBe(2); + expect(features[0].attributes.OBJECTID).toBe(2); + expect(features[1].attributes.OBJECTID).toBe(3); + } catch (error) { + console.error(error); + throw error; + } + }); + }); + + describe('using defined id field', () => { + test('handles single value', async () => { + try { + const response = await request(koop.server).get( + '/file-geojson/rest/services/points-w-metadata-id/FeatureServer/0/query?objectIds=2', + ); + expect(response.status).toBe(200); + const { features } = response.body; + expect(features.length).toBe(1); + expect(features[0].attributes.id).toBe(2); + } catch (error) { + console.error(error); + throw error; + } + }); + + test('handles delimited values', async () => { + try { + const response = await request(koop.server).get( + '/file-geojson/rest/services/points-w-metadata-id/FeatureServer/0/query?objectIds=2,3', + ); + expect(response.status).toBe(200); + const { features } = response.body; + expect(features.length).toBe(2); + expect(features[0].attributes.id).toBe(2); + expect(features[1].attributes.id).toBe(3); + } catch (error) { + console.error(error); + throw error; + } + }); + }); + + describe('without OBJECTID or idField', () => { + let objectIds; + beforeAll(async () => { + const response = await request(koop.server).get( + '/file-geojson/rest/services/points-wo-objectid/FeatureServer/0/query', + ); + objectIds = response.body.features.map((feature) => { + return feature.attributes.OBJECTID; + }); + }); + + test('handles single value', async () => { + try { + const response = await request(koop.server).get( + `/file-geojson/rest/services/points-wo-objectid/FeatureServer/0/query?objectIds=${objectIds[1]}`, // eslint-disable-line + ); + expect(response.status).toBe(200); + const { features } = response.body; + expect(features.length).toBe(1); + expect(features[0].attributes.OBJECTID).toBe(objectIds[1]); + } catch (error) { + console.error(error); + throw error; + } + }); + + test('handles delimited values', async () => { + try { + const response = await request(koop.server).get( + `/file-geojson/rest/services/points-wo-objectid/FeatureServer/0/query?objectIds=${objectIds[1]},${objectIds[2]}`, // eslint-disable-line + ); + expect(response.status).toBe(200); + const { features } = response.body; + expect(features.length).toBe(2); + expect(features[0].attributes.OBJECTID).toBe(objectIds[1]); + expect(features[1].attributes.OBJECTID).toBe(objectIds[2]); + } catch (error) { + console.error(error); + throw error; + } + }); + }); + }); + + describe('where', () => { + test('handle query with "+" as whitespace', async () => { + try { + const response = await request(koop.server).get( + '/file-geojson/rest/services/points-w-objectid/FeatureServer/0/query?WHERE=label+is+not+null', // eslint-disable-line + ); + expect(response.status).toBe(200); + const { features } = response.body; + expect(features.length).toBe(3); + } catch (error) { + console.error(error); + throw error; + } + }); + }); + + describe('resultRecordCount', () => { + test('should respect resultRecordCount applied from winnow', async () => { + try { + const response = await request(koop.server).get( + '/file-geojson/rest/services/points-w-objectid/FeatureServer/0/query?resultRecordCount=2', + ); + expect(response.status).toBe(200); + const { features } = response.body; + expect(features.length).toBe(2); + } catch (error) { + console.error(error); + throw error; + } + }); + + test('should respect resultRecordCount applied from passthrough provider', async () => { + try { + const response = await request(koop.server).get( + '/file-geojson/rest/services/pass-through/FeatureServer/0/query?resultRecordCount=3', + ); + expect(response.status).toBe(200); + const { features, exceededTransferLimit } = response.body; + expect(features.length).toBe(3); + expect(exceededTransferLimit).toBe(true); + } catch (error) { + console.error(error); + throw error; + } + }); + }); + + describe('exceededTransferLimit', () => { + test('should be calculated by Koop and equal true', async () => { + try { + const response = await request(koop.server).get( + '/file-geojson/rest/services/points-w-objectid/FeatureServer/0/query?resultRecordCount=2', + ); + expect(response.status).toBe(200); + const { features, exceededTransferLimit } = response.body; + expect(features.length).toBe(2); + expect(exceededTransferLimit).toBe(true); + } catch (error) { + console.error(error); + throw error; + } + }); + + test('should be calculated by Koop and equal false', async () => { + try { + const response = await request(koop.server).get( + '/file-geojson/rest/services/points-w-objectid/FeatureServer/0/query', + ); + expect(response.status).toBe(200); + const { features, exceededTransferLimit } = response.body; + expect(features.length).toBe(3); + expect(exceededTransferLimit).toBe(false); + } catch (error) { + console.error(error); + throw error; + } + }); + + test('should be acquired from provider metadata', async () => { + try { + const response = await request(koop.server).get( + '/file-geojson/rest/services/points-w-metadata-exceeded-transfer-limit/FeatureServer/0/query?resultRecordCount=2', // eslint-disable-line + ); + expect(response.status).toBe(200); + const { features, exceededTransferLimit } = response.body; + expect(features.length).toBe(2); + expect(exceededTransferLimit).toBe(true); + } catch (error) { + console.error(error); + throw error; + } + }); + + test('should be acquired from provider metadata', async () => { + try { + const response = await request(koop.server).get( + '/file-geojson/rest/services/points-w-metadata-exceeded-transfer-limit/FeatureServer/0/query', // eslint-disable-line + ); + expect(response.status).toBe(200); + const { features, exceededTransferLimit } = response.body; + expect(features.length).toBe(3); + expect(exceededTransferLimit).toBe(true); + } catch (error) { + console.error(error); + throw error; + } + }); + }); + + describe('geometry', () => { + describe('intersects', () => { + describe('searchGeometry: envelope', () => { + test('return multipolygon that intersects search envelope', async () => { try { const response = await request(koop.server).get( - '/file-geojson/rest/services/points-w-objectid/FeatureServer/0/query?objectIds=', + `/file-geojson/rest/services/multi-polygon/FeatureServer/0/query?geometry={"xmin":-130,"xmax":-110,"ymin":30,"ymax":38}&spatialRel=esriSpatialRelIntersects&geometryType=esriGeometryEnvelope&inSR=4326`, // eslint-disable-line ); expect(response.status).toBe(200); const { features } = response.body; - expect(features.length).toBe(3); + expect(features.length).toBe(1); } catch (error) { console.error(error); throw error; } }); - describe('using OBJECTID field', () => { - test('handles single value', async () => { - try { - const response = await request(koop.server).get( - '/file-geojson/rest/services/points-w-objectid/FeatureServer/0/query?objectIds=2', - ); - expect(response.status).toBe(200); - const { features } = response.body; - expect(features.length).toBe(1); - expect(features[0].attributes.OBJECTID).toBe(2); - } catch (error) { - console.error(error); - throw error; - } - }); - - test('handles delimited values', async () => { - try { - const response = await request(koop.server).get( - '/file-geojson/rest/services/points-w-objectid/FeatureServer/0/query?objectIds=2,3', - ); - expect(response.status).toBe(200); - const { features } = response.body; - expect(features.length).toBe(2); - expect(features[0].attributes.OBJECTID).toBe(2); - expect(features[1].attributes.OBJECTID).toBe(3); - } catch (error) { - console.error(error); - throw error; - } - }); - }); - - describe('using defined id field', () => { - test('handles single value', async () => { - try { - const response = await request(koop.server).get( - '/file-geojson/rest/services/points-w-metadata-id/FeatureServer/0/query?objectIds=2', - ); - expect(response.status).toBe(200); - const { features } = response.body; - expect(features.length).toBe(1); - expect(features[0].attributes.id).toBe(2); - } catch (error) { - console.error(error); - throw error; - } - }); - - test('handles delimited values', async () => { - try { - const response = await request(koop.server).get( - '/file-geojson/rest/services/points-w-metadata-id/FeatureServer/0/query?objectIds=2,3', - ); - expect(response.status).toBe(200); - const { features } = response.body; - expect(features.length).toBe(2); - expect(features[0].attributes.id).toBe(2); - expect(features[1].attributes.id).toBe(3); - } catch (error) { - console.error(error); - throw error; - } - }); - }); - - describe('without OBJECTID or idField', () => { - let objectIds; - beforeAll(async () => { - const response = await request(koop.server).get( - '/file-geojson/rest/services/points-wo-objectid/FeatureServer/0/query', - ); - objectIds = response.body.features.map((feature) => { - return feature.attributes.OBJECTID; - }); - }); - - test('handles single value', async () => { - try { - const response = await request(koop.server).get( - `/file-geojson/rest/services/points-wo-objectid/FeatureServer/0/query?objectIds=${objectIds[1]}`, - ); - expect(response.status).toBe(200); - const { features } = response.body; - expect(features.length).toBe(1); - expect(features[0].attributes.OBJECTID).toBe(objectIds[1]); - } catch (error) { - console.error(error); - throw error; - } - }); - - test('handles delimited values', async () => { - try { - const response = await request(koop.server).get( - `/file-geojson/rest/services/points-wo-objectid/FeatureServer/0/query?objectIds=${objectIds[1]},${objectIds[2]}`, - ); - expect(response.status).toBe(200); - const { features } = response.body; - expect(features.length).toBe(2); - expect(features[0].attributes.OBJECTID).toBe(objectIds[1]); - expect(features[1].attributes.OBJECTID).toBe(objectIds[2]); - } catch (error) { - console.error(error); - throw error; - } - }); - }); - }); - - describe('where', () => { - test('handle query with "+" as whitespace', async () => { + test('return 0 when search envelope fails to intersect any features', async () => { try { const response = await request(koop.server).get( - '/file-geojson/rest/services/points-w-objectid/FeatureServer/0/query?WHERE=label+is+not+null', + `/file-geojson/rest/services/multi-polygon/FeatureServer/0/query?geometry={"xmin":-10,"xmax":0,"ymin":30,"ymax":38}&spatialRel=esriSpatialRelIntersects&geometryType=esriGeometryEnvelope&inSR=4326`, // eslint-disable-line ); expect(response.status).toBe(200); const { features } = response.body; - expect(features.length).toBe(3); + expect(features.length).toBe(0); } catch (error) { console.error(error); throw error; } }); }); - - describe('resultRecordCount', () => { - test('should respect resultRecordCount applied from winnow', async () => { + describe('searchGeometry: multipolygon', () => { + test('return multipolygon that intersects search multipolygon', async () => { try { const response = await request(koop.server).get( - '/file-geojson/rest/services/points-w-objectid/FeatureServer/0/query?resultRecordCount=2', + `/file-geojson/rest/services/multi-polygon/FeatureServer/0/query?geometry=%7B%22rings%22%3A%5B%5B%5B-125,45%5D,%5B-120,45%5D,%5B-120,40%5D,%5B-125,40%5D,%5B-125,45%5D%5D,%5B%5B-116,37%5D,%5B-114,37%5D,%5B-114,36%5D,%5B-116,36%5D,%5B-116,37%5D%5D%5D%7D&geometryType=esriGeometryPolygon`, // eslint-disable-line ); expect(response.status).toBe(200); const { features } = response.body; - expect(features.length).toBe(2); + expect(features.length).toBe(1); } catch (error) { console.error(error); throw error; } }); - test('should respect resultRecordCount applied from passthrough provider', async () => { + test('return 0 when search envelope fails to intersect any features', async () => { try { const response = await request(koop.server).get( - '/file-geojson/rest/services/pass-through/FeatureServer/0/query?resultRecordCount=3', + `/file-geojson/rest/services/multi-polygon/FeatureServer/0/query?geometry={"xmin":-10,"xmax":0,"ymin":30,"ymax":38}&spatialRel=esriSpatialRelIntersects&geometryType=esriGeometryEnvelope&inSR=4326`, // eslint-disable-line ); expect(response.status).toBe(200); - const { features, exceededTransferLimit } = response.body; - expect(features.length).toBe(3); - expect(exceededTransferLimit).toBe(true); + const { features } = response.body; + expect(features.length).toBe(0); } catch (error) { console.error(error); throw error; } }); }); + }); - describe('exceededTransferLimit', () => { - test('should be calculated by Koop and equal true', async () => { + describe('intersects', () => { + describe('searchGeometry: envelope', () => { + test('return multipolygon that intersects search envelope', async () => { try { const response = await request(koop.server).get( - '/file-geojson/rest/services/points-w-objectid/FeatureServer/0/query?resultRecordCount=2', + `/file-geojson/rest/services/multi-polygon/FeatureServer/0/query?geometry={"xmin":-130,"xmax":-110,"ymin":30,"ymax":38}&spatialRel=esriSpatialRelIntersects&geometryType=esriGeometryEnvelope&inSR=4326`, // eslint-disable-line ); expect(response.status).toBe(200); - const { features, exceededTransferLimit } = response.body; - expect(features.length).toBe(2); - expect(exceededTransferLimit).toBe(true); + const { features } = response.body; + expect(features.length).toBe(1); } catch (error) { console.error(error); throw error; } }); - test('should be calculated by Koop and equal false', async () => { + test('return 0 when search envelope fails to intersect any features', async () => { try { const response = await request(koop.server).get( - '/file-geojson/rest/services/points-w-objectid/FeatureServer/0/query', + `/file-geojson/rest/services/multi-polygon/FeatureServer/0/query?geometry={"xmin":-10,"xmax":0,"ymin":30,"ymax":38}&spatialRel=esriSpatialRelIntersects&geometryType=esriGeometryEnvelope&inSR=4326`, // eslint-disable-line ); expect(response.status).toBe(200); - const { features, exceededTransferLimit } = response.body; - expect(features.length).toBe(3); - expect(exceededTransferLimit).toBe(false); + const { features } = response.body; + expect(features.length).toBe(0); } catch (error) { console.error(error); throw error; } }); - - test('should be acquired from provider metadata', async () => { + }); + describe('searchGeometry: multipolygon', () => { + test('return multipolygon that intersects search multipolygon', async () => { try { const response = await request(koop.server).get( - '/file-geojson/rest/services/points-w-metadata-exceeded-transfer-limit/FeatureServer/0/query?resultRecordCount=2', + `/file-geojson/rest/services/multi-polygon/FeatureServer/0/query?geometry=%7B%22rings%22%3A%5B%5B%5B-125,45%5D,%5B-120,45%5D,%5B-120,40%5D,%5B-125,40%5D,%5B-125,45%5D%5D,%5B%5B-116,37%5D,%5B-114,37%5D,%5B-114,36%5D,%5B-116,36%5D,%5B-116,37%5D%5D%5D%7D&geometryType=esriGeometryPolygon`, // eslint-disable-line ); expect(response.status).toBe(200); - const { features, exceededTransferLimit } = response.body; - expect(features.length).toBe(2); - expect(exceededTransferLimit).toBe(true); + const { features } = response.body; + expect(features.length).toBe(1); } catch (error) { console.error(error); throw error; } }); - test('should be acquired from provider metadata', async () => { + test('return 0 when search envelope fails to intersect any features', async () => { try { const response = await request(koop.server).get( - '/file-geojson/rest/services/points-w-metadata-exceeded-transfer-limit/FeatureServer/0/query', + `/file-geojson/rest/services/multi-polygon/FeatureServer/0/query?geometry={"xmin":-10,"xmax":0,"ymin":30,"ymax":38}&spatialRel=esriSpatialRelIntersects&geometryType=esriGeometryEnvelope&inSR=4326`, // eslint-disable-line ); expect(response.status).toBe(200); - const { features, exceededTransferLimit } = response.body; - expect(features.length).toBe(3); - expect(exceededTransferLimit).toBe(true); + const { features } = response.body; + expect(features.length).toBe(0); } catch (error) { console.error(error); throw error; @@ -249,5 +369,124 @@ describe('koop', () => { }); }); }); + + describe('intersects', () => { + test('return multipolygon that intersects search envelope', async () => { + try { + const response = await request(koop.server).get( + `/file-geojson/rest/services/multi-polygon/FeatureServer/0/query?geometry={"xmin":-130,"xmax":-110,"ymin":30,"ymax":38}&spatialRel=esriSpatialRelIntersects&geometryType=esriGeometryEnvelope&inSR=4326`, // eslint-disable-line + ); + expect(response.status).toBe(200); + const { features } = response.body; + expect(features.length).toBe(1); + } catch (error) { + console.error(error); + throw error; + } + }); + + test('return 0 when search envelope fails to intersect any features', async () => { + try { + const response = await request(koop.server).get( + `/file-geojson/rest/services/multi-polygon/FeatureServer/0/query?geometry={"xmin":-10,"xmax":0,"ymin":30,"ymax":38}&spatialRel=esriSpatialRelIntersects&geometryType=esriGeometryEnvelope&inSR=4326`, // eslint-disable-line + ); + expect(response.status).toBe(200); + const { features } = response.body; + expect(features.length).toBe(0); + } catch (error) { + console.error(error); + throw error; + } + }); + }); + + describe('envelope-intersects', () => { + test('return features if intersects the envelope of the search geometry', async () => { + try { + const response = await request(koop.server).get( + `/file-geojson/rest/services/diagonal-feature/FeatureServer/0/query?geometry={"paths":[[[-96,53.4],[-93.9,53.4]]],"spatialReference":{"wkid":4326}}&geometryType=esriGeometryLine&spatialRel=esriSpatialRelEnvelopeIntersects`, // eslint-disable-line + ); + expect(response.status).toBe(200); + const { features } = response.body; + expect(features.length).toBe(1); + } catch (error) { + console.error(error); + throw error; + } + }); + test('return 0 features if none intersect the envelope of the search geometry', async () => { + try { + const response = await request(koop.server).get( + `/file-geojson/rest/services/diagonal-feature/FeatureServer/0/query?geometry={"paths":[[[-96,53.4],[-94.9,53.4]]],"spatialReference":{"wkid":4326}}&geometryType=esriGeometryLine&spatialRel=esriSpatialRelEnvelopeIntersects`, // eslint-disable-line + ); + expect(response.status).toBe(200); + const { features } = response.body; + expect(features.length).toBe(0); + } catch (error) { + console.error(error); + throw error; + } + }); + }); + + describe('contains', () => { + test('return multipolygon contained in search multipolygon', async () => { + try { + const response = await request(koop.server).get( + `/file-geojson/rest/services/multi-polygon/FeatureServer/0/query?geometry=%7B%22rings%22%3A%5B%5B%5B-125,45%5D,%5B-120,45%5D,%5B-120,40%5D,%5B-125,40%5D,%5B-125,45%5D%5D,%5B%5B-116,37%5D,%5B-114,37%5D,%5B-114,36%5D,%5B-116,36%5D,%5B-116,37%5D%5D%5D%7D&geometryType=esriGeometryPolygon&spatialRel=esriSpatialRelContains`, // eslint-disable-line + ); + expect(response.status).toBe(200); + const { features } = response.body; + expect(features.length).toBe(1); + } catch (error) { + console.error(error); + throw error; + } + }); + + test('return 0 when search envelope fails to contain any features', async () => { + try { + const response = await request(koop.server).get( + `/file-geojson/rest/services/multi-polygon/FeatureServer/0/query?geometry={"xmin":-10,"xmax":0,"ymin":30,"ymax":38}&geometryType=esriGeometryEnvelope&inSR=4326&spatialRel=esriSpatialRelContains`, // eslint-disable-line + ); + expect(response.status).toBe(200); + const { features } = response.body; + expect(features.length).toBe(0); + } catch (error) { + console.error(error); + throw error; + } + }); + }); + + describe('within', () => { + test('return features where search geometry is within feature', async () => { + try { + const response = await request(koop.server).get( + `/file-geojson/rest/services/multi-polygon/FeatureServer/0/query?geometry=%7B%22rings%22%3A%5B%5B%5B-122,44%5D,%5B-121,44%5D,%5B-121,41%5D,%5B-122,41%5D,%5B-125,44%5D%5D%5D%7D&geometryType=esriGeometryPolygon&spatialRel=esriSpatialRelWithin`, // eslint-disable-line + ); + expect(response.status).toBe(200); + const { features } = response.body; + expect(features.length).toBe(1); + } catch (error) { + console.error(error); + throw error; + } + }); + + test('return 0 when search geometry is not within any features', async () => { + try { + const response = await request(koop.server).get( + `/file-geojson/rest/services/multi-polygon/FeatureServer/0/query?geometry=%7B%22rings%22%3A%5B%5B%5B-12,44%5D,%5B-11,44%5D,%5B-11,41%5D,%5B-12,41%5D,%5B-12,44%5D%5D%5D%7D&geometryType=esriGeometryPolygon&spatialRel=esriSpatialRelWithin`, // eslint-disable-line + ); + expect(response.status).toBe(200); + const { features } = response.body; + expect(features.length).toBe(0); + } catch (error) { + console.error(error); + throw error; + } + }); + }); }); }); diff --git a/test/provider-data/diagonal-feature.geojson b/test/provider-data/diagonal-feature.geojson new file mode 100644 index 000000000..dfc607044 --- /dev/null +++ b/test/provider-data/diagonal-feature.geojson @@ -0,0 +1,22 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "coordinates": [ + [ + -95, + 53.2 + ], + [ + -94, + 53.54 + ] + ], + "type": "LineString" + } + } + ] +} \ No newline at end of file diff --git a/test/provider-data/multi-polygon.geojson b/test/provider-data/multi-polygon.geojson new file mode 100644 index 000000000..b8d8860fb --- /dev/null +++ b/test/provider-data/multi-polygon.geojson @@ -0,0 +1,31 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "coordinates": [ + [ + [ + [-125, 45], + [-125, 40], + [-120, 40], + [-120, 45], + [-125, 45] + ]], + [ + [ + [-116, 37], + [-116, 36], + [-114, 36], + [-114, 37], + [-116, 37] + ] + ] + ], + "type": "MultiPolygon" + } + } + ] +}