Skip to content

Union() incorrectly treats non-overlapping polygons as single polygon with holes #15

@korrawit

Description

@korrawit

Issue Description

Summary

The Union() method incorrectly converts non-overlapping polygons into a single Polygon with multiple rings (treating the second+ polygons as holes) instead of returning a proper MultiPolygon with separate polygons.

Expected Behavior

When unioning two non-overlapping multipolygons, the result should be a MultiPolygon containing two separate polygons:

// Input: Two non-overlapping squares
mp1 := geom.MultiPolygon{{{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 1, Y: 1}, {X: 0, Y: 1}, {X: 0, Y: 0}}}}
mp2 := geom.MultiPolygon{{{{X: 2, Y: 0}, {X: 3, Y: 0}, {X: 3, Y: 1}, {X: 2, Y: 1}, {X: 2, Y: 0}}}}

result := mp1.Union(mp2)

// Expected: geom.MultiPolygon with 2 separate polygons, each with 1 ring
// Expected GeoJSON structure:
// [
//   [[[0,0], [1,0], [1,1], [0,1], [0,0]]],  // Polygon 1
//   [[[2,0], [3,0], [3,1], [2,1], [2,0]]]   // Polygon 2
// ]

Actual Behavior

The result is a MultiPolygon with a single Polygon containing 2 rings, where the second ring is incorrectly treated as a hole:

result := mp1.Union(mp2)

// Actual: geom.MultiPolygon with 1 polygon containing 2 rings (second ring treated as hole)
// Actual GeoJSON structure:
// [
//   [
//     [[0,0], [1,0], [1,1], [0,1], [0,0]],    // Ring 0: exterior
//     [[2,0], [3,0], [3,1], [2,1], [2,0]]     // Ring 1: incorrectly treated as hole
//   ]
// ]

Minimal Reproducible Example

package main

import (
	"fmt"
	"github.com/ctessum/geom"
)

func main() {
	// Two non-overlapping squares
	mp1 := geom.MultiPolygon{
		{
			{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 1, Y: 1}, {X: 0, Y: 1}, {X: 0, Y: 0}},
		},
	}
	mp2 := geom.MultiPolygon{
		{
			{{X: 2, Y: 0}, {X: 3, Y: 0}, {X: 3, Y: 1}, {X: 2, Y: 1}, {X: 2, Y: 0}},
		},
	}

	result := mp1.Union(mp2)

	// Check result
	if mp, ok := result.(geom.MultiPolygon); ok {
		if len(mp) == 2 {
			fmt.Printf("✓ Correct: MultiPolygon with %d polygons\n", len(mp))
		} else if len(mp) == 1 && len(mp[0]) > 1 {
			fmt.Printf("✗ Bug: MultiPolygon with 1 polygon containing %d rings\n", len(mp[0]))
			fmt.Printf("   Ring 0: %d points (exterior)\n", len(mp[0][0]))
			fmt.Printf("   Ring 1: %d points (incorrectly treated as hole)\n", len(mp[0][1]))
			fmt.Printf("   Area: %.2f (coincidentally correct, but structure is wrong)\n", mp[0].Area())
		}
	} else if p, ok := result.(geom.Polygon); ok {
		fmt.Printf("✗ Bug: Single Polygon with %d rings\n", len(p))
		fmt.Printf("   Ring 0: %d points\n", len(p[0]))
		fmt.Printf("   Ring 1: %d points (incorrectly treated as hole)\n", len(p[1]))
		fmt.Printf("   Area: %.2f (coincidentally correct, but structure is wrong)\n", p.Area())
	}
}

Output:

✗ Bug: MultiPolygon with 1 polygon containing 2 rings
   Ring 0: 6 points (exterior)
   Ring 1: 6 points (incorrectly treated as hole)
   Area: 2.00 (coincidentally correct, but structure is wrong)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions