fix: synthesize T-junction sidewalk caps

This commit is contained in:
2026-07-28 10:20:47 +08:00
parent 03ebb7c0ed
commit 2b18d0eb2b

View File

@@ -530,8 +530,10 @@ function filteredSidewalkCorners(intersections, maxDimensionMeters) {
function buildSidewalkCorners(intersections, plain, network, maxDimensionMeters) { function buildSidewalkCorners(intersections, plain, network, maxDimensionMeters) {
const out = filteredSidewalkCorners(intersections, maxDimensionMeters); const out = filteredSidewalkCorners(intersections, maxDimensionMeters);
const fallback = synthesizeMissingSidewalkCorners(out, plain, network, maxDimensionMeters); const missing = synthesizeMissingSidewalkCorners(out, plain, network, maxDimensionMeters);
out.features.push(...fallback); out.features.push(...missing);
const caps = synthesizeTJunctionSidewalkCaps(out, plain, network, maxDimensionMeters);
out.features.push(...caps);
return out; return out;
} }
@@ -554,13 +556,11 @@ function synthesizeMissingSidewalkCorners(existing, plain, network, maxDimension
const edges = buildIntersectionEdges(intersection, roads, roadFeatures, intersectionFeature); const edges = buildIntersectionEdges(intersection, roads, roadFeatures, intersectionFeature);
if (!edges.length) continue; if (!edges.length) continue;
const qualifyingPairs = qualifyingCornerPairs(edges); const qualifyingPairs = qualifyingCornerPairs(edges);
if (qualifyingPairs.length < 3) continue; if (!qualifyingPairs.length) continue;
const current = existingByIntersection.get(intersectionId) || []; const current = existingByIntersection.get(intersectionId) || [];
if (qualifyingPairs.length - current.length !== 1) continue;
const currentCenters = current.map((entry) => entry.center); const currentCenters = current.map((entry) => entry.center);
const targetDimension = median(current.map((entry) => maxFeatureDimensionMeters(entry.feature)).filter(Number.isFinite)); const missing = [];
const candidates = [];
for (const [one, two] of qualifyingPairs) { for (const [one, two] of qualifyingPairs) {
const candidate = synthesizeCornerFeature(one, two, intersectionFeature, maxDimensionMeters); const candidate = synthesizeCornerFeature(one, two, intersectionFeature, maxDimensionMeters);
@@ -572,17 +572,67 @@ function synthesizeMissingSidewalkCorners(existing, plain, network, maxDimension
if (dimension === null || dimension > maxDimensionMeters) continue; if (dimension === null || dimension > maxDimensionMeters) continue;
if (polygonAreaMeters2(candidate) < 0.4) continue; if (polygonAreaMeters2(candidate) < 0.4) continue;
if (currentCenters.some((point) => pointDistance(point, candidateCenter) <= 0.6)) continue; if (currentCenters.some((point) => pointDistance(point, candidateCenter) <= 0.6)) continue;
candidates.push({ missing.push(candidate);
feature: candidate,
center: candidateCenter,
dimension,
score: Math.abs(dimension - targetDimension),
});
} }
if (!candidates.length) continue; if (missing.length !== 1) continue;
candidates.sort((a, b) => a.score - b.score || a.dimension - b.dimension); synthesized.push(missing[0]);
synthesized.push(candidates[0].feature); }
return synthesized;
}
function synthesizeTJunctionSidewalkCaps(existing, plain, network, maxDimensionMeters) {
const roadFeatures = new Map((plain.features || [])
.filter((feature) => feature.properties?.type === "road")
.map((feature) => [Number(feature.properties.id), feature]));
const intersectionFeatures = new Map((plain.features || [])
.filter((feature) => feature.properties?.type === "intersection")
.map((feature) => [Number(feature.properties.id), feature]));
const roads = new Map((network.roads || []).map(([id, road]) => [Number(id), road]));
const intersections = new Map((network.intersections || []).map(([id, intersection]) => [Number(id), intersection]));
const existingByIntersection = assignCornersToIntersections(existing.features || [], intersectionFeatures);
const synthesized = [];
for (const [intersectionId, intersection] of intersections.entries()) {
const intersectionFeature = intersectionFeatures.get(intersectionId);
if (!intersectionFeature) continue;
if (intersectionFeature.properties?.intersection_kind !== "Intersection") continue;
if (new Set(intersection.roads || []).size !== 3) continue;
const edges = buildIntersectionEdges(intersection, roads, roadFeatures, intersectionFeature);
if (!edges.length) continue;
const current = existingByIntersection.get(intersectionId) || [];
const smallCurrent = current.filter((entry) => {
const dimension = maxFeatureDimensionMeters(entry.feature);
return Number.isFinite(dimension) && dimension <= maxDimensionMeters;
});
if (smallCurrent.length !== 2) continue;
const candidates = [];
for (const [one, two] of qualifyingCornerPairs(edges)) {
const candidate = synthesizeCornerFeature(one, two, intersectionFeature, 100);
if (!candidate) continue;
const center = featureCenter(candidate);
const dimension = maxFeatureDimensionMeters(candidate);
const area = polygonAreaMeters2(candidate);
if (!center || !Number.isFinite(dimension) || !Number.isFinite(area)) continue;
candidates.push({ feature: candidate, center, dimension, area });
}
// For a true T-junction, the remaining large candidate is the sidewalk "cap"
// opposite the side street, not another curb-return corner.
const caps = candidates.filter(({ dimension, area, center }) => (
dimension > maxDimensionMeters &&
dimension <= 12 &&
area >= 6 &&
area <= 20 &&
pointInPolygon(center, intersectionFeature.geometry.coordinates) &&
!smallCurrent.some((entry) => pointDistance(entry.center, center) <= 1)
));
if (caps.length !== 1) continue;
caps[0].feature.properties.source = "fallback_t_cap";
synthesized.push(caps[0].feature);
} }
return synthesized; return synthesized;
@@ -905,12 +955,6 @@ function polygonAreaMeters2(feature) {
return Math.abs(area) / 2; return Math.abs(area) / 2;
} }
function median(values) {
if (!values.length) return 0;
const sorted = [...values].sort((a, b) => a - b);
return sorted[Math.floor(sorted.length / 2)];
}
function maxFeatureDimensionMeters(feature) { function maxFeatureDimensionMeters(feature) {
const points = []; const points = [];
collectCoords(feature.geometry?.coordinates, points); collectCoords(feature.geometry?.coordinates, points);