11#include " domain/ImportValidationService.h"
22
3+ #include < algorithm>
4+ #include < cmath>
5+ #include < limits>
36#include < string>
47#include < unordered_map>
58#include < unordered_set>
@@ -9,6 +12,12 @@ namespace safecrowd::domain {
912namespace {
1013
1114constexpr double kMinimumConnectionWidth = 0.9 ;
15+ constexpr double kConnectionBoundaryTolerance = 0.25 ;
16+
17+ struct Vector2D {
18+ double x{0.0 };
19+ double y{0.0 };
20+ };
1221
1322bool hasValidFloorReference (const std::unordered_set<std::string>& floorIds, const std::string& floorId) {
1423 return floorIds.empty () || (!floorId.empty () && floorIds.contains (floorId));
@@ -19,6 +28,195 @@ bool isVerticalConnection(const Connection2D& connection) {
1928 || connection.isStair || connection.isRamp ;
2029}
2130
31+ Vector2D subtract (const Point2D& lhs, const Point2D& rhs) {
32+ return {
33+ .x = lhs.x - rhs.x ,
34+ .y = lhs.y - rhs.y ,
35+ };
36+ }
37+
38+ Point2D add (const Point2D& point, const Vector2D& delta) {
39+ return {
40+ .x = point.x + delta.x ,
41+ .y = point.y + delta.y ,
42+ };
43+ }
44+
45+ Vector2D scale (const Vector2D& value, double factor) {
46+ return {
47+ .x = value.x * factor,
48+ .y = value.y * factor,
49+ };
50+ }
51+
52+ double dot (const Vector2D& lhs, const Vector2D& rhs) {
53+ return (lhs.x * rhs.x ) + (lhs.y * rhs.y );
54+ }
55+
56+ double length (const Vector2D& value) {
57+ return std::sqrt (dot (value, value));
58+ }
59+
60+ Vector2D normalize (const Vector2D& value) {
61+ const double magnitude = length (value);
62+ if (magnitude <= 1e-12 ) {
63+ return {};
64+ }
65+
66+ return scale (value, 1.0 / magnitude);
67+ }
68+
69+ double distanceBetween (const Point2D& lhs, const Point2D& rhs) {
70+ return length (subtract (lhs, rhs));
71+ }
72+
73+ Point2D segmentMidpoint (const LineSegment2D& segment) {
74+ return {
75+ .x = (segment.start .x + segment.end .x ) * 0.5 ,
76+ .y = (segment.start .y + segment.end .y ) * 0.5 ,
77+ };
78+ }
79+
80+ Vector2D segmentNormal (const LineSegment2D& segment) {
81+ const auto direction = normalize (subtract (segment.end , segment.start ));
82+ return {
83+ .x = -direction.y ,
84+ .y = direction.x ,
85+ };
86+ }
87+
88+ double distancePointToSegment (const Point2D& point, const LineSegment2D& segment) {
89+ const auto dx = segment.end .x - segment.start .x ;
90+ const auto dy = segment.end .y - segment.start .y ;
91+ const auto lengthSquared = dx * dx + dy * dy;
92+ if (lengthSquared <= 1e-12 ) {
93+ return distanceBetween (point, segment.start );
94+ }
95+
96+ const auto t = std::clamp (
97+ ((point.x - segment.start .x ) * dx + (point.y - segment.start .y ) * dy) / lengthSquared,
98+ 0.0 ,
99+ 1.0 );
100+ const Point2D projected{
101+ .x = segment.start .x + t * dx,
102+ .y = segment.start .y + t * dy,
103+ };
104+ return distanceBetween (point, projected);
105+ }
106+
107+ double distancePointToRingBoundary (const Point2D& point, const std::vector<Point2D>& ring) {
108+ if (ring.empty ()) {
109+ return std::numeric_limits<double >::infinity ();
110+ }
111+
112+ auto bestDistance = std::numeric_limits<double >::infinity ();
113+ for (std::size_t index = 0 ; index < ring.size (); ++index) {
114+ const auto & start = ring[index];
115+ const auto & end = ring[(index + 1 ) % ring.size ()];
116+ bestDistance = std::min (bestDistance, distancePointToSegment (point, {.start = start, .end = end}));
117+ }
118+ return bestDistance;
119+ }
120+
121+ double distancePointToPolygonBoundary (const Point2D& point, const Polygon2D& polygon) {
122+ auto bestDistance = distancePointToRingBoundary (point, polygon.outline );
123+ for (const auto & hole : polygon.holes ) {
124+ bestDistance = std::min (bestDistance, distancePointToRingBoundary (point, hole));
125+ }
126+ return bestDistance;
127+ }
128+
129+ bool pointInRingInclusive (const Point2D& point, const std::vector<Point2D>& ring) {
130+ if (ring.size () < 3 ) {
131+ return false ;
132+ }
133+
134+ bool inside = false ;
135+ for (std::size_t index = 0 , previous = ring.size () - 1 ; index < ring.size (); previous = index++) {
136+ const auto & start = ring[previous];
137+ const auto & end = ring[index];
138+ if (distancePointToSegment (point, {.start = start, .end = end}) <= kConnectionBoundaryTolerance ) {
139+ return true ;
140+ }
141+
142+ const bool crossesY = (start.y > point.y ) != (end.y > point.y );
143+ if (!crossesY) {
144+ continue ;
145+ }
146+
147+ const auto xAtPointY = start.x + ((point.y - start.y ) * (end.x - start.x ) / (end.y - start.y ));
148+ if (point.x <= xAtPointY + 1e-12 ) {
149+ inside = !inside;
150+ }
151+ }
152+
153+ return inside;
154+ }
155+
156+ bool pointInPolygonInclusive (const Point2D& point, const Polygon2D& polygon) {
157+ if (!pointInRingInclusive (point, polygon.outline )) {
158+ return false ;
159+ }
160+
161+ for (const auto & hole : polygon.holes ) {
162+ if (pointInRingInclusive (point, hole)) {
163+ return false ;
164+ }
165+ }
166+
167+ return true ;
168+ }
169+
170+ bool spanTouchesPolygon (const LineSegment2D& span, const Polygon2D& polygon) {
171+ const auto midpoint = segmentMidpoint (span);
172+ const auto normal = segmentNormal (span);
173+ const double probeDistance = std::max (0.35 , distanceBetween (span.start , span.end ) * 0.35 );
174+
175+ const std::vector<Point2D> probes = {
176+ span.start ,
177+ span.end ,
178+ midpoint,
179+ add (midpoint, scale (normal, probeDistance)),
180+ add (midpoint, scale (normal, -probeDistance)),
181+ };
182+
183+ for (const auto & probe : probes) {
184+ if (pointInPolygonInclusive (probe, polygon)
185+ || distancePointToPolygonBoundary (probe, polygon) <= kConnectionBoundaryTolerance ) {
186+ return true ;
187+ }
188+ }
189+
190+ return false ;
191+ }
192+
193+ const Zone2D* findZoneById (const FacilityLayout2D& layout, const std::string& zoneId) {
194+ const auto it = std::find_if (layout.zones .begin (), layout.zones .end (), [&](const auto & zone) {
195+ return zone.id == zoneId;
196+ });
197+ return it == layout.zones .end () ? nullptr : &(*it);
198+ }
199+
200+ bool connectionSpanMatchesReferencedZones (const FacilityLayout2D& layout, const Connection2D& connection) {
201+ if (isVerticalConnection (connection)) {
202+ return true ;
203+ }
204+
205+ const auto * fromZone = findZoneById (layout, connection.fromZoneId );
206+ const auto * toZone = findZoneById (layout, connection.toZoneId );
207+ if (fromZone == nullptr || toZone == nullptr ) {
208+ return true ;
209+ }
210+
211+ if (connection.kind == ConnectionKind::Exit) {
212+ const auto * walkableZone = fromZone->kind == ZoneKind::Exit ? toZone : fromZone;
213+ return spanTouchesPolygon (connection.centerSpan , walkableZone->area );
214+ }
215+
216+ return spanTouchesPolygon (connection.centerSpan , fromZone->area )
217+ && spanTouchesPolygon (connection.centerSpan , toZone->area );
218+ }
219+
22220bool canTravel (const Connection2D& connection, const std::string& fromZoneId, const std::string& toZoneId) {
23221 switch (connection.directionality ) {
24222 case TravelDirection::Bidirectional:
@@ -190,6 +388,16 @@ std::vector<ImportIssue> ImportValidationService::validate(const FacilityLayout2
190388 .targetId = connection.toZoneId ,
191389 });
192390 }
391+ if (!connectionSpanMatchesReferencedZones (layout, connection)) {
392+ issues.push_back ({
393+ .severity = ImportIssueSeverity::Error,
394+ .code = ImportIssueCode::InvalidGeometry,
395+ .message = " Connection span is not aligned with the referenced zone boundary." ,
396+ .sourceId = connection.id ,
397+ .targetId = connection.toZoneId ,
398+ .isBlocking = true ,
399+ });
400+ }
193401 }
194402
195403 for (const auto & zone : layout.zones ) {
0 commit comments