diff --git a/ResPlan.Library/Models.cs b/ResPlan.Library/Models.cs index 27ddf61..701633c 100644 --- a/ResPlan.Library/Models.cs +++ b/ResPlan.Library/Models.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Text.Json.Serialization; using MessagePack; using NetTopologySuite.Geometries; +using NetTopologySuite.Geometries.Utilities; using ResPlan.Library.Data; namespace ResPlan.Library @@ -51,6 +53,61 @@ public class Plan [Key(3)] public Graph ReferenceGraph { get; set; } + + public void Rotate(double angleRadians, Coordinate center) + { + var transform = new AffineTransformation(); + transform.Rotate(angleRadians, center.X, center.Y); + + ApplyTransformation(transform); + } + + public void Translate(double dx, double dy) + { + var transform = new AffineTransformation(); + transform.Translate(dx, dy); + + ApplyTransformation(transform); + } + + private void ApplyTransformation(AffineTransformation transform) + { + // Transform geometries + foreach (var key in Geometries.Keys.ToList()) + { + var originalList = Geometries[key]; + var newList = new List(); + foreach (var geom in originalList) + { + var newGeom = transform.Transform(geom); + newList.Add(newGeom); + } + Geometries[key] = newList; + } + + // Update Bounds + var newEnvelope = new Envelope(); + foreach (var list in Geometries.Values) + { + foreach (var geom in list) + { + newEnvelope.ExpandToInclude(geom.EnvelopeInternal); + } + } + Bounds = newEnvelope; + + // Transform Graph Nodes + if (ReferenceGraph != null && ReferenceGraph.Nodes != null) + { + foreach (var node in ReferenceGraph.Nodes.Values) + { + if (node.Geometry != null) + { + node.Geometry = transform.Transform(node.Geometry); + } + } + } + } } [MessagePackObject] diff --git a/ResPlan.Library/PlanGenerationConstraints.cs b/ResPlan.Library/PlanGenerationConstraints.cs new file mode 100644 index 0000000..f3abc62 --- /dev/null +++ b/ResPlan.Library/PlanGenerationConstraints.cs @@ -0,0 +1,13 @@ +using System; +using System.Numerics; +using NetTopologySuite.Geometries; + +namespace ResPlan.Library +{ + public class PlanGenerationConstraints + { + public Polygon BoundingPolygon { get; set; } + public Vector2? FrontDoorFacing { get; set; } + public Vector2? GarageFacing { get; set; } + } +} diff --git a/ResPlan.Library/PlanLoader.cs b/ResPlan.Library/PlanLoader.cs index c045ec4..6b281c0 100644 --- a/ResPlan.Library/PlanLoader.cs +++ b/ResPlan.Library/PlanLoader.cs @@ -16,11 +16,14 @@ public class PlanLoader { private static readonly WKTReader _wktReader = new WKTReader(); - public static async Task> LoadPlansAsync(string jsonPath = null, string pklPathOverride = null, int? maxItems = null, Action logger = null) + public static async Task> LoadPlansAsync(string jsonPath = null, string pklPathOverride = null, int? maxItems = null, Action logger = null, PlanGenerationConstraints constraints = null) { if (!string.IsNullOrEmpty(jsonPath) && File.Exists(jsonPath)) { - return LoadPlansFromJson(jsonPath); + // In this path, we don't apply constraints inside LoadPlansFromJson for now, or we should? + // The prompt says "When using the API". It's better to be consistent. + var loaded = LoadPlansFromJson(jsonPath); + return ApplyConstraints(loaded, constraints, logger); } // Python Loading Path (Subprocess approach due to Python.NET threading issues in some envs) @@ -115,7 +118,79 @@ public static async Task> LoadPlansAsync(string jsonPath = null, stri } } - return plans; + return ApplyConstraints(plans, constraints, actualLogger); + } + + private static List ApplyConstraints(List plans, PlanGenerationConstraints constraints, Action logger) + { + if (constraints == null) return plans; + + if (constraints.GarageFacing.HasValue) + { + logger?.Invoke("Warning: Garage/Driveway facing constraints are not supported by the current ResPlan dataset."); + } + + var result = new List(); + foreach (var plan in plans) + { + // Apply Facing Constraint + if (constraints.FrontDoorFacing.HasValue) + { + // Find front door + Geometry frontDoor = null; + if (plan.Geometries.ContainsKey("front_door")) + { + var fds = plan.Geometries["front_door"]; + if (fds.Count > 0) + { + // If multiple, pick first? + frontDoor = fds[0]; + } + } + + if (frontDoor != null) + { + var centroid = frontDoor.Centroid.Coordinate; + var planCenter = plan.Bounds.Centre; // Envelope center + + // Vector from plan center to door center + var currentVec = new NetTopologySuite.Geometries.Coordinate(centroid.X - planCenter.X, centroid.Y - planCenter.Y); + + // If center and door are same (rare), skip + if (Math.Abs(currentVec.X) > 1e-6 || Math.Abs(currentVec.Y) > 1e-6) + { + var targetX = constraints.FrontDoorFacing.Value.X; + var targetY = constraints.FrontDoorFacing.Value.Y; + + // Angle of current vector + var currentAngle = Math.Atan2(currentVec.Y, currentVec.X); + // Angle of target + var targetAngle = Math.Atan2(targetY, targetX); + + var diff = targetAngle - currentAngle; + + plan.Rotate(diff, planCenter); + } + } + } + + // Apply Bounding Constraint + if (constraints.BoundingPolygon != null) + { + // Check if plan bounds are within polygon + // Since plan.Bounds is an Envelope, we convert to geometry + var geometryFactory = new GeometryFactory(); + var planGeom = geometryFactory.ToGeometry(plan.Bounds); + + if (!constraints.BoundingPolygon.Contains(planGeom)) + { + continue; // Filter out + } + } + + result.Add(plan); + } + return result; } private static Plan ConvertDataToPlan(ResPlanData data)