fix: synthesize T-junction sidewalk caps
This commit is contained in:
@@ -530,8 +530,10 @@ function filteredSidewalkCorners(intersections, maxDimensionMeters) {
|
||||
|
||||
function buildSidewalkCorners(intersections, plain, network, maxDimensionMeters) {
|
||||
const out = filteredSidewalkCorners(intersections, maxDimensionMeters);
|
||||
const fallback = synthesizeMissingSidewalkCorners(out, plain, network, maxDimensionMeters);
|
||||
out.features.push(...fallback);
|
||||
const missing = synthesizeMissingSidewalkCorners(out, plain, network, maxDimensionMeters);
|
||||
out.features.push(...missing);
|
||||
const caps = synthesizeTJunctionSidewalkCaps(out, plain, network, maxDimensionMeters);
|
||||
out.features.push(...caps);
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -554,13 +556,11 @@ function synthesizeMissingSidewalkCorners(existing, plain, network, maxDimension
|
||||
const edges = buildIntersectionEdges(intersection, roads, roadFeatures, intersectionFeature);
|
||||
if (!edges.length) continue;
|
||||
const qualifyingPairs = qualifyingCornerPairs(edges);
|
||||
if (qualifyingPairs.length < 3) continue;
|
||||
if (!qualifyingPairs.length) continue;
|
||||
|
||||
const current = existingByIntersection.get(intersectionId) || [];
|
||||
if (qualifyingPairs.length - current.length !== 1) continue;
|
||||
const currentCenters = current.map((entry) => entry.center);
|
||||
const targetDimension = median(current.map((entry) => maxFeatureDimensionMeters(entry.feature)).filter(Number.isFinite));
|
||||
const candidates = [];
|
||||
const missing = [];
|
||||
|
||||
for (const [one, two] of qualifyingPairs) {
|
||||
const candidate = synthesizeCornerFeature(one, two, intersectionFeature, maxDimensionMeters);
|
||||
@@ -572,17 +572,67 @@ function synthesizeMissingSidewalkCorners(existing, plain, network, maxDimension
|
||||
if (dimension === null || dimension > maxDimensionMeters) continue;
|
||||
if (polygonAreaMeters2(candidate) < 0.4) continue;
|
||||
if (currentCenters.some((point) => pointDistance(point, candidateCenter) <= 0.6)) continue;
|
||||
candidates.push({
|
||||
feature: candidate,
|
||||
center: candidateCenter,
|
||||
dimension,
|
||||
score: Math.abs(dimension - targetDimension),
|
||||
});
|
||||
missing.push(candidate);
|
||||
}
|
||||
|
||||
if (!candidates.length) continue;
|
||||
candidates.sort((a, b) => a.score - b.score || a.dimension - b.dimension);
|
||||
synthesized.push(candidates[0].feature);
|
||||
if (missing.length !== 1) continue;
|
||||
synthesized.push(missing[0]);
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -905,12 +955,6 @@ function polygonAreaMeters2(feature) {
|
||||
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) {
|
||||
const points = [];
|
||||
collectCoords(feature.geometry?.coordinates, points);
|
||||
|
||||
Reference in New Issue
Block a user