diff --git a/src/SimplePZMapmaker.Core/GeographicImporter.WorldBuildings.cs b/src/SimplePZMapmaker.Core/GeographicImporter.WorldBuildings.cs index 3597a19..65c02f9 100644 --- a/src/SimplePZMapmaker.Core/GeographicImporter.WorldBuildings.cs +++ b/src/SimplePZMapmaker.Core/GeographicImporter.WorldBuildings.cs @@ -7,7 +7,9 @@ public static (int Added,int Skipped) ImportWorldBuildings(string json,WorldProj foreach(var feature in features.Where(f=>f.Polygon&&f.Tags.ContainsKey("building"))){ token.ThrowIfCancellationRequested();var sourceId=feature.Tags.GetValueOrDefault("__pz_source");if(sourceId is not null&&existing.Contains(sourceId))continue; var rings=feature.Rings.Select(r=>r.Select(point=>{var p=projection.Project(point);return(X:p.X+reference.OriginCellX*256,Y:p.Y+reference.OriginCellY*256);}).ToList()).ToList();var ring=rings[0];var x=(int)Math.Floor(ring.Min(p=>p.X));var y=(int)Math.Floor(ring.Min(p=>p.Y));var width=Math.Max(6,(int)Math.Ceiling(ring.Max(p=>p.X)-x));var height=Math.Max(6,(int)Math.Ceiling(ring.Max(p=>p.Y)-y));if(width>64||height>64)continue; - var owners=new[]{WorldAssembler.MemberAt(world,x,y),WorldAssembler.MemberAt(world,x+width,y),WorldAssembler.MemberAt(world,x,y+height),WorldAssembler.MemberAt(world,x+width,y+height)};if(owners.Any(m=>m is null)||owners.Select(m=>m!.Id).Distinct().Count()<2)continue; + // shared when the footprint or the free line a section keeps around a building (MapGenerator.CanPlace) reaches another section; + // a section rejects such an outline as outside its map, so it would be lost at the seam + var owners=new[]{x-1,x,x+width}.SelectMany(cx=>new[]{y-1,y,y+height}.Select(cy=>WorldAssembler.MemberAt(world,cx,cy))).ToArray();if(new[]{owners[4],owners[5],owners[7],owners[8]}.Any(m=>m is null)||owners.Where(m=>m is not null).Select(m=>m!.Id).Distinct().Count()<2)continue; var tags=feature.Tags;var sourceKind=tags.GetValueOrDefault("building","yes");var kind=tags.ContainsKey("shop")?"shop":sourceKind switch{"retail" or "commercial" or "train_station"=>"shop","apartments" or "residential"=>"apartments","school" or "college" or "kindergarten"=>"school","hospital"=>"clinic","hotel"=>"hotel","industrial"=>"industrial","warehouse" or "shed" or "service"=>"warehouse","garages" or "garage"=>"garage",_=>"house"}; string? Tag(string name)=>tags.TryGetValue(name,out var value)?value[..Math.Min(100,value.Length)]:null; var b=new Building{X=x,Y=y,Width=width,Height=height,Kind=kind,Name=Tag("name")??BuildingDesigner.Names[kind],SourceId=sourceId,SourceKind=sourceKind,SourceLevels=Tag("building:levels"),SourceBasements=Tag("building:levels:underground"),SourceRoof=Tag("roof:shape")};var levels=kind is "apartments" or "school" or "clinic" or "hotel"?2:1; diff --git a/src/SimplePZMapmaker.Core/GeographicImporter.cs b/src/SimplePZMapmaker.Core/GeographicImporter.cs index 8454fb3..3d9f7f9 100644 --- a/src/SimplePZMapmaker.Core/GeographicImporter.cs +++ b/src/SimplePZMapmaker.Core/GeographicImporter.cs @@ -107,7 +107,7 @@ public static ImportResult Import(string json,MapProject project,GeoBounds? boun } if(w>64||h>64||!MapGenerator.CanPlace(p,b)){ skipped++; - var reason=w>64||h>64?"Outline too large":b.X<1||b.Y<1||b.X+b.Width>=p.Width||b.Y+b.Height>=p.Height?"Outside the map":b.X/256!=(b.X+b.Width)/256||b.Y/256!=(b.Y+b.Height)/256?"Cell boundary":p.Buildings.Any(a=>b.Xa.X&&b.Ya.Y)?"Building overlap":"Water or road"; + var reason=w>64||h>64?"Outline too large":b.X<1||b.Y<1||b.X+b.Width>=p.Width||b.Y+b.Height>=p.Height?"Outside the map":p.Buildings.Any(a=>b.Xa.X&&b.Ya.Y)?"Building overlap":p.Decorations.Any(d=>d.Parts.Any(part=>d.X+part.Dx>=b.X&&d.X+part.Dx<=b.X+b.Width&&d.Y+part.Dy>=b.Y&&d.Y+part.Dy<=b.Y+b.Height))?"Environment object":"Water or road"; skipReasons[reason]=skipReasons.GetValueOrDefault(reason)+1;if(skippedOutlines.Count<256)skippedOutlines.Add(new(b.SourceId,b.Name,reason,x,y,w,h));continue; } p.Buildings.Add(b);imported++; diff --git a/tests/SimplePZMapmaker.Tests/WorldGeographyTests.cs b/tests/SimplePZMapmaker.Tests/WorldGeographyTests.cs index 79a1c30..1b49d69 100644 --- a/tests/SimplePZMapmaker.Tests/WorldGeographyTests.cs +++ b/tests/SimplePZMapmaker.Tests/WorldGeographyTests.cs @@ -10,6 +10,13 @@ public static class WorldGeographyTests yield return("seam repair joins roads and rivers without erasing protected objects or source",()=>{var w=WorldTests.Fixture(2);w.Maps[0].Project.Terrain[20*256+255]=Terrain.Road;w.Maps[0].Project.Terrain[30*256+255]=Terrain.Water;var before=JsonSerializer.Serialize(w);var result=WorldSeams.Repair(w,"both");Check(result.ChangedCells>0&&result.World.Maps[1].Project.Terrain[20*256]==Terrain.Road&&result.World.Maps[1].Project.Terrain[30*256]==Terrain.Water,"Road/river remains disconnected");Check(before==JsonSerializer.Serialize(w),"Repair mutated original");WorldAssembler.Validate(result.World);}); yield return("ASCII elevation keeps north-first order nodata and centre georeference",()=>{const string asc="ncols 2\nnrows 2\nxllcenter 32.005\nyllcenter 54.775\ncellsize 0.01\nNODATA_value -9999\n120 130\n100 -9999\n";var grid=ElevationSurvey.ParseAscii(asc,"EPSG:4326","Survey","CC0");Check(grid.Sample(32.005,54.785)==120&&grid.Sample(32.005,54.775)==100&&grid.Sample(32.015,54.775)==null,"Grid orientation or NODATA changed");var round=ElevationSurvey.ParseAscii(grid.ToAscii(),"EPSG:4326","Survey","CC0");Check(round.Values.SequenceEqual(grid.Values),"Export lost height samples");Reject(()=>ElevationSurvey.ParseAscii(asc,"EPSG:32636","Survey","CC0"));}); yield return("ASCII elevation rejects incomplete values and projected degrees confusion",()=>{Reject(()=>ElevationSurvey.ParseAscii("ncols 2\nnrows 2\nxllcorner 32\nyllcorner 54\ncellsize .01\n10 20 30","EPSG:4326","Survey","CC0"));Reject(()=>ElevationSurvey.ParseAscii("ncols 1\nnrows 1\nxllcorner 500000\nyllcorner 6000000\ncellsize 30\n100","EPSG:4326","Survey","CC0"));}); + yield return("an outline in the free line along a seam becomes a shared building instead of being lost",()=>{var w=WorldTests.Fixture(2);w.GeoReference=new(){Bounds=new(){West=32,East=32.02,South=54.77,North=54.79},MetresPerTile=1};var projection=new GeographicProjection(w.GeoReference,100,100); + object Way(int id,double x0,double x1){var points=new[]{(x0,80.4),(x1,80.4),(x1,94.4),(x0,94.4),(x0,80.4)}.Select(point=>{var g=projection.Geographic(point.Item1,point.Item2);return new{lon=g.Longitude,lat=g.Latitude};});return new{type="way",id,tags=new Dictionary{{"building","house"}},geometry=points};} + // 256.4..268: starts on the second section's first column, which that section keeps free (CanPlace) -> "Outside the map" there + var json=JsonSerializer.Serialize(new{elements=new[]{Way(1,256.4,268.6),Way(2,230.4,243.6)}});var notes=new List();var result=GeographicImporter.ImportWorldBuildings(json,w,42,notes); + Check(result.Added==1&&w.Buildings.Single().SourceId=="osm-way-1","Seam-margin outline was not shared, or an inner outline was");WorldAssembler.Validate(w); + var section=w.Maps[1].Project;var local=GeographicImporter.Import(JsonSerializer.Serialize(new{elements=new[]{Way(1,256.4,268.6)}}),section,null,projection:new GeographicProjection(w.GeoReference,section.WorldX,section.WorldY)); + Check(local.Project.Buildings.Count==0&&local.SkipReasons.ContainsKey("Outside the map"),"The section importer should refuse the seam-margin outline");}); yield return("OSM full footprint spanning sections becomes one common building",()=>{var w=WorldTests.Fixture(2);w.GeoReference=new(){Bounds=new(){West=32,East=32.02,South=54.77,North=54.79},MetresPerTile=1};var projection=new GeographicProjection(w.GeoReference,100,100);var points=new[]{(250d,80d),(266d,80d),(266d,94d),(250d,94d),(250d,80d)}.Select(point=>{var g=projection.Geographic(point.Item1,point.Item2);return new{lon=g.Longitude,lat=g.Latitude};});var json=JsonSerializer.Serialize(new{elements=new[]{new{type="way",id=42,tags=new Dictionary{{"building","house"},{"building:levels","2"}},geometry=points}}});var notes=new List();var result=GeographicImporter.ImportWorldBuildings(json,w,42,notes);Check(result.Added==1&&w.Buildings.Count==1&&w.Buildings[0].LevelsOrigin=="osm-levels"&&w.Buildings[0].SourceId=="osm-way-42","Boundary OSM building lost or duplicated");WorldAssembler.Validate(w);}); } static void Check(bool ok,string msg){if(!ok)throw new Exception(msg);}static void Reject(Action action){try{action();}catch(ArgumentException){return;}throw new Exception("Expected rejection");}