Add OSM area preflight
This commit is contained in:
@@ -67,6 +67,7 @@ function parseOsm(xml) {
|
||||
const nodePattern = /<node\b([^>]*?)\/>|<node\b([^>]*?)>([\s\S]*?)<\/node>/g;
|
||||
for (const match of xml.matchAll(nodePattern)) {
|
||||
const attrs = xmlAttrs(match[1] || match[2] || "");
|
||||
if (attrs.action === "delete") continue;
|
||||
if (attrs.id) nodeIds.add(attrs.id);
|
||||
nodeStats.total += 1;
|
||||
const tags = parseTags(match[3] || "");
|
||||
@@ -80,6 +81,8 @@ function parseOsm(xml) {
|
||||
buildingsWithHeight: 0,
|
||||
buildingsWithLevels: 0,
|
||||
buildingsWithBadHeight: 0,
|
||||
buildingsWithBadLevels: 0,
|
||||
buildingGeometryIssues: [],
|
||||
missingNodeRefs: 0,
|
||||
grass: 0,
|
||||
scrub: 0,
|
||||
@@ -87,6 +90,7 @@ function parseOsm(xml) {
|
||||
};
|
||||
for (const match of xml.matchAll(/<way\b([^>]*)>([\s\S]*?)<\/way>/g)) {
|
||||
const attrs = xmlAttrs(match[1]);
|
||||
if (attrs.action === "delete") continue;
|
||||
const body = match[2];
|
||||
const tags = parseTags(body);
|
||||
const refs = [];
|
||||
@@ -110,6 +114,14 @@ function parseOsm(xml) {
|
||||
if (isExplicitHeight(tags)) wayStats.buildingsWithHeight += 1;
|
||||
if (tags["building:levels"]) wayStats.buildingsWithLevels += 1;
|
||||
if (tags.height && !parseHeightMeters(tags.height)) wayStats.buildingsWithBadHeight += 1;
|
||||
if (tags["building:levels"] && !parseBuildingLevels(tags["building:levels"])) {
|
||||
wayStats.buildingsWithBadLevels += 1;
|
||||
}
|
||||
if (refs.length < 4) {
|
||||
wayStats.buildingGeometryIssues.push({ id: way.id, issue: "has fewer than 4 node refs" });
|
||||
} else if (!way.closed) {
|
||||
wayStats.buildingGeometryIssues.push({ id: way.id, issue: "is not closed" });
|
||||
}
|
||||
}
|
||||
if (tags.landuse === "grass") wayStats.grass += 1;
|
||||
if (tags.natural === "scrub") wayStats.scrub += 1;
|
||||
@@ -122,11 +134,13 @@ function parseOsm(xml) {
|
||||
buildingsWithHeight: 0,
|
||||
buildingsWithLevels: 0,
|
||||
buildingsWithBadHeight: 0,
|
||||
buildingsWithBadLevels: 0,
|
||||
healthyBuildingMultipolygons: 0,
|
||||
issues: [],
|
||||
};
|
||||
for (const match of xml.matchAll(/<relation\b([^>]*)>([\s\S]*?)<\/relation>/g)) {
|
||||
const attrs = xmlAttrs(match[1]);
|
||||
if (attrs.action === "delete") continue;
|
||||
const body = match[2];
|
||||
const tags = parseTags(body);
|
||||
const members = [];
|
||||
@@ -139,6 +153,9 @@ function parseOsm(xml) {
|
||||
if (isExplicitHeight(tags)) relationStats.buildingsWithHeight += 1;
|
||||
if (tags["building:levels"]) relationStats.buildingsWithLevels += 1;
|
||||
if (tags.height && !parseHeightMeters(tags.height)) relationStats.buildingsWithBadHeight += 1;
|
||||
if (tags["building:levels"] && !parseBuildingLevels(tags["building:levels"])) {
|
||||
relationStats.buildingsWithBadLevels += 1;
|
||||
}
|
||||
|
||||
const health = buildingRelationHealth(attrs.id || "", members, ways);
|
||||
if (health.ok) relationStats.healthyBuildingMultipolygons += 1;
|
||||
@@ -163,7 +180,10 @@ function parseBounds(xml) {
|
||||
maxLon: Number(attrs.maxlon),
|
||||
maxLat: Number(attrs.maxlat),
|
||||
};
|
||||
return Object.values(bounds).every(Number.isFinite) ? bounds : null;
|
||||
return Object.values(bounds).every(Number.isFinite) &&
|
||||
bounds.minLon < bounds.maxLon && bounds.minLat < bounds.maxLat
|
||||
? bounds
|
||||
: null;
|
||||
}
|
||||
|
||||
function isExplicitHeight(tags) {
|
||||
@@ -177,6 +197,48 @@ function parseHeightMeters(value) {
|
||||
return Number.isFinite(height) && height > 0 ? height : null;
|
||||
}
|
||||
|
||||
function parseBuildingLevels(value) {
|
||||
const levels = Number(String(value).trim());
|
||||
return Number.isFinite(levels) && levels > 0;
|
||||
}
|
||||
|
||||
function analyzeOsmPreflight(osm) {
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
if (!osm.bounds) errors.push("OSM has no valid <bounds>.");
|
||||
if (osm.ways.missingNodeRefs) {
|
||||
errors.push(`OSM ways reference ${osm.ways.missingNodeRefs} missing node(s).`);
|
||||
}
|
||||
for (const issue of osm.ways.buildingGeometryIssues) {
|
||||
errors.push(`Building way ${issue.id}: ${issue.issue}.`);
|
||||
}
|
||||
if (osm.ways.buildingsWithBadHeight || osm.relations.buildingsWithBadHeight) {
|
||||
errors.push("Some building height tags could not be parsed as positive meters.");
|
||||
}
|
||||
for (const issue of osm.relations.issues) {
|
||||
errors.push(`Building relation ${issue.id}: ${issue.issues.join("; ")}.`);
|
||||
}
|
||||
const badLevels = osm.ways.buildingsWithBadLevels + osm.relations.buildingsWithBadLevels;
|
||||
if (badLevels) {
|
||||
warnings.push(`${badLevels} building:levels tag(s) could not be parsed as positive numbers.`);
|
||||
}
|
||||
return {
|
||||
errors: uniqueLines(errors),
|
||||
warnings: uniqueLines(warnings),
|
||||
summary: {
|
||||
bounds: osm.bounds,
|
||||
nodes: osm.nodes.total,
|
||||
ways: osm.ways.total,
|
||||
relations: osm.relations.total,
|
||||
buildingWays: osm.ways.buildings,
|
||||
buildingMultipolygons: osm.relations.buildingMultipolygons,
|
||||
missingNodeRefs: osm.ways.missingNodeRefs,
|
||||
buildingWayIssues: osm.ways.buildingGeometryIssues.length,
|
||||
buildingRelationIssues: osm.relations.issues.length,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function buildingRelationHealth(id, members, ways) {
|
||||
const issues = [];
|
||||
const outerMembers = members.filter((member) => member.type === "way" && member.role === "outer");
|
||||
@@ -307,6 +369,15 @@ function stageManifestStatus(area, configPath = null) {
|
||||
const hasReimportManifest = fs.existsSync(reimportManifest);
|
||||
const derivedConfig = path.join(area.outputs.pipelineDir, "osm2streets-qgis.config.json");
|
||||
const stages = [
|
||||
{
|
||||
stage: "preflight",
|
||||
expected: fs.existsSync(stageManifestPath(area, "preflight")),
|
||||
inputs: {
|
||||
...(configPath ? { config: configPath } : {}),
|
||||
osm: area.input,
|
||||
},
|
||||
outputs: {},
|
||||
},
|
||||
{
|
||||
stage: "intermediates",
|
||||
expected: (
|
||||
@@ -763,6 +834,7 @@ function formatBytes(bytes) {
|
||||
|
||||
module.exports = {
|
||||
BUDGETS,
|
||||
analyzeOsmPreflight,
|
||||
analyzeArea,
|
||||
artifactStatus,
|
||||
classifyAreaQuality,
|
||||
|
||||
Reference in New Issue
Block a user