Skip to content

Commit 9e62bcb

Browse files
committed
Require projected surface height ranges
1 parent f32a756 commit 9e62bcb

4 files changed

Lines changed: 138 additions & 49 deletions

File tree

src/PlateauResoniteLink/Application/Importing/CityGmlSurfaceProjectionPolicy.cs

Lines changed: 42 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -41,24 +41,24 @@ internal static HashSet<string> GetCulledSurfaceIdsBeforeProjection(
4141
return [];
4242
}
4343

44-
SurfaceProjectionInfo[] candidates = surfaces
45-
.Select(surface => CreateSurfaceProjectionInfo(surface, cityObjectOrigin, cityObjectCartesian))
46-
.Where(static info => info.MinimumY.HasValue && info.MaximumY.HasValue)
47-
.ToArray();
44+
SurfaceProjectionInfo[] candidates = CreateSurfaceProjectionInfos(
45+
surfaces,
46+
cityObjectOrigin,
47+
cityObjectCartesian);
4848

4949
if (candidates.Length == 0)
5050
{
5151
return [];
5252
}
5353

54-
double objectMinimumY = candidates.Min(static info => info.MinimumY!.Value);
55-
double objectMaximumY = candidates.Max(static info => info.MaximumY!.Value);
54+
double objectMinimumY = candidates.Min(static info => info.MinimumY);
55+
double objectMaximumY = candidates.Max(static info => info.MaximumY);
5656

5757
return candidates
5858
.Where(static info => !info.IsGeneratedLod1RoofSurface)
5959
.Where(static info => info.IsNearHorizontal)
60-
.Where(info => info.MaximumY!.Value <= objectMinimumY + BuildingBottomCullBandMeters)
61-
.Where(info => objectMaximumY > info.MaximumY!.Value + BuildingBottomCullBandMeters)
60+
.Where(info => info.MaximumY <= objectMinimumY + BuildingBottomCullBandMeters)
61+
.Where(info => objectMaximumY > info.MaximumY + BuildingBottomCullBandMeters)
6262
.Select(static info => info.PolygonId)
6363
.ToHashSet(StringComparer.Ordinal);
6464
}
@@ -74,10 +74,10 @@ internal static HashSet<string> GetCulledSurfaceIdsBeforeProjection(
7474
return null;
7575
}
7676

77-
SurfaceProjectionInfo[] surfaceInfos = surfaces
78-
.Select(surface => CreateSurfaceProjectionInfo(surface, cityObjectOrigin, cityObjectCartesian))
79-
.Where(static info => info.MinimumY.HasValue && info.MaximumY.HasValue)
80-
.ToArray();
77+
SurfaceProjectionInfo[] surfaceInfos = CreateSurfaceProjectionInfos(
78+
surfaces,
79+
cityObjectOrigin,
80+
cityObjectCartesian);
8181
if (surfaceInfos.Length == 0)
8282
{
8383
return null;
@@ -118,43 +118,63 @@ internal static HashSet<string> GetCulledSurfaceIdsBeforeProjection(
118118
return ComputePolygonNormal(positions);
119119
}
120120

121-
private static SurfaceProjectionInfo CreateSurfaceProjectionInfo(
122-
ParsedSurface surface,
121+
private static SurfaceProjectionInfo[] CreateSurfaceProjectionInfos(
122+
IEnumerable<ParsedSurface> surfaces,
123123
GeodeticPoint cityObjectOrigin,
124124
LocalCartesian? cityObjectCartesian)
125125
{
126+
List<SurfaceProjectionInfo> infos = [];
127+
foreach (ParsedSurface surface in surfaces)
128+
{
129+
if (TryCreateSurfaceProjectionInfo(surface, cityObjectOrigin, cityObjectCartesian, out SurfaceProjectionInfo info))
130+
{
131+
infos.Add(info);
132+
}
133+
}
134+
135+
return [.. infos];
136+
}
137+
138+
private static bool TryCreateSurfaceProjectionInfo(
139+
ParsedSurface surface,
140+
GeodeticPoint cityObjectOrigin,
141+
LocalCartesian? cityObjectCartesian,
142+
out SurfaceProjectionInfo info)
143+
{
144+
info = default;
126145
Float3[] positions = surface.Vertices
127146
.Select(point => CreateScenePosition(point, cityObjectOrigin, cityObjectCartesian))
128147
.ToArray();
129148
if (positions.Length == 0)
130149
{
131-
return new SurfaceProjectionInfo(surface.PolygonId, null, null, false, IsGeneratedLod1RoofSurface(surface.PolygonId));
150+
return false;
132151
}
133152

134153
Float3? normal = ComputePolygonNormal(positions);
135154
bool isNearHorizontal = normal is not null && Math.Abs(normal.Y) >= 0.98;
136155

137-
return new SurfaceProjectionInfo(
156+
info = new SurfaceProjectionInfo(
138157
surface.PolygonId,
139158
positions.Min(static position => position.Y),
140159
positions.Max(static position => position.Y),
141160
isNearHorizontal,
142161
IsGeneratedLod1RoofSurface(surface.PolygonId));
162+
return true;
143163
}
144164

145165
private static (double MinimumY, double MaximumY) ResolveFacadeUvVerticalRange(
146166
IReadOnlyList<SurfaceProjectionInfo> contextSurfaceInfos,
147167
IReadOnlyList<SurfaceProjectionInfo> allSurfaceInfos)
148168
{
149-
double minimumY = contextSurfaceInfos.Min(static info => info.MinimumY!.Value);
150-
double maximumY = contextSurfaceInfos.Max(static info => info.MaximumY!.Value);
169+
double minimumY = contextSurfaceInfos.Min(static info => info.MinimumY);
170+
double maximumY = contextSurfaceInfos.Max(static info => info.MaximumY);
151171
if (maximumY - minimumY > 1e-6 || contextSurfaceInfos.Count == allSurfaceInfos.Count)
152172
{
153173
return (minimumY, maximumY);
154174
}
155175

156-
double fallbackMinimumY = allSurfaceInfos.Min(static info => info.MinimumY!.Value);
157-
double fallbackMaximumY = allSurfaceInfos.Max(static info => info.MaximumY!.Value);
176+
double fallbackMinimumY = allSurfaceInfos.Min(static info => info.MinimumY);
177+
double fallbackMaximumY = allSurfaceInfos.Max(static info => info.MaximumY);
158178
return fallbackMaximumY - fallbackMinimumY > maximumY - minimumY
159179
? (fallbackMinimumY, fallbackMaximumY)
160180
: (minimumY, maximumY);
@@ -215,8 +235,8 @@ private static Float3 CreateScenePosition(
215235

216236
private readonly record struct SurfaceProjectionInfo(
217237
string PolygonId,
218-
double? MinimumY,
219-
double? MaximumY,
238+
double MinimumY,
239+
double MaximumY,
220240
bool IsNearHorizontal,
221241
bool IsGeneratedLod1RoofSurface);
222242
}

src/PlateauResoniteLink/Application/Importing/GeneratedLod1RoofCityObjectFactory.cs

Lines changed: 47 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -152,10 +152,10 @@ private static bool TryCreateNoWallRoofSlab(
152152
return false;
153153
}
154154

155-
SurfaceProjectionInfo[] surfaceInfos = cityObject.Surfaces
156-
.Select(surface => CreateSurfaceProjectionInfo(surface, cityObjectOrigin, cityObjectCartesian))
157-
.Where(static info => info.MinimumY.HasValue && info.MaximumY.HasValue)
158-
.ToArray();
155+
SurfaceProjectionInfo[] surfaceInfos = CreateSurfaceProjectionInfos(
156+
cityObject.Surfaces,
157+
cityObjectOrigin,
158+
cityObjectCartesian);
159159
if (surfaceInfos.Length == 0)
160160
{
161161
return false;
@@ -188,19 +188,19 @@ private static bool TryGetNoWallTopSurfaces(
188188
out ParsedSurface[]? topSurfaces)
189189
{
190190
topSurfaces = null;
191-
SurfaceProjectionInfo[] surfaceInfos = cityObject.Surfaces
192-
.Select(surface => CreateSurfaceProjectionInfo(surface, cityObjectOrigin, cityObjectCartesian))
193-
.Where(static info => info.MinimumY.HasValue && info.MaximumY.HasValue)
194-
.ToArray();
191+
SurfaceProjectionInfo[] surfaceInfos = CreateSurfaceProjectionInfos(
192+
cityObject.Surfaces,
193+
cityObjectOrigin,
194+
cityObjectCartesian);
195195
if (surfaceInfos.Length == 0)
196196
{
197197
return false;
198198
}
199199

200-
double objectMaximumY = surfaceInfos.Max(static info => info.MaximumY!.Value);
200+
double objectMaximumY = surfaceInfos.Max(static info => info.MaximumY);
201201
SurfaceProjectionInfo[] topSurfaceInfos = surfaceInfos
202202
.Where(static info => info.IsNearHorizontal)
203-
.Where(info => info.MaximumY!.Value >= objectMaximumY - 0.1)
203+
.Where(info => info.MaximumY >= objectMaximumY - 0.1)
204204
.ToArray();
205205
if (topSurfaceInfos.Length == 0
206206
|| topSurfaceInfos.Any(static info => info.Surface.InteriorRings.Length != 0))
@@ -217,10 +217,10 @@ private static bool TryGetNoWallTopSurfaces(
217217
.ToArray();
218218
if (lowerSurfaceInfos.Length != 0)
219219
{
220-
double objectMinimumY = lowerSurfaceInfos.Min(static info => info.MinimumY!.Value);
220+
double objectMinimumY = lowerSurfaceInfos.Min(static info => info.MinimumY);
221221
topSurfaceInfos = topSurfaceInfos
222-
.Where(info => info.MinimumY!.Value > objectMinimumY + BuildingBottomCullBandMeters)
223-
.Where(info => info.MinimumY!.Value - NoWallRoofThicknessMeters > objectMinimumY + BuildingBottomCullBandMeters)
222+
.Where(info => info.MinimumY > objectMinimumY + BuildingBottomCullBandMeters)
223+
.Where(info => info.MinimumY - NoWallRoofThicknessMeters > objectMinimumY + BuildingBottomCullBandMeters)
224224
.ToArray();
225225
if (topSurfaceInfos.Length == 0)
226226
{
@@ -442,22 +442,22 @@ private static bool TryCreateFootprint(
442442
out Lod1RoofFootprint? footprint)
443443
{
444444
footprint = null;
445-
SurfaceProjectionInfo[] surfaceInfos = cityObject.Surfaces
446-
.Select(surface => CreateSurfaceProjectionInfo(surface, cityObjectOrigin, cityObjectCartesian))
447-
.Where(static info => info.MinimumY.HasValue && info.MaximumY.HasValue)
448-
.ToArray();
445+
SurfaceProjectionInfo[] surfaceInfos = CreateSurfaceProjectionInfos(
446+
cityObject.Surfaces,
447+
cityObjectOrigin,
448+
cityObjectCartesian);
449449
if (surfaceInfos.Length == 0)
450450
{
451451
return false;
452452
}
453453

454-
double objectMinimumY = surfaceInfos.Min(static info => info.MinimumY!.Value);
455-
double objectMaximumY = surfaceInfos.Max(static info => info.MaximumY!.Value);
454+
double objectMinimumY = surfaceInfos.Min(static info => info.MinimumY);
455+
double objectMaximumY = surfaceInfos.Max(static info => info.MaximumY);
456456
double geometryHeight = objectMaximumY - objectMinimumY;
457457
SurfaceProjectionInfo[] topCandidates = surfaceInfos
458458
.Where(static info => info.IsNearHorizontal)
459-
.Where(info => info.MaximumY!.Value >= objectMaximumY - 0.1)
460-
.Where(info => info.MinimumY!.Value > objectMinimumY + BuildingBottomCullBandMeters)
459+
.Where(info => info.MaximumY >= objectMaximumY - 0.1)
460+
.Where(info => info.MinimumY > objectMinimumY + BuildingBottomCullBandMeters)
461461
.ToArray();
462462
if (topCandidates.Length != 1)
463463
{
@@ -495,27 +495,47 @@ private static bool TryCreateFootprint(
495495
return true;
496496
}
497497

498-
private static SurfaceProjectionInfo CreateSurfaceProjectionInfo(
499-
ParsedSurface surface,
498+
private static SurfaceProjectionInfo[] CreateSurfaceProjectionInfos(
499+
IEnumerable<ParsedSurface> surfaces,
500500
GeodeticPoint cityObjectOrigin,
501501
LocalCartesian cityObjectCartesian)
502502
{
503+
List<SurfaceProjectionInfo> infos = [];
504+
foreach (ParsedSurface surface in surfaces)
505+
{
506+
if (TryCreateSurfaceProjectionInfo(surface, cityObjectOrigin, cityObjectCartesian, out SurfaceProjectionInfo info))
507+
{
508+
infos.Add(info);
509+
}
510+
}
511+
512+
return [.. infos];
513+
}
514+
515+
private static bool TryCreateSurfaceProjectionInfo(
516+
ParsedSurface surface,
517+
GeodeticPoint cityObjectOrigin,
518+
LocalCartesian cityObjectCartesian,
519+
out SurfaceProjectionInfo info)
520+
{
521+
info = default;
503522
Float3[] positions = surface.Vertices
504523
.Select(point => CreateScenePosition(point, cityObjectOrigin, cityObjectCartesian))
505524
.ToArray();
506525
if (positions.Length == 0)
507526
{
508-
return new SurfaceProjectionInfo(surface, null, null, false);
527+
return false;
509528
}
510529

511530
Float3? normal = ComputePolygonNormal(positions);
512531
bool isNearHorizontal = normal is not null && Math.Abs(normal.Y) >= 0.98;
513532

514-
return new SurfaceProjectionInfo(
533+
info = new SurfaceProjectionInfo(
515534
surface,
516535
positions.Min(static position => position.Y),
517536
positions.Max(static position => position.Y),
518537
isNearHorizontal);
538+
return true;
519539
}
520540

521541
private static GeodeticPoint[] RemoveClosingPoint(GeodeticPoint[] vertices)
@@ -682,8 +702,8 @@ private static double Dot(Float3 left, Float3 right)
682702

683703
private readonly record struct SurfaceProjectionInfo(
684704
ParsedSurface Surface,
685-
double? MinimumY,
686-
double? MaximumY,
705+
double MinimumY,
706+
double MaximumY,
687707
bool IsNearHorizontal);
688708

689709
private readonly record struct NoWallRoofRing(

tests/PlateauResoniteLink.Tests/Profiles/CityGmlSurfaceProjectionPolicyTests.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,28 @@ public void TryCreateFacadeUvProjectionContextIgnoresGeneratedNoWallRoofSurfaces
100100
Assert.InRange(context.Value.MaximumY, 6.0 - 1e-5, 6.0 + 1e-5);
101101
}
102102

103+
[Fact]
104+
public void TryCreateFacadeUvProjectionContextSkipsEmptySurfacesBeforeResolvingHeightRange()
105+
{
106+
GeodeticPoint origin = new(35.0, 139.0, 0.0);
107+
LocalCartesian cartesian = CreateCartesian(origin);
108+
ParsedSurface empty = CreateSurface("empty", ParsedSurfaceSemantic.Wall, []);
109+
ParsedSurface wall = CreateSurface(
110+
"wall",
111+
ParsedSurfaceSemantic.Wall,
112+
CreateVerticalQuadVertices(origin, widthMeters: 8.0, heightMeters: 6.0));
113+
114+
FacadeUvProjectionContext? context = CityGmlSurfaceProjectionPolicy.TryCreateFacadeUvProjectionContext(
115+
"bldg",
116+
[empty, wall],
117+
origin,
118+
cartesian);
119+
120+
Assert.NotNull(context);
121+
Assert.InRange(context.Value.MinimumY, -1e-5, 1e-5);
122+
Assert.InRange(context.Value.MaximumY, 6.0 - 1e-5, 6.0 + 1e-5);
123+
}
124+
103125
[Fact]
104126
public void GetCulledSurfaceIdsBeforeProjectionKeepsGeneratedNoWallRoofBottomAtObjectMinimum()
105127
{

tests/PlateauResoniteLink.Tests/Profiles/GeneratedLod1RoofCityObjectFactoryTests.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,33 @@ BuildingAttributeContext.Empty with
508508
static surface => Assert.Null(surface.TexturePayload));
509509
}
510510

511+
[Fact]
512+
public void CreateSkipsEmptySurfacesBeforeSelectingGeneratedRoofTopCandidate()
513+
{
514+
ParsedSurface empty = CreateSurface("empty", ParsedSurfaceSemantic.Roof, [], null, texturePayload: null);
515+
ParsedSurface top = CreateSurface(
516+
"lod1-top",
517+
ParsedSurfaceSemantic.Roof,
518+
altitude: 10.0);
519+
ParsedSurface bottom = CreateSurface(
520+
"lod1-bottom",
521+
ParsedSurfaceSemantic.Ground,
522+
altitude: 0.0);
523+
ParsedCityObject cityObject = CreateCityObject(
524+
[empty, top, bottom],
525+
CoordinateReferenceSystem.Parse("EPSG:6697"),
526+
BuildingAttributeContext.Empty with
527+
{
528+
RoofShape = new BuildingCodeValue<CityGmlRoofShape>(CityGmlRoofShape.Shed, "shed"),
529+
});
530+
531+
ParsedCityObject generated = GeneratedLod1RoofCityObjectFactory.Create(cityObject);
532+
533+
Assert.DoesNotContain(generated.Surfaces, static surface => surface.PolygonId == "lod1-top");
534+
Assert.Contains(generated.Surfaces, static surface => surface.PolygonId == "empty");
535+
Assert.Equal(4, generated.Surfaces.Count(static surface => surface.PolygonId.Contains("_generated_", System.StringComparison.Ordinal)));
536+
}
537+
511538
[Fact]
512539
public void CreateSkipsObjectThatAlreadyHasGeneratedRoofSurface()
513540
{

0 commit comments

Comments
 (0)