v1
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "lol/liquid-shape-distortions"]
|
||||||
|
path = lol/liquid-shape-distortions
|
||||||
|
url = https://github.com/collidingScopes/liquid-shape-distortions
|
||||||
Binary file not shown.
@@ -0,0 +1,26 @@
|
|||||||
|
const TEMPLATE = document.createElement("template");
|
||||||
|
TEMPLATE.innerHTML = `<span class="hours">--</span><span class="deux-points">:</span><span class="minutes">--</span>`
|
||||||
|
|
||||||
|
class ClockElement extends HTMLElement {
|
||||||
|
constructor(){
|
||||||
|
super()
|
||||||
|
setInterval(this.updateContent.bind(this), 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback(){
|
||||||
|
this.replaceChildren(TEMPLATE.content.cloneNode(true))
|
||||||
|
this.updateContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
updateContent(){
|
||||||
|
if(!this.querySelector(".hours"))
|
||||||
|
return
|
||||||
|
|
||||||
|
let now = new Date();
|
||||||
|
this.querySelector(".hours").textContent = now.getHours().toString().padStart(2, "0")
|
||||||
|
this.querySelector(".minutes").textContent = now.getMinutes().toString().padStart(2, "0")
|
||||||
|
this.setAttribute("datetime", now.toISOString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("gavle-clock", ClockElement)
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
const TEMPLATE = document.createElement("template");
|
||||||
|
|
||||||
|
class NextBusElement extends HTMLElement {
|
||||||
|
#stop
|
||||||
|
|
||||||
|
get stopId(){
|
||||||
|
return this.#stop
|
||||||
|
}
|
||||||
|
|
||||||
|
set stopId(value){
|
||||||
|
this.#stop = value
|
||||||
|
this.updateContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback(){
|
||||||
|
this.replaceChildren(TEMPLATE.content.cloneNode(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
updateContent(){
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static observedAttributes = ["stop-id"]
|
||||||
|
|
||||||
|
attributeChangedCallback(name, oldVal, newVal){
|
||||||
|
switch(name){
|
||||||
|
case "stop-id":
|
||||||
|
this.stopId = newVal
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("camp-next-bus", NextBusElement);
|
||||||
+263
@@ -0,0 +1,263 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>GavleTv × Dernier métro</title>
|
||||||
|
<style>
|
||||||
|
@font-face {
|
||||||
|
font-family: "bianzhidai nobg pearl";
|
||||||
|
src: url("./bianzhidai_noBG-Pearl.otf");
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--tcl-red: #e2001a;
|
||||||
|
}
|
||||||
|
|
||||||
|
body, html {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: black;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#gavle-tv {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thinking {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 10;
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
background: white;
|
||||||
|
border: white solid 4px;
|
||||||
|
box-shadow: 8px 8px 0 black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thinking iframe {
|
||||||
|
width: 200%;
|
||||||
|
height: 200%;
|
||||||
|
transform-origin: 0 0;
|
||||||
|
transform: scale(0.5);
|
||||||
|
border: none;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thinking img {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
opacity: 0.5;
|
||||||
|
mix-blend-mode: darken;
|
||||||
|
}
|
||||||
|
|
||||||
|
#next-line {
|
||||||
|
font-family: monospace;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 15px;
|
||||||
|
font-size: 20px;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
grid-template-columns: min-content 1fr;
|
||||||
|
grid-template-rows: min-content 1fr;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
background: var(--tcl-red);
|
||||||
|
border: none;
|
||||||
|
z-index: 100;
|
||||||
|
|
||||||
|
transform: translateY(0);
|
||||||
|
transition: ease-out 0.8s transform;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
grid-column: 2;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > h1 {
|
||||||
|
font-size: 1.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > img:first-child {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1;
|
||||||
|
height: 1.3em;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#next-line[hidden] {
|
||||||
|
transform: translateY(-100%);
|
||||||
|
transition-timing-function: ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
#last-passage {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 15px;
|
||||||
|
right: 15px;
|
||||||
|
max-width: calc(100% - 30px);
|
||||||
|
max-height: calc(100% - 30px);
|
||||||
|
|
||||||
|
font-size: 20px;
|
||||||
|
font-family: monospace;
|
||||||
|
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
background: var(--tcl-red);
|
||||||
|
padding: 15px;
|
||||||
|
margin: 0;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .last-passage {
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
|
||||||
|
&[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
grid-template-columns: min-content 1fr;
|
||||||
|
grid-template-rows: 1fr min-content;
|
||||||
|
column-gap: 10px;
|
||||||
|
row-gap: 5px;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
grid-column: 2;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > img {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1 / 3;
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
& h1 {
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
& h1 small {
|
||||||
|
font-size: inherit;
|
||||||
|
font-weight: normal;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
& img {
|
||||||
|
height: 2em;
|
||||||
|
max-width: 3em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ul, ol {
|
||||||
|
padding-left: calc(15px + 2em);
|
||||||
|
margin: 0.75em 0;
|
||||||
|
padding-left: 1em;
|
||||||
|
|
||||||
|
& > li:not(:last-child) {
|
||||||
|
margin-bottom: 0.75em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#next-line:not([hidden]) ~ #last-passage {
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
|
||||||
|
& img {
|
||||||
|
opacity: 0.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#next-line ~ #last-passage {
|
||||||
|
transition: color 0.5s steps(5);
|
||||||
|
|
||||||
|
& img {
|
||||||
|
transition: opacity 0.5s steps(5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes deux-points {
|
||||||
|
0% { opacity: 1; }
|
||||||
|
25% { opacity: 1; }
|
||||||
|
25.1% { opacity: 0; }
|
||||||
|
100% { opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
#clock {
|
||||||
|
display: block;
|
||||||
|
font-family: "bianzhidai nobg pearl";
|
||||||
|
position: fixed;
|
||||||
|
bottom: 5px;
|
||||||
|
left: 15px;
|
||||||
|
font-size: 17vh;
|
||||||
|
z-index: 1000;
|
||||||
|
color: white;
|
||||||
|
mix-blend-mode: exclusion;
|
||||||
|
line-height: 0.8;
|
||||||
|
|
||||||
|
.deux-points {
|
||||||
|
animation: deux-points steps(4) 1s infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<iframe id="gavle-tv" src="./liquid-shape-distortions/index.html" frameborder="0"></iframe>
|
||||||
|
<script src="index.js" type="module"></script>
|
||||||
|
<div id="next-line" hidden>
|
||||||
|
<img class="line-picto" src="" />
|
||||||
|
<h1 class="line-name"></h1>
|
||||||
|
<p>
|
||||||
|
<span class="stop-name"></span>:
|
||||||
|
prochain départ <span class="stop-passage-time-relative"></span>
|
||||||
|
(<time class="stop-passage-time"></time>)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div id="last-passage" style="width: 100%;">
|
||||||
|
<section style="max-width: 350px;">
|
||||||
|
Quelques petites choses a faire avant de partir :
|
||||||
|
<ul>
|
||||||
|
<li><strong>Ranger</strong> les établis et les machines</li>
|
||||||
|
<li>Faire un petit coup de <strong>vaisselle</strong></li>
|
||||||
|
</ul>
|
||||||
|
Bonne nuit <strong>:)</strong>
|
||||||
|
</section>
|
||||||
|
<template class="last-passage-template">
|
||||||
|
<article class="last-passage">
|
||||||
|
<img class="line-picto" src="" />
|
||||||
|
<h1>
|
||||||
|
<span class="line-name" ></span>
|
||||||
|
<small class="stop-name"></small>
|
||||||
|
</h1>
|
||||||
|
<p>
|
||||||
|
Dernier passage dans <span class="stop-passage-time-relative"></span>
|
||||||
|
(<time class="stop-passage-time"></time>)
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="module" src="./components/clock.js"></script>
|
||||||
|
<gavle-clock id="clock"></gavle-clock>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+185
@@ -0,0 +1,185 @@
|
|||||||
|
import { getNextPassage } from "./tcl.js";
|
||||||
|
|
||||||
|
const MIN_TIME_BETWEEN_DAYS = 2 * 60 * 60000 // 2 hours
|
||||||
|
const MAX_LAST_PASSAGE_TIME = 30 * 60000 // 30 min
|
||||||
|
const MAX_DERNIER_METRO_MESSAGE = 3
|
||||||
|
|
||||||
|
let tracked_stops = [
|
||||||
|
"line:SYTNEX:A/forward/stop_point:SYTNEX:46052", // Metro A dir Vaulx, Hotel de ville
|
||||||
|
"line:SYTNEX:A/backward/stop_point:SYTNEX:42743", // Metro A dir Perrache, Hotel de ville
|
||||||
|
"line:SYTNEX:9/forward/stop_point:SYTNEX:2494", // Bus 9 dir Satoney, Pont de lattre RD
|
||||||
|
"line:SYTNEX:9/backward/stop_point:SYTNEX:2494", // Bus 9 dir cordeliers, Pont de lattre RD
|
||||||
|
"line:SYTNEX:C/forward/stop_point:SYTNEX:10787", // Métro C dir Cuire, Croix-Paquet
|
||||||
|
"line:SYTNEX:C6/forward/stop_point:SYTNEX:2496", // Bus C6 arret Pont de lattre RD campus lyon ouest
|
||||||
|
"line:SYTNEX:C6/backward/stop_point:SYTNEX:2495", // Bus C6 arret Pont de lattre RD Gare part dieu v. merle
|
||||||
|
"line:SYTNEX:C13/forward/stop_point:SYTNEX:10169", // Bus C13 arret Austerlitz montessuy gutemberg
|
||||||
|
"line:SYTNEX:C13/backward/stop_point:SYTNEX:10169", // Bus C13 arret Austerlitz vers grange blanche
|
||||||
|
"line:SYTNEX:PL2/backward/stop_point:SYTNEX:10889", // Bus PL2 arret Pont de lattre RD vers Musée des confluences
|
||||||
|
"line:SYTNEX:PL2/forward/stop_point:SYTNEX:10169", // BUS PL2 arret Austerlitz vers Cuire
|
||||||
|
"line:SYTNEX:C23/backward/stop_point:SYTNEX:10889", // Bus C13 arret Pont de lattre RD vers Flachet - Alain Gilles
|
||||||
|
"line:SYTNEX:C23/forward/stop_point:SYTNEX:10889", // Bus C13 arret Pont de lattre RD vers Cite internationale
|
||||||
|
]
|
||||||
|
|
||||||
|
let stops_cache = {}
|
||||||
|
|
||||||
|
async function shuffleStops(){
|
||||||
|
tracked_stops = tracked_stops
|
||||||
|
.map(value => ({ value, sort: Math.random() }))
|
||||||
|
.sort((a, b) => a.sort - b.sort)
|
||||||
|
.map(({ value }) => value)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getNextStop(){
|
||||||
|
let stop = tracked_stops.shift()
|
||||||
|
tracked_stops.push(stop)
|
||||||
|
return stop
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showNextLine(){
|
||||||
|
let stop = await getNextStop()
|
||||||
|
|
||||||
|
let nextPassage = await getNextPassage(stop)
|
||||||
|
stops_cache[stop] = nextPassage
|
||||||
|
|
||||||
|
let time = nextPassage.time
|
||||||
|
|
||||||
|
if(!time){
|
||||||
|
time = nextPassage.line.timetable.find(it => it.time.getTime() > Date.now())?.time
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!time){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let modal = document.getElementById("next-line")
|
||||||
|
modal.hidden = false
|
||||||
|
|
||||||
|
modal.querySelector(".line-picto").src = nextPassage.line.picto
|
||||||
|
modal.querySelector(".line-name").textContent = nextPassage.line.displayName
|
||||||
|
modal.querySelector(".stop-name").textContent = nextPassage.displayName
|
||||||
|
|
||||||
|
let timeFormatter = new Intl.DateTimeFormat("fr-FR", {
|
||||||
|
timeStyle: "short"
|
||||||
|
});
|
||||||
|
let timeEl = modal.querySelector(".stop-passage-time")
|
||||||
|
timeEl.textContent = timeFormatter.format(time)
|
||||||
|
|
||||||
|
let relativeTimeFOrmatter = new Intl.RelativeTimeFormat("fr-FR", {
|
||||||
|
style: "short",
|
||||||
|
numeric: "auto"
|
||||||
|
});
|
||||||
|
|
||||||
|
let time_minutes = Math.floor((time.getTime() - Date.now()) / 60000)
|
||||||
|
|
||||||
|
let relative_time_str;
|
||||||
|
if(time_minutes > 60) {
|
||||||
|
let time_hours = Math.floor(time_minutes / 60)
|
||||||
|
time_minutes -= time_hours * 60
|
||||||
|
|
||||||
|
relative_time_str = relativeTimeFOrmatter.format(
|
||||||
|
time_hours,
|
||||||
|
"hours"
|
||||||
|
)
|
||||||
|
if(time_minutes > 0){
|
||||||
|
relative_time_str += ` et ${Math.abs(time_minutes)} min`
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
relative_time_str = relativeTimeFOrmatter.format(
|
||||||
|
time_minutes,
|
||||||
|
"minutes"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
modal.querySelector(".stop-passage-time-relative").textContent = relative_time_str
|
||||||
|
|
||||||
|
setTimeout(() => modal.hidden = true, 15000)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLastPassageTime(stopOrLine){
|
||||||
|
|
||||||
|
let timetable = stopOrLine.timetable || stopOrLine.line.timetable
|
||||||
|
|
||||||
|
if(!timetable){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let now = new Date()
|
||||||
|
|
||||||
|
let last_passage = timetable
|
||||||
|
.find((it, i) => {
|
||||||
|
if((timetable[i+1]) && it.time.getTime() > now && (it.time.getTime() + MIN_TIME_BETWEEN_DAYS) < timetable[i+1].time.getTime()){
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return last_passage
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateLastMetro(){
|
||||||
|
let last_passage_list = Object.values(stops_cache)
|
||||||
|
.map(it => [it, getLastPassageTime(it)])
|
||||||
|
.filter(([_, lastPassage]) => lastPassage && (lastPassage.time.getTime() - Date.now()) < MAX_LAST_PASSAGE_TIME)
|
||||||
|
|
||||||
|
let hidden_offset = 0
|
||||||
|
let last_hidden = true
|
||||||
|
for(let el of document.querySelectorAll("#last-passage > .last-passage")){
|
||||||
|
if(last_hidden && el.hidden){
|
||||||
|
hidden_offset++
|
||||||
|
} else {
|
||||||
|
last_hidden = false
|
||||||
|
}
|
||||||
|
el.remove()
|
||||||
|
}
|
||||||
|
|
||||||
|
let container = document.getElementById("last-passage")
|
||||||
|
let template = document.querySelector("#last-passage template.last-passage-template");
|
||||||
|
|
||||||
|
if(hidden_offset >= last_passage_list.length){
|
||||||
|
hidden_offset = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if(last_passage_list.length > 0){
|
||||||
|
let i = 0
|
||||||
|
for(let [stop, last_passage] of last_passage_list){
|
||||||
|
let el = template.content.cloneNode(true)
|
||||||
|
|
||||||
|
el.children[0].hidden = i < hidden_offset || i >= hidden_offset + MAX_DERNIER_METRO_MESSAGE;
|
||||||
|
|
||||||
|
el.querySelector(".line-picto").src = stop.line.picto
|
||||||
|
el.querySelector(".line-name").textContent = stop.line.displayName
|
||||||
|
el.querySelector(".stop-name").textContent = stop.displayName
|
||||||
|
|
||||||
|
let timeFormatter = new Intl.DateTimeFormat("fr-FR", {
|
||||||
|
timeStyle: "short"
|
||||||
|
});
|
||||||
|
let timeEl = el.querySelector(".stop-passage-time")
|
||||||
|
timeEl.textContent = timeFormatter.format(last_passage.time)
|
||||||
|
timeEl.datetime = last_passage.time.toISOString()
|
||||||
|
|
||||||
|
let relativeTimeFormatter = new Intl.RelativeTimeFormat("fr-FR", {
|
||||||
|
style: "short",
|
||||||
|
numeric: "auto"
|
||||||
|
});
|
||||||
|
|
||||||
|
el.querySelector(".stop-passage-time-relative").textContent = relativeTimeFormatter.format(
|
||||||
|
Math.floor((last_passage.time.getTime() - Date.now()) / 60000),
|
||||||
|
"minutes"
|
||||||
|
)
|
||||||
|
|
||||||
|
container.prepend(el)
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
container.hidden = false
|
||||||
|
} else {
|
||||||
|
container.hidden = true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
shuffleStops()
|
||||||
|
.then(() => showNextLine())
|
||||||
|
.then(() => updateLastMetro())
|
||||||
|
|
||||||
|
setInterval(() => showNextLine(), 17000)
|
||||||
|
setInterval(() => updateLastMetro(), 60000)
|
||||||
Submodule
+1
Submodule lol/liquid-shape-distortions added at a30cc65745
Binary file not shown.
|
After Width: | Height: | Size: 880 KiB |
@@ -0,0 +1,189 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
+164
@@ -0,0 +1,164 @@
|
|||||||
|
const DAY_IN_MS = 8.64e+7;
|
||||||
|
|
||||||
|
class StopId {
|
||||||
|
constructor(stopIdString){
|
||||||
|
let [lineId, direction, stopId] = stopIdString.split(/\//)
|
||||||
|
this.lineId = lineId
|
||||||
|
this.stopId = stopId
|
||||||
|
this.direction = direction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getNextTripJSON(stop_id){
|
||||||
|
let res = await fetch(`/api/interface/tcl/next-trips/stops/${encodeURIComponent(stop_id.stopId)}/${encodeURIComponent(stop_id.lineId)}/${stop_id.direction}`);
|
||||||
|
if(!res.ok){
|
||||||
|
throw new Error(`Server responded with ${res.status} ${res.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = await res.json();
|
||||||
|
|
||||||
|
if(!result.data){
|
||||||
|
throw new Error(`Stop ${stop_id.stopId} not found`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getStopsJSON(stop_id){
|
||||||
|
let res = await fetch(`/api/interface/tcl/lines/${encodeURIComponent(stop_id.lineId)}/stops`);
|
||||||
|
if(!res.ok){
|
||||||
|
throw new Error(`Server responded with ${res.status} ${res.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = await res.json();
|
||||||
|
|
||||||
|
if(!result.data){
|
||||||
|
throw new Error(`Line ${stop_id.lineId} not found`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getTimetableJSON(stop_id, date){
|
||||||
|
let res = await fetch(`/api/interface/tcl/timetables/${encodeURIComponent(stop_id.stopId)}/${encodeURIComponent(stop_id.lineId)}/${stop_id.direction}?date=${date.getFullYear()}-${(date.getMonth()+1).toString().padStart(2, "0")}-${date.getDate().toString().padStart(2, "0")}`);
|
||||||
|
if(!res.ok){
|
||||||
|
throw new Error(`Server responded with ${res.status} ${res.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = await res.json();
|
||||||
|
|
||||||
|
if(!result.data){
|
||||||
|
throw new Error(`Timetable for ${stop_id.lineId} not found`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getLineDetailsJSON(stop_id){
|
||||||
|
let res = await fetch(`/api/interface/tcl/lines/${encodeURIComponent(stop_id.lineId)}`);
|
||||||
|
if(!res.ok){
|
||||||
|
throw new Error(`Server responded with ${res.status} ${res.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = await res.json();
|
||||||
|
|
||||||
|
if(!result.data){
|
||||||
|
throw new Error(`Line ${stop_id.lineId} not found`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIconURL(stop_id){
|
||||||
|
return new URL(`/api/valkyrie/assets/lines/${stop_id.lineId}.svg?type=image%2Fsvg%2Bxml`, window.location).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getLineDetails(stop_id){
|
||||||
|
let line_json = await getLineDetailsJSON(stop_id)
|
||||||
|
|
||||||
|
let route = line_json.routes.find(it => it.direction == stop_id.direction)
|
||||||
|
|
||||||
|
let line = {
|
||||||
|
id: stop_id.lineId,
|
||||||
|
displayName: route?.name,
|
||||||
|
picto: getIconURL(stop_id),
|
||||||
|
name: line_json.code,
|
||||||
|
direction: route?.name,
|
||||||
|
timetable: null
|
||||||
|
}
|
||||||
|
|
||||||
|
return line
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getTimetable(stop_id, date){
|
||||||
|
let timetable_json = await getTimetableJSON(stop_id, date)
|
||||||
|
|
||||||
|
let timetable = []
|
||||||
|
for(let scheduled_time of timetable_json.scheduleTimes){
|
||||||
|
let time = new Date(scheduled_time.dateTime)
|
||||||
|
timetable.push({
|
||||||
|
displayHours: time.getHours().toString().padStart(2, "0"),
|
||||||
|
displayMinutes: time.getMinutes().toString().padStart(2, "0"),
|
||||||
|
displayTime: `${time.getHours().toString().padStart(2, "0")}h${time.getMinutes().toString().padStart(2, "0")}`,
|
||||||
|
time
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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 stopId = new StopId(stop_description);
|
||||||
|
|
||||||
|
let proms = [
|
||||||
|
getLineDetails(stopId),
|
||||||
|
getNextTripJSON(stopId),
|
||||||
|
getStopsJSON(stopId),
|
||||||
|
Promise.resolve([])
|
||||||
|
]
|
||||||
|
|
||||||
|
let timeTableAmount = options?.timetable || +2;
|
||||||
|
if(timeTableAmount){
|
||||||
|
for(let dayOffset = 0; dayOffset<timeTableAmount; dayOffset++){
|
||||||
|
let now = new Date(Date.now()+(dayOffset*DAY_IN_MS))
|
||||||
|
proms.push(await getTimetable(stopId, now))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let [line, next_trip, line_stops, ...all_timetables] = await Promise.all(proms);
|
||||||
|
|
||||||
|
console.log(stopId)
|
||||||
|
console.log("line", line)
|
||||||
|
console.log("next trip", next_trip)
|
||||||
|
console.log("stop", line_stops)
|
||||||
|
console.log("timetable", all_timetables)
|
||||||
|
|
||||||
|
let line_timetables = []
|
||||||
|
for(let timetable of all_timetables){
|
||||||
|
line_timetables.push(...timetable)
|
||||||
|
}
|
||||||
|
line.timetable = line_timetables
|
||||||
|
|
||||||
|
let stop = line_stops.find(it => it.id == stopId.stopId)
|
||||||
|
|
||||||
|
let nextPassage = {
|
||||||
|
line,
|
||||||
|
displayName: stop?.name,
|
||||||
|
displayTime: "<unsupported>",
|
||||||
|
time: next_trip.schedules?.[0] ? new Date(next_trip.schedules?.[0].dateTime) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextPassage
|
||||||
|
}
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
daemon off;
|
||||||
|
#error_log /tmp/gavle-error.log;
|
||||||
|
pid /dev/null;
|
||||||
|
|
||||||
|
|
||||||
|
include /etc/nginx/modules-enabled/*.conf;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 768;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
sendfile on;
|
||||||
|
tcp_nopush on;
|
||||||
|
types_hash_max_size 2048;
|
||||||
|
|
||||||
|
resolver 10.0.0.2 [fd45:ae2e:e260:101::] valid=30s;
|
||||||
|
|
||||||
|
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
access_log /dev/stdout;
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 8000 ssl default_server;
|
||||||
|
ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
|
||||||
|
ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||||
|
|
||||||
|
root /home/lol/dernier-metro;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_ssl_server_name on;
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_pass https://carte-interactive.tcl.fr/;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /lol {
|
||||||
|
root /home/lol/dernier-metro;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+2
@@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
sudo systemctl restart getty@tty1.service
|
||||||
Reference in New Issue
Block a user