685 lines
23 KiB
Bash
Executable File
685 lines
23 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
|
||
set -Eeuo pipefail
|
||
|
||
WM_DEPLOY_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||
WM_ROOT_DIR="$(cd "$WM_DEPLOY_DIR/.." && pwd)"
|
||
WM_ENV_FILE="${WATERMAPS_ENV_FILE:-$WM_DEPLOY_DIR/.env.production}"
|
||
WM_IMAGES_ENV_FILE="${WATERMAPS_IMAGES_ENV_FILE:-$WM_DEPLOY_DIR/.env.images}"
|
||
WM_COMPOSE_FILE="$WM_DEPLOY_DIR/compose.production.yml"
|
||
|
||
wm_die() {
|
||
printf 'Fehler: %s\n' "$*" >&2
|
||
exit 1
|
||
}
|
||
|
||
wm_log() {
|
||
printf '[watermaps] %s\n' "$*"
|
||
}
|
||
|
||
wm_load_env() {
|
||
if [[ ! -f "$WM_ENV_FILE" ]]; then
|
||
wm_die "Konfiguration fehlt: $WM_ENV_FILE (Vorlage: deploy/.env.production.example)"
|
||
fi
|
||
|
||
set -a
|
||
# shellcheck disable=SC1090
|
||
source "$WM_ENV_FILE"
|
||
set +a
|
||
|
||
WATERMAPS_DATA_DIR="${WATERMAPS_DATA_DIR:-/srv/watermaps-data}"
|
||
WATERMAPS_RUNTIME_DIR="${WATERMAPS_RUNTIME_DIR:-/srv/watermaps-runtime}"
|
||
export WATERMAPS_DATA_DIR WATERMAPS_RUNTIME_DIR
|
||
|
||
wm_require_safe_absolute_dir "$WATERMAPS_DATA_DIR"
|
||
wm_require_safe_absolute_dir "$WATERMAPS_RUNTIME_DIR"
|
||
wm_validate_postgres_password
|
||
}
|
||
|
||
wm_validate_postgres_password() {
|
||
local password="${WATERMAPS_POSTGRES_PASSWORD:-}"
|
||
|
||
[[ "${#password}" -ge 24 &&
|
||
"$password" != *[[:space:]]* &&
|
||
"$password" != *REPLACE* ]] ||
|
||
wm_die "WATERMAPS_POSTGRES_PASSWORD muss ein gesetztes Secret mit mindestens 24 Zeichen ohne Leerzeichen sein."
|
||
}
|
||
|
||
wm_require_safe_absolute_dir() {
|
||
local directory="$1"
|
||
[[ "$directory" == /* ]] || wm_die "Pfad muss absolut sein: $directory"
|
||
[[ "$directory" != "/" ]] || wm_die "Das Wurzelverzeichnis darf nicht als Datenpfad verwendet werden."
|
||
[[ "$directory" != "/srv" ]] || wm_die "Bitte ein Unterverzeichnis von /srv verwenden."
|
||
}
|
||
|
||
wm_assert_data_mount() {
|
||
mountpoint --quiet "$WATERMAPS_DATA_DIR" ||
|
||
wm_die "Persistentes Datenvolume ist nicht unter $WATERMAPS_DATA_DIR eingehängt."
|
||
}
|
||
|
||
wm_compose() {
|
||
wm_validate_images_env "$WM_IMAGES_ENV_FILE"
|
||
docker compose \
|
||
--project-directory "$WM_ROOT_DIR" \
|
||
--env-file "$WM_ENV_FILE" \
|
||
--env-file "$WM_IMAGES_ENV_FILE" \
|
||
--file "$WM_COMPOSE_FILE" \
|
||
"$@"
|
||
}
|
||
|
||
wm_use_images_env() {
|
||
local images_env_file="$1"
|
||
wm_validate_images_env "$images_env_file"
|
||
WM_IMAGES_ENV_FILE="$images_env_file"
|
||
export WATERMAPS_IMAGES_ENV_FILE="$images_env_file"
|
||
}
|
||
|
||
wm_validate_images_env() {
|
||
local images_env_file="$1"
|
||
local revision app_image route_data_image
|
||
|
||
[[ -f "$images_env_file" ]] ||
|
||
wm_die "Image-Konfiguration fehlt: $images_env_file"
|
||
|
||
revision="$(wm_env_value "$images_env_file" WATERMAPS_DEPLOY_REVISION)"
|
||
app_image="$(wm_env_value "$images_env_file" WATERMAPS_APP_IMAGE)"
|
||
route_data_image="$(wm_env_value "$images_env_file" WATERMAPS_ROUTE_DATA_IMAGE)"
|
||
|
||
[[ "$revision" =~ ^[0-9a-f]{40}$ ]] ||
|
||
wm_die "WATERMAPS_DEPLOY_REVISION muss eine vollständige Git-Commit-SHA sein."
|
||
wm_validate_image_reference "$app_image" WATERMAPS_APP_IMAGE
|
||
wm_validate_image_reference "$route_data_image" WATERMAPS_ROUTE_DATA_IMAGE
|
||
wm_validate_release_image "$app_image" "$revision" WATERMAPS_APP_IMAGE
|
||
wm_validate_release_image \
|
||
"$route_data_image" \
|
||
"$revision" \
|
||
WATERMAPS_ROUTE_DATA_IMAGE
|
||
}
|
||
|
||
wm_env_value() {
|
||
local env_file="$1"
|
||
local requested_key="$2"
|
||
local value
|
||
|
||
value="$(
|
||
awk -F= -v requested_key="$requested_key" '
|
||
$1 == requested_key {
|
||
print substr($0, length($1) + 2)
|
||
matches += 1
|
||
}
|
||
END {
|
||
if (matches != 1) {
|
||
exit 1
|
||
}
|
||
}
|
||
' "$env_file"
|
||
)" || wm_die "$requested_key fehlt oder ist mehrfach in $env_file vorhanden."
|
||
printf '%s\n' "$value"
|
||
}
|
||
|
||
wm_validate_image_reference() {
|
||
local image_reference="$1"
|
||
local variable_name="$2"
|
||
|
||
[[ -n "$image_reference" &&
|
||
"$image_reference" != *[[:space:]]* &&
|
||
"$image_reference" == */* &&
|
||
"$image_reference" != *REPLACE* ]] ||
|
||
wm_die "$variable_name enthält keine gültige Registry-Image-Referenz."
|
||
}
|
||
|
||
wm_validate_release_image() {
|
||
local image_reference="$1"
|
||
local revision="$2"
|
||
local variable_name="$3"
|
||
|
||
[[ "$image_reference" == *":$revision" ||
|
||
"$image_reference" =~ @sha256:[0-9a-f]{64}$ ]] ||
|
||
wm_die "$variable_name muss auf den Commit-Tag $revision oder einen SHA256-Digest zeigen."
|
||
}
|
||
|
||
wm_resolve_image_digest() {
|
||
local image_reference="$1"
|
||
local requested_repository last_component resolved_reference
|
||
|
||
requested_repository="${image_reference%%@*}"
|
||
last_component="${requested_repository##*/}"
|
||
if [[ "$last_component" == *:* ]]; then
|
||
requested_repository="${requested_repository%:*}"
|
||
fi
|
||
|
||
resolved_reference="$(
|
||
docker image inspect \
|
||
--format '{{range .RepoDigests}}{{println .}}{{end}}' \
|
||
"$image_reference" |
|
||
awk -v requested_repository="$requested_repository" '
|
||
index($0, requested_repository "@sha256:") == 1 {
|
||
print
|
||
exit
|
||
}
|
||
'
|
||
)"
|
||
[[ "$resolved_reference" =~ @sha256:[0-9a-f]{64}$ ]] ||
|
||
wm_die "Für $image_reference konnte kein lokaler Registry-Digest ermittelt werden."
|
||
printf '%s\n' "$resolved_reference"
|
||
}
|
||
|
||
wm_route_file() {
|
||
printf '%s/local/germany-netherlands-fairways.json\n' "$WATERMAPS_DATA_DIR"
|
||
}
|
||
|
||
wm_route_marker() {
|
||
printf '%s/local/.germany-netherlands-fairways.ready\n' "$WATERMAPS_DATA_DIR"
|
||
}
|
||
|
||
wm_marine_marker() {
|
||
printf '%s/local/.marine-features.ready\n' "$WATERMAPS_DATA_DIR"
|
||
}
|
||
|
||
wm_acquire_route_lock() {
|
||
install -d -m 0755 "$WATERMAPS_RUNTIME_DIR/locks"
|
||
exec 9>"$WATERMAPS_RUNTIME_DIR/locks/route-update.lock"
|
||
if ! flock --nonblock 9; then
|
||
wm_log "Ein Routingdaten-Update oder Deployment läuft bereits."
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
wm_assert_route_data() {
|
||
wm_route_data_ready || wm_die "Routingdaten sind nicht vollständig bereit."
|
||
}
|
||
|
||
wm_route_data_ready() {
|
||
wm_route_data_files_ready "$(wm_route_file)" "$(wm_route_marker)"
|
||
}
|
||
|
||
wm_marker_value() {
|
||
local marker="$1"
|
||
local requested_key="$2"
|
||
awk -F= -v requested_key="$requested_key" '
|
||
$1 == requested_key {
|
||
value = substr($0, length($1) + 2)
|
||
matches += 1
|
||
}
|
||
END {
|
||
if (matches != 1) {
|
||
exit 1
|
||
}
|
||
print value
|
||
}
|
||
' "$marker"
|
||
}
|
||
|
||
wm_write_route_marker() {
|
||
local route_file="$1"
|
||
local marker="$2"
|
||
local temporary_marker
|
||
|
||
[[ -s "$route_file" ]] || return 1
|
||
chmod 0644 "$route_file"
|
||
temporary_marker="$(mktemp "$(dirname "$marker")/.routing-ready.XXXXXX")"
|
||
{
|
||
printf 'format_version=1\n'
|
||
printf 'generated_at=%s\n' "$(date --utc +%Y-%m-%dT%H:%M:%SZ)"
|
||
printf 'route_file_name=%s\n' "$(basename "$route_file")"
|
||
printf 'size_bytes=%s\n' "$(stat --format=%s "$route_file")"
|
||
printf 'sha256=%s\n' "$(sha256sum "$route_file" | awk '{ print $1 }')"
|
||
printf 'generator_version=2\n'
|
||
printf 'source=germany+netherlands\n'
|
||
} >"$temporary_marker"
|
||
chmod 0644 "$temporary_marker"
|
||
mv -f "$temporary_marker" "$marker"
|
||
}
|
||
|
||
wm_route_data_files_ready() {
|
||
local route_file="$1"
|
||
local marker="$2"
|
||
local file_mode marker_format marker_file_name marker_size marker_checksum
|
||
local marker_generator marker_source actual_size actual_checksum
|
||
|
||
if [[ ! -s "$route_file" ]]; then
|
||
wm_log "Routingindex fehlt oder ist leer: $route_file"
|
||
return 1
|
||
fi
|
||
if [[ ! -s "$marker" ]]; then
|
||
wm_log "Bereitschaftsmarker fehlt: $marker"
|
||
return 1
|
||
fi
|
||
if [[ ! "$marker" -nt "$route_file" ]]; then
|
||
wm_log "Bereitschaftsmarker ist älter als der Routingindex."
|
||
return 1
|
||
fi
|
||
|
||
if [[ ! -r "$route_file" ]]; then
|
||
wm_log "Routingindex ist für den prüfenden Benutzer nicht lesbar: $route_file"
|
||
return 1
|
||
fi
|
||
file_mode="$(stat --format=%a "$route_file")"
|
||
if (( (8#$file_mode & 4) == 0 )); then
|
||
wm_log "Routingindex ist für den unprivilegierten App-Container nicht lesbar (Modus $file_mode)."
|
||
return 1
|
||
fi
|
||
|
||
if ! jq --exit-status '
|
||
(.version == 1)
|
||
and (.generatorVersion == 2)
|
||
and (.source == "germany+netherlands")
|
||
and (.sources | type == "array" and length == 2)
|
||
and (([.sources[].region] | sort) == ["germany", "netherlands"])
|
||
and (([.sources[].file] | sort) == [
|
||
"germany-latest.osm.pbf",
|
||
"netherlands-latest.osm.pbf"
|
||
])
|
||
and (all(.sources[];
|
||
(.checksumMd5 | type == "string" and test("^[0-9a-f]{32}$"))
|
||
and (.sizeBytes | type == "number" and . > 0)
|
||
))
|
||
and (.ways | type == "array" and length > 0)
|
||
' "$route_file" >/dev/null; then
|
||
wm_log "Routingindex enthält nicht exakt die erwarteten Deutschland-/Niederlande-Quellen."
|
||
return 1
|
||
fi
|
||
|
||
marker_format="$(wm_marker_value "$marker" format_version 2>/dev/null || true)"
|
||
marker_file_name="$(wm_marker_value "$marker" route_file_name 2>/dev/null || true)"
|
||
marker_size="$(wm_marker_value "$marker" size_bytes 2>/dev/null || true)"
|
||
marker_checksum="$(wm_marker_value "$marker" sha256 2>/dev/null || true)"
|
||
marker_generator="$(wm_marker_value "$marker" generator_version 2>/dev/null || true)"
|
||
marker_source="$(wm_marker_value "$marker" source 2>/dev/null || true)"
|
||
actual_size="$(stat --format=%s "$route_file")"
|
||
actual_checksum="$(sha256sum "$route_file" | awk '{ print $1 }')"
|
||
|
||
if [[ "$marker_format" != "1" ||
|
||
"$marker_file_name" != "$(basename "$route_file")" ||
|
||
"$marker_size" != "$actual_size" ||
|
||
! "$marker_checksum" =~ ^[0-9a-f]{64}$ ||
|
||
"$marker_checksum" != "$actual_checksum" ||
|
||
"$marker_generator" != "2" ||
|
||
"$marker_source" != "germany+netherlands" ]]; then
|
||
wm_log "Bereitschaftsmarker stimmt nicht mit dem Routingindex überein: $marker"
|
||
return 1
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
wm_geofabrik_checksum() {
|
||
local region="$1"
|
||
local checksum_file="$WATERMAPS_DATA_DIR/geofabrik/${region}-latest.osm.pbf.md5"
|
||
local checksum
|
||
|
||
[[ -s "$checksum_file" ]] || return 1
|
||
checksum="$(awk 'NR == 1 { print tolower($1) }' "$checksum_file")"
|
||
[[ "$checksum" =~ ^[0-9a-f]{32}$ ]] || return 1
|
||
printf '%s\n' "$checksum"
|
||
}
|
||
|
||
wm_postgres_query() {
|
||
local query="$1"
|
||
|
||
wm_compose exec --no-TTY postgres \
|
||
psql \
|
||
--no-psqlrc \
|
||
--username seacompass \
|
||
--dbname seacompass \
|
||
--tuples-only \
|
||
--no-align \
|
||
--field-separator '|' \
|
||
--set ON_ERROR_STOP=1 \
|
||
--command "$query"
|
||
}
|
||
|
||
wm_marine_features_ready() {
|
||
local marker germany_checksum netherlands_checksum database_counts
|
||
local total_count harbour_count lock_count bridge_count
|
||
local marker_format marker_source marker_germany marker_netherlands
|
||
local marker_total marker_harbours marker_locks marker_bridges
|
||
|
||
marker="$(wm_marine_marker)"
|
||
if [[ ! -s "$marker" ]]; then
|
||
wm_log "Bereitschaftsmarker für Marine-Features fehlt: $marker"
|
||
return 1
|
||
fi
|
||
|
||
germany_checksum="$(wm_geofabrik_checksum germany 2>/dev/null || true)"
|
||
netherlands_checksum="$(wm_geofabrik_checksum netherlands 2>/dev/null || true)"
|
||
if [[ -z "$germany_checksum" || -z "$netherlands_checksum" ]]; then
|
||
wm_log "Geofabrik-Prüfsummen für den Marine-Feature-Stand fehlen oder sind ungültig."
|
||
return 1
|
||
fi
|
||
|
||
marker_format="$(wm_marker_value "$marker" format_version 2>/dev/null || true)"
|
||
marker_source="$(wm_marker_value "$marker" source 2>/dev/null || true)"
|
||
marker_germany="$(wm_marker_value "$marker" germany_md5 2>/dev/null || true)"
|
||
marker_netherlands="$(wm_marker_value "$marker" netherlands_md5 2>/dev/null || true)"
|
||
marker_total="$(wm_marker_value "$marker" total_osm_features 2>/dev/null || true)"
|
||
marker_harbours="$(wm_marker_value "$marker" harbour_count 2>/dev/null || true)"
|
||
marker_locks="$(wm_marker_value "$marker" lock_count 2>/dev/null || true)"
|
||
marker_bridges="$(wm_marker_value "$marker" bridge_count 2>/dev/null || true)"
|
||
|
||
if [[ "$marker_format" != "1" ||
|
||
"$marker_source" != "germany+netherlands" ||
|
||
"$marker_germany" != "$germany_checksum" ||
|
||
"$marker_netherlands" != "$netherlands_checksum" ]]; then
|
||
wm_log "Marine-Feature-Marker passt nicht zu den aktuellen Deutschland-/Niederlande-Snapshots."
|
||
return 1
|
||
fi
|
||
|
||
if ! database_counts="$(
|
||
wm_postgres_query "
|
||
SELECT
|
||
count(*)::bigint,
|
||
count(*) FILTER (WHERE layer = 'harbours')::bigint,
|
||
count(*) FILTER (WHERE layer = 'locks')::bigint,
|
||
count(*) FILTER (WHERE layer = 'bridges')::bigint
|
||
FROM marine_features
|
||
WHERE source = 'osm';
|
||
"
|
||
)"; then
|
||
wm_log "Marine-Feature-Tabellen sind in PostGIS nicht abfragbar."
|
||
return 1
|
||
fi
|
||
database_counts="${database_counts//[[:space:]]/}"
|
||
IFS='|' read -r total_count harbour_count lock_count bridge_count <<<"$database_counts"
|
||
|
||
for count in "$total_count" "$harbour_count" "$lock_count" "$bridge_count"; do
|
||
if [[ ! "$count" =~ ^[1-9][0-9]*$ ]]; then
|
||
wm_log "PostGIS enthält keine vollständigen OSM-Hafen-/Schleusen-/Brückendaten."
|
||
return 1
|
||
fi
|
||
done
|
||
|
||
if [[ "$marker_total" != "$total_count" ||
|
||
"$marker_harbours" != "$harbour_count" ||
|
||
"$marker_locks" != "$lock_count" ||
|
||
"$marker_bridges" != "$bridge_count" ]]; then
|
||
wm_log "Marine-Feature-Marker stimmt nicht mit den OSM-Zeilen in PostGIS überein."
|
||
return 1
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
wm_assert_marine_features() {
|
||
wm_marine_features_ready ||
|
||
wm_die "Marine-Features sind nicht vollständig in PostGIS bereit."
|
||
}
|
||
|
||
wm_wait_for_health() {
|
||
local service="$1"
|
||
local timeout_seconds="${2:-180}"
|
||
local container_id status
|
||
local deadline=$((SECONDS + timeout_seconds))
|
||
|
||
container_id="$(wm_compose ps --quiet "$service")"
|
||
[[ -n "$container_id" ]] || wm_die "Container für $service läuft nicht."
|
||
|
||
while ((SECONDS < deadline)); do
|
||
status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$container_id")"
|
||
case "$status" in
|
||
healthy|running)
|
||
wm_log "$service ist bereit."
|
||
return 0
|
||
;;
|
||
unhealthy|exited|dead)
|
||
docker inspect --format '{{json .State}}' "$container_id" >&2 || true
|
||
return 1
|
||
;;
|
||
esac
|
||
sleep 3
|
||
done
|
||
|
||
wm_log "Timeout beim Warten auf $service."
|
||
return 1
|
||
}
|
||
|
||
wm_smoke_test_route() {
|
||
wm_compose exec --no-TTY watermaps node --input-type=module --eval '
|
||
// ROUTE_SOURCE_VALIDATOR_START
|
||
const localEdgeSource = "local-geofabrik-germany+netherlands";
|
||
const postgisEdgeSource = "postgis-osm";
|
||
const bboxSource = String.raw`-?\d{1,3}\.\d{3}(?:--?\d{1,3}\.\d{3}){3}`;
|
||
const localGraphPattern = new RegExp(
|
||
`^fairway-graph:local-geofabrik-${bboxSource}$`
|
||
);
|
||
const combinedGraphPattern = new RegExp(
|
||
`^fairway-graph:combined-postgis-(${bboxSource})\\+local-geofabrik-\\1$`
|
||
);
|
||
const hasExpectedRouteSources = (dataSources) => {
|
||
if (
|
||
!Array.isArray(dataSources)
|
||
|| dataSources.length < 2
|
||
|| dataSources.some((source) => typeof source !== "string")
|
||
|| new Set(dataSources).size !== dataSources.length
|
||
) {
|
||
return false;
|
||
}
|
||
|
||
const graphSources = dataSources.filter((source) =>
|
||
source.startsWith("fairway-graph:")
|
||
);
|
||
const edgeSources = dataSources.filter((source) =>
|
||
!source.startsWith("fairway-graph:")
|
||
);
|
||
if (
|
||
graphSources.length !== 1
|
||
|| edgeSources.length === 0
|
||
|| edgeSources.some(
|
||
(source) => source !== localEdgeSource && source !== postgisEdgeSource
|
||
)
|
||
) {
|
||
return false;
|
||
}
|
||
|
||
const graphSource = graphSources[0];
|
||
return (
|
||
localGraphPattern.test(graphSource)
|
||
&& edgeSources.length === 1
|
||
&& edgeSources[0] === localEdgeSource
|
||
) || (
|
||
combinedGraphPattern.test(graphSource)
|
||
&& (
|
||
edgeSources.includes(localEdgeSource)
|
||
|| edgeSources.includes(postgisEdgeSource)
|
||
)
|
||
);
|
||
};
|
||
// ROUTE_SOURCE_VALIDATOR_END
|
||
const routeChecks = [
|
||
{
|
||
name: "Emden–Ditzum",
|
||
start: { lat: 53.3422, lon: 7.1871 },
|
||
destination: { lat: 53.465, lon: 7.4734 },
|
||
minimumAlternatives: 2
|
||
},
|
||
{
|
||
name: "Ems-Jade-Kanal bei Rahe",
|
||
start: { lat: 53.4498, lon: 7.4509 },
|
||
destination: { lat: 53.4646, lon: 7.4742 },
|
||
minimumAlternatives: 0,
|
||
minimumCoordinates: 25,
|
||
minimumDistanceNm: 1.2,
|
||
maximumDistanceNm: 1.35,
|
||
maximumSegmentNm: 0.55,
|
||
corridorToleranceNm: 0.01,
|
||
corridorCoordinates: [
|
||
[7.4648841, 53.4595046],
|
||
[7.46945, 53.4616984],
|
||
[7.4724784, 53.4646149]
|
||
]
|
||
},
|
||
{
|
||
name: "Norddeich–Norderney",
|
||
start: { lat: 53.6234, lon: 7.1559 },
|
||
destination: { lat: 53.7023, lon: 7.1658 },
|
||
minimumAlternatives: 2
|
||
},
|
||
{
|
||
name: "Emden–Delfzijl",
|
||
start: { lat: 53.3395697, lon: 7.1848883 },
|
||
destination: { lat: 53.330353, lon: 6.9334717 },
|
||
minimumAlternatives: 0,
|
||
minimumCoordinates: 30,
|
||
minimumDistanceNm: 9.5,
|
||
maximumDistanceNm: 10.5,
|
||
maximumSegmentNm: 2,
|
||
maximumLongitude: 7.19,
|
||
corridorCoordinates: [
|
||
[7.1848883, 53.3395697],
|
||
[7.0011017, 53.313849],
|
||
[6.9427276, 53.3256427]
|
||
]
|
||
},
|
||
{
|
||
name: "Weesp–Utrecht (Werkspoorhaven)",
|
||
start: { lat: 52.309, lon: 5.0423 },
|
||
destination: { lat: 52.1058659, lon: 5.0788596 },
|
||
minimumAlternatives: 2
|
||
},
|
||
{
|
||
name: "Lemmer–Sneek",
|
||
start: { lat: 52.844, lon: 5.71 },
|
||
destination: { lat: 53.033, lon: 5.66 },
|
||
minimumAlternatives: 2
|
||
},
|
||
{
|
||
name: "Smal Weesp (Niederlande)",
|
||
start: { lat: 52.3043984, lon: 5.0210794 },
|
||
destination: { lat: 52.307897, lon: 5.0330976 },
|
||
minimumAlternatives: 0
|
||
}
|
||
];
|
||
|
||
for (const routeCheck of routeChecks) {
|
||
const response = await fetch("http://127.0.0.1:5174/api/routes", {
|
||
method: "POST",
|
||
headers: { "content-type": "application/json" },
|
||
body: JSON.stringify({
|
||
start: routeCheck.start,
|
||
destination: routeCheck.destination,
|
||
vesselProfile: { draughtM: 1, safetyReserveM: 0.3 }
|
||
})
|
||
});
|
||
if (!response.ok) {
|
||
console.error(routeCheck.name, response.status, await response.text());
|
||
process.exit(1);
|
||
}
|
||
|
||
const route = await response.json();
|
||
const alternatives = Array.isArray(route.alternatives) ? route.alternatives : [];
|
||
const routeCoordinates = route.geometry?.coordinates;
|
||
const distanceNm = (first, second) => {
|
||
const toRadians = (value) => value * Math.PI / 180;
|
||
const deltaLat = toRadians(second[1] - first[1]);
|
||
const deltaLon = toRadians(second[0] - first[0]);
|
||
const firstLat = toRadians(first[1]);
|
||
const secondLat = toRadians(second[1]);
|
||
const haversine =
|
||
Math.sin(deltaLat / 2) ** 2
|
||
+ Math.cos(firstLat) * Math.cos(secondLat) * Math.sin(deltaLon / 2) ** 2;
|
||
return 3440.065 * 2 * Math.atan2(Math.sqrt(haversine), Math.sqrt(1 - haversine));
|
||
};
|
||
const largestSegmentNm = Array.isArray(routeCoordinates)
|
||
? routeCoordinates.slice(1).reduce(
|
||
(largest, coordinate, index) =>
|
||
Math.max(largest, distanceNm(routeCoordinates[index], coordinate)),
|
||
0
|
||
)
|
||
: Number.POSITIVE_INFINITY;
|
||
const expectedCorridorCoordinates = routeCheck.corridorCoordinates ?? [];
|
||
const corridorCoordinatesAreValid =
|
||
expectedCorridorCoordinates.length === 0 ||
|
||
(
|
||
Array.isArray(routeCoordinates) &&
|
||
expectedCorridorCoordinates.every((expectedCoordinate) =>
|
||
routeCoordinates.some(
|
||
(coordinate) =>
|
||
distanceNm(coordinate, expectedCoordinate) <=
|
||
(routeCheck.corridorToleranceNm ?? 0.15)
|
||
)
|
||
)
|
||
);
|
||
const alternativeRoutesAreValid = alternatives.every((alternative) =>
|
||
alternative.routingMode === "fairway"
|
||
&& Array.isArray(alternative.geometry?.coordinates)
|
||
&& alternative.geometry.coordinates.length >= 2
|
||
&& hasExpectedRouteSources(alternative.dataSources)
|
||
);
|
||
const routeSignatures = [
|
||
route.geometry?.coordinates,
|
||
...alternatives.map((alternative) => alternative.geometry?.coordinates)
|
||
].map((coordinates) => JSON.stringify(coordinates));
|
||
if (
|
||
route.routingMode !== "fairway" ||
|
||
!Array.isArray(routeCoordinates) ||
|
||
routeCoordinates.length < (routeCheck.minimumCoordinates ?? 2) ||
|
||
!hasExpectedRouteSources(route.dataSources) ||
|
||
alternatives.length < routeCheck.minimumAlternatives ||
|
||
route.distanceNm < (routeCheck.minimumDistanceNm ?? 0) ||
|
||
route.distanceNm > (routeCheck.maximumDistanceNm ?? Number.POSITIVE_INFINITY) ||
|
||
largestSegmentNm > (routeCheck.maximumSegmentNm ?? Number.POSITIVE_INFINITY) ||
|
||
routeCoordinates.some(
|
||
([lon]) => lon > (routeCheck.maximumLongitude ?? Number.POSITIVE_INFINITY)
|
||
) ||
|
||
!corridorCoordinatesAreValid ||
|
||
!alternativeRoutesAreValid ||
|
||
new Set(routeSignatures).size !== routeSignatures.length
|
||
) {
|
||
console.error(
|
||
`${routeCheck.name}: Routen- oder Alternativenprüfung des lokalen Deutschland-/Niederlande-Index fehlgeschlagen.`,
|
||
JSON.stringify({
|
||
routingMode: route.routingMode,
|
||
dataSources: route.dataSources,
|
||
distanceNm: route.distanceNm,
|
||
coordinateCount: routeCoordinates?.length,
|
||
largestSegmentNm,
|
||
alternativeCount: alternatives.length,
|
||
minimumAlternatives: routeCheck.minimumAlternatives,
|
||
corridorCoordinatesAreValid
|
||
})
|
||
);
|
||
process.exit(1);
|
||
}
|
||
}
|
||
'
|
||
}
|
||
|
||
wm_smoke_test_features() {
|
||
wm_compose exec --no-TTY watermaps node --input-type=module --eval '
|
||
const params = new URLSearchParams({
|
||
bbox: "7.118483884871649,53.302596677614666,7.543960668999219,53.50669706666667",
|
||
layers: "harbours,locks,bridges"
|
||
});
|
||
const response = await fetch(`http://127.0.0.1:5174/api/features?${params}`);
|
||
if (!response.ok) {
|
||
console.error("Feature-Smoke-Test", response.status, await response.text());
|
||
process.exit(1);
|
||
}
|
||
|
||
const collection = await response.json();
|
||
const features = Array.isArray(collection.features) ? collection.features : [];
|
||
const layers = new Set(features.map((feature) => feature?.properties?.layer));
|
||
const contactFeature = features.find((feature) =>
|
||
["harbours", "locks"].includes(feature?.properties?.layer)
|
||
&& typeof feature?.properties?.phone === "string"
|
||
&& feature.properties.phone.trim().length > 0
|
||
);
|
||
if (
|
||
collection?.metadata?.source !== "postgis"
|
||
|| !layers.has("harbours")
|
||
|| !layers.has("locks")
|
||
|| !layers.has("bridges")
|
||
|| !contactFeature
|
||
) {
|
||
console.error(
|
||
"PostGIS-Feature-Prüfung für die Emden–Aurich-Teststrecke fehlgeschlagen.",
|
||
JSON.stringify({
|
||
source: collection?.metadata?.source,
|
||
featureCount: features.length,
|
||
layers: [...layers],
|
||
hasCallableHarbourOrLock: Boolean(contactFeature)
|
||
})
|
||
);
|
||
process.exit(1);
|
||
}
|
||
'
|
||
}
|