diff --git a/packages/turf-clean-coords/README.md b/packages/turf-clean-coords/README.md index 4552217d6d..79ffc6fd66 100644 --- a/packages/turf-clean-coords/README.md +++ b/packages/turf-clean-coords/README.md @@ -1,21 +1,21 @@ # @turf/clean-coords - - -## cleanCoords - -Removes redundant coordinates from any GeoJSON Geometry. - -### Parameters - -* `geojson` **([Geometry][1] | [Feature][2])** Feature or Geometry -* `options` **[Object][3]** Optional parameters (optional, default `{}`) - - * `options.mutate` **[boolean][4]** allows GeoJSON input to be mutated (optional, default `false`) - -### Examples - -```javascript +## CleanCoordsResultGeneric response type for cleanCoordsType: any## cleanCoords### Parameters* `geojson` **any** +* `options` **{mutate: [boolean][1]?, epsilon: [number][2]?}?** Returns **any** **Meta*** **deprecated**: loosely typed version deprecated. Will be removed in next major version## cleanCoordsRemoves redundant coordinates from any GeoJSON Type.Always returns the same geojson type that it receives.When operating on MultiPoint geometries will remove co-incident points.Obeys consistent structural sharing rules based on the `mutate` option:* Feature and Geometry (nested and top-level) objects will *always* be new + when mutate is false (default) and *always* return the provided object + when mutate is true (i.e., ===). +* bbox, id, and properties members on Features, and bbox on Geometries and + FeatureCollections will *always* be === to the original. Note that this + does not copy across other forrign members when mutate is false. +* Members other than the geometry(ies) or features members will always be + \=== to the original. +* Geometry(ies) and features members and their nested arrays will be reused + from the passed object or generated new based on performance and + simplicity in the implementation - and therefore will not be deep clones.### Parameters* `geojson` **[GeoJSON][3]** Any valid GeoJSON type +* `options` **[Object][4]** Optional parameters (optional, default `{}`) + + * `options.mutate` **[boolean][1]** allows GeoJSON input to be mutated (optional, default `false`) + * `options.epsilon` **[number][2]?** Fractional number to compare with the cross product result. Useful for dealing with floating points such as lng/lat points### Examples```javascript var line = turf.lineString([[0, 0], [0, 2], [0, 5], [0, 8], [0, 8], [0, 10]]); var multiPoint = turf.multiPoint([[0, 0], [0, 0], [2, 2]]); @@ -24,17 +24,7 @@ turf.cleanCoords(line).geometry.coordinates; turf.cleanCoords(multiPoint).geometry.coordinates; //= [[0, 0], [2, 2]] -``` - -Returns **([Geometry][1] | [Feature][2])** the cleaned input Feature/Geometry - -[1]: https://tools.ietf.org/html/rfc7946#section-3.1 - -[2]: https://tools.ietf.org/html/rfc7946#section-3.2 - -[3]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object - -[4]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean +```Returns **([Geometry][5] | [Feature][6])** the cleaned input Feature/Geometry[1]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean[2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number[3]: https://tools.ietf.org/html/rfc7946#section-3[4]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object[5]: https://tools.ietf.org/html/rfc7946#section-3.1[6]: https://tools.ietf.org/html/rfc7946#section-3.2 diff --git a/packages/turf-clean-coords/bench.ts b/packages/turf-clean-coords/bench.ts index b567b4755a..24d714d0a6 100644 --- a/packages/turf-clean-coords/bench.ts +++ b/packages/turf-clean-coords/bench.ts @@ -3,6 +3,7 @@ import { fileURLToPath } from "url"; import { glob } from "glob"; import { loadJsonFileSync } from "load-json-file"; import Benchmark from "benchmark"; +import { GeoJSON } from "geojson"; import { cleanCoords } from "./index.js"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -10,70 +11,55 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); /** * Benchmark Results * - * geometry: 0.675ms - * multiline: 0.044ms - * multipoint: 0.291ms - * multipolygon: 0.062ms - * point: 0.010ms - * polygon-with-hole: 0.017ms - * polygon: 0.010ms - * simple-line: 0.008ms - * triangle: 0.020ms + * Note - the original author benchmarked alternative methods of avoiding + * mutation and concluded that @turf/clone and JSON.parse were ~10x slower than + * direct use of geometry/features. * - * // mutate=false (using geometry/feature) - * geometry x 1,524,640 ops/sec ±4.60% (74 runs sampled) - * multiline x 1,511,608 ops/sec ±8.79% (72 runs sampled) - * multipoint x 382,429 ops/sec ±3.56% (84 runs sampled) - * multipolygon x 808,277 ops/sec ±2.84% (82 runs sampled) - * point x 14,675,464 ops/sec ±4.42% (80 runs sampled) - * polygon-with-hole x 1,493,507 ops/sec ±5.53% (72 runs sampled) - * polygon x 2,386,278 ops/sec ±1.27% (86 runs sampled) - * simple-line x 4,195,499 ops/sec ±2.88% (86 runs sampled) - * triangle x 2,254,753 ops/sec ±1.10% (88 runs sampled) + * // mutate=false + * triplicate-issue1255 x 3,062,616 ops/sec ±0.39% (94 runs sampled) + * triangle x 2,082,533 ops/sec ±0.34% (96 runs sampled) + * simple-line x 3,779,713 ops/sec ±0.40% (95 runs sampled) + * segment x 6,582,228 ops/sec ±1.30% (82 runs sampled) + * polygon x 2,585,401 ops/sec ±0.25% (99 runs sampled) + * polygon-with-hole x 1,518,417 ops/sec ±0.34% (93 runs sampled) + * point x 26,543,371 ops/sec ±0.88% (89 runs sampled) + * multipolygon x 974,694 ops/sec ±1.27% (97 runs sampled) + * multipoint x 2,537,537 ops/sec ±0.56% (95 runs sampled) + * multiline x 1,974,913 ops/sec ±0.39% (93 runs sampled) + * line-3-coords x 6,701,852 ops/sec ±0.54% (97 runs sampled) + * geometry x 1,582,544 ops/sec ±1.38% (93 runs sampled) + * geometry-collection x 1,015,123 ops/sec ±0.30% (98 runs sampled) + * feature-collection x 945,614 ops/sec ±0.23% (95 runs sampled) + * closed-linestring x 8,822,010 ops/sec ±0.89% (93 runs sampled) + * clean-segment x 18,363,540 ops/sec ±0.68% (94 runs sampled) * - * // mutate=false (using @turf/clone) - * geometry x 202,410 ops/sec ±1.43% (88 runs sampled) - * multiline x 235,421 ops/sec ±3.48% (86 runs sampled) - * multipoint x 280,757 ops/sec ±1.59% (87 runs sampled) - * multipolygon x 127,353 ops/sec ±1.35% (88 runs sampled) - * point x 18,233,987 ops/sec ±1.34% (86 runs sampled) - * polygon-with-hole x 199,203 ops/sec ±2.61% (84 runs sampled) - * polygon x 355,616 ops/sec ±1.58% (86 runs sampled) - * simple-line x 515,430 ops/sec ±2.40% (83 runs sampled) - * triangle x 286,315 ops/sec ±1.64% (86 runs sampled) - * - * // mutate=false (using JSON.parse + JSON.stringify) - * geometry x 93,681 ops/sec ±7.66% (75 runs sampled) - * multiline x 112,836 ops/sec ±4.60% (82 runs sampled) - * multipoint x 113,937 ops/sec ±1.09% (90 runs sampled) - * multipolygon x 71,131 ops/sec ±1.32% (90 runs sampled) - * point x 18,181,612 ops/sec ±1.36% (91 runs sampled) - * polygon-with-hole x 100,011 ops/sec ±1.14% (85 runs sampled) - * polygon x 154,331 ops/sec ±1.31% (89 runs sampled) - * simple-line x 193,304 ops/sec ±1.33% (90 runs sampled) - * triangle x 130,921 ops/sec ±3.37% (87 runs sampled) * * // mutate=true - * geometry x 2,016,365 ops/sec ±1.83% (85 runs sampled) - * multiline x 2,266,787 ops/sec ±3.69% (79 runs sampled) - * multipoint x 411,246 ops/sec ±0.81% (89 runs sampled) - * multipolygon x 1,011,846 ops/sec ±1.34% (85 runs sampled) - * point x 17,635,961 ops/sec ±1.47% (89 runs sampled) - * polygon-with-hole x 2,110,166 ops/sec ±1.59% (89 runs sampled) - * polygon x 2,887,298 ops/sec ±1.75% (86 runs sampled) - * simple-line x 7,109,682 ops/sec ±1.52% (87 runs sampled) - * triangle x 3,116,940 ops/sec ±0.71% (87 runs sampled) + * triplicate-issue1255 x 5,643,789 ops/sec ±0.45% (95 runs sampled) + * triangle x 6,429,094 ops/sec ±0.47% (95 runs sampled) + * simple-line x 22,923,304 ops/sec ±0.84% (90 runs sampled) + * segment x 22,916,879 ops/sec ±0.86% (92 runs sampled) + * polygon x 4,867,685 ops/sec ±0.55% (92 runs sampled) + * polygon-with-hole x 2,746,040 ops/sec ±0.69% (95 runs sampled) + * point x 32,975,988 ops/sec ±1.20% (92 runs sampled) + * multipolygon x 2,022,668 ops/sec ±0.47% (96 runs sampled) + * multipoint x 2,549,962 ops/sec ±1.48% (92 runs sampled) + * multiline x 19,285,512 ops/sec ±1.20% (91 runs sampled) + * line-3-coords x 22,508,235 ops/sec ±1.52% (90 runs sampled) + * geometry x 2,925,115 ops/sec ±0.35% (96 runs sampled) + * geometry-collection x 2,052,061 ops/sec ±0.60% (96 runs sampled) + * feature-collection x 1,914,939 ops/sec ±0.33% (97 runs sampled) + * closed-linestring x 10,339,559 ops/sec ±0.56% (93 runs sampled) + * clean-segment x 22,354,825 ops/sec ±0.84% (95 runs sampled) */ const suite = new Benchmark.Suite("turf-clean-coords"); glob .sync(path.join(__dirname, "test", "in", "*.geojson")) .forEach((filepath) => { const { name } = path.parse(filepath); - const geojson = loadJsonFileSync(filepath); - console.time(name); - cleanCoords(geojson); - console.timeEnd(name); - suite.add(name, () => cleanCoords(geojson, true)); + const geojson = loadJsonFileSync(filepath) as GeoJSON; + suite.add(name, () => cleanCoords(geojson)); + //suite.add(name, () => cleanCoords(geojson, { mutate: true })); }); -suite.on("cycle", (e) => console.log(String(e.target))).run(); +suite.on("cycle", (e: any) => console.log(String(e.target))).run(); diff --git a/packages/turf-clean-coords/index.ts b/packages/turf-clean-coords/index.ts index 9c8660252c..9fd91bb271 100644 --- a/packages/turf-clean-coords/index.ts +++ b/packages/turf-clean-coords/index.ts @@ -1,18 +1,79 @@ -import { Position } from "geojson"; -import { feature } from "@turf/helpers"; -import { getCoords, getType } from "@turf/invariant"; +import { + GeoJSON, + Feature, + Geometry, + FeatureCollection, + Position, + GeoJsonProperties, + GeometryCollection, + MultiPoint, +} from "geojson"; +import { feature, featureCollection } from "@turf/helpers"; import { booleanPointOnLine } from "@turf/boolean-point-on-line"; import { lineString } from "@turf/helpers"; -// To-Do => Improve Typescript GeoJSON handling +/** Generic response type for cleanCoords */ +type CleanCoordsResult = + T extends Feature + ? Feature + : T extends Geometry + ? T + : GeoJSON; +function cleanCoords( + geojson: FeatureCollection, + options?: { mutate?: boolean; epsilon?: number } +): FeatureCollection; +function cleanCoords( + geojson: Feature, + options?: { mutate?: boolean; epsilon?: number } +): Feature; +function cleanCoords( + geojson: GeometryCollection, + options?: { mutate?: boolean; epsilon?: number } +): GeometryCollection; +function cleanCoords( + geojson: G, + options?: { mutate?: boolean; epsilon?: number } +): G; +function cleanCoords( + geojson: T, + options?: { mutate?: boolean; epsilon?: number } +): CleanCoordsResult; +function cleanCoords( + geojson: GeoJSON, + options?: { mutate?: boolean; epsilon?: number } +): GeoJSON; +/** @deprecated loosely typed version deprecated. Will be removed in next major version */ +function cleanCoords( + geojson: any, + options?: { mutate?: boolean; epsilon?: number } +): any; /** - * Removes redundant coordinates from any GeoJSON Geometry. + * Removes redundant coordinates from any GeoJSON Type. + * + * Always returns the same geojson type that it receives. + * + * When operating on MultiPoint geometries will remove co-incident points. + * + * Obeys consistent structural sharing rules based on the `mutate` option: + * - Feature and Geometry (nested and top-level) objects will *always* be new + * when mutate is false (default) and *always* return the provided object + * when mutate is true (i.e., ===). + * - bbox, id, and properties members on Features, and bbox on Geometries and + * FeatureCollections will *always* be === to the original. Note that this + * does not copy across other forrign members when mutate is false. + * - Members other than the geometry(ies) or features members will always be + * === to the original. + * - Geometry(ies) and features members and their nested arrays will be reused + * from the passed object or generated new based on performance and + * simplicity in the implementation - and therefore will not be deep clones. * * @function - * @param {Geometry|Feature} geojson Feature or Geometry + * @param {GeoJSON} geojson Any valid GeoJSON type * @param {Object} [options={}] Optional parameters * @param {boolean} [options.mutate=false] allows GeoJSON input to be mutated + * @param {number} [options.epsilon] Fractional number to compare with the cross product result. Useful for dealing with floating points such as lng/lat points * @returns {Geometry|Feature} the cleaned input Feature/Geometry * @example * var line = turf.lineString([[0, 0], [0, 2], [0, 5], [0, 8], [0, 8], [0, 10]]); @@ -25,87 +86,123 @@ import { lineString } from "@turf/helpers"; * //= [[0, 0], [2, 2]] */ function cleanCoords( - geojson: any, + geojson: GeoJSON, options: { mutate?: boolean; + epsilon?: number; } = {} -) { +): GeoJSON { // Backwards compatible with v4.0 - var mutate = typeof options === "object" ? options.mutate : options; + const mutate = + (typeof options === "object" ? options.mutate : options) ?? false; if (!geojson) throw new Error("geojson is required"); - var type = getType(geojson); // Store new "clean" points in this Array - var newCoords = []; + let coordinates: any; + switch (geojson.type) { + case "FeatureCollection": + coordinates = mutate ? geojson.features : []; + for (let i = 0; i < geojson.features.length; i++) { + coordinates[i] = cleanCoords(geojson.features[i], options); + } + if (mutate) { + geojson.features = coordinates; + return geojson; + } + return featureCollection(coordinates, { bbox: geojson.bbox }); + + case "GeometryCollection": + coordinates = mutate ? geojson.geometries : []; + for (let i = 0; i < geojson.geometries.length; i++) { + coordinates[i] = cleanCoords(geojson.geometries[i], options); + } + if (mutate) { + geojson.geometries = coordinates; + return geojson; + } + return { + type: "GeometryCollection", + geometries: coordinates, + }; + + case "Feature": + coordinates = cleanCoords(geojson.geometry, options); + if (mutate) { + geojson.geometry = coordinates; + return geojson; + } + return feature(coordinates, geojson.properties, { + bbox: geojson.bbox, + id: geojson.id, + }); + + case "Point": + coordinates = geojson.coordinates; + break; + + case "MultiPoint": + // MultiPoint need to check exact equality for all points + coordinates = multipointDeduplicate(geojson); + break; - switch (type) { case "LineString": - newCoords = cleanLine(geojson, type); + coordinates = cleanLine(geojson.coordinates, options.epsilon); break; + case "MultiLineString": + coordinates = mutate ? geojson.coordinates : []; + for (let i = 0; i < geojson.coordinates.length; i++) { + coordinates[i] = cleanLine(geojson.coordinates[i], options.epsilon); + } + break; + case "Polygon": - getCoords(geojson).forEach(function (line) { - newCoords.push(cleanLine(line, type)); - }); + coordinates = cleanRings(geojson.coordinates, mutate, options.epsilon); break; + case "MultiPolygon": - getCoords(geojson).forEach(function (polygons: any) { - var polyPoints: Position[] = []; - polygons.forEach(function (ring: Position[]) { - polyPoints.push(cleanLine(ring, type)); - }); - newCoords.push(polyPoints); - }); - break; - case "Point": - return geojson; - case "MultiPoint": - var existing: Record = {}; - getCoords(geojson).forEach(function (coord: any) { - var key = coord.join("-"); - if (!Object.prototype.hasOwnProperty.call(existing, key)) { - newCoords.push(coord); - existing[key] = true; - } - }); + coordinates = mutate ? geojson.coordinates : []; + for (let i = 0; i < geojson.coordinates.length; i++) { + coordinates[i] = cleanRings( + geojson.coordinates[i], + mutate, + options.epsilon + ); + } break; + default: - throw new Error(type + " geometry not supported"); + // Defensive: throw if there is no type + throw new Error( + `type "${(geojson as any).type}" is not a valid GeoJSON type` + ); } - // Support input mutation - if (geojson.coordinates) { - if (mutate === true) { - geojson.coordinates = newCoords; - return geojson; - } - return { type: type, coordinates: newCoords }; - } else { - if (mutate === true) { - geojson.geometry.coordinates = newCoords; - return geojson; - } - return feature({ type: type, coordinates: newCoords }, geojson.properties, { - bbox: geojson.bbox, - id: geojson.id, - }); + // All others have returned, so here we just need to process geometries + if (mutate) { + geojson.coordinates = coordinates; + return geojson; } + const geometry: Geometry = { type: geojson.type, coordinates }; + // Add bbox iff it exists on the incoming + if (geojson.bbox) geometry.bbox = geojson?.bbox; + return { type: geojson.type, coordinates }; } /** - * Clean Coords + * Clean Line * * @private * @param {Array|LineString} line Line - * @param {string} type Type of geometry - * @returns {Array} Cleaned coordinates + * @param {number} epsilon Cross-product tolerance + * @returns {Position} Cleaned coordinates */ -function cleanLine(line: Position[], type: string) { - const points = getCoords(line); +function cleanLine(line: Position[], epsilon?: number): Position[] { // handle "clean" segment - if (points.length === 2 && !equals(points[0], points[1])) return points; + if (line.length === 2) return line; - const newPoints = []; + // Do not require a new array until we know we need it + let result: null | Position[] = null; // Segments based approach. With initial segment a-b, keep comparing to a // longer segment a-c and as long as b is still on a-c, b is a redundant @@ -114,32 +211,62 @@ function cleanLine(line: Position[], type: string) { b = 1, c = 2; - // Guaranteed we'll use the first point. - newPoints.push(points[a]); // While there is still room to extend the segment ... - while (c < points.length) { - if (booleanPointOnLine(points[b], lineString([points[a], points[c]]))) { + while (c < line.length) { + if ( + booleanPointOnLine(line[b], lineString([line[a], line[c]]), { epsilon }) + ) { + // When we land here and are yet to have a result array, we know we need + // one, so create with the slice up to a (as b is being discarded) + if (!result) result = line.slice(0, a + 1); + // b is on a-c, so we can discard point b, and extend a-b to be the same // as a-c as the basis for comparison during the next iteration. b = c; } else { // b is NOT on a-c, suggesting a-c is not an extension of a-b. Commit a-b // as a necessary segment. - newPoints.push(points[b]); + result?.push(line[b]); // Make our a-b for the next iteration start from the end of the segment // that was just locked in i.e. next a-b should be the current b-(b+1). a = b; b++; - c = b; } // Plan to look at the next point during the next iteration. c++; } - // No remaining points, so commit the current a-b segment. - newPoints.push(points[b]); - if (type === "Polygon" || type === "MultiPolygon") { + // No remaining points, so commit the endpoint + result?.push(line[b]); + + // If here and result is still null, then structurally share the segment + return result ?? line; +} + +/** + * Clean Rings + * + * @private + * @param {Array|LineString} rings Input rings + * @param {boolean} mutate Mutate passed rings + * @param {number} epsilon Cross-product tolerance + * @returns {Position} Cleaned coordinates + */ +function cleanRings( + rings: Position[][], + mutate: boolean, + epsilon?: number +): Position[][] { + // Re-use the polygon's rings array when mutating to maximize sharing + // It would be possible to follow a similar approach to cleanLine and only + // create a new raings array when we know something has changed even for + // mutate === false, but the extra complexity is not worth it + const outRings = mutate ? rings : []; + for (let i = 0; i < rings.length; i++) { + const ring = rings[i]; + let cleaned = cleanLine(ring, epsilon); + // For polygons need to make sure the start / end point wasn't one of the // points that needed to be cleaned. // https://github.com/Turfjs/turf/issues/2406 @@ -148,25 +275,33 @@ function cleanLine(line: Position[], type: string) { // [b, c, ..., z, b] if ( booleanPointOnLine( - newPoints[0], - lineString([newPoints[1], newPoints[newPoints.length - 2]]) + cleaned[0], + lineString([cleaned[1], cleaned[cleaned.length - 2]]), + { epsilon } ) ) { - newPoints.shift(); // Discard starting point. - newPoints.pop(); // Discard closing point. - newPoints.push(newPoints[0]); // Duplicate the new closing point to end of array. + // We are about to mutate the cleaned ring, but if no other changes were + // made to the ring, it will still be === to the original ring[i]. + // Therefore, if not mutating, we clone the ring iff required. + if (!mutate && cleaned === ring) cleaned = [...cleaned]; + + cleaned.shift(); // Discard starting point. + cleaned.pop(); // Discard closing point. + cleaned.push(cleaned[0]); // Duplicate the new closing point to end of array. } // (Multi)Polygons must have at least 4 points and be closed. - if (newPoints.length < 4) { + if (cleaned.length < 4) { throw new Error("invalid polygon, fewer than 4 points"); } - if (!equals(newPoints[0], newPoints[newPoints.length - 1])) { + if (!equals(cleaned[0], cleaned[cleaned.length - 1])) { throw new Error("invalid polygon, first and last points not equal"); } + + outRings[i] = cleaned; } - return newPoints; + return outRings; } /** @@ -177,9 +312,28 @@ function cleanLine(line: Position[], type: string) { * @param {Position} pt2 point * @returns {boolean} true if they are equals */ -function equals(pt1: Position, pt2: Position) { +function equals(pt1: Position, pt2: Position): boolean { return pt1[0] === pt2[0] && pt1[1] === pt2[1]; } -export { cleanCoords }; +/** + * Eliminate co-incident points in a MultiPoint geometry. + * + * @private + * @param {MultiPoint} geom Input geometry + * @returns {Position[]} Deduplicated coordinates + */ +function multipointDeduplicate(geom: MultiPoint): Position[] { + const seen = new Map(); + for (const point of geom.coordinates) { + seen.set(point.join("-"), point); + } + if (seen.size === geom.coordinates.length) { + return geom.coordinates; + } else { + return [...seen.values()]; + } +} + +export { cleanCoords, CleanCoordsResult }; export default cleanCoords; diff --git a/packages/turf-clean-coords/package.json b/packages/turf-clean-coords/package.json index 9ddfbeef7b..d56952e39a 100644 --- a/packages/turf-clean-coords/package.json +++ b/packages/turf-clean-coords/package.json @@ -5,7 +5,8 @@ "author": "Turf Authors", "contributors": [ "Stefano Borghi <@stebogit>", - "James Beard <@smallsaucepan>" + "James Beard <@smallsaucepan>", + "Brendan Ratter <@bratter>" ], "license": "MIT", "bugs": { diff --git a/packages/turf-clean-coords/test.ts b/packages/turf-clean-coords/test.ts index 8bd99215f9..c4f2670ffe 100644 --- a/packages/turf-clean-coords/test.ts +++ b/packages/turf-clean-coords/test.ts @@ -1,6 +1,7 @@ import fs from "fs"; import test from "tape"; import path from "path"; +import { GeoJSON } from "geojson"; import { geojsonEquality } from "geojson-equality-ts"; import { fileURLToPath } from "url"; import { loadJsonFileSync } from "load-json-file"; @@ -34,7 +35,7 @@ test("turf-clean-coords", (t) => { fixtures.forEach((fixture) => { const filename = fixture.filename; const name = fixture.name; - const geojson = fixture.geojson; + const geojson = fixture.geojson as any as GeoJSON; const results = cleanCoords(geojson); if (process.env.REGEN) @@ -108,7 +109,11 @@ test("turf-clean-coords -- truncate", (t) => { }); test("turf-clean-coords -- throws", (t) => { - t.throws(() => cleanCoords(null), /geojson is required/, "missing geojson"); + t.throws( + () => cleanCoords(null as any), + /geojson is required/, + "missing geojson" + ); t.end(); }); @@ -318,3 +323,29 @@ test("turf-clean-coords - multipolygon - issue #918", (t) => { t.end(); }); + +test("turf-clean-coords - degenerate linestring", (t) => { + const ls = lineString([ + [1, 1], + [1, 1], + ]); + const clean = cleanCoords(ls); + + t.true(geojsonEquality(clean, ls), "degenerate linestring is valid"); + t.end(); +}); + +test("turf-clean-coords - epsilon passed to booleanPointOnLine", (t) => { + const ls = lineString([ + [1, 1], + [1, 1.5], + [1.000001, 2], + ]); + const clean = cleanCoords(ls); + const cleanEpsilon = cleanCoords(ls, { epsilon: 0.001 }); + + // When passing epsilon through correctly, this case will only have two points in the output + t.equals(clean.geometry.coordinates.length, 3); + t.equals(cleanEpsilon.geometry.coordinates.length, 2); + t.end(); +}); diff --git a/packages/turf-clean-coords/test/in/feature-collection.geojson b/packages/turf-clean-coords/test/in/feature-collection.geojson new file mode 100644 index 0000000000..47df10dca0 --- /dev/null +++ b/packages/turf-clean-coords/test/in/feature-collection.geojson @@ -0,0 +1,49 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [0, 0], + [5, 0], + [5, 0], + [10, 0], + [10, 10], + [0, 10], + [0, 5], + [0, 0] + ], + [ + [1, 5], + [1, 7], + [1, 8.5], + [4.5, 8.5], + [4.5, 7], + [4.5, 5], + [1, 5] + ] + ] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [11, 11], + [11, 11], + [11.5, 11], + [12, 11], + [12, 12], + [11.5, 11.5], + [11, 11] + ] + } + } + ] +} diff --git a/packages/turf-clean-coords/test/in/geometry-collection.geojson b/packages/turf-clean-coords/test/in/geometry-collection.geojson new file mode 100644 index 0000000000..80c60606a9 --- /dev/null +++ b/packages/turf-clean-coords/test/in/geometry-collection.geojson @@ -0,0 +1,41 @@ +{ + "type": "GeometryCollection", + "geometries": [ + { + "type": "Polygon", + "coordinates": [ + [ + [0, 0], + [5, 0], + [5, 0], + [10, 0], + [10, 10], + [0, 10], + [0, 5], + [0, 0] + ], + [ + [1, 5], + [1, 7], + [1, 8.5], + [4.5, 8.5], + [4.5, 7], + [4.5, 5], + [1, 5] + ] + ] + }, + { + "type": "LineString", + "coordinates": [ + [11, 11], + [11, 11], + [11.5, 11], + [12, 11], + [12, 12], + [11.5, 11.5], + [11, 11] + ] + } + ] +} diff --git a/packages/turf-clean-coords/test/in/multipolygon.geojson b/packages/turf-clean-coords/test/in/multipolygon.geojson index dd1fce7b77..5d6f7b6b0b 100644 --- a/packages/turf-clean-coords/test/in/multipolygon.geojson +++ b/packages/turf-clean-coords/test/in/multipolygon.geojson @@ -28,10 +28,10 @@ [ [ [11, 11], - [11.5, 11.5], - [12, 12], - [12, 11], [11.5, 11], + [12, 11], + [12, 12], + [11.5, 11.5], [11, 11], [11, 11] ] diff --git a/packages/turf-clean-coords/test/out/feature-collection.geojson b/packages/turf-clean-coords/test/out/feature-collection.geojson new file mode 100644 index 0000000000..22dc4055b8 --- /dev/null +++ b/packages/turf-clean-coords/test/out/feature-collection.geojson @@ -0,0 +1,41 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [0, 0], + [10, 0], + [10, 10], + [0, 10], + [0, 0] + ], + [ + [1, 5], + [1, 8.5], + [4.5, 8.5], + [4.5, 5], + [1, 5] + ] + ] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [11, 11], + [12, 11], + [12, 12], + [11, 11] + ] + } + } + ] +} diff --git a/packages/turf-clean-coords/test/out/geometry-collection.geojson b/packages/turf-clean-coords/test/out/geometry-collection.geojson new file mode 100644 index 0000000000..f42594c888 --- /dev/null +++ b/packages/turf-clean-coords/test/out/geometry-collection.geojson @@ -0,0 +1,33 @@ +{ + "type": "GeometryCollection", + "geometries": [ + { + "type": "Polygon", + "coordinates": [ + [ + [0, 0], + [10, 0], + [10, 10], + [0, 10], + [0, 0] + ], + [ + [1, 5], + [1, 8.5], + [4.5, 8.5], + [4.5, 5], + [1, 5] + ] + ] + }, + { + "type": "LineString", + "coordinates": [ + [11, 11], + [12, 11], + [12, 12], + [11, 11] + ] + } + ] +}