fix(routing): allow bidirectional travel on two-way corridors
- u_turn_costs: 60 (was infinite -> U-turns forbidden -> huge detours) - distance_influence 70 -> 20, de-prioritise service/living_street/track - restore safe ignored_highways (was "" -> footways imported, bad snapping) - profiles_ch -> profiles_lm (edge-based CH too heavy with turn_costs) - server port 8989 -> 8080 to match compose mapping, healthcheck and API - add normalize-oneway + check-bidirectional tooling
This commit is contained in:
@@ -1,18 +1,43 @@
|
||||
# --------------------------------------------------------------------------
|
||||
# GraphHopper v11 — إعدادات محرك التوجيه
|
||||
#
|
||||
# ملاحظة جوهرية عن الاتجاهات (oneway):
|
||||
# الاتجاه المسموح لكل حافة (edge) يُحسب مرة واحدة أثناء الاستيراد من وسوم OSM
|
||||
# (oneway / oneway:motorcar / junction=roundabout ...). لا يمكن لأي custom_model
|
||||
# هنا أن يفتح اتجاهاً مغلقاً في البيانات. لذلك أي طريق رئيسي ثنائي الاتجاه يظهر
|
||||
# كأحادي الاتجاه = مشكلة بيانات، وتُعالج قبل البناء عبر:
|
||||
# infrastructure/scripts/normalize-oneway.sh
|
||||
# أما الالتفافات الطويلة "للعودة من نفس الطريق" فهي مشكلة إعدادات، وتُعالج هنا
|
||||
# بواسطة turn_costs.u_turn_costs (كانت لانهائية = ممنوع الالتفاف نهائياً).
|
||||
# --------------------------------------------------------------------------
|
||||
graphhopper:
|
||||
datareader.file: /data/master_map.osm.pbf
|
||||
graph.location: /data/graph-cache
|
||||
import.osm.ignored_highways: ""
|
||||
graph.encoded_values: road_class,road_environment,max_speed,road_access,surface
|
||||
|
||||
# كانت "" أي: لا تتجاهل شيئاً — فتُستورد الأرصفة والمسارات كحافات ويقفز عليها
|
||||
# الـ snapping فيخرج مسار غريب بجانب الشارع. نعيد القيمة الافتراضية الآمنة.
|
||||
import.osm.ignored_highways: "footway,cycleway,path,pedestrian,steps"
|
||||
|
||||
# قائمة واحدة بلا مسافات — GraphHopper يفصل على الفاصلة فقط
|
||||
graph.encoded_values: road_class,road_environment,max_speed,road_access,surface,roundabout,lanes
|
||||
|
||||
profiles:
|
||||
- name: car
|
||||
# قيود الدوران الحقيقية من OSM (no_left_turn ...) مع تكلفة محدودة
|
||||
# للالتفاف 180°. بدون u_turn_costs تكون التكلفة لانهائية، فيرفض المحرك
|
||||
# أي عودة على نفس الطريق ويولّد التفافات ضخمة.
|
||||
turn_costs:
|
||||
vehicle_types: [motorcar, motor_vehicle]
|
||||
u_turn_costs: 60 # ثانية — يسمح بالالتفاف عند فتحات الجزيرة الوسطية دون تفضيله
|
||||
custom_model:
|
||||
distance_influence: 70
|
||||
# 70 ث/كم كانت تدفع المحرك لاختيار الأقصر جغرافياً (حواري وطرق خدمة)
|
||||
# على حساب الشارع الرئيسي. 20 توازن بين الزمن والمسافة.
|
||||
distance_influence: 20
|
||||
speed:
|
||||
# القاعدة العامة أولاً ثم التخصيص؛ limit_to يأخذ الأدنى دائماً.
|
||||
# (قاعدة MOTORWAY/TRUNK القديمة بـ 60 كانت بلا أثر لأن العام 60 أصلاً)
|
||||
- if: "true"
|
||||
limit_to: 60
|
||||
- if: "road_class == MOTORWAY || road_class == TRUNK"
|
||||
limit_to: 60
|
||||
- if: "road_class == PRIMARY"
|
||||
limit_to: 45
|
||||
- if: "road_class == SECONDARY"
|
||||
@@ -23,12 +48,29 @@ graphhopper:
|
||||
limit_to: 14
|
||||
- if: "road_class == LIVING_STREET || road_class == SERVICE"
|
||||
limit_to: 14
|
||||
priority:
|
||||
# تفضيل الشرايين الرئيسية بدل قصّ الطريق عبر الأحياء
|
||||
- if: "road_class == LIVING_STREET"
|
||||
multiply_by: 0.5
|
||||
- if: "road_class == SERVICE"
|
||||
multiply_by: 0.6
|
||||
- if: "road_class == TRACK"
|
||||
multiply_by: 0.3
|
||||
|
||||
profiles_ch:
|
||||
# CH مُعطّل: مع turn_costs يصبح البناء edge-based ويستهلك ذاكرة كبيرة على
|
||||
# ملف مدمج (الأردن+سوريا+مصر)، كما أن الـ API يرسل ch.disable في كل طلب
|
||||
# يحتوي custom_model أو alternative_route أصلاً. LM يعطي السرعة المطلوبة.
|
||||
profiles_ch: []
|
||||
profiles_lm:
|
||||
- profile: car
|
||||
|
||||
routing.non_ch.max_visited_nodes: 2000000
|
||||
|
||||
server:
|
||||
application_connectors:
|
||||
- type: http
|
||||
port: 8989
|
||||
# 8080 داخل الحاوية: docker-compose يعرّض 8989:8080، والـ healthcheck
|
||||
# و GRAPH_HOPPER_URL في الـ API يستهدفان 8080. القيمة القديمة (8989)
|
||||
# كانت تخالف الثلاثة.
|
||||
port: 8080
|
||||
bind_host: 0.0.0.0
|
||||
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
#!/bin/bash
|
||||
# --------------------------------------------------------------------------
|
||||
# check-bidirectional.sh — فحص التوجيه في الاتجاهين على نفس المقطع
|
||||
#
|
||||
# المنطق: على طريق ثنائي الاتجاه سليم، مسافة A→B ومسافة B→A يجب أن تكونا
|
||||
# متقاربتين. فارق كبير (> عتبة النسبة) = المحرك يجبر التفافاً في أحد
|
||||
# الاتجاهين، أي أن الحافة أحادية الاتجاه في البيانات أو أن قيود الدوران
|
||||
# تمنع العودة.
|
||||
#
|
||||
# الاستخدام:
|
||||
# bash infrastructure/scripts/check-bidirectional.sh # حالات مدمجة
|
||||
# bash infrastructure/scripts/check-bidirectional.sh 32.05 36.09 32.07 36.12
|
||||
#
|
||||
# متغيّرات: GH_URL (افتراضي http://localhost:8989)، RATIO (افتراضي 1.5)
|
||||
# --------------------------------------------------------------------------
|
||||
set -uo pipefail
|
||||
|
||||
GH_URL="${GH_URL:-http://localhost:8989}"
|
||||
RATIO="${RATIO:-1.5}"
|
||||
|
||||
# lat1 lon1 lat2 lon2 label
|
||||
CASES=(
|
||||
"32.0537 36.0891 32.0721 36.1204 طريق السخنة (الزرقاء)"
|
||||
"31.9539 35.9106 31.9705 35.9412 عمّان — محور رئيسي"
|
||||
)
|
||||
if [ "$#" -ge 4 ]; then
|
||||
CASES=("$1 $2 $3 $4 مقطع مخصص")
|
||||
fi
|
||||
|
||||
dist() { # lat1 lon1 lat2 lon2 → متر أو FAIL
|
||||
curl -fsS -X POST "$GH_URL/route" -H 'Content-Type: application/json' \
|
||||
-d "{\"points\":[[$2,$1],[$4,$3]],\"profile\":\"car\",\"instructions\":false,\"calc_points\":false}" \
|
||||
2>/dev/null | sed -n 's/.*"distance":\([0-9.]*\).*/\1/p' | head -1
|
||||
}
|
||||
|
||||
echo "════════════════════════════════════════════════════════════"
|
||||
echo " فحص التوجيه ثنائي الاتجاه — $GH_URL"
|
||||
echo "════════════════════════════════════════════════════════════"
|
||||
|
||||
FAIL=0
|
||||
for c in "${CASES[@]}"; do
|
||||
read -r a b x y label <<< "$c"
|
||||
fwd=$(dist "$a" "$b" "$x" "$y")
|
||||
rev=$(dist "$x" "$y" "$a" "$b")
|
||||
echo ""
|
||||
echo "▸ $label"
|
||||
if [ -z "$fwd" ] || [ -z "$rev" ]; then
|
||||
echo " ✗ لم يُعِد المحرك مساراً في أحد الاتجاهين (تحقّق أن الحاوية تعمل والنقاط داخل التغطية)"
|
||||
FAIL=1
|
||||
continue
|
||||
fi
|
||||
printf " ذهاب : %.0f م\n عودة : %.0f م\n" "$fwd" "$rev"
|
||||
verdict=$(awk -v f="$fwd" -v r="$rev" -v t="$RATIO" \
|
||||
'BEGIN{ if(f<=0||r<=0){print "bad"; exit} m=(f>r?f/r:r/f); printf "%s %.2f", (m>t?"fail":"ok"), m }')
|
||||
set -- $verdict
|
||||
if [ "$1" = "ok" ]; then
|
||||
echo " ✓ متقارب (نسبة ${2})"
|
||||
else
|
||||
echo " ✗ فارق كبير (نسبة ${2:-؟}) — اتجاه واحد مفروض أو التفاف إجباري"
|
||||
echo " شغّل: bash infrastructure/scripts/normalize-oneway.sh audit"
|
||||
FAIL=1
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
[ "$FAIL" = "0" ] && echo "✅ كل الحالات سليمة." || echo "⚠️ توجد حالات تحتاج معالجة."
|
||||
exit $FAIL
|
||||
Executable
+80
@@ -0,0 +1,80 @@
|
||||
#!/bin/bash
|
||||
# --------------------------------------------------------------------------
|
||||
# normalize-oneway.sh — تشغيل تصحيح الاتجاهات وإعادة بناء رسم GraphHopper
|
||||
#
|
||||
# الاستخدام:
|
||||
# 1) تقرير فقط (لا يعدّل شيئاً):
|
||||
# bash infrastructure/scripts/normalize-oneway.sh audit
|
||||
# → infrastructure/osm-data/oneway-audit.csv
|
||||
#
|
||||
# 2) تعديل طرق محددة بالاسم (الأكثر أماناً):
|
||||
# bash infrastructure/scripts/normalize-oneway.sh apply --names "طريق السخنة"
|
||||
#
|
||||
# 3) تعديل قائمة way_ids معتمدة بعد مراجعة التقرير:
|
||||
# bash infrastructure/scripts/normalize-oneway.sh apply \
|
||||
# --allowlist infrastructure/osm-data/oneway-allowlist.txt
|
||||
#
|
||||
# 4) تعديل شامل لأصناف كاملة (خطر — يكسر الطرق المزدوجة الصحيحة):
|
||||
# bash infrastructure/scripts/normalize-oneway.sh apply --force
|
||||
#
|
||||
# بعد apply يعيد السكربت بناء الرسم: يحذف graph-cache ويقلع map-routing.
|
||||
# النسخة الأصلية من الـ PBF تبقى كما هي (master_map.osm.pbf.orig).
|
||||
# --------------------------------------------------------------------------
|
||||
set -euo pipefail
|
||||
|
||||
DATA_DIR="./infrastructure/osm-data"
|
||||
PBF="$DATA_DIR/master_map.osm.pbf"
|
||||
VENV="./.venv-osm"
|
||||
PY="$VENV/bin/python"
|
||||
SCRIPT="./infrastructure/scripts/normalize_oneway.py"
|
||||
|
||||
MODE="${1:-audit}"
|
||||
shift || true
|
||||
|
||||
[ -f "$PBF" ] || { echo "❌ الملف غير موجود: $PBF"; exit 1; }
|
||||
|
||||
# ── بيئة pyosmium ────────────────────────────────────────────────────────
|
||||
if [ ! -x "$PY" ]; then
|
||||
echo "📦 تهيئة بيئة pyosmium..."
|
||||
python3 -m venv "$VENV"
|
||||
"$VENV/bin/pip" install --quiet --upgrade pip
|
||||
"$VENV/bin/pip" install --quiet osmium
|
||||
fi
|
||||
|
||||
if [ "$MODE" = "audit" ]; then
|
||||
"$PY" "$SCRIPT" audit --input "$PBF" --report "$DATA_DIR/oneway-audit.csv" "$@"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$MODE" != "apply" ]; then
|
||||
echo "❌ الوضع غير معروف: $MODE (المتاح: audit | apply)"; exit 1
|
||||
fi
|
||||
|
||||
# ── تعديل البيانات ───────────────────────────────────────────────────────
|
||||
TMP="$DATA_DIR/master_map.normalized.osm.pbf"
|
||||
rm -f "$TMP"
|
||||
"$PY" "$SCRIPT" apply --input "$PBF" --output "$TMP" "$@"
|
||||
|
||||
if [ ! -f "$PBF.orig" ]; then
|
||||
echo "💾 حفظ نسخة أصلية: $PBF.orig"
|
||||
cp "$PBF" "$PBF.orig"
|
||||
fi
|
||||
mv "$TMP" "$PBF"
|
||||
|
||||
# ── إعادة بناء الرسم ─────────────────────────────────────────────────────
|
||||
# GraphHopper لا يعيد قراءة الـ PBF إن كان graph-cache موجوداً.
|
||||
echo "🧹 حذف graph-cache وإعادة بناء الرسم..."
|
||||
docker compose stop routing || true
|
||||
rm -rf "$DATA_DIR/graph-cache"
|
||||
docker compose up -d routing
|
||||
|
||||
echo "⏳ انتظار جهوزية المحرك (قد يستغرق البناء عدة دقائق)..."
|
||||
for i in $(seq 1 120); do
|
||||
if curl -fsS http://localhost:8989/health >/dev/null 2>&1; then
|
||||
echo "✅ map-routing جاهز."
|
||||
exit 0
|
||||
fi
|
||||
sleep 10
|
||||
done
|
||||
echo "⚠️ لم يستجب المحرك بعد 20 دقيقة — راجع: docker compose logs -f routing"
|
||||
exit 1
|
||||
Executable
+225
@@ -0,0 +1,225 @@
|
||||
#!/usr/bin/env python3
|
||||
# --------------------------------------------------------------------------
|
||||
# normalize_oneway.py — تصحيح وسوم الاتجاه الواحد قبل بناء رسم GraphHopper
|
||||
#
|
||||
# لماذا هذا السكربت أصلاً:
|
||||
# GraphHopper يثبّت اتجاه كل حافة أثناء الاستيراد اعتماداً على وسوم OSM
|
||||
# (oneway, oneway:motorcar, junction=roundabout). لا يوجد أي إعداد في
|
||||
# config.yml — ولا أي custom_model — يستطيع فتح اتجاه مُغلق في البيانات.
|
||||
# فإن كان طريق رئيسي ثنائي الاتجاه في الواقع (مثل طريق السخنة) موسوماً
|
||||
# oneway=yes في OSM، فالحل الوحيد هو تعديل البيانات قبل البناء.
|
||||
#
|
||||
# وضعان:
|
||||
# audit : لا يعدّل شيئاً. يُخرج CSV بكل الطرق أحادية الاتجاه المرشحة
|
||||
# (way_id, highway, name, الطول التقريبي) لمراجعتها بشرياً.
|
||||
# apply : يكتب ملف PBF جديداً بعد إزالة/تصحيح وسوم الاتجاه، إما
|
||||
# لقائمة معتمدة (--allowlist) أو لأصناف طرق كاملة (--classes).
|
||||
#
|
||||
# تحذير مهم:
|
||||
# الطرق المزدوجة (dual carriageway) هي جسمان منفصلان كل منهما أحادي الاتجاه
|
||||
# بشكل صحيح تماماً. إزالة oneway عنها تُنتج مسارات غير قانونية (سير بعكس
|
||||
# السير). لذلك الافتراضي هو --allowlist. استخدام --classes عملية شاملة
|
||||
# وخطرة، وتتطلب --force، ويجب أن تسبقها مراجعة تقرير audit.
|
||||
# --------------------------------------------------------------------------
|
||||
import argparse
|
||||
import csv
|
||||
import math
|
||||
import sys
|
||||
|
||||
try:
|
||||
import osmium
|
||||
except ImportError:
|
||||
sys.exit(
|
||||
"pyosmium غير مثبت.\n"
|
||||
" python3 -m venv .venv-osm && ./.venv-osm/bin/pip install osmium\n"
|
||||
"ثم شغّل السكربت عبر ./.venv-osm/bin/python"
|
||||
)
|
||||
|
||||
ONEWAY_TRUE = {"yes", "true", "1", "-1"}
|
||||
|
||||
# الوسوم التي تحدد الاتجاه لسيارة. جميعها يجب أن يُزال معاً، وإلا بقي القيد.
|
||||
DIRECTIONAL_TAGS = ("oneway", "oneway:motorcar", "oneway:motor_vehicle", "oneway:vehicle")
|
||||
|
||||
# أصناف لا تُمسّ أبداً: المسارات المنفصلة والروابط والدوارات أحادية بطبيعتها.
|
||||
NEVER_TOUCH_HIGHWAY = {"motorway", "motorway_link"}
|
||||
|
||||
DEFAULT_CLASSES = ["trunk", "primary", "secondary", "tertiary"]
|
||||
|
||||
|
||||
def haversine_len(nodes):
|
||||
"""طول تقريبي بالأمتار من عقد الطريق (يتطلب مواقع العقد)."""
|
||||
total = 0.0
|
||||
prev = None
|
||||
for n in nodes:
|
||||
if not n.location.valid():
|
||||
prev = None
|
||||
continue
|
||||
if prev is not None:
|
||||
lat1, lon1, lat2, lon2 = map(
|
||||
math.radians, (prev.lat, prev.lon, n.location.lat, n.location.lon)
|
||||
)
|
||||
a = (
|
||||
math.sin((lat2 - lat1) / 2) ** 2
|
||||
+ math.cos(lat1) * math.cos(lat2) * math.sin((lon2 - lon1) / 2) ** 2
|
||||
)
|
||||
total += 2 * 6371000 * math.asin(math.sqrt(a))
|
||||
prev = n.location
|
||||
return total
|
||||
|
||||
|
||||
def is_oneway(tags):
|
||||
for t in DIRECTIONAL_TAGS:
|
||||
if tags.get(t) in ONEWAY_TRUE:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def is_protected(tags):
|
||||
"""أحادي الاتجاه لسبب هيكلي صحيح — لا يُلمس."""
|
||||
if tags.get("highway") in NEVER_TOUCH_HIGHWAY:
|
||||
return True
|
||||
if (tags.get("highway") or "").endswith("_link"):
|
||||
return True
|
||||
if tags.get("junction") in ("roundabout", "circular"):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class Auditor(osmium.SimpleHandler):
|
||||
def __init__(self, classes, writer):
|
||||
super().__init__()
|
||||
self.classes = set(classes)
|
||||
self.writer = writer
|
||||
self.count = 0
|
||||
self.meters = 0.0
|
||||
|
||||
def way(self, w):
|
||||
tags = dict(w.tags)
|
||||
hw = tags.get("highway")
|
||||
if hw not in self.classes or not is_oneway(tags) or is_protected(tags):
|
||||
return
|
||||
length = haversine_len(w.nodes)
|
||||
self.count += 1
|
||||
self.meters += length
|
||||
self.writer.writerow(
|
||||
[
|
||||
w.id,
|
||||
hw,
|
||||
tags.get("name", ""),
|
||||
tags.get("name:ar", ""),
|
||||
round(length),
|
||||
tags.get("lanes", ""),
|
||||
tags.get("oneway", ""),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class Rewriter(osmium.SimpleHandler):
|
||||
"""ينسخ الملف كاملاً ويحذف وسوم الاتجاه عن الطرق المستهدفة فقط."""
|
||||
|
||||
def __init__(self, writer, classes, allowlist, names):
|
||||
super().__init__()
|
||||
self.writer = writer
|
||||
self.classes = set(classes) if classes else set()
|
||||
self.allowlist = allowlist or set()
|
||||
self.names = {n.strip() for n in (names or []) if n.strip()}
|
||||
self.changed = 0
|
||||
|
||||
def node(self, n):
|
||||
self.writer.add_node(n)
|
||||
|
||||
def relation(self, r):
|
||||
self.writer.add_relation(r)
|
||||
|
||||
def _targeted(self, w, tags):
|
||||
if is_protected(tags):
|
||||
return False
|
||||
if w.id in self.allowlist:
|
||||
return True
|
||||
if self.names and (
|
||||
tags.get("name") in self.names or tags.get("name:ar") in self.names
|
||||
):
|
||||
return True
|
||||
return bool(self.classes) and tags.get("highway") in self.classes
|
||||
|
||||
def way(self, w):
|
||||
tags = dict(w.tags)
|
||||
if is_oneway(tags) and self._targeted(w, tags):
|
||||
for t in DIRECTIONAL_TAGS:
|
||||
tags.pop(t, None)
|
||||
# أثر مقصود: علامة تسمح بتتبع ما عدّلناه لاحقاً في PostGIS/التقارير
|
||||
tags["oneway"] = "no"
|
||||
tags["gh:oneway_normalized"] = "yes"
|
||||
self.changed += 1
|
||||
self.writer.add_way(w.replace(tags=tags))
|
||||
else:
|
||||
self.writer.add_way(w)
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description="تصحيح وسوم الاتجاه الواحد في ملف PBF")
|
||||
p.add_argument("mode", choices=["audit", "apply"])
|
||||
p.add_argument("--input", required=True)
|
||||
p.add_argument("--output", help="ملف PBF الناتج (مطلوب في apply)")
|
||||
p.add_argument("--report", default="oneway-audit.csv")
|
||||
p.add_argument(
|
||||
"--classes",
|
||||
default=",".join(DEFAULT_CLASSES),
|
||||
help="أصناف highway المستهدفة (audit) أو المشمولة بالتعديل الشامل (apply + --force)",
|
||||
)
|
||||
p.add_argument("--allowlist", help="ملف نصي: way_id في كل سطر")
|
||||
p.add_argument("--names", help="أسماء طرق مفصولة بفاصلة، مثل: طريق السخنة")
|
||||
p.add_argument(
|
||||
"--force",
|
||||
action="store_true",
|
||||
help="مطلوب لتطبيق التعديل على أصناف كاملة بدل قائمة معتمدة",
|
||||
)
|
||||
args = p.parse_args()
|
||||
|
||||
classes = [c.strip() for c in args.classes.split(",") if c.strip()]
|
||||
|
||||
if args.mode == "audit":
|
||||
with open(args.report, "w", newline="", encoding="utf-8") as fh:
|
||||
wr = csv.writer(fh)
|
||||
wr.writerow(
|
||||
["way_id", "highway", "name", "name_ar", "length_m", "lanes", "oneway"]
|
||||
)
|
||||
h = Auditor(classes, wr)
|
||||
h.apply_file(args.input, locations=True)
|
||||
print(f"✅ audit: {h.count} طريق أحادي الاتجاه، ~{h.meters/1000:.1f} كم")
|
||||
print(f" التقرير: {args.report}")
|
||||
print(" راجعه ثم مرّر way_ids المعتمدة عبر --allowlist في وضع apply.")
|
||||
return
|
||||
|
||||
if not args.output:
|
||||
sys.exit("apply يتطلب --output")
|
||||
|
||||
allow = set()
|
||||
if args.allowlist:
|
||||
with open(args.allowlist, encoding="utf-8") as fh:
|
||||
for line in fh:
|
||||
line = line.split("#")[0].strip()
|
||||
if line.isdigit():
|
||||
allow.add(int(line))
|
||||
|
||||
names = args.names.split(",") if args.names else []
|
||||
blanket = not allow and not names
|
||||
if blanket and not args.force:
|
||||
sys.exit(
|
||||
"لا allowlist ولا names → هذا تعديل شامل على أصناف كاملة.\n"
|
||||
"يكسر الطرق المزدوجة (dual carriageway) الموسومة بشكل صحيح.\n"
|
||||
"أعد التشغيل مع --force إن كان هذا مقصوداً بعد مراجعة تقرير audit."
|
||||
)
|
||||
|
||||
writer = osmium.SimpleWriter(args.output)
|
||||
try:
|
||||
h = Rewriter(writer, classes if blanket else [], allow, names)
|
||||
h.apply_file(args.input, locations=False)
|
||||
finally:
|
||||
writer.close()
|
||||
print(f"✅ apply: عُدّل {h.changed} طريق → {args.output}")
|
||||
print(" الخطوة التالية: احذف graph-cache وأعد بناء GraphHopper.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user