@@ -359,3 +359,183 @@ import GeoInterface as GI
359359 end
360360 end
361361end
362+
363+ # Spherical Sutherland-Hodgman Tests
364+ @testset " ConvexConvexSutherlandHodgman - Spherical" begin
365+ using GeometryOps. UnitSpherical: UnitSphereFromGeographic
366+
367+ # Transform lon/lat to UnitSphericalPoint
368+ _transform = UnitSphereFromGeographic()
369+ lonlat_to_point(lon, lat) = _transform((lon, lat))
370+
371+ alg = GO. ConvexConvexSutherlandHodgman(GO. Spherical())
372+
373+ @testset " Basic spherical intersection" begin
374+ # Two overlapping 2°×2° spherical cells
375+ poly_a = GI. Polygon([[
376+ lonlat_to_point(0.0 , 0.0 ),
377+ lonlat_to_point(2.0 , 0.0 ),
378+ lonlat_to_point(2.0 , 2.0 ),
379+ lonlat_to_point(0.0 , 2.0 ),
380+ lonlat_to_point(0.0 , 0.0 ),
381+ ]])
382+
383+ poly_b = GI. Polygon([[
384+ lonlat_to_point(1.0 , 1.0 ),
385+ lonlat_to_point(3.0 , 1.0 ),
386+ lonlat_to_point(3.0 , 3.0 ),
387+ lonlat_to_point(1.0 , 3.0 ),
388+ lonlat_to_point(1.0 , 1.0 ),
389+ ]])
390+
391+ result = GO. intersection(alg, poly_a, poly_b; target= GI. PolygonTrait())
392+ area = GO. area(GO. Spherical(), result)
393+
394+ # Should be approximately 1°×1° intersection area
395+ @test area > 0
396+ @test area < GO. area(GO. Spherical(), poly_a)
397+ end
398+
399+ @testset " Adjacent polygons (shared edge) - THE FIX" begin
400+ # Two 1°×1° cells sharing lon=126 edge
401+ # This is the main bug that was reported
402+ poly_a = GI. Polygon([[
403+ lonlat_to_point(125.0 , 53.0 ),
404+ lonlat_to_point(126.0 , 53.0 ),
405+ lonlat_to_point(126.0 , 54.0 ),
406+ lonlat_to_point(125.0 , 54.0 ),
407+ lonlat_to_point(125.0 , 53.0 ),
408+ ]])
409+
410+ poly_b = GI. Polygon([[
411+ lonlat_to_point(126.0 , 53.0 ),
412+ lonlat_to_point(127.0 , 53.0 ),
413+ lonlat_to_point(127.0 , 54.0 ),
414+ lonlat_to_point(126.0 , 54.0 ),
415+ lonlat_to_point(126.0 , 53.0 ),
416+ ]])
417+
418+ result_ab = GO. intersection(alg, poly_a, poly_b; target= GI. PolygonTrait())
419+ result_ba = GO. intersection(alg, poly_b, poly_a; target= GI. PolygonTrait())
420+
421+ area_ab = GO. area(GO. Spherical(), result_ab)
422+ area_ba = GO. area(GO. Spherical(), result_ba)
423+
424+ # Adjacent polygons should have zero/negligible intersection
425+ @test area_ab < 1e-10
426+ @test area_ba < 1e-10
427+
428+ # Operation should be symmetric
429+ @test area_ab ≈ area_ba atol= 1e-10
430+ end
431+
432+ @testset " Vertex on edge (no overlap)" begin
433+ # Subject polygon (lon 125-126, lat 53-54)
434+ poly_a = GI. Polygon([[
435+ lonlat_to_point(125.0 , 53.0 ),
436+ lonlat_to_point(126.0 , 53.0 ),
437+ lonlat_to_point(126.0 , 54.0 ),
438+ lonlat_to_point(125.0 , 54.0 ),
439+ lonlat_to_point(125.0 , 53.0 ),
440+ ]])
441+
442+ # Polygon with vertex at (126.0, 53.5) - ON poly_a's lon=126 edge but outside
443+ poly_b = GI. Polygon([[
444+ lonlat_to_point(126.0 , 53.5 ), # ON poly_a's lon=126 edge
445+ lonlat_to_point(127.0 , 53.0 ),
446+ lonlat_to_point(127.0 , 54.0 ),
447+ lonlat_to_point(126.0 , 53.5 ),
448+ ]])
449+
450+ result = GO. intersection(alg, poly_a, poly_b; target= GI. PolygonTrait())
451+ area = GO. area(GO. Spherical(), result)
452+
453+ # Should be zero, not the area of poly_b!
454+ @test area < 1e-10
455+ end
456+
457+ @testset " Overlapping spherical polygons" begin
458+ poly_a = GI. Polygon([[
459+ lonlat_to_point(125.0 , 53.0 ),
460+ lonlat_to_point(127.0 , 53.0 ),
461+ lonlat_to_point(127.0 , 55.0 ),
462+ lonlat_to_point(125.0 , 55.0 ),
463+ lonlat_to_point(125.0 , 53.0 ),
464+ ]])
465+
466+ poly_b = GI. Polygon([[
467+ lonlat_to_point(126.0 , 54.0 ),
468+ lonlat_to_point(128.0 , 54.0 ),
469+ lonlat_to_point(128.0 , 56.0 ),
470+ lonlat_to_point(126.0 , 56.0 ),
471+ lonlat_to_point(126.0 , 54.0 ),
472+ ]])
473+
474+ result = GO. intersection(alg, poly_a, poly_b; target= GI. PolygonTrait())
475+ area = GO. area(GO. Spherical(), result)
476+
477+ # Should be approximately 1°×1° = area of overlap region
478+ expected_poly = GI. Polygon([[
479+ lonlat_to_point(126.0 , 54.0 ),
480+ lonlat_to_point(127.0 , 54.0 ),
481+ lonlat_to_point(127.0 , 55.0 ),
482+ lonlat_to_point(126.0 , 55.0 ),
483+ lonlat_to_point(126.0 , 54.0 ),
484+ ]])
485+ expected_area = GO. area(GO. Spherical(), expected_poly)
486+
487+ @test area ≈ expected_area rtol= 0.05
488+ end
489+
490+ @testset " Containment" begin
491+ outer = GI. Polygon([[
492+ lonlat_to_point(120.0 , 50.0 ),
493+ lonlat_to_point(130.0 , 50.0 ),
494+ lonlat_to_point(130.0 , 60.0 ),
495+ lonlat_to_point(120.0 , 60.0 ),
496+ lonlat_to_point(120.0 , 50.0 ),
497+ ]])
498+
499+ inner = GI. Polygon([[
500+ lonlat_to_point(124.0 , 54.0 ),
501+ lonlat_to_point(126.0 , 54.0 ),
502+ lonlat_to_point(126.0 , 56.0 ),
503+ lonlat_to_point(124.0 , 56.0 ),
504+ lonlat_to_point(124.0 , 54.0 ),
505+ ]])
506+
507+ result = GO. intersection(alg, outer, inner; target= GI. PolygonTrait())
508+ inner_area = GO. area(GO. Spherical(), inner)
509+ result_area = GO. area(GO. Spherical(), result)
510+
511+ @test result_area ≈ inner_area rtol= 0.01
512+ end
513+
514+ @testset " Original bug report case" begin
515+ # Subject polygon (lon 125-126, lat 53-54)
516+ subject = GI. Polygon([[
517+ lonlat_to_point(125.0 , 53.0 ),
518+ lonlat_to_point(126.0 , 53.0 ),
519+ lonlat_to_point(126.0 , 54.0 ),
520+ lonlat_to_point(125.0 , 54.0 ),
521+ lonlat_to_point(125.0 , 53.0 ),
522+ ]])
523+
524+ # Clip polygon - adjacent, with vertex at (126.0, 53.23) ON subject's edge
525+ clip = GI. Polygon([[
526+ lonlat_to_point(126.0 , 53.23 ), # ON subject's lon=126 edge!
527+ lonlat_to_point(126.86 , 52.32 ),
528+ lonlat_to_point(127.86 , 53.25 ),
529+ lonlat_to_point(126.95 , 54.15 ),
530+ lonlat_to_point(126.0 , 53.23 ),
531+ ]])
532+
533+ result = GO. intersection(alg, subject, clip; target= GI. PolygonTrait())
534+ result_area = GO. area(GO. Spherical(), result)
535+ clip_area = GO. area(GO. Spherical(), clip)
536+
537+ # BUG FIX: result_area should be ~0, NOT clip_area
538+ # The ratio should be ~0, not ~1
539+ @test result_area < clip_area * 0.01 # Less than 1% of clip area
540+ end
541+ end
0 commit comments