optimized events
Test and publish container images / test (push) Successful in 2m26s
Test and publish container images / publish (push) Failing after 3s

This commit is contained in:
BuTzZ
2026-07-28 14:36:47 +02:00
parent b98ec8bc6f
commit 593dbd5f85
42 changed files with 2405 additions and 283 deletions
+159 -5
View File
@@ -55,6 +55,10 @@ export type RouteBridgeAssessment = {
marginM: number | null;
status: RouteBridgeStatus;
source: string;
phone?: string | null;
website?: string | null;
email?: string | null;
operator?: string | null;
};
export type RouteBridgeReport = {
@@ -79,6 +83,9 @@ const SAMPLE_TARGETS: Array<{ label: RouteWeatherSample["label"]; ratio: number
const ROUTE_BRIDGE_BBOX_MARGIN_DEG = 0.02;
const ROUTE_BRIDGE_MAX_DISTANCE_NM = 0.08;
const BRIDGE_TIGHT_MARGIN_M = 0.5;
const SAME_NAMED_BRIDGE_DISTANCE_NM = 0.03;
const UNNAMED_BRIDGE_DISTANCE_NM = 0.005;
const NAMED_TO_UNNAMED_BRIDGE_DISTANCE_NM = 0.01;
export async function createRouteWeatherReport(
route: RouteResult,
@@ -451,7 +458,29 @@ function bridgeAssessmentFromFeature(
requiredAirDraftM,
marginM,
status,
source: stringProperty(properties.source) ?? "OSM/Geofabrik"
source: stringProperty(properties.source) ?? "OSM/Geofabrik",
phone: firstStringProperty(properties, [
"contact:phone",
"phone",
"telephone",
"contact_phone"
]),
website: firstStringProperty(properties, [
"contact:website",
"website",
"url",
"contact_website"
]),
email: firstStringProperty(properties, [
"contact:email",
"email",
"contact_email"
]),
operator: firstStringProperty(properties, [
"operator",
"operator:name",
"owner"
])
};
}
@@ -572,11 +601,123 @@ function dedupeBridges(bridges: RouteBridgeAssessment[]) {
const byId = new Map<string, RouteBridgeAssessment>();
for (const bridge of bridges) {
const existing = byId.get(bridge.id);
if (!existing || bridge.distanceNm < existing.distanceNm) {
byId.set(bridge.id, bridge);
}
byId.set(bridge.id, existing ? mergeBridgeAssessments(existing, bridge) : bridge);
}
return [...byId.values()];
const merged: RouteBridgeAssessment[] = [];
for (const bridge of byId.values()) {
const duplicateIndex = merged.findIndex((candidate) =>
bridgeAssessmentsMatch(candidate, bridge)
);
if (duplicateIndex < 0) {
merged.push(bridge);
continue;
}
merged[duplicateIndex] = mergeBridgeAssessments(
merged[duplicateIndex]!,
bridge
);
}
return merged;
}
function bridgeAssessmentsMatch(
left: RouteBridgeAssessment,
right: RouteBridgeAssessment
) {
const distanceNm = haversineDistanceNm(left.coordinate, right.coordinate);
const leftName = normalizedBridgeName(left.name);
const rightName = normalizedBridgeName(right.name);
if (leftName && rightName) {
return (
leftName === rightName &&
distanceNm <= SAME_NAMED_BRIDGE_DISTANCE_NM
);
}
if (!leftName && !rightName) {
return distanceNm <= UNNAMED_BRIDGE_DISTANCE_NM;
}
return distanceNm <= NAMED_TO_UNNAMED_BRIDGE_DISTANCE_NM;
}
function mergeBridgeAssessments(
left: RouteBridgeAssessment,
right: RouteBridgeAssessment
): RouteBridgeAssessment {
const primary =
bridgeInformationScore(right) > bridgeInformationScore(left) ? right : left;
const secondary = primary === left ? right : left;
const clearanceSource = smallestClearanceBridge(left, right);
const clearanceM = clearanceSource?.clearanceM ?? null;
const requiredAirDraftM =
primary.requiredAirDraftM ?? secondary.requiredAirDraftM;
const marginM =
clearanceM !== null && requiredAirDraftM !== null
? clearanceM - requiredAirDraftM
: null;
const closestToRoute =
left.distanceNm <= right.distanceNm ? left : right;
const name = primary.name ?? secondary.name;
const clearanceLabel =
clearanceSource?.clearanceLabel ??
(clearanceM !== null ? `H ${formatMeters(clearanceM)}` : null);
return {
...primary,
name,
label: [name, clearanceLabel].filter(Boolean).join(" ") || primary.label,
coordinate: closestToRoute.coordinate,
distanceNm: Math.min(left.distanceNm, right.distanceNm),
clearanceM,
clearanceLabel,
requiredAirDraftM,
marginM,
status: bridgeStatus(clearanceM, requiredAirDraftM),
source: unique([left.source, right.source]).join(", "),
phone: primary.phone ?? secondary.phone ?? null,
website: primary.website ?? secondary.website ?? null,
email: primary.email ?? secondary.email ?? null,
operator: primary.operator ?? secondary.operator ?? null
};
}
function smallestClearanceBridge(
left: RouteBridgeAssessment,
right: RouteBridgeAssessment
) {
const candidates = [left, right].filter(
(bridge) =>
bridge.clearanceM !== null && Number.isFinite(bridge.clearanceM)
);
return candidates.sort(
(first, second) => first.clearanceM! - second.clearanceM!
)[0] ?? null;
}
function bridgeInformationScore(bridge: RouteBridgeAssessment) {
return [
bridge.name,
bridge.clearanceM,
bridge.phone,
bridge.website,
bridge.email,
bridge.operator
].filter((value) => value !== null && value !== undefined && value !== "")
.length;
}
function normalizedBridgeName(value: string | null) {
if (!value) {
return "";
}
const normalized = value
.normalize("NFKD")
.replace(/\p{Diacritic}/gu, "")
.toLocaleLowerCase("de-DE")
.replace(/[^\p{Letter}\p{Number}]+/gu, " ")
.trim();
return normalized === "brucke" || normalized === "bridge" ? "" : normalized;
}
function geometryLineStrings(geometry: Geometry): LonLat[][] {
@@ -770,6 +911,19 @@ function stringProperty(value: unknown) {
return typeof value === "string" && value.trim() ? value.trim() : null;
}
function firstStringProperty(
properties: Record<string, unknown>,
keys: readonly string[]
) {
for (const key of keys) {
const value = stringProperty(properties[key]);
if (value) {
return value;
}
}
return null;
}
function normalizeMeters(value: unknown) {
if (typeof value === "number") {
return Number.isFinite(value) ? value : null;