|
import json |
|
import requests |
|
import re |
|
|
|
def normalize_line_name(line_name): |
|
""" |
|
Normalise les noms de lignes pour éviter les doublons causés par des préfixes ou variations. |
|
Exemples : |
|
- "RATP Métro 14" devient "Métro 14" |
|
- "SNCF RER A" devient "RER A" |
|
""" |
|
line_name = line_name.lower() |
|
line_name = re.sub(r"\b(ratp|sncf)\b", "", line_name) |
|
line_name = re.sub(r"\s+", " ", line_name).strip() |
|
return line_name.capitalize() |
|
|
|
|
|
headers = { |
|
'accept': 'application/json', |
|
'apiKey': '3WsgOWybmrTiEwa3q8ZsvovvwPkrctnX' |
|
} |
|
|
|
base_url_api = 'https://prim.iledefrance-mobilites.fr/marketplace/v2/navitia/line_reports/physical_modes/physical_mode%3A' |
|
|
|
categories = { |
|
"Métro": "Metro", |
|
"RER": "RapidTransit", |
|
"Bus": "Bus", |
|
"Transilien": "LocalTrain", |
|
"Tram": "Tramway" |
|
} |
|
|
|
|
|
def fetch_disruptions(category_key, category_value): |
|
url_api = f"{base_url_api}{category_value}/line_reports?" |
|
try: |
|
response_api = requests.get(url_api, headers=headers) |
|
response_api.raise_for_status() |
|
data = response_api.json() |
|
print(f"Données récupérées avec succès pour {category_key}.") |
|
return data.get("disruptions", []) |
|
except requests.exceptions.RequestException as e: |
|
print(f"Erreur lors de la récupération des données pour {category_key}: {e}") |
|
return [] |
|
|
|
def extract_lines_from_message(messages): |
|
""" |
|
Extraire les lignes mentionnées dans les messages d'une perturbation. |
|
""" |
|
lines = [] |
|
for msg in messages: |
|
text = msg.get("text", "") |
|
|
|
matches = re.findall(r"(Métro \d+|RER [A-Z]|Bus \d+|Transilien \w+|Tram \d+)", text) |
|
lines.extend(matches) |
|
return lines or ["Toutes les lignes"] |
|
|
|
|
|
all_disruptions = {} |
|
for category, api_value in categories.items(): |
|
disruptions = fetch_disruptions(category, api_value) |
|
active_disruptions = [ |
|
{ |
|
"id": disruption.get("id"), |
|
"cause": disruption.get("cause"), |
|
"severity": disruption.get("severity", {}).get("name", "N/A"), |
|
"effect": disruption.get("effect"), |
|
"message": disruption.get("messages")[0].get("text") if disruption.get("messages") else "No message", |
|
|
|
"impacted_lines": [ |
|
obj.get("pt_object", {}).get("name", "Unknown Line") |
|
for obj in disruption.get("impacted_objects", []) |
|
if obj.get("pt_object", {}).get("embedded_type") == "line" |
|
] or extract_lines_from_message(disruption.get("messages", [])), |
|
} |
|
for disruption in disruptions |
|
if disruption.get("status") == "active" |
|
and all(excluded not in (disruption.get("cause", "").lower() + " " + |
|
" ".join([msg.get("text", "").lower() for msg in disruption.get("messages", [])])) |
|
for excluded in ["ascenseur", "transdev"]) |
|
] |
|
all_disruptions[category] = active_disruptions |
|
|
|
|
|
html_output_path = "./tmp/traffic_report.html" |
|
with open(html_output_path, "w", encoding="utf-8") as html_file: |
|
|
|
seen_lines = set() |
|
summary_data = [] |
|
|
|
|
|
allowed_severities = {"perturbée", "bloquante"} |
|
|
|
for category, disruptions in all_disruptions.items(): |
|
for disruption in disruptions: |
|
|
|
severity = disruption.get("severity", "").lower() |
|
if severity not in allowed_severities: |
|
continue |
|
|
|
for line in disruption["impacted_lines"]: |
|
|
|
normalized_line = normalize_line_name(line) |
|
if normalized_line not in seen_lines: |
|
summary_data.append({ |
|
"category": category, |
|
"line": normalized_line, |
|
"cause": disruption["cause"], |
|
"severity": disruption["severity"] |
|
}) |
|
seen_lines.add(normalized_line) |
|
|
|
|
|
html_file.write(f""" |
|
<!DOCTYPE html> |
|
<html lang="fr"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Rapport des Perturbations</title> |
|
<style> |
|
body {{ |
|
font-family: Arial, sans-serif; |
|
margin: 0; |
|
padding: 0; |
|
background-color: #f4f4f9; |
|
}} |
|
h1 {{ |
|
text-align: center; |
|
padding: 20px 0; |
|
background-color: #007BFF; |
|
color: white; |
|
margin: 0; |
|
}} |
|
h2 {{ |
|
color: #007BFF; |
|
text-align: left; |
|
margin: 20px 40px; |
|
}} |
|
table {{ |
|
width: 90%; |
|
margin: 20px auto; |
|
border-collapse: collapse; |
|
box-shadow: 0 4px 8px rgba(0,0,0,0.1); |
|
}} |
|
th, td {{ |
|
padding: 12px; |
|
text-align: left; |
|
border: 1px solid #ddd; |
|
}} |
|
th {{ |
|
background-color: #007BFF; |
|
color: white; |
|
}} |
|
/* Suppression de l'alternance des couleurs */ |
|
/* tr:nth-child(even) {{ |
|
background-color: #f9f9f9; |
|
}} */ |
|
tr:hover {{ |
|
background-color: #f1f1f1; |
|
}} |
|
.summary {{ |
|
margin: 20px 40px; |
|
padding: 20px; |
|
background-color: #FFF9C4; |
|
border: 1px solid #FFEB3B; |
|
border-radius: 5px; |
|
}} |
|
.severity-perturbee {{ |
|
background-color: #F9A972; /* Orange */ |
|
color: black; |
|
}} |
|
.severity-bloquante {{ |
|
background-color: #EC2828; /* Rouge */ |
|
color: white; |
|
}} |
|
.category-bold {{ |
|
font-weight: bold; |
|
}} |
|
</style> |
|
</head> |
|
<body> |
|
<h1>Rapport des Perturbations</h1> |
|
|
|
<!-- Section Récapitulatif --> |
|
<div class="summary"> |
|
<h2>Récapitulatif des Lignes Impactées (Perturbée / Bloquante)</h2> |
|
<table> |
|
<thead> |
|
<tr> |
|
<th>Catégorie</th> |
|
<th>Ligne</th> |
|
<th>Cause</th> |
|
<th>Gravité</th> |
|
</tr> |
|
</thead> |
|
<tbody> |
|
""") |
|
|
|
|
|
for item in summary_data: |
|
severity_class = "severity-perturbee" if item["severity"].lower() == "perturbée" else "severity-bloquante" |
|
html_file.write(f""" |
|
<tr class="{severity_class}"> |
|
<td class="category-bold">{item["category"]}</td> |
|
<td>{item["line"]}</td> |
|
<td>{item["cause"]}</td> |
|
<td>{item["severity"]}</td> |
|
</tr> |
|
""") |
|
|
|
|
|
html_file.write(""" |
|
</tbody> |
|
</table> |
|
</div> |
|
""") |
|
|
|
|
|
for category, disruptions in all_disruptions.items(): |
|
html_file.write(f"<h2>{category}</h2>") |
|
html_file.write(""" |
|
<table> |
|
<thead> |
|
<tr> |
|
<th>Ligne</th> |
|
<th>Cause</th> |
|
<th>Gravité</th> |
|
<th>Message</th> |
|
</tr> |
|
</thead> |
|
<tbody> |
|
""") |
|
for disruption in disruptions: |
|
for line in disruption["impacted_lines"]: |
|
html_file.write(f""" |
|
<tr> |
|
<td>{line}</td> |
|
<td>{disruption["cause"]}</td> |
|
<td>{disruption["severity"]}</td> |
|
<td>{disruption["message"]}</td> |
|
</tr> |
|
""") |
|
html_file.write(""" |
|
</tbody> |
|
</table> |
|
""") |
|
|
|
|
|
html_file.write(""" |
|
</body> |
|
</html> |
|
""") |
|
|
|
print(f"HTML report generated at {html_output_path}") |
|
|
|
def generate_traffic_html(): |
|
all_disruptions = {} |
|
for category, api_value in categories.items(): |
|
disruptions = fetch_disruptions(category, api_value) |
|
active_disruptions = [ |
|
{ |
|
"id": disruption.get("id"), |
|
"cause": disruption.get("cause"), |
|
"severity": disruption.get("severity", {}).get("name", "N/A"), |
|
"effect": disruption.get("effect"), |
|
"message": disruption["messages"][0].get("text") if disruption.get("messages") else "No message", |
|
"impacted_lines": [ |
|
obj.get("pt_object", {}).get("name", "Unknown Line") |
|
for obj in disruption.get("impacted_objects", []) |
|
if obj.get("pt_object", {}).get("embedded_type") == "line" |
|
] or extract_lines_from_message(disruption.get("messages", [])), |
|
} |
|
for disruption in disruptions |
|
if disruption.get("status") == "active" |
|
and all(excluded not in (disruption.get("cause", "").lower() + " " + |
|
" ".join([msg.get("text", "").lower() for msg in disruption.get("messages", [])])) |
|
for excluded in ["ascenseur", "transdev"]) |
|
] |
|
all_disruptions[category] = active_disruptions |
|
|
|
|
|
seen_lines = set() |
|
summary_data = [] |
|
allowed_severities = {"perturbée", "bloquante"} |
|
|
|
for category, disruptions in all_disruptions.items(): |
|
for disruption in disruptions: |
|
severity = disruption.get("severity", "").lower() |
|
if severity not in allowed_severities: |
|
continue |
|
for line in disruption["impacted_lines"]: |
|
normalized_line = normalize_line_name(line) |
|
if normalized_line not in seen_lines: |
|
summary_data.append({ |
|
"category": category, |
|
"line": normalized_line, |
|
"cause": disruption["cause"], |
|
"severity": disruption["severity"] |
|
}) |
|
seen_lines.add(normalized_line) |
|
|
|
|
|
|
|
categories_presentes = sorted(list(all_disruptions.keys())) |
|
|
|
html_content = f""" |
|
<!DOCTYPE html> |
|
<html lang="fr"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Rapport des Perturbations</title> |
|
<style> |
|
body {{ |
|
font-family: Arial, sans-serif; |
|
margin: 0; |
|
padding: 0; |
|
background-color: #f4f4f9; |
|
}} |
|
h1 {{ |
|
text-align: center; |
|
padding: 20px 0; |
|
background-color: #007BFF; |
|
color: white; |
|
margin: 0; |
|
}} |
|
h2 {{ |
|
color: #007BFF; |
|
text-align: left; |
|
margin: 20px 40px; |
|
}} |
|
.filter-container {{ |
|
margin: 20px 40px; |
|
}} |
|
table {{ |
|
width: 90%; |
|
margin: 20px auto; |
|
border-collapse: collapse; |
|
box-shadow: 0 4px 8px rgba(0,0,0,0.1); |
|
}} |
|
th, td {{ |
|
padding: 12px; |
|
text-align: left; |
|
border: 1px solid #ddd; |
|
}} |
|
th {{ |
|
background-color: #007BFF; |
|
color: white; |
|
}} |
|
tr:hover {{ |
|
background-color: #f1f1f1; |
|
}} |
|
.summary {{ |
|
margin: 20px 40px; |
|
padding: 20px; |
|
background-color: #FFF9C4; |
|
border: 1px solid #FFEB3B; |
|
border-radius: 5px; |
|
}} |
|
.severity-perturbee {{ |
|
background-color: #F9A972; /* Orange */ |
|
color: black; |
|
}} |
|
.severity-bloquante {{ |
|
background-color: #EC2828; /* Rouge */ |
|
color: white; |
|
}} |
|
.category-bold {{ |
|
font-weight: bold; |
|
}} |
|
</style> |
|
</head> |
|
<body> |
|
<h1>Rapport des Perturbations</h1> |
|
|
|
<div class="filter-container"> |
|
<label for="categoryFilter">Filtrer par catégorie :</label> |
|
<select id="categoryFilter"> |
|
<option value="all">Toutes</option> |
|
""" |
|
|
|
|
|
for cat in categories_presentes: |
|
html_content += f'<option value="{cat}">{cat}</option>' |
|
|
|
html_content += """ |
|
</select> |
|
</div> |
|
|
|
<div class="summary"> |
|
<h2>Récapitulatif des Lignes Impactées (Perturbée / Bloquante)</h2> |
|
<table id="summaryTable"> |
|
<thead> |
|
<tr> |
|
<th>Catégorie</th> |
|
<th>Ligne</th> |
|
<th>Cause</th> |
|
<th>Gravité</th> |
|
</tr> |
|
</thead> |
|
<tbody> |
|
""" |
|
|
|
|
|
for item in summary_data: |
|
severity_class = "severity-perturbee" if item["severity"].lower() == "perturbée" else "severity-bloquante" |
|
|
|
html_content += f""" |
|
<tr class="{severity_class}" data-category="{item["category"]}"> |
|
<td class="category-bold">{item["category"]}</td> |
|
<td>{item["line"]}</td> |
|
<td>{item["cause"]}</td> |
|
<td>{item["severity"]}</td> |
|
</tr> |
|
""" |
|
|
|
html_content += """ |
|
</tbody> |
|
</table> |
|
</div> |
|
""" |
|
|
|
|
|
for category, disruptions in all_disruptions.items(): |
|
|
|
html_content += f'<h2 data-category="{category}">{category}</h2>' |
|
html_content += f""" |
|
<table data-category="{category}"> |
|
<thead> |
|
<tr> |
|
<th>Ligne</th> |
|
<th>Cause</th> |
|
<th>Gravité</th> |
|
<th>Message</th> |
|
</tr> |
|
</thead> |
|
<tbody> |
|
""" |
|
for disruption in disruptions: |
|
for line in disruption["impacted_lines"]: |
|
sev_class = "severity-perturbee" if disruption["severity"].lower() == "perturbée" else "severity-bloquante" |
|
html_content += f""" |
|
<tr class="{sev_class}" data-category="{category}"> |
|
<td>{line}</td> |
|
<td>{disruption["cause"]}</td> |
|
<td>{disruption["severity"]}</td> |
|
<td>{disruption["message"]}</td> |
|
</tr> |
|
""" |
|
html_content += """ |
|
</tbody> |
|
</table> |
|
""" |
|
|
|
|
|
html_content += """ |
|
<script> |
|
const categoryFilter = document.getElementById('categoryFilter'); |
|
categoryFilter.addEventListener('change', () => { |
|
const selectedCategory = categoryFilter.value; |
|
|
|
// Filtrer les lignes du récapitulatif |
|
const summaryRows = document.querySelectorAll('#summaryTable tbody tr'); |
|
summaryRows.forEach(row => { |
|
const rowCategory = row.getAttribute('data-category'); |
|
if (selectedCategory === 'all' || rowCategory === selectedCategory) { |
|
row.style.display = ''; |
|
} else { |
|
row.style.display = 'none'; |
|
} |
|
}); |
|
|
|
// Filtrer les tables de détails |
|
const detailTables = document.querySelectorAll('table[data-category]'); |
|
detailTables.forEach(table => { |
|
const tableCat = table.getAttribute('data-category'); |
|
if (selectedCategory === 'all' || tableCat === selectedCategory) { |
|
table.style.display = ''; |
|
} else { |
|
table.style.display = 'none'; |
|
} |
|
}); |
|
|
|
// Filtrer aussi les titres des catégories |
|
const headings = document.querySelectorAll('h2[data-category]'); |
|
headings.forEach(heading => { |
|
const headingCat = heading.getAttribute('data-category'); |
|
if (selectedCategory === 'all' || headingCat === selectedCategory) { |
|
heading.style.display = ''; |
|
} else { |
|
heading.style.display = 'none'; |
|
} |
|
}); |
|
}); |
|
</script> |
|
""" |
|
|
|
return html_content |