Files
2026-06-13 20:42:03 +02:00

190 lines
6.5 KiB
JavaScript

const BASE_MAP_URL = new URL("/public-transport/lines/", document.baseURI)
const DAY_IN_MS = 8.64e+7;
async function waitForElement(iframe, selector, options = {
timeout: 15000,
multiple: false
}){
let timeout = options?.timeout || 15000
let multiple = options?.multiple || false
let el = null;
let startDate = Date.now()
if(multiple){
while(!(
el = iframe.contentWindow.document.querySelectorAll(selector)
)?.length){
await new Promise(res => setTimeout(res, 500))
if(Date.now() > startDate+timeout) {
throw new Error("Element search timeout")
}
}
} else {
while((
el = iframe.contentWindow.document.querySelector(selector)
) == null){
await new Promise(res => setTimeout(res, 500))
if(Date.now() > startDate+timeout) {
throw new Error("Element search timeout")
}
}
}
return el
}
async function spawnIframe(url){
/** @type {HTMLIFrameElement} */
let iframe = document.createElement("iframe")
let container = document.createElement("div")
container.classList.add("thinking")
container.style.left = Math.round(Math.random()*(window.innerWidth-400))+"px"
container.style.top = Math.round(Math.random()*(window.innerHeight-350))+"px"
let img = document.createElement("img")
img.src = "/lol/math.gif"
container.append(img)
container.append(iframe)
document.body.append(container)
await navigateIframe(iframe, url)
return iframe
}
async function navigateIframe(iframe, url){
let prom = new Promise(res => iframe.addEventListener("load", res(), {once: true}))
iframe.src = new URL(url, BASE_MAP_URL).toString()
await prom
return iframe
}
async function getLineDetails(iframe){
let lineElement = await waitForElement(iframe, `[class|="content"] [class|="pictoAndDirection"]`)
let linePictoElement = await waitForElement(iframe, `[class|="content"] [class|="linePictoSvg"]`)
let directionTextElement = await waitForElement(iframe, `[class|="content"] [class|="directionText"]`)
let line = {
displayName: lineElement.getAttribute("aria-label"),
picto: linePictoElement.src,
name: linePictoElement.getAttribute("aria-label"),
direction: directionTextElement.innerText,
timetable: null
}
return line
}
async function getStopTimeTable(iframe, stop_description, amount=1){
let timetable = []
for(let dayOffset = 0; dayOffset<amount; dayOffset++){
let now = new Date(Date.now()+(dayOffset*DAY_IN_MS))
await navigateIframe(iframe, "./"+stop_description+`/timetable?date=${now.getFullYear()}-${(now.getMonth()+1).toString().padStart(2, "0")}-${(now.getDate()).toString().padStart(2, "0")}`)
let timeTableElList = await waitForElement(iframe, `[class|="content"] [class|="timeTableContainer"] table`, {multiple: true});
for(let timeTableEl of timeTableElList){
let trancheHoraires = timeTableEl.querySelectorAll("thead tr:first-child th")
for(let i = 0; i< trancheHoraires.length; i++){
let el = trancheHoraires[i]
let hours = parseInt(el.innerText.trim())
if(Number.isFinite(hours)){
let allHoraires = timeTableEl.querySelectorAll(`tbody tr td:nth-child(${i+1})`)
for(let hel of allHoraires){
let minutes = parseInt(hel.innerText.trim())
if(Number.isFinite(minutes)){
timetable.push({
displayHours: el.innerText,
displayMinutes: hel.innerText,
displayTime: `${el.innerText}${hel.innerText}`,
time: new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
hours,
minutes
)
})
}
}
}
}
}
}
return timetable
}
/**
* Get next Bus/Metro/Tram passage of given stop description
* @param {String} stop_description Stop description string
*
* To get stop description go to https://carte-interactive.tcl.fr/public-transport/lines/
* then find your line and your stop in this line.
* Your URL must look like something like this
* https://carte-interactive.tcl.fr/public-transport/lines/line:SYTNEX:C/forward/stop_point:SYTNEX:10787
* Stop description is everything after "lines/" ("line:SYTNEX:C/forward/stop_point:SYTNEX:10787" in example above)
*/
export async function getNextPassage(stop_description, options = {
timetable: +2 // Today and tomorrow
}){
let iframe = await spawnIframe("./"+stop_description)
let nextPassageElement = await waitForElement(iframe, `[class|="content"] [class|="stopCard"] [class|="schedule"]`);
let stopName = await waitForElement(iframe, `[class|="content"] [class|="stopCard"] [class|="nameStop"]`);
let line = await getLineDetails(iframe, stop_description)
let timetable = []
let timeTableAmount = options?.timetable || +2;
if(timeTableAmount){
timetable = await getStopTimeTable(iframe, stop_description, timeTableAmount)
} else {
timetable = null
}
line.timetable = timetable
let nextPassage = {
line,
displayName: stopName.innerText,
displayTime: nextPassageElement.innerText
}
{
let timeText = nextPassageElement.innerText.trim();
let time;
if(timeText.endsWith("min")){
time = new Date(Date.now() + parseInt(timeText)*60000)
} else {
let match = timeText.match(/^(\d+):(\d+)/)
if(match){
let now = new Date()
time = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
parseInt(match[1]),
parseInt(match[2])
)
if(time.getTime() < Date.now()){
time.setTime(time.getTime() + DAY_IN_MS)
}
}
}
nextPassage.time = time
}
setTimeout(() => {
iframe.parentElement.remove()
}, Math.random()*1000)
return nextPassage
}