From 01e58da85ca59ad160835f8ac83076dbee408e55 Mon Sep 17 00:00:00 2001 From: Brendan Ratter Date: Thu, 29 Jan 2026 00:41:24 -0500 Subject: [PATCH 1/3] Support for collections and consistency improvements - Added support for FeatureCollection and GeometryCollection types - Improved GeoJSON type signatures, returning the same type that as passed - Added ability to pass epsilon through to booleanPointOnLine - Ensured clarity on structural sharing and documented - Removed some redundant checks --- packages/turf-clean-coords/README.md | 41 ++- packages/turf-clean-coords/index.ts | 291 +++++++++++++----- packages/turf-clean-coords/test.ts | 35 ++- .../test/in/feature-collection.geojson | 46 +++ .../test/in/geometry-collection.geojson | 38 +++ .../test/out/feature-collection.geojson | 83 +++++ .../test/out/geometry-collection.geojson | 75 +++++ 7 files changed, 521 insertions(+), 88 deletions(-) create mode 100644 packages/turf-clean-coords/test/in/feature-collection.geojson create mode 100644 packages/turf-clean-coords/test/in/geometry-collection.geojson create mode 100644 packages/turf-clean-coords/test/out/feature-collection.geojson create mode 100644 packages/turf-clean-coords/test/out/geometry-collection.geojson diff --git a/packages/turf-clean-coords/README.md b/packages/turf-clean-coords/README.md index 4552217d6d..eb15a08e48 100644 --- a/packages/turf-clean-coords/README.md +++ b/packages/turf-clean-coords/README.md @@ -4,14 +4,33 @@ ## cleanCoords -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. ### Parameters -* `geojson` **([Geometry][1] | [Feature][2])** Feature or Geometry -* `options` **[Object][3]** Optional parameters (optional, default `{}`) +* `geojson` **[GeoJSON][1]** Any valid GeoJSON type +* `options` **[Object][2]** Optional parameters (optional, default `{}`) - * `options.mutate` **[boolean][4]** allows GeoJSON input to be mutated (optional, default `false`) + * `options.mutate` **[boolean][3]** allows GeoJSON input to be mutated (optional, default `false`) + * `options.epsilon` **[number][4]?** Fractional number to compare with the cross product result. Useful for dealing with floating points such as lng/lat points ### Examples @@ -26,15 +45,19 @@ turf.cleanCoords(multiPoint).geometry.coordinates; //= [[0, 0], [2, 2]] ``` -Returns **([Geometry][1] | [Feature][2])** the cleaned input Feature/Geometry +Returns **([Geometry][5] | [Feature][6])** the cleaned input Feature/Geometry + +[1]: https://tools.ietf.org/html/rfc7946#section-3 + +[2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object -[1]: https://tools.ietf.org/html/rfc7946#section-3.1 +[3]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean -[2]: https://tools.ietf.org/html/rfc7946#section-3.2 +[4]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number -[3]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object +[5]: https://tools.ietf.org/html/rfc7946#section-3.1 -[4]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean +[6]: https://tools.ietf.org/html/rfc7946#section-3.2 diff --git a/packages/turf-clean-coords/index.ts b/packages/turf-clean-coords/index.ts index 9c8660252c..9a0b61e45e 100644 --- a/packages/turf-clean-coords/index.ts +++ b/packages/turf-clean-coords/index.ts @@ -1,18 +1,62 @@ -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 - +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: GeoJSON, + options?: { mutate?: boolean; epsilon?: number } +): GeoJSON; /** - * 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 +69,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 +194,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 +258,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; } /** @@ -181,5 +299,24 @@ function equals(pt1: Position, pt2: Position) { return pt1[0] === pt2[0] && pt1[1] === pt2[1]; } +/** + * 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 }; export default cleanCoords; 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..866355edc2 --- /dev/null +++ b/packages/turf-clean-coords/test/in/feature-collection.geojson @@ -0,0 +1,46 @@ +{ + "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..83f935b6a0 --- /dev/null +++ b/packages/turf-clean-coords/test/in/geometry-collection.geojson @@ -0,0 +1,38 @@ +{ + "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/out/feature-collection.geojson b/packages/turf-clean-coords/test/out/feature-collection.geojson new file mode 100644 index 0000000000..925c6a947a --- /dev/null +++ b/packages/turf-clean-coords/test/out/feature-collection.geojson @@ -0,0 +1,83 @@ +{ + "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..22448487b6 --- /dev/null +++ b/packages/turf-clean-coords/test/out/geometry-collection.geojson @@ -0,0 +1,75 @@ +{ + "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 + ] + ] + } + ] +} From f1328ad1e9873bf46a2eacc798941a0feceb2f60 Mon Sep 17 00:00:00 2001 From: Brendan Ratter Date: Sat, 31 Jan 2026 18:16:34 -0500 Subject: [PATCH 2/3] turf-clean-coords: Update bench and tests - Update benchmark results - Fix test formatting - Minor typescript enhancements --- packages/turf-clean-coords/bench.ts | 96 ++++++-------- packages/turf-clean-coords/index.ts | 2 +- packages/turf-clean-coords/package.json | 3 +- .../test/in/feature-collection.geojson | 85 +++++++------ .../test/in/geometry-collection.geojson | 71 ++++++----- .../test/in/multipolygon.geojson | 6 +- .../test/out/feature-collection.geojson | 120 ++++++------------ .../test/out/geometry-collection.geojson | 104 +++++---------- 8 files changed, 198 insertions(+), 289 deletions(-) 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 9a0b61e45e..766d89e24c 100644 --- a/packages/turf-clean-coords/index.ts +++ b/packages/turf-clean-coords/index.ts @@ -295,7 +295,7 @@ function cleanRings( * @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]; } 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/in/feature-collection.geojson b/packages/turf-clean-coords/test/in/feature-collection.geojson index 866355edc2..47df10dca0 100644 --- a/packages/turf-clean-coords/test/in/feature-collection.geojson +++ b/packages/turf-clean-coords/test/in/feature-collection.geojson @@ -1,46 +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] + "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] - ] + } + }, + { + "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 index 83f935b6a0..80c60606a9 100644 --- a/packages/turf-clean-coords/test/in/geometry-collection.geojson +++ b/packages/turf-clean-coords/test/in/geometry-collection.geojson @@ -1,38 +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] + "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] - ] - }] + }, + { + "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 index 925c6a947a..22dc4055b8 100644 --- a/packages/turf-clean-coords/test/out/feature-collection.geojson +++ b/packages/turf-clean-coords/test/out/feature-collection.geojson @@ -1,83 +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 - ] - ] - } - } - ] + "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 index 22448487b6..f42594c888 100644 --- a/packages/turf-clean-coords/test/out/geometry-collection.geojson +++ b/packages/turf-clean-coords/test/out/geometry-collection.geojson @@ -1,75 +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 - ] - ] - } - ] + "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] + ] + } + ] } From 533f24f52357f127616ce57294c50fa7ca9a8ca5 Mon Sep 17 00:00:00 2001 From: Brendan Ratter Date: Sun, 1 Feb 2026 16:57:26 -0500 Subject: [PATCH 3/3] turf-clean-coords: Improved type signature - Explict infer for better inference on union types - Added overload equivalent to old type signature with deprecation warning --- packages/turf-clean-coords/README.md | 47 +++++----------------------- packages/turf-clean-coords/index.ts | 19 ++++++++++- 2 files changed, 25 insertions(+), 41 deletions(-) diff --git a/packages/turf-clean-coords/README.md b/packages/turf-clean-coords/README.md index eb15a08e48..79ffc6fd66 100644 --- a/packages/turf-clean-coords/README.md +++ b/packages/turf-clean-coords/README.md @@ -1,18 +1,7 @@ # @turf/clean-coords - - -## cleanCoords - -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 +## 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 @@ -22,19 +11,11 @@ Obeys consistent structural sharing rules based on the `mutate` option: \=== 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][1]** Any valid GeoJSON type -* `options` **[Object][2]** Optional parameters (optional, default `{}`) - - * `options.mutate` **[boolean][3]** allows GeoJSON input to be mutated (optional, default `false`) - * `options.epsilon` **[number][4]?** Fractional number to compare with the cross product result. Useful for dealing with floating points such as lng/lat points + 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 `{}`) -### Examples - -```javascript + * `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]]); @@ -43,21 +24,7 @@ turf.cleanCoords(line).geometry.coordinates; turf.cleanCoords(multiPoint).geometry.coordinates; //= [[0, 0], [2, 2]] -``` - -Returns **([Geometry][5] | [Feature][6])** the cleaned input Feature/Geometry - -[1]: https://tools.ietf.org/html/rfc7946#section-3 - -[2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object - -[3]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean - -[4]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number - -[5]: https://tools.ietf.org/html/rfc7946#section-3.1 - -[6]: https://tools.ietf.org/html/rfc7946#section-3.2 +```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/index.ts b/packages/turf-clean-coords/index.ts index 766d89e24c..9fd91bb271 100644 --- a/packages/turf-clean-coords/index.ts +++ b/packages/turf-clean-coords/index.ts @@ -12,6 +12,14 @@ import { feature, featureCollection } from "@turf/helpers"; import { booleanPointOnLine } from "@turf/boolean-point-on-line"; import { lineString } from "@turf/helpers"; +/** 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 } @@ -28,10 +36,19 @@ 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 Type. * @@ -318,5 +335,5 @@ function multipointDeduplicate(geom: MultiPoint): Position[] { } } -export { cleanCoords }; +export { cleanCoords, CleanCoordsResult }; export default cleanCoords;