Ajout des velov et des derniers passages
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
const TEMPLATE = document.createElement("template");
|
||||
TEMPLATE.innerHTML = `<style>
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
scrollbar-width: none;
|
||||
|
||||
--markee-speed: 100;
|
||||
--markee-play-state: running;
|
||||
--markee-interaction-pause-delay: 3s;
|
||||
}
|
||||
|
||||
.host::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
::slotted(*) {
|
||||
flex-shrink: 0;
|
||||
width: fit-content;
|
||||
}
|
||||
</style><slot></slot>`;
|
||||
|
||||
const CHECK_OPTIONS_INTERVAL = 100;
|
||||
|
||||
class MarkeeComponent extends HTMLElement {
|
||||
#options = {
|
||||
speed: 0,
|
||||
scrollPauseDelay: 1000,
|
||||
playing: true,
|
||||
scrollPlaying: true,
|
||||
};
|
||||
|
||||
#lastFrame = 0;
|
||||
|
||||
#animationFrameRequest = -1;
|
||||
|
||||
#checkOptionInterval = -1;
|
||||
|
||||
#scrollPlayingTimeout = -1;
|
||||
|
||||
/** @type {{element: Element, rect: DOMRect, margin: number}?} */
|
||||
#nextCollectedElement = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.requestTick = this.requestTick.bind(this);
|
||||
this.updateOptions = this.updateOptions.bind(this);
|
||||
|
||||
this.attachShadow({ mode: "open" });
|
||||
this.shadowRoot.appendChild(TEMPLATE.content.cloneNode(true));
|
||||
}
|
||||
|
||||
tick() {
|
||||
let now = Date.now();
|
||||
let delta = (now - this.#lastFrame) / 1000;
|
||||
this.#lastFrame = now;
|
||||
|
||||
if (!this.#options.playing || !this.#options.scrollPlaying || this.scrollWidth - 800 <= this.clientWidth) return;
|
||||
|
||||
this.scrollLeft += Math.max(this.#options.speed * delta, 1);
|
||||
|
||||
this.checkElementRecycling();
|
||||
}
|
||||
|
||||
requestTick() {
|
||||
this.tick();
|
||||
if (this.#options.playing) {
|
||||
this.#animationFrameRequest = requestAnimationFrame(this.requestTick, 30);
|
||||
}
|
||||
}
|
||||
|
||||
updateOptions() {
|
||||
let style = window.getComputedStyle(this);
|
||||
|
||||
let speed = parseFloat(style.getPropertyValue("--markee-speed"));
|
||||
if (!Number.isNaN(speed)) {
|
||||
this.#options.speed = speed;
|
||||
}
|
||||
|
||||
let wasPlaying = this.#options.playing;
|
||||
|
||||
let playState = style.getPropertyValue("--markee-play-state");
|
||||
if (playState) {
|
||||
this.#options.playing = playState != "paused";
|
||||
} else {
|
||||
this.#options.playing = true;
|
||||
}
|
||||
|
||||
let scrollPauseDelay = style.getPropertyValue(
|
||||
"--markee-interaction-pause-delay"
|
||||
);
|
||||
if (scrollPauseDelay) {
|
||||
let value = parseFloat(scrollPauseDelay);
|
||||
if (scrollPauseDelay.endsWith("ms")) {
|
||||
this.#options.scrollPauseDelay = value;
|
||||
} else if (scrollPauseDelay.endsWith("s")) {
|
||||
this.#options.scrollPauseDelay = value * 1000;
|
||||
} else {
|
||||
this.#options.scrollPauseDelay = 1000;
|
||||
}
|
||||
}
|
||||
|
||||
if (wasPlaying != this.#options.playing) {
|
||||
this.#lastFrame = Date.now();
|
||||
cancelAnimationFrame(this.#animationFrameRequest);
|
||||
this.requestTick();
|
||||
}
|
||||
}
|
||||
|
||||
checkElementRecycling() {
|
||||
if (
|
||||
this.children.length > 0 &&
|
||||
(!this.#nextCollectedElement ||
|
||||
this.#nextCollectedElement.element != this.children[0])
|
||||
) {
|
||||
let element = this.children[0];
|
||||
{
|
||||
let i = 0;
|
||||
while(element.style.display == "none" && i < this.children.length){
|
||||
i += 1;
|
||||
element = this.children[i]
|
||||
}
|
||||
}
|
||||
|
||||
let style = window.getComputedStyle(element);
|
||||
|
||||
this.#nextCollectedElement = {
|
||||
element,
|
||||
rect: element.getBoundingClientRect(),
|
||||
margin: parseFloat(style.marginLeft) + parseFloat(style.marginRight),
|
||||
};
|
||||
}
|
||||
|
||||
if (!this.#nextCollectedElement) return;
|
||||
|
||||
let lastElement = this.children[this.children.length-1];
|
||||
{
|
||||
let i = lastElement.children.length-1;
|
||||
while(lastElement && lastElement.style.display == "none" && i >= 0){
|
||||
i -= 1;
|
||||
lastElement = this.children[i]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (lastElement) {
|
||||
let rect = lastElement.getBoundingClientRect();
|
||||
let thisRect = this.getBoundingClientRect();
|
||||
let style = window.getComputedStyle(lastElement);
|
||||
let margin = parseFloat(style.marginLeft) + parseFloat(style.marginRight);
|
||||
|
||||
if (thisRect.width == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (rect.right + margin < thisRect.left + this.clientWidth) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
this.scrollLeft >=
|
||||
this.scrollWidth -
|
||||
this.clientWidth -
|
||||
this.#nextCollectedElement.rect.width -
|
||||
this.#nextCollectedElement.margin
|
||||
) {
|
||||
this.scrollLeft -=
|
||||
this.#nextCollectedElement.rect.width +
|
||||
this.#nextCollectedElement.margin;
|
||||
this.appendChild(this.#nextCollectedElement.element);
|
||||
this.#nextCollectedElement = null;
|
||||
}
|
||||
}
|
||||
|
||||
handlePointer(e) {
|
||||
this.#options.scrollPlaying = false;
|
||||
clearTimeout(this.#scrollPlayingTimeout);
|
||||
this.#scrollPlayingTimeout = setTimeout(() => {
|
||||
this.#options.scrollPlaying = true;
|
||||
}, this.#options.scrollPauseDelay);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.updateOptions();
|
||||
this.#lastFrame = Date.now();
|
||||
this.requestTick();
|
||||
this.#checkOptionInterval = setInterval(
|
||||
this.updateOptions,
|
||||
CHECK_OPTIONS_INTERVAL
|
||||
);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
clearInterval(this.#checkOptionInterval);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("gavle-marquee", MarkeeComponent);
|
||||
+125
-2
@@ -1,7 +1,27 @@
|
||||
import { getNextPassage } from "../tcl.js";
|
||||
|
||||
const LAST_PASSAGE_WARNING_THREASHOLD_MS = 30*60*1000;
|
||||
|
||||
const TEMPLATE = document.createElement("template");
|
||||
TEMPLATE.innerHTML = `
|
||||
<img class="line-picto" src="" />
|
||||
<h1>
|
||||
<span>vers <span class="line-name"></span></span>
|
||||
<small class="stop-name"></small>
|
||||
</h1>
|
||||
<p class="next-passage">
|
||||
Prochain passage <span class="stop-passage-time-relative"></span>
|
||||
(<time class="stop-passage-time"></time>)
|
||||
</p>
|
||||
<p class="last-passage">
|
||||
<span class="next-and-last" >Prochain et </span>Dernier passage <span class="last-passage-time-relative"></span>
|
||||
(<time class="last-passage-time"></time>)
|
||||
</p>
|
||||
`
|
||||
|
||||
class NextBusElement extends HTMLElement {
|
||||
#stop
|
||||
#autoupdateTimeout
|
||||
|
||||
get stopId(){
|
||||
return this.#stop
|
||||
@@ -9,15 +29,118 @@ class NextBusElement extends HTMLElement {
|
||||
|
||||
set stopId(value){
|
||||
this.#stop = value
|
||||
}
|
||||
|
||||
handleAutoUpdate(){
|
||||
clearTimeout(this.#autoupdateTimeout)
|
||||
this.updateContent()
|
||||
this.#autoupdateTimeout = setTimeout(this.handleAutoUpdate.bind(this), 60000 + ((Math.random() * 10000) - 5000))
|
||||
}
|
||||
|
||||
connectedCallback(){
|
||||
this.replaceChildren(TEMPLATE.content.cloneNode(true))
|
||||
this.handleAutoUpdate()
|
||||
}
|
||||
|
||||
updateContent(){
|
||||
async updateContent(){
|
||||
try {
|
||||
let nextPassage = await getNextPassage(this.stopId)
|
||||
|
||||
let time = nextPassage.time
|
||||
|
||||
if(!time){
|
||||
time = nextPassage.line.timetable.find(it => it.time.getTime() > Date.now())?.time
|
||||
}
|
||||
|
||||
if(!time){
|
||||
return
|
||||
}
|
||||
|
||||
this.querySelector(".line-picto").src = nextPassage.line.picto
|
||||
this.querySelector(".line-name").textContent = nextPassage.line.displayName
|
||||
this.querySelector(".stop-name").textContent = nextPassage.displayName
|
||||
|
||||
let timeFormatter = new Intl.DateTimeFormat("fr-FR", {
|
||||
timeStyle: "short"
|
||||
});
|
||||
let timeEl = this.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"
|
||||
)
|
||||
}
|
||||
this.querySelector(".stop-passage-time-relative").textContent = relative_time_str
|
||||
|
||||
{
|
||||
let last_passage = nextPassage.getLastPassageTime()
|
||||
if(last_passage){
|
||||
let time = last_passage.time
|
||||
let timeEl = this.querySelector(".last-passage-time")
|
||||
timeEl.textContent = timeFormatter.format(time)
|
||||
|
||||
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"
|
||||
)
|
||||
}
|
||||
this.querySelector(".last-passage-time-relative").textContent = relative_time_str
|
||||
this.querySelector(".last-passage").style.display = ""
|
||||
this.querySelector(".last-passage").classList.toggle("warning", (time.getTime() - Date.now()) <= LAST_PASSAGE_WARNING_THREASHOLD_MS)
|
||||
} else {
|
||||
this.querySelector(".last-passage").style.display = "none"
|
||||
}
|
||||
|
||||
if(last_passage && last_passage.time.getTime() == time.getTime()){
|
||||
this.querySelector(".next-and-last").style.display = ""
|
||||
this.querySelector(".next-passage").style.display = "none"
|
||||
} else {
|
||||
this.querySelector(".next-and-last").style.display = "none"
|
||||
this.querySelector(".next-passage").style.display = ""
|
||||
}
|
||||
}
|
||||
|
||||
this.style.display = ""
|
||||
} catch(e){
|
||||
this.style.display = "none"
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
static observedAttributes = ["stop-id"]
|
||||
@@ -31,4 +154,4 @@ class NextBusElement extends HTMLElement {
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("camp-next-bus", NextBusElement);
|
||||
customElements.define("gavle-next-bus", NextBusElement);
|
||||
@@ -0,0 +1,62 @@
|
||||
import { getNextPassage, getVelovBikeState } from "../tcl.js";
|
||||
|
||||
const TEMPLATE = document.createElement("template");
|
||||
TEMPLATE.innerHTML = `
|
||||
<img class="velov-picto" src="/api/valkyrie/assets/sprites/bike-stations.svg?type=image%2Fsvg%2Bxml" />
|
||||
<h1 class="station-name"></h1>
|
||||
<p><span class="bike-available"></span> vélo<span class="plurial">s</span> disponible<span class="plurial">s</span></p>
|
||||
`
|
||||
|
||||
class VelovStationElement extends HTMLElement {
|
||||
#station
|
||||
#autoupdateTimeout
|
||||
|
||||
get stationId(){
|
||||
return this.#station
|
||||
}
|
||||
|
||||
set stationId(value){
|
||||
this.#station = value
|
||||
}
|
||||
|
||||
handleAutoUpdate(){
|
||||
clearTimeout(this.#autoupdateTimeout)
|
||||
this.updateContent()
|
||||
this.#autoupdateTimeout = setTimeout(this.handleAutoUpdate.bind(this), 60000 + ((Math.random() * 10000) - 5000))
|
||||
}
|
||||
|
||||
connectedCallback(){
|
||||
this.replaceChildren(TEMPLATE.content.cloneNode(true))
|
||||
this.handleAutoUpdate()
|
||||
}
|
||||
|
||||
async updateContent(){
|
||||
try {
|
||||
if(!this.stationId)
|
||||
throw new Error("Missing station id")
|
||||
|
||||
let station_data = await getVelovBikeState(this.stationId)
|
||||
|
||||
this.querySelector(".station-name").textContent = station_data.name
|
||||
this.querySelector(".bike-available").textContent = station_data.properties.available_bikes
|
||||
this.querySelector(".bike-available").parentElement.classList.toggle("plurial-content", station_data.properties.available_bikes > 1);
|
||||
|
||||
this.style.display = ""
|
||||
} catch(e){
|
||||
this.style.display = "none"
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
static observedAttributes = ["station-id"]
|
||||
|
||||
attributeChangedCallback(name, oldVal, newVal){
|
||||
switch(name){
|
||||
case "station-id":
|
||||
this.stationId = newVal
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("gavle-velov-station", VelovStationElement);
|
||||
Reference in New Issue
Block a user