Skip to content

fix(polygonize): deduplicate segments in planar graph#3045

Open
Traksewt wants to merge 2 commits into
Turfjs:masterfrom
agriwebb:fix/polygonize-duplicate-segment
Open

fix(polygonize): deduplicate segments in planar graph#3045
Traksewt wants to merge 2 commits into
Turfjs:masterfrom
agriwebb:fix/polygonize-duplicate-segment

Conversation

@Traksewt

@Traksewt Traksewt commented Mar 31, 2026

Copy link
Copy Markdown
  • Meaningful title, including the name of the package being modified.
  • Summary of the changes.
  • Heads up if this is a breaking change.
  • Any issues this resolves.
  • Inclusion of your details in the contributors field of package.json - you've earned it!
  • Confirmation you've read the steps for preparing a pull request.

Summary

When input geometry contains duplicate line segments (the same segment appearing more than once), @turf/polygonize silently returns 0 polygons instead of the expected result.

Bug

// A simple square with one duplicated segment — should produce 1 polygon
const lines = featureCollection([
  lineString([[0, 0], [1, 0]]),
  lineString([[1, 0], [1, 1]]),
  lineString([[1, 1], [0, 1]]),
  lineString([[0, 1], [0, 0]]),
  lineString([[0, 0], [1, 0]]),  // duplicate of first segment
]);

polygonize(lines); // => 0 polygons (expected 1)

Root cause

Graph.addEdge() creates a new pair of directed edges for every input segment, including duplicates. The duplicate edges corrupt the planar graph topology, causing edge-ring traversal to fail.

Fix

Track seen edge IDs in Graph and skip segments that have already been added. Two lines in addEdge() plus cleanup in removeEdge().

Test

Added a regression test with the above scenario — confirms polygonize correctly returns 1 polygon when a duplicate segment is present.

This is not a breaking change. Inputs that previously worked continue to produce the same output. Only the failure case (duplicate segments -> 0 polygons) is fixed.

Relates to #1714.


Note: This is a focused subset of #1714, which was opened in 2019 but covered multiple fixes in a single large PR (1000+ lines). This PR intentionally targets just the duplicate segment issue to keep the change small and reviewable per the contributing guidelines. Happy to follow up with separate PRs for the other polygonize issues if this lands.

@Traksewt

Traksewt commented Apr 2, 2026

Copy link
Copy Markdown
Author

Hi @NickCis and @DenisCarriere, I have a few fixes to make this more robust, I can do them 1 at a time. Let me know if it is of interest and if there is anything else you need. Thanks.

@NickCis

NickCis commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

Hey @DenisCarriere , it has been a while since I wrote the code. So basically you can do what you want with it! It's fine by me.

@DenisCarriere

Copy link
Copy Markdown
Contributor

Unfortunately I haven't monitored the code base in a very long time, I'm not the person anymore to review PRs

@Traksewt

Copy link
Copy Markdown
Author

Hey @DenisCarriere, do you know who I should contact on this now? Who is the active maintainer?

@mfedderly mfedderly left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👋 Got a chance to check this out today. Thanks for doing the digging on this!

The thing that popped out to to me in the review of this is that all of these objects with string keys on them is probably really slow. I know we're already doing this for the node ids, so this is kind of the natural extension of that code. As implemented the bench seems to have regressed 20-30% for this PR against the baseline.

I think its possible (with a larger change) to fix this bug and improve performance overall by leveraging the newer Set and Map primitives. I took a crack at reworking the Node storage in Graph and saw 15-70% improvements depending on how complicated the input was. #3074

What do you think about the change in #3074? Would you be willing to do something similar in your PR for the edge storage in Graph? I think you can have the edgeIndex be something like Map<Node, Set<Node>> and have edges be Set<Edge>. I haven't looked into that part too closely.

removeEdge(edge: Edge) {
this.edges = this.edges.filter((e) => !e.isEqual(edge));
edge.deleteEdge();
delete this.edgeIds[`${edge.from.id}->${edge.to.id}`];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to myself: from a quick look I don't think we need to delete the symmetric edge here, because calls to removeEdge are doing the deletions in a second removeEdge call.

@mfedderly mfedderly added this to the v7.4 milestone Jun 22, 2026
mfedderly added a commit that referenced this pull request Jun 22, 2026
* Migrate @turf/polygonize to Map/Set

When looking at #3045 it occurred to me that this code probably hasn't been updated since we moved from ES5 to ES8 support in Turf.
Specifically: Node was getting a very log string assigned as its id, and then we were using an Object keyed by those ids to do lookups.

This changes Node's id to a number (incremented as Graph creates them). Graph keeps both a Set<Node> and an index of Map<longitude, Map<latitude, Node>> used
to quickly figure out if a Node has already been created for a given set of coordinates for reuse.

* Rename byLng to byLat for clarity of the nodeIdx structure
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants