From 7b0f2cb79710909577b27adfe2a5a1b622c19fc0 Mon Sep 17 00:00:00 2001 From: Dewey Dunnington Date: Fri, 18 Sep 2026 15:03:05 +0000 Subject: [PATCH 01/13] Fix collection interior handling (#32) * add the first failing case * add some failing comented out cases * uncomment them * generated change * remove the multiline cases and start from main * implement contains with with min dim handling * touches update * fix remaining within/contains cases --- tests/relations/gcol.jsonc | 164 ++++++++++++++++++++++++++++++++++++- tg.c | 140 +++++++++++++++++++++++++++---- 2 files changed, 287 insertions(+), 17 deletions(-) diff --git a/tests/relations/gcol.jsonc b/tests/relations/gcol.jsonc index ca52f75..5ea4662 100644 --- a/tests/relations/gcol.jsonc +++ b/tests/relations/gcol.jsonc @@ -58,5 +58,167 @@ "overlaps": ["F", "F"], "touches": ["F", "F"] } - } + }, + // Highest dimension handling: only the highest dimension counts towards + // the "interior" for the purposes of predicate evaluation + { + "geoms": [ + "GEOMETRYCOLLECTION (POINT (0 0), LINESTRING (0 0, 0 1))", + "POINT (0 0)" + ], + "dims": [ 1, 0 ], + "relate": [ "FF10F0FF2", "F0FFFF102" ], + "predicates": { + "equals": ["F", "F"], + "intersects": ["T", "T"], + "disjoint": ["F", "F"], + "contains": ["F", "F"], + "within": ["F", "F"], + "covers": ["T", "F"], + "coveredby": ["F", "T"], + "crosses": ["F", "F"], + "overlaps": ["F", "F"], + "touches": ["T", "T"] + } + }, + { + "geoms": [ + "GEOMETRYCOLLECTION (POINT (0 0))", + "POINT (0 0)" + ], + "dims": [ 0, 0 ], + "relate": [ "0FFFFFFF2", "0FFFFFFF2" ], + "predicates": { + "equals": ["T", "T"], + "intersects": ["T", "T"], + "disjoint": ["F", "F"], + "contains": ["T", "T"], + "within": ["T", "T"], + "covers": ["T", "T"], + "coveredby": ["T", "T"], + "crosses": ["F", "F"], + "overlaps": ["F", "F"], + "touches": ["F", "F"] + } + }, + { + "geoms": [ + "GEOMETRYCOLLECTION (POINT (0 0), LINESTRING (0 0, 0 1))", + "POINT (0 0.5)" + ], + "dims": [ 1, 0 ], + "relate": [ "0F1FF0FF2", "0FFFFF102" ], + "predicates": { + "equals": ["F", "F"], + "intersects": ["T", "T"], + "disjoint": ["F", "F"], + "contains": ["T", "F"], + "within": ["F", "T"], + "covers": ["T", "F"], + "coveredby": ["F", "T"], + "crosses": ["F", "F"], + "overlaps": ["F", "F"], + "touches": ["F", "F"] + } + }, + { + "geoms": [ + "GEOMETRYCOLLECTION (POINT (-1 -1), LINESTRING (0 0, 0 1))", + "POINT (-1 -1)" + ], + "dims": [ 1, 0 ], + "relate": [ "0F1FF0FF2", "0FFFFF102" ], + "predicates": { + "equals": ["F", "F"], + "intersects": ["T", "T"], + "disjoint": ["F", "F"], + "contains": ["T", "F"], + "within": ["F", "T"], + "covers": ["T", "F"], + "coveredby": ["F", "T"], + "crosses": ["F", "F"], + "overlaps": ["F", "F"], + "touches": ["F", "F"] + } + }, + { + "geoms": [ + "GEOMETRYCOLLECTION (POINT (0 0), POLYGON ((0 0, 0 1, 1 0, 0 0)))", + "POINT (0 0)" + ], + "dims": [ 2, 0 ], + "relate": [ "FF20F1FF2", "F0FFFF212" ], + "predicates": { + "equals": ["F", "F"], + "intersects": ["T", "T"], + "disjoint": ["F", "F"], + "contains": ["F", "F"], + "within": ["F", "F"], + "covers": ["T", "F"], + "coveredby": ["F", "T"], + "crosses": ["F", "F"], + "overlaps": ["F", "F"], + "touches": ["T", "T"] + } + }, + { + "geoms": [ + "GEOMETRYCOLLECTION (POINT (0 0), POLYGON ((0 0, 0 1, 1 0, 0 0)))", + "POINT (0.25 0.25)" + ], + "dims": [ 2, 0 ], + "relate": [ "0F2FF1FF2", "0FFFFF212" ], + "predicates": { + "equals": ["F", "F"], + "intersects": ["T", "T"], + "disjoint": ["F", "F"], + "contains": ["T", "F"], + "within": ["F", "T"], + "covers": ["T", "F"], + "coveredby": ["F", "T"], + "crosses": ["F", "F"], + "overlaps": ["F", "F"], + "touches": ["F", "F"] + } + }, + { + "geoms": [ + "GEOMETRYCOLLECTION (LINESTRING (0 0, 0 1), POLYGON ((0 0, 0 1, 1 0, 0 0)))", + "LINESTRING (0 0, 0 1)" + ], + "dims": [ 2, 1 ], + "relate": [ "FF2101FF2", "F1FF0F212" ], + "predicates": { + "equals": ["F", "F"], + "intersects": ["T", "T"], + "disjoint": ["F", "F"], + "contains": ["F", "F"], + "within": ["F", "F"], + "covers": ["T", "F"], + "coveredby": ["F", "T"], + "crosses": ["F", "F"], + "overlaps": ["F", "F"], + "touches": ["T", "T"] + } + }, + { + "geoms": [ + "GEOMETRYCOLLECTION (LINESTRING (0 0, 0 1), POLYGON ((0 0, 0 1, 1 0, 0 0)))", + "LINESTRING (0 0, 0.25 0.25)" + ], + "dims": [ 2, 1 ], + "relate": [ "102F01FF2", "1FF00F212" ], + "predicates": { + "equals": ["F", "F"], + "intersects": ["T", "T"], + "disjoint": ["F", "F"], + "contains": ["T", "F"], + "within": ["F", "T"], + "covers": ["T", "F"], + "coveredby": ["F", "T"], + "crosses": ["F", "F"], + "overlaps": ["F", "F"], + "touches": ["F", "F"] + } + }, ] diff --git a/tg.c b/tg.c index bb56fa0..2453d71 100644 --- a/tg.c +++ b/tg.c @@ -6199,16 +6199,30 @@ static bool point_contains_base_geom(struct tg_point point, struct geom_contains_iter_ctx { const struct tg_geom *geom; + int min_dim; + int max_cover_dim; + int max_contains_dim; bool result; }; static bool geom_contains_iter0(const struct tg_geom *geom, void *udata) { struct geom_contains_iter_ctx *ctx = udata; + if (tg_geom_is_empty(geom)) { + return true; + } + int dim = tg_geom_de9im_dims(geom); if (tg_geom_contains(geom, ctx->geom)) { - // found a child object that contains geom, end inner loop - ctx->result = true; - return false; + if (dim > ctx->max_contains_dim) { + ctx->max_contains_dim = dim; + } + if (dim > ctx->max_cover_dim) { + ctx->max_cover_dim = dim; + } + } else if (tg_geom_covers(geom, ctx->geom) && + dim > ctx->max_cover_dim) + { + ctx->max_cover_dim = dim; } return true; } @@ -6216,16 +6230,23 @@ static bool geom_contains_iter0(const struct tg_geom *geom, void *udata) { static bool geom_contains_iter(const struct tg_geom *geom, void *udata) { struct geom_contains_iter_ctx *ctx = udata; // skip empty geometries - if (!tg_geom_is_empty(geom)) { - struct geom_contains_iter_ctx ctx0 = { .geom = geom }; + if (!tg_geom_is_empty(geom) && + tg_geom_de9im_dims(geom) >= ctx->min_dim) + { + struct geom_contains_iter_ctx ctx0 = { + .geom = geom, + .max_cover_dim = -1, + .max_contains_dim = -1, + }; tg_geom_foreach(ctx->geom, geom_contains_iter0, &ctx0); - if (!ctx0.result) { - // unmark and quit the loop - ctx->result = false; + if (ctx0.max_contains_dim >= 0 && + ctx0.max_contains_dim == ctx0.max_cover_dim) + { + // At least one highest-dimension child of 'other' intersects the + // interior of 'geom'. Coverage is checked before this iteration. + ctx->result = true; return false; } - // mark that at least one geom is contained - ctx->result = true; } return true; } @@ -6245,8 +6266,15 @@ static bool base_geom_contains_geom(const struct tg_geom *geom, case TG_MULTILINESTRING: case TG_MULTIPOLYGON: case TG_GEOMETRYCOLLECTION: { - // all children of 'other' must be fully within 'geom' - struct geom_contains_iter_ctx ctx = { .geom = geom }; + // All children of 'other' must be covered by 'geom', but only a + // highest-dimension child must intersect the interior of 'geom'. + if (!tg_geom_covers(geom, other)) { + return false; + } + struct geom_contains_iter_ctx ctx = { + .geom = geom, + .min_dim = tg_geom_de9im_dims(other), + }; tg_geom_foreach(other, geom_contains_iter, &ctx); return ctx.result; } @@ -6480,9 +6508,17 @@ static bool poly_touches_geom(struct tg_poly *poly, return false; } +static bool geometrycollection_touches_geom(const struct tg_geom *geom, + const struct tg_geom *other); + static bool base_geom_touches_geom(const struct tg_geom *geom, const struct tg_geom *other) { + if (other && other->head.base == BASE_GEOM && + other->head.type == TG_GEOMETRYCOLLECTION) + { + return geometrycollection_touches_geom(other, geom); + } if ((geom->head.flags&IS_EMPTY) != IS_EMPTY) { switch (geom->head.type) { case TG_POINT: @@ -6493,8 +6529,7 @@ static bool base_geom_touches_geom(const struct tg_geom *geom, return poly_touches_geom(geom->poly, other); case TG_MULTIPOINT: case TG_MULTILINESTRING: - case TG_MULTIPOLYGON: - case TG_GEOMETRYCOLLECTION: { + case TG_MULTIPOLYGON: { bool touches = false; if (geom->multi) { for (int i = 0; i < geom->multi->ngeoms; i++) { @@ -6507,21 +6542,94 @@ static bool base_geom_touches_geom(const struct tg_geom *geom, } } return touches; - }} + } + case TG_GEOMETRYCOLLECTION: + return geometrycollection_touches_geom(geom, other); + } } return false; } +struct geometrycollection_touches_ctx { + const struct tg_geom *other; + int collection_dim; + int other_dim; + bool touches; + bool interior_intersects; +}; + +struct geometrycollection_touches_child_ctx { + struct geometrycollection_touches_ctx *ctx; + const struct tg_geom *child; +}; + +static bool geometrycollection_touches_other_iter(const struct tg_geom *geom, + void *udata) +{ + struct geometrycollection_touches_child_ctx *child_ctx = udata; + struct geometrycollection_touches_ctx *ctx = child_ctx->ctx; + if (tg_geom_is_empty(geom) || + tg_geom_de9im_dims(geom) < ctx->other_dim) + { + return true; + } + if (tg_geom_touches(child_ctx->child, geom)) { + ctx->touches = true; + } else if (tg_geom_intersects(child_ctx->child, geom)) { + ctx->interior_intersects = true; + return false; + } + return true; +} + +static bool geometrycollection_touches_iter(const struct tg_geom *geom, + void *udata) +{ + struct geometrycollection_touches_ctx *ctx = udata; + if (tg_geom_is_empty(geom) || + tg_geom_de9im_dims(geom) < ctx->collection_dim) + { + return true; + } + struct geometrycollection_touches_child_ctx child_ctx = { + .ctx = ctx, + .child = geom, + }; + tg_geom_foreach(ctx->other, geometrycollection_touches_other_iter, + &child_ctx); + return !ctx->interior_intersects; +} + +static bool geometrycollection_touches_geom(const struct tg_geom *geom, + const struct tg_geom *other) +{ + struct geometrycollection_touches_ctx ctx = { + .other = other, + .collection_dim = tg_geom_de9im_dims(geom), + .other_dim = tg_geom_de9im_dims(other), + }; + tg_geom_foreach(geom, geometrycollection_touches_iter, &ctx); + return ctx.touches && !ctx.interior_intersects; +} + /// Tests whether a geometry 'a' touches 'b'. /// They have at least one point in common, but their interiors do not /// intersect. /// @see GeometryPredicates bool tg_geom_touches(const struct tg_geom *geom, const struct tg_geom *other) { if (geom) { + bool other_is_geometrycollection = other && + other->head.base == BASE_GEOM && + other->head.type == TG_GEOMETRYCOLLECTION; + bool geom_is_geometrycollection = geom->head.base == BASE_GEOM && + geom->head.type == TG_GEOMETRYCOLLECTION; + if (other_is_geometrycollection && !geom_is_geometrycollection) { + return tg_geom_touches(other, geom); + } switch (geom->head.base) { case BASE_GEOM: return base_geom_touches_geom(geom, other); - case BASE_POINT: + case BASE_POINT: return point_touches_geom(((struct boxed_point*)geom)->point, other); case BASE_LINE: From 3a3a390987ad2b9ff97f3665ac9a6c120eb4f7ef Mon Sep 17 00:00:00 2001 From: tidwall Date: Fri, 18 Sep 2026 08:04:11 -0700 Subject: [PATCH 02/13] Invert conditions and increase coverage --- tg.c | 64 ++++++++++++++++++++++++++++-------------------------------- 1 file changed, 30 insertions(+), 34 deletions(-) diff --git a/tg.c b/tg.c index 2453d71..d1cf935 100644 --- a/tg.c +++ b/tg.c @@ -6208,21 +6208,20 @@ struct geom_contains_iter_ctx { static bool geom_contains_iter0(const struct tg_geom *geom, void *udata) { struct geom_contains_iter_ctx *ctx = udata; - if (tg_geom_is_empty(geom)) { - return true; - } - int dim = tg_geom_de9im_dims(geom); - if (tg_geom_contains(geom, ctx->geom)) { - if (dim > ctx->max_contains_dim) { - ctx->max_contains_dim = dim; - } - if (dim > ctx->max_cover_dim) { + if (!tg_geom_is_empty(geom)) { + int dim = tg_geom_de9im_dims(geom); + if (tg_geom_contains(geom, ctx->geom)) { + if (dim > ctx->max_contains_dim) { + ctx->max_contains_dim = dim; + } + if (dim > ctx->max_cover_dim) { + ctx->max_cover_dim = dim; + } + } else if (tg_geom_covers(geom, ctx->geom) && + dim > ctx->max_cover_dim) + { ctx->max_cover_dim = dim; } - } else if (tg_geom_covers(geom, ctx->geom) && - dim > ctx->max_cover_dim) - { - ctx->max_cover_dim = dim; } return true; } @@ -6425,7 +6424,7 @@ static bool line_touches_base_geom(struct tg_line *line, } } } - return false; + // fallthrough } } return false; @@ -6568,16 +6567,13 @@ static bool geometrycollection_touches_other_iter(const struct tg_geom *geom, { struct geometrycollection_touches_child_ctx *child_ctx = udata; struct geometrycollection_touches_ctx *ctx = child_ctx->ctx; - if (tg_geom_is_empty(geom) || - tg_geom_de9im_dims(geom) < ctx->other_dim) - { - return true; - } - if (tg_geom_touches(child_ctx->child, geom)) { - ctx->touches = true; - } else if (tg_geom_intersects(child_ctx->child, geom)) { - ctx->interior_intersects = true; - return false; + if (!tg_geom_is_empty(geom) && tg_geom_de9im_dims(geom) >= ctx->other_dim) { + if (tg_geom_touches(child_ctx->child, geom)) { + ctx->touches = true; + } else if (tg_geom_intersects(child_ctx->child, geom)) { + ctx->interior_intersects = true; + return false; + } } return true; } @@ -6586,18 +6582,18 @@ static bool geometrycollection_touches_iter(const struct tg_geom *geom, void *udata) { struct geometrycollection_touches_ctx *ctx = udata; - if (tg_geom_is_empty(geom) || - tg_geom_de9im_dims(geom) < ctx->collection_dim) + if (!tg_geom_is_empty(geom) && + tg_geom_de9im_dims(geom) >= ctx->collection_dim) { - return true; + struct geometrycollection_touches_child_ctx child_ctx = { + .ctx = ctx, + .child = geom, + }; + tg_geom_foreach(ctx->other, geometrycollection_touches_other_iter, + &child_ctx); + return !ctx->interior_intersects; } - struct geometrycollection_touches_child_ctx child_ctx = { - .ctx = ctx, - .child = geom, - }; - tg_geom_foreach(ctx->other, geometrycollection_touches_other_iter, - &child_ctx); - return !ctx->interior_intersects; + return true; } static bool geometrycollection_touches_geom(const struct tg_geom *geom, From 354485195a7e21d345ac91a276754ead87d1c1be Mon Sep 17 00:00:00 2001 From: Dewey Dunnington Date: Fri, 18 Sep 2026 10:37:25 -0500 Subject: [PATCH 03/13] add failing test case --- tests/relations/poly.jsonc | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/relations/poly.jsonc b/tests/relations/poly.jsonc index dd91da8..7496f28 100644 --- a/tests/relations/poly.jsonc +++ b/tests/relations/poly.jsonc @@ -788,4 +788,26 @@ "touches": ["T", "T"] } }, + // The line's interior passes through the polygon corner without entering + // the polygon interior, so the geometries touch. + { + "geoms": [ + "POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))", + "LINESTRING (1 3, 3 1)" + ], + "dims": [ 2, 1 ], + "relate": [ "FF20F1102", "F01FF0212" ], + "predicates": { + "equals": ["F", "F"], + "intersects": ["T", "T"], + "disjoint": ["F", "F"], + "contains": ["F", "F"], + "within": ["F", "F"], + "covers": ["F", "F"], + "coveredby": ["F", "F"], + "crosses": ["F", "F"], + "overlaps": ["F", "F"], + "touches": ["T", "T"] + } + }, ] From b44d0f816112e609d67ad2607198a335fded45dc Mon Sep 17 00:00:00 2001 From: Dewey Dunnington Date: Fri, 18 Sep 2026 10:44:17 -0500 Subject: [PATCH 04/13] draft solution for one case --- tg.c | 58 ++++++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/tg.c b/tg.c index d1cf935..5f136f2 100644 --- a/tg.c +++ b/tg.c @@ -3034,6 +3034,8 @@ struct intersegiterctx { bool allow_on_edge; bool seg_a_on; bool seg_b_on; + bool has_intersection; + double intersection_t; // bool yes; }; @@ -3062,6 +3064,7 @@ static bool intersegiter(struct tg_segment seg, int index, void *udata) { if (ccol && dcol) { // lines are parallel. ctx->count = 0; + ctx->has_intersection = false; } else if (!ccol || !dcol) { if (!ctx->seg_a_on) { if (pteq(a, c) || pteq(a, d)) { @@ -3075,6 +3078,16 @@ static bool intersegiter(struct tg_segment seg, int index, void *udata) { return true; } } + double rx = b.x-a.x; + double ry = b.y-a.y; + double sx = d.x-c.x; + double sy = d.y-c.y; + double t = ((c.x-a.x)*sy-(c.y-a.y)*sx) / (rx*sy-ry*sx); + if (ctx->has_intersection && t == ctx->intersection_t) { + return true; + } + ctx->has_intersection = true; + ctx->intersection_t = t; ctx->count++; } return ctx->count < 2; @@ -4133,40 +4146,45 @@ bool tg_poly_intersects_poly(const struct tg_poly *poly, return true; } +static bool ring_line_boundary_intersects_iter(struct tg_segment aseg, + int aidx, struct tg_segment bseg, int bidx, void *udata) +{ + (void)aseg; + (void)aidx; + (void)bseg; + (void)bidx; + *(bool *)udata = true; + return false; +} + +static bool ring_line_boundary_intersects(const struct tg_ring *ring, + const struct tg_line *line) +{ + bool intersects = false; + tg_ring_line_search(ring, line, ring_line_boundary_intersects_iter, + &intersects); + return intersects; +} + bool tg_poly_touches_line(const struct tg_poly *a, const struct tg_line *b) { if (!tg_rect_intersects_rect(tg_poly_rect(a), tg_line_rect(b))) { return false; } // Check if the line is inside any of the polygon holes - int npoints = tg_line_num_points(b); int nholes = tg_poly_num_holes(a); for (int i = 0; i < nholes; i++) { const struct tg_ring *hole = tg_poly_hole_at(a, i); if (tg_ring_contains_line(hole, b, true, false)) { - // Yes, now check if any of the points touch the hole boundary. - for (int j = 0; j < npoints; j++) { - struct tg_point point = tg_line_point_at(b, j); - if (tg_line_covers_point((struct tg_line*)hole, point)) { - return true; - } - } - return false; + // Yes, now check if the line touches the hole boundary. + return ring_line_boundary_intersects(hole, b); } } - // Check if at least one line point touches the polygon exterior. + // Check if the line touches the polygon exterior. The intersection may + // fall in the interior of a line segment rather than on a line point. const struct tg_ring *ring = tg_poly_exterior(a); - bool touches = false; - for (int i = 0; i < npoints; i++) { - struct tg_point point = tg_line_point_at(b, i); - // Cast the exterior ring to a polygon to avoid holes. - if (tg_poly_touches_point((struct tg_poly*)ring, point)) { - touches = true; - break; - } - } - if (!touches) { + if (!ring_line_boundary_intersects(ring, b)) { return false; } int nsegs = tg_line_num_segments(b); From 8540b3246b1a39e596698df1508f22832fb42472 Mon Sep 17 00:00:00 2001 From: Dewey Dunnington Date: Fri, 18 Sep 2026 10:44:33 -0500 Subject: [PATCH 05/13] failing test case for a harder case --- tests/relations/poly.jsonc | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/relations/poly.jsonc b/tests/relations/poly.jsonc index 7496f28..b226afc 100644 --- a/tests/relations/poly.jsonc +++ b/tests/relations/poly.jsonc @@ -810,4 +810,26 @@ "touches": ["T", "T"] } }, + // Two distinct tangencies do not imply that the line crosses the polygon + // interior. Both contacts are isolated polygon vertices. + { + "geoms": [ + "POLYGON ((0 0, 1 2, 2 0, 3 1, 4 2, 5 0, 5 -1, 0 -1, 0 0))", + "LINESTRING (-1 2, 6 2)" + ], + "dims": [ 2, 1 ], + "relate": [ "FF20F1102", "F01FF0212" ], + "predicates": { + "equals": ["F", "F"], + "intersects": ["T", "T"], + "disjoint": ["F", "F"], + "contains": ["F", "F"], + "within": ["F", "F"], + "covers": ["F", "F"], + "coveredby": ["F", "F"], + "crosses": ["F", "F"], + "overlaps": ["F", "F"], + "touches": ["T", "T"] + } + }, ] From fb8df46a3458936d67bf96830fc693022351e0c0 Mon Sep 17 00:00:00 2001 From: Dewey Dunnington Date: Fri, 18 Sep 2026 12:16:39 -0500 Subject: [PATCH 06/13] possibly fix the harder case --- tg.c | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/tg.c b/tg.c index 5f136f2..510eb53 100644 --- a/tg.c +++ b/tg.c @@ -3029,6 +3029,7 @@ bool tg_ring_contains_segment(const struct tg_ring *ring, } struct intersegiterctx { + const struct tg_ring *ring; struct tg_segment seg; int count; bool allow_on_edge; @@ -3039,8 +3040,34 @@ struct intersegiterctx { // bool yes; }; +static bool ring_vertex_is_convex_tangent(const struct tg_ring *ring, + int index, struct tg_segment seg) +{ + // A line through a convex vertex is tangent when both incident ring + // edges stay on the same side of the line. A reflex vertex or incident + // edges on opposite sides expose polygon interior to the line. + int nsegs = ring->nsegs; + struct tg_point prev = ring->points[(index+nsegs-1)%nsegs]; + struct tg_point vertex = ring->points[index]; + struct tg_point next = ring->points[(index+1)%nsegs]; + double in_x = vertex.x-prev.x; + double in_y = vertex.y-prev.y; + double out_x = next.x-vertex.x; + double out_y = next.y-vertex.y; + double turn = in_x*out_y-in_y*out_x; + bool convex = ring->clockwise ? turn < 0 : turn > 0; + if (!convex) { + return false; + } + double seg_x = seg.b.x-seg.a.x; + double seg_y = seg.b.y-seg.a.y; + double prev_side = seg_x*(prev.y-vertex.y)-seg_y*(prev.x-vertex.x); + double next_side = seg_x*(next.y-vertex.y)-seg_y*(next.x-vertex.x); + return (prev_side < 0 && next_side < 0) || + (prev_side > 0 && next_side > 0); +} + static bool intersegiter(struct tg_segment seg, int index, void *udata) { - (void)index; struct intersegiterctx *ctx = udata; if (!tg_segment_intersects_segment(ctx->seg, seg)) { @@ -3078,6 +3105,12 @@ static bool intersegiter(struct tg_segment seg, int index, void *udata) { return true; } } + if (ccol || dcol) { + int vertex = ccol ? index : (index+1)%ctx->ring->nsegs; + if (ring_vertex_is_convex_tangent(ctx->ring, vertex, ctx->seg)) { + return true; + } + } double rx = b.x-a.x; double ry = b.y-a.y; double sx = d.x-c.x; @@ -3110,6 +3143,7 @@ bool tg_ring_intersects_segment(const struct tg_ring *ring, // are on the outside and are passing over segments. If the segment passes // over at least two ring segments then it's intersecting. struct intersegiterctx ctx = { + .ring = ring, .seg = seg, .allow_on_edge = allow_on_edge, }; From 382dd81d08fa6f159d35c7142f4d5b263e2f4473 Mon Sep 17 00:00:00 2001 From: Dewey Dunnington Date: Fri, 18 Sep 2026 12:37:07 -0500 Subject: [PATCH 07/13] one more previously failing case --- tests/relations/poly.jsonc | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/relations/poly.jsonc b/tests/relations/poly.jsonc index b226afc..79fe3b9 100644 --- a/tests/relations/poly.jsonc +++ b/tests/relations/poly.jsonc @@ -832,4 +832,26 @@ "touches": ["T", "T"] } }, + // The line overlaps the polygon boundary, but neither line endpoint lies + // on that boundary. + { + "geoms": [ + "POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))", + "LINESTRING (-1 2, 3 2)" + ], + "dims": [ 2, 1 ], + "relate": [ "FF21F1102", "F11FF0212" ], + "predicates": { + "equals": ["F", "F"], + "intersects": ["T", "T"], + "disjoint": ["F", "F"], + "contains": ["F", "F"], + "within": ["F", "F"], + "covers": ["F", "F"], + "coveredby": ["F", "F"], + "crosses": ["F", "F"], + "overlaps": ["F", "F"], + "touches": ["T", "T"] + } + }, ] From 5bb244f1b96d819747166b19794eff14508e6f10 Mon Sep 17 00:00:00 2001 From: Dewey Dunnington Date: Fri, 18 Sep 2026 13:07:46 -0500 Subject: [PATCH 08/13] one more test case --- tests/relations/poly.jsonc | 42 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/relations/poly.jsonc b/tests/relations/poly.jsonc index 79fe3b9..d28e905 100644 --- a/tests/relations/poly.jsonc +++ b/tests/relations/poly.jsonc @@ -854,4 +854,46 @@ "touches": ["T", "T"] } }, + // Line that touches a polygon at a vertex of its hole + { + "geoms": [ + "POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), (3 3, 5 5, 7 3, 7 7, 3 7, 3 3))", + "LINESTRING (4 5, 6 5)" + ], + "dims": [ 2, 1 ], + "relate": [ "FF20F1102", "F01FF0212" ], + "predicates": { + "equals": ["F", "F"], + "intersects": ["T", "T"], + "disjoint": ["F", "F"], + "contains": ["F", "F"], + "within": ["F", "F"], + "covers": ["F", "F"], + "coveredby": ["F", "F"], + "crosses": ["F", "F"], + "overlaps": ["F", "F"], + "touches": ["T", "T"] + } + }, + // Line that touches a polygon at an edge of its hole (not just a vertex) + { + "geoms": [ + "POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), (3 3, 4 5, 6 5, 7 3, 7 7, 3 7, 3 3))", + "LINESTRING (3.5 5, 6.5 5)" + ], + "dims": [ 2, 1 ], + "relate": [ "FF20F1102", "F01FF0212" ], + "predicates": { + "equals": ["F", "F"], + "intersects": ["T", "T"], + "disjoint": ["F", "F"], + "contains": ["F", "F"], + "within": ["F", "F"], + "covers": ["F", "F"], + "coveredby": ["F", "F"], + "crosses": ["F", "F"], + "overlaps": ["F", "F"], + "touches": ["T", "T"] + } + }, ] From 72462d02451846c84e817ca13fd2b0a0ac04ce1f Mon Sep 17 00:00:00 2001 From: Dewey Dunnington Date: Mon, 28 Sep 2026 21:57:29 -0500 Subject: [PATCH 09/13] add failing tests cases --- tests/relations/mpoint.jsonc | 44 +++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/tests/relations/mpoint.jsonc b/tests/relations/mpoint.jsonc index ace2faa..502a821 100644 --- a/tests/relations/mpoint.jsonc +++ b/tests/relations/mpoint.jsonc @@ -198,5 +198,47 @@ "overlaps": ["F", "F"], "touches": ["F", "F"] } + }, + // Within and contains with one interior point and one boundary point. + { + "geoms": [ + "MULTIPOINT ((0 0), (1 1))", + "POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))" + ], + "dims": [ 0, 2 ], + "relate": [ "00FFFF212", "0F20F1FF2" ], + "predicates": { + "equals": ["F", "F"], + "intersects": ["T", "T"], + "disjoint": ["F", "F"], + "contains": ["F", "T"], + "within": ["T", "F"], + "covers": ["F", "T"], + "coveredby": ["T", "F"], + "crosses": ["F", "F"], + "overlaps": ["F", "F"], + "touches": ["F", "F"] + } + }, + // All points lie on the polygon boundary, so coveredby is true but within is false. + { + "geoms": [ + "MULTIPOINT ((0 0), (1 0))", + "POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))" + ], + "dims": [ 0, 2 ], + "relate": [ "F0FFFF212", "FF20F1FF2" ], + "predicates": { + "equals": ["F", "F"], + "intersects": ["T", "T"], + "disjoint": ["F", "F"], + "contains": ["F", "F"], + "within": ["F", "F"], + "covers": ["F", "T"], + "coveredby": ["T", "F"], + "crosses": ["F", "F"], + "overlaps": ["F", "F"], + "touches": ["T", "T"] + } } -] \ No newline at end of file +] From 6e7523a830454b0e03a8b3ad798ed12c993ef43e Mon Sep 17 00:00:00 2001 From: Dewey Dunnington Date: Mon, 28 Sep 2026 22:24:23 -0500 Subject: [PATCH 10/13] check covers and containment for multi geom vs polygon containment --- tg.c | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tg.c b/tg.c index 510eb53..33870fc 100644 --- a/tg.c +++ b/tg.c @@ -6122,17 +6122,20 @@ static bool poly_contains_base_geom(struct tg_poly *poly, case TG_MULTILINESTRING: case TG_MULTIPOLYGON: case TG_GEOMETRYCOLLECTION: { - bool contains = false; + bool covers = false; + int contains_count = 0; if (geom->multi && geom->multi->ngeoms > 0) { - contains = true; + covers = true; for (int i = 0; i < geom->multi->ngeoms; i++) { - if (!poly_contains_geom(poly, geom->multi->geoms[i])) { - contains = false; + if (!poly_covers_geom(poly, geom->multi->geoms[i])) { + covers = false; break; } + + contains_count += poly_contains_geom(poly, geom->multi->geoms[i]); } } - return contains; + return covers && contains_count > 0; }} } return false; From c574d3e8fcef85ffca8045dbe40ab1d792c514a3 Mon Sep 17 00:00:00 2001 From: Dewey Dunnington Date: Mon, 28 Sep 2026 22:30:56 -0500 Subject: [PATCH 11/13] skip interior containment checks that aren't necessary --- tg.c | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tg.c b/tg.c index 33870fc..edb45db 100644 --- a/tg.c +++ b/tg.c @@ -6122,20 +6122,19 @@ static bool poly_contains_base_geom(struct tg_poly *poly, case TG_MULTILINESTRING: case TG_MULTIPOLYGON: case TG_GEOMETRYCOLLECTION: { - bool covers = false; - int contains_count = 0; + bool contains = false; if (geom->multi && geom->multi->ngeoms > 0) { - covers = true; + contains = true; for (int i = 0; i < geom->multi->ngeoms; i++) { if (!poly_covers_geom(poly, geom->multi->geoms[i])) { - covers = false; + contains = false; break; } - contains_count += poly_contains_geom(poly, geom->multi->geoms[i]); + contains = contains || poly_contains_geom(poly, geom->multi->geoms[i]); } } - return covers && contains_count > 0; + return contains; }} } return false; From a138f2c96a87f8656bf0728d72a90b3ea1ba820e Mon Sep 17 00:00:00 2001 From: Dewey Dunnington Date: Mon, 28 Sep 2026 22:33:06 -0500 Subject: [PATCH 12/13] one more test case --- tests/relations/mpoint.jsonc | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/relations/mpoint.jsonc b/tests/relations/mpoint.jsonc index 502a821..81994de 100644 --- a/tests/relations/mpoint.jsonc +++ b/tests/relations/mpoint.jsonc @@ -240,5 +240,26 @@ "overlaps": ["F", "F"], "touches": ["T", "T"] } + }, + // An interior point followed by an outside point is neither within nor coveredby. + { + "geoms": [ + "MULTIPOINT ((1 1), (3 3))", + "POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))" + ], + "dims": [ 0, 2 ], + "relate": [ "0F0FFF212", "0F2FF10F2" ], + "predicates": { + "equals": ["F", "F"], + "intersects": ["T", "T"], + "disjoint": ["F", "F"], + "contains": ["F", "F"], + "within": ["F", "F"], + "covers": ["F", "F"], + "coveredby": ["F", "F"], + "crosses": ["T", "T"], + "overlaps": ["F", "F"], + "touches": ["F", "F"] + } } ] From 1079388684915d551f7c4982d6916df3fb3b297d Mon Sep 17 00:00:00 2001 From: Dewey Dunnington Date: Mon, 28 Sep 2026 23:07:01 -0500 Subject: [PATCH 13/13] fix covers for the collection with an empty case --- tests/relations/gcol.jsonc | 42 ++++++++++++++++++++++++++++++++++++++ tg.c | 14 +++++++++++-- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/tests/relations/gcol.jsonc b/tests/relations/gcol.jsonc index 5ea4662..bd26779 100644 --- a/tests/relations/gcol.jsonc +++ b/tests/relations/gcol.jsonc @@ -221,4 +221,46 @@ "touches": ["F", "F"] } }, + // An empty child does not prevent containment of the interior point. + { + "geoms": [ + "GEOMETRYCOLLECTION (POINT EMPTY, POINT (1 1))", + "POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))" + ], + "dims": [ 0, 2 ], + "relate": [ "0FFFFF212", "0F2FF1FF2" ], + "predicates": { + "equals": ["F", "F"], + "intersects": ["T", "T"], + "disjoint": ["F", "F"], + "contains": ["F", "T"], + "within": ["T", "F"], + "covers": ["F", "T"], + "coveredby": ["T", "F"], + "crosses": ["F", "F"], + "overlaps": ["F", "F"], + "touches": ["F", "F"] + } + }, + // A collection containing only empty points is not within or coveredby the polygon. + { + "geoms": [ + "GEOMETRYCOLLECTION (POINT EMPTY, POINT EMPTY)", + "POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))" + ], + "dims": [ 0, 2 ], + "relate": [ "FFFFFF212", "FF2FF1FF2" ], + "predicates": { + "equals": ["F", "F"], + "intersects": ["F", "F"], + "disjoint": ["T", "T"], + "contains": ["F", "F"], + "within": ["F", "F"], + "covers": ["F", "F"], + "coveredby": ["F", "F"], + "crosses": ["F", "F"], + "overlaps": ["F", "F"], + "touches": ["F", "F"] + } + }, ] diff --git a/tg.c b/tg.c index edb45db..5d19fef 100644 --- a/tg.c +++ b/tg.c @@ -5939,12 +5939,18 @@ static bool poly_covers_base_geom(struct tg_poly *poly, case TG_MULTIPOLYGON: case TG_GEOMETRYCOLLECTION: if (!geom->multi || geom->multi->ngeoms == 0) return false; + int empty_count = 0; for (int i = 0; i < geom->multi->ngeoms; i++) { + if ((geom->multi->geoms[i]->head.flags&IS_EMPTY) == IS_EMPTY) { + ++empty_count; + continue; + } + if (!poly_covers_geom(poly, geom->multi->geoms[i])) { return false; } } - return true; + return empty_count != geom->multi->ngeoms; } } return false; @@ -6124,13 +6130,17 @@ static bool poly_contains_base_geom(struct tg_poly *poly, case TG_GEOMETRYCOLLECTION: { bool contains = false; if (geom->multi && geom->multi->ngeoms > 0) { - contains = true; for (int i = 0; i < geom->multi->ngeoms; i++) { + if ((geom->multi->geoms[i]->head.flags&IS_EMPTY) == IS_EMPTY) { + continue; + } + if (!poly_covers_geom(poly, geom->multi->geoms[i])) { contains = false; break; } + // Require at least one interior intersection contains = contains || poly_contains_geom(poly, geom->multi->geoms[i]); } }