oeffisearch/client/js/canvas.js
2020-02-07 14:09:42 +01:00

322 lines
10 KiB
JavaScript

'use strict';
import { moreJourneys } from './journeysView.js';
import { dataStorage } from './app.js';
const padZeroes = (str) => ('00' + str).slice(-2);
const formatTime = (date) => {
date = new Date(date * 1000);
return `${padZeroes(date.getHours())}:${padZeroes(date.getMinutes())}`
};
const textFor = (leg) => leg.line && leg.line.name || "";
const colorFor = (leg, type) => {
let product = leg.line && leg.line.product || "walk";
return colors[type][product] || colors[type].default;
};
const flatten = (arr) => [].concat(...arr);
const colors = {
fill: {
'tram': '#cc5555',
'subway': '#5555cc',
'suburban': '#55aa55',
'nationalExp': '#fff',
'national': '#fff',
'regionalExp': '#888',
'regional': '#888',
'bus': '#aa55aa',
default: '#888'
},
text: {
'nationalExp': '#ee3333',
'national': '#ee3333',
default: '#fff'
},
icon: {
'walk': 'directions_walk',
'transfer': 'directions_transfer',
'subway': 'directions_subway',
'bus': 'directions_bus',
'tram': 'tram',
default: 'train'
}
};
let rectWidth, padding, rectWidthWithPadding, canvas, ctx, dpr;
const canvasState = {
journeys: [],
offsetX: 0,
firstJourney: 1,
};
const textCache = {};
let textCacheWidth;
let textCacheDpr;
export const setupCanvas = (data) => {
canvasState.offsetX = (window.innerWidth / dpr) > 600 ? 120 : 60,
canvas = document.getElementById('canvas');
ctx = canvas.getContext('2d');
canvasState.journeys = Object.keys(data.journeys).sort((a, b) => Number(a) - Number(b)).map(k => data.journeys[k]);
canvasState.reqId = data.reqId;
canvasState.firstJourney = Number(Object.keys(data.journeys).sort((a, b) => Number(a) - Number(b))[0]);
canvas.addEventListener('mousedown', mouseDownHandler, {passive: true});
canvas.addEventListener('touchstart', mouseDownHandler, {passive: true});
updateTextCache();
resizeHandler();
};
const addTextToCache = (text, color, fixedHeight) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
ctx.shadowBlur = dataStorage.settings.fancyCanvas ? 5 : 0;
ctx.shadowColor = '#00000080';
let height, width
if (fixedHeight) {
height = 15;
ctx.font = `${height}px sans-serif`;
width = ctx.measureText(text).width;
} else {
const measureAccuracy = 50;
ctx.font = `${measureAccuracy}px sans-serif`;
width = rectWidth - 10;
height = Math.abs(measureAccuracy * (width / (1 - ctx.measureText(text).width)));
}
canvas.width = width * dpr;
canvas.height = (Math.ceil(height) + 1) * dpr;
ctx.scale(dpr, dpr);
ctx.font = `${height}px sans-serif`;
ctx.fillStyle = color;
if (height > 10) {
ctx.fillText(text, 0, height);
textCache[text] = canvas;
}
};
const updateTextCache = () => {
textCache.length = 0;
textCacheWidth = rectWidth;
textCacheDpr = dpr;
for (let journey of canvasState.journeys) {
for (let leg of journey.legs) {
addTextToCache(textFor(leg), colorFor(leg, "text"));
let times = [];
if (journey.legs.indexOf(leg) == 0) times.push(leg.departure);
if (journey.legs.indexOf(leg) == journey.legs.length - 1) times.push(leg.arrival);
for (let time of times) {
addTextToCache(formatTime(time.prognosedTime || time.plannedTime), "#fff", 15);
}
}
}
};
const renderJourneys = () => {
ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
ctx.fillStyle = '#00000080';
ctx.fillRect(0, 0, canvas.width / dpr, canvas.height / dpr);
let x = canvasState.offsetX, y;
let firstVisibleJourney = Math.max(0, Math.floor((-canvasState.offsetX + padding) / rectWidthWithPadding));
let numVisibleJourneys = Math.ceil(canvas.width / dpr / rectWidthWithPadding);
let visibleJourneys = canvasState.journeys.slice(firstVisibleJourney, firstVisibleJourney + numVisibleJourneys);
if (!visibleJourneys.length) return;
let firstDeparture = visibleJourneys[0].legs[0].departure.plannedTime;
let lastArrival = Math.max.apply(Math,
visibleJourneys.map(journey => journey.legs[journey.legs.length-1].arrival.plannedTime)
.concat(visibleJourneys.map(journey => journey.legs[journey.legs.length-1].arrival.prognosedTime))
);
let scaleFactor = 1/(lastArrival - firstDeparture) * (canvas.height - 64 * dpr) / dpr;
let time = canvasState.journeys[0].legs[0].departure.plannedTime;
ctx.font = `${(window.innerWidth / dpr) > 600 ? 20 : 15}px sans-serif`;
ctx.fillStyle = '#aaa';
while (time < lastArrival) {
let y = (time - firstDeparture) * scaleFactor + 32;
ctx.fillText(formatTime(time), (window.innerWidth / dpr) > 600 ? 30 : 10, y);
ctx.fillRect(0, y, canvas.width / dpr, 1);
time += Math.floor(120/scaleFactor);
}
ctx.fillStyle = '#fa5';
y = (Number(new Date()) / 1000 - firstDeparture) * scaleFactor + 32;
ctx.fillRect(0, y-2, canvas.width / dpr, 5);
const p = new Path2D('M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z');
ctx.fillStyle = '#fff';
ctx.shadowBlur = dataStorage.settings.fancyCanvas ? 5 : 0;
ctx.shadowColor = '#00000080';
ctx.save();
ctx.scale(3, 3);
ctx.translate(x / 3 - 15, canvas.height / dpr / 6 - 24);
ctx.rotate(-Math.PI*1.5);
ctx.fill(p);
ctx.restore();
ctx.beginPath();
ctx.arc(x - 80,canvas.height / dpr / 2 - 35,50,0,2*Math.PI);
ctx.fillStyle = '#ffffff40';
ctx.fill();
ctx.strokeStyle = '#00000020';
ctx.stroke();
for (let journey of canvasState.journeys) {
journey.legs.reverse();
for (let leg of journey.legs) {
let depDelayed = leg.departure.prognosedTime && leg.departure.prognosedTime != leg.departure.plannedTime
let arrDelayed = leg.arrival.prognosedTime && leg.arrival.prognosedTime != leg.arrival.plannedTime
if (depDelayed || arrDelayed) {
const start = leg.departure.plannedTime;
const stop = leg.arrival.plannedTime;
const duration = (stop - start) * scaleFactor;
y = (start - firstDeparture) * scaleFactor + 32;
ctx.shadowBlur = 0;
ctx.fillStyle = '#44444480';
ctx.strokeStyle = '#ffffff80';
ctx.fillRect(x-padding, y, rectWidth, duration);
ctx.strokeRect(x-padding, y, rectWidth, duration);
}
}
x += rectWidthWithPadding;
}
x = canvasState.offsetX;
for (let journey of canvasState.journeys) {
for (let leg of journey.legs) {
const start = leg.departure.prognosedTime || leg.departure.plannedTime;
const stop = leg.arrival.prognosedTime || leg.arrival.plannedTime;
const duration = (stop - start) * scaleFactor;
y = (start - firstDeparture) * scaleFactor + 32;
ctx.shadowBlur = dataStorage.settings.fancyCanvas ? 10 : 0;
ctx.shadowColor = '#00000060';
if (leg.isWalking || leg.isTransfer) {
ctx.fillStyle = '#777';
ctx.fillRect(x + rectWidth / 2 - rectWidth / 10, y, rectWidth / 5, duration);
} else {
ctx.fillStyle = colorFor(leg, "fill");
ctx.fillRect(x, y, rectWidth, duration);
}
ctx.shadowBlur = 0;
let preRenderedText = textCache[textFor(leg)];
if ((preRenderedText.height / dpr) < duration - 5) {
ctx.scale(1 / dpr, 1 / dpr);
ctx.drawImage(preRenderedText, dpr * (x + 5), Math.floor(dpr * (y + duration / 2) - preRenderedText.height / 1.5));
ctx.scale(dpr, dpr);
}
/* draw journey start and end time */
let time;
// note: leg order is reversed at this point in time
let times = [];
if (journey.legs.indexOf(leg) == journey.legs.length - 1) times.push([leg.departure, y - 9.5]);
if (journey.legs.indexOf(leg) == 0) times.push([leg.arrival, y + duration + 7.5]);
for (let [time, y] of times) {
preRenderedText = textCache[formatTime(time.prognosedTime || time.plannedTime)];
ctx.scale(1 / dpr, 1 / dpr);
ctx.drawImage(preRenderedText, Math.ceil(dpr * (x + ((rectWidth - preRenderedText.width/dpr)) / 2)), dpr * (y - 7.5)); //Math.floor(dpr * (x + rectWidth / 2) - preRenderedText.width), dpr * (y - 1));
ctx.scale(dpr, dpr);
}
}
journey.legs.reverse();
x += rectWidthWithPadding;
}
ctx.fillStyle = '#fff';
ctx.shadowBlur = dataStorage.settings.fancyCanvas ? 5 : 0;
ctx.shadowColor = '#00000080';
ctx.save();
ctx.scale(3, 3);
ctx.translate(x / 3 + 5, canvas.height / dpr / 6);
ctx.rotate(Math.PI*1.5);
ctx.fill(p);
ctx.restore();
ctx.beginPath();
ctx.arc(x + 50,canvas.height / dpr / 2 - 35,50,0,2*Math.PI);
ctx.fillStyle = '#ffffff40';
ctx.fill();
ctx.strokeStyle = '#00000020';
ctx.stroke();
};
const resizeHandler = () => {
dpr = window.devicePixelRatio || 1;
if (!document.getElementById('canvas')) return;
rectWidth = (window.innerWidth / dpr) > 600 ? 100 : 80;
padding = (window.innerWidth / dpr) > 600 ? 20 : 5;
rectWidthWithPadding = rectWidth + 2 * padding;
if (rectWidth !== textCacheWidth || dpr !== textCacheDpr) updateTextCache();
const rect = document.getElementById('header').getBoundingClientRect();
canvas.width = window.innerWidth * dpr;
canvas.height = (window.innerHeight - rect.height) * dpr;
canvas.style.width = `${window.innerWidth}px`;
canvas.style.height = `${window.innerHeight - rect.height}px`;
ctx.restore();
ctx.save();
ctx.scale(dpr, dpr);
renderJourneys();
};
const mouseUpHandler = (evt) => {
let x = evt.x || evt.changedTouches[0].pageX;
if (canvasState.dragging && Math.abs(canvasState.dragStartMouse - x) < 20) {
let num = Math.floor((x - canvasState.offsetX + 2 * padding) / rectWidthWithPadding);
if (num >= 0) {
if (num < canvasState.journeys.length) {
window.location.hash += '/' + (num+canvasState.firstJourney);
} else {
moreJourneys(canvasState.reqId, "later");
}
} else {
moreJourneys(canvasState.reqId, "earlier");
}
}
canvasState.dragging = false;
};
const mouseDownHandler = (evt) => {
let x = evt.x || evt.changedTouches[0].pageX;
canvasState.dragStartMouse = x;
canvasState.dragStartOffset = canvasState.offsetX;
canvasState.dragging = true;
};
const mouseMoveHandler = (evt) => {
if (canvasState.dragging) {
evt.preventDefault();
let x = evt.x || evt.changedTouches[0].pageX;
canvasState.offsetX = canvasState.dragStartOffset - (canvasState.dragStartMouse - x);
renderJourneys();
return true;
}
};
window.addEventListener('mouseup', mouseUpHandler);
window.addEventListener('touchend', mouseUpHandler);
window.addEventListener('mousemove', mouseMoveHandler);
window.addEventListener('touchmove', mouseMoveHandler);
window.addEventListener('resize', resizeHandler);
window.addEventListener('zoom', resizeHandler);