optimized events
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user