Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/SimplePZMapmaker.Core/GeographicImporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.X<a.X+a.Width+1&&b.X+b.Width+1>a.X&&b.Y<a.Y+a.Height+1&&b.Y+b.Height+1>a.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.X<a.X+a.Width+1&&b.X+b.Width+1>a.X&&b.Y<a.Y+a.Height+1&&b.Y+b.Height+1>a.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++;
Expand Down
7 changes: 7 additions & 0 deletions tests/SimplePZMapmaker.Tests/WorldGeographyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string,string>{{"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<string>();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<string,string>{{"building","house"},{"building:levels","2"}},geometry=points}}});var notes=new List<string>();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");}
Expand Down