V1.0
@ -1,2 +1,2 @@
|
||||
# dvb
|
||||
DVB Departure Screen
|
||||
# DVB
|
||||
Serverless DVB Departure Screen
|
92
app/index.html
Normal file
@ -0,0 +1,92 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<link href="res/fonts/Army/import.css" rel="stylesheet" type="text/css">
|
||||
<link href="res/css/main.css" rel="stylesheet" type="text/css">
|
||||
<link href="res/css/layout.css" rel="stylesheet" type="text/css">
|
||||
<link href="res/css/screen.css" rel="stylesheet" type="text/css">
|
||||
<link href="res/css/top.css" rel="stylesheet" type="text/css">
|
||||
<link href="res/css/weather.css" rel="stylesheet" type="text/css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="fill" id="app">
|
||||
<div class="container fill">
|
||||
<div class="top">
|
||||
<div class="fill" id="datetime">
|
||||
<dvb-time></dvb-time>
|
||||
<dvb-date></dvb-date>
|
||||
</div>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="fill" id="screen">
|
||||
|
||||
<!-- ZELLESCHER -->
|
||||
<dvb-line stop-Id="33000312" line="11"></dvb-line>
|
||||
<dvb-line stop-Id="33000312" line="61"></dvb-line>
|
||||
|
||||
<!-- STREHLI -->
|
||||
<dvb-line stop-Id="33000311" line="66"></dvb-line>
|
||||
|
||||
<!-- Räcknitzhöhe -->
|
||||
<dvb-line stop-Id="33000313" line="85"></dvb-line>
|
||||
</div>
|
||||
</div>
|
||||
<div class="side">
|
||||
<div class="fill weather-container">
|
||||
<dvb-weather offset="0"></dvb-weather>
|
||||
<dvb-weather offset="14400"></dvb-weather>
|
||||
<dvb-weather offset="28800"></dvb-weather>
|
||||
<dvb-weather offset="43200"></dvb-weather>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- VUE Templates -->
|
||||
|
||||
<script type="text/x-template" id="tmpl-dvb-weather">
|
||||
<div class="weather-item">
|
||||
<span class="temperature">{{cur}}</span>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-template" id="tmpl-dvb-line">
|
||||
<div class="fill line">
|
||||
<div class="head">
|
||||
<div class="title">
|
||||
<span>{{stopName}}</span>
|
||||
</div>
|
||||
<br>
|
||||
<div class="lineNumber">{{lineNumber}}</div>
|
||||
</div>
|
||||
<div class="body">
|
||||
<template v-for="(direction, name) in directions">
|
||||
<div class="title">
|
||||
<span>{{ name }}</span>
|
||||
</div>
|
||||
<div v-for="data in direction" v-if="data.RealTime" class="departure">
|
||||
{{ new Date() | timeInterval(data.RealTime) }}
|
||||
<span class="min">min</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<!-- VENDOR -->
|
||||
<script src="res/js/vendor/vue.min.js"></script>
|
||||
|
||||
<!-- VUE Controller -->
|
||||
<script src="res/js/vue/dvb-date.js"></script>
|
||||
<script src="res/js/vue/dvb-time.js"></script>
|
||||
<script src="res/js/vue/dvb-weather.js"></script>
|
||||
<script src="res/js/vue/dvb-line.js"></script>
|
||||
|
||||
<script src="res/js/main.js"></script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
22
app/res/css/layout.css
Normal file
@ -0,0 +1,22 @@
|
||||
/* LAYOUT */
|
||||
|
||||
.container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr calc(100vh / 4);
|
||||
grid-template-rows: auto 1fr;
|
||||
grid-template-areas: "top side" "body side";
|
||||
background: var(--clr-light);
|
||||
color: var(--clr-dark);
|
||||
}
|
||||
|
||||
.container .top {
|
||||
grid-area: top;
|
||||
}
|
||||
|
||||
.container .side {
|
||||
grid-area: side;
|
||||
}
|
||||
|
||||
.container .body {
|
||||
grid-area: body;
|
||||
}
|
33
app/res/css/main.css
Normal file
@ -0,0 +1,33 @@
|
||||
* {
|
||||
--clr-light: hsl(85, 70%, 83%);
|
||||
--clr-dark: hsl(31, 16%, 27%);
|
||||
--clr-accent-1: hsl(45, 76%, 50%);
|
||||
--clr-accent-2: hsl(46, 35%, 51%);
|
||||
--clr-accent-3: hsl(29, 75%, 60%);
|
||||
--font-xxxl: 10em;
|
||||
--font-xxl: 7.5em;
|
||||
--font-xl: 2em;
|
||||
--font-lg: 1.5em;
|
||||
--font-md: 1em;
|
||||
--font-sm: .8em;
|
||||
--font-xs: .6em;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
font-family: Army;
|
||||
}
|
||||
|
||||
|
||||
/* UTILITY */
|
||||
|
||||
.fill {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
77
app/res/css/screen.css
Normal file
@ -0,0 +1,77 @@
|
||||
/* SCREEN */
|
||||
|
||||
#screen {
|
||||
box-shadow: 1em -1em 8em -4em rgba(0, 0, 0, 1);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
#screen .line {
|
||||
flex: 1;
|
||||
padding: .5em;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
background: hsla(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
|
||||
/* HEAD */
|
||||
|
||||
#screen .head {
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
#screen .head .title {
|
||||
font-size: var(--font-xl);
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
padding: .15em 0em;
|
||||
}
|
||||
|
||||
#screen .head .title>span {
|
||||
background-color: var(--clr-accent-1);
|
||||
padding: 1em 0;
|
||||
}
|
||||
|
||||
#screen .head .lineNumber {
|
||||
font-size: var(--font-xxl);
|
||||
}
|
||||
|
||||
|
||||
/* BODY */
|
||||
|
||||
#screen .body {
|
||||
flex: 1 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#screen .body .departure {
|
||||
font-size: var(--font-xl);
|
||||
}
|
||||
|
||||
#screen .body .departure .min {
|
||||
font-size: var(--font-xs);
|
||||
}
|
||||
|
||||
#screen .body .title {
|
||||
font-size: var(--font-xl);
|
||||
padding: .15em 0em;
|
||||
overflow: hidden;
|
||||
margin: .5em 0;
|
||||
}
|
||||
|
||||
#screen .body .title>span {
|
||||
background-color: var(--clr-accent-3);
|
||||
padding: var(--font-xl) 0;
|
||||
}
|
26
app/res/css/top.css
Normal file
@ -0,0 +1,26 @@
|
||||
/* DATETIME */
|
||||
|
||||
#datetime {
|
||||
display: grid;
|
||||
grid-template: 1fr auto / 1fr auto;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
|
||||
/* TIME */
|
||||
|
||||
#time {
|
||||
grid-column: 1 / span 1;
|
||||
grid-row: 1;
|
||||
font-size: var(--font-xxxl);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
/* DATE */
|
||||
|
||||
#date {
|
||||
grid-column: 2;
|
||||
grid-row: 2;
|
||||
font-size: var(--font-xl);
|
||||
}
|
25
app/res/css/weather.css
Normal file
@ -0,0 +1,25 @@
|
||||
/* WEATHER */
|
||||
|
||||
.weather-container {
|
||||
display: grid;
|
||||
grid-template-rows: repeat(4, 1fr);
|
||||
grid-template-columns: 100%;
|
||||
grid-gap: 1em;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.weather-container .weather-item {
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: contain;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.weather-container .weather-item .temperature {
|
||||
position: absolute;
|
||||
bottom: 10%;
|
||||
right: 10%;
|
||||
width: 80%;
|
||||
text-align: right;
|
||||
font-size: var(--font-xl);
|
||||
}
|
BIN
app/res/fonts/Army/Army.ttf
Normal file
6
app/res/fonts/Army/import.css
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
@font-face {
|
||||
font-family: 'Army';
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
src: local('Army'), url('Army.tff');
|
||||
}
|
BIN
app/res/img/1.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
app/res/img/10.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
app/res/img/11.png
Normal file
After Width: | Height: | Size: 29 KiB |
BIN
app/res/img/12.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
app/res/img/2.png
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
app/res/img/3.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
app/res/img/4.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
app/res/img/6.png
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
app/res/img/7.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
app/res/img/8.png
Normal file
After Width: | Height: | Size: 35 KiB |
BIN
app/res/img/9.png
Normal file
After Width: | Height: | Size: 19 KiB |
18
app/res/js/main.js
Normal file
@ -0,0 +1,18 @@
|
||||
'use strict'
|
||||
|
||||
const S = {
|
||||
stations: [33000312, 33000311],
|
||||
url: 'https://webapi.vvo-online.de/dm'
|
||||
}
|
||||
|
||||
function parseTime(str) {
|
||||
if (str === undefined)
|
||||
return
|
||||
if (str instanceof Date)
|
||||
return str
|
||||
return new Date(parseInt(str.slice(6, -2).split('+')[0]))
|
||||
}
|
||||
|
||||
new Vue({
|
||||
el: '#app'
|
||||
})
|
60
app/res/js/vendor/fittext.js
vendored
Normal file
@ -0,0 +1,60 @@
|
||||
/*!
|
||||
* FitText.js 1.0 jQuery free version
|
||||
*
|
||||
* Copyright 2011, Dave Rupert http://daverupert.com
|
||||
* Released under the WTFPL license
|
||||
* http://sam.zoy.org/wtfpl/
|
||||
* Modified by Slawomir Kolodziej http://slawekk.info
|
||||
*
|
||||
* Date: Tue Aug 09 2011 10:45:54 GMT+0200 (CEST)
|
||||
*/
|
||||
(function () {
|
||||
|
||||
var addEvent = function (el, type, fn) {
|
||||
if (el.addEventListener)
|
||||
el.addEventListener(type, fn, false);
|
||||
else
|
||||
el.attachEvent('on' + type, fn);
|
||||
};
|
||||
|
||||
var extend = function (obj, ext) {
|
||||
for (var key in ext)
|
||||
if (ext.hasOwnProperty(key))
|
||||
obj[key] = ext[key];
|
||||
return obj;
|
||||
};
|
||||
|
||||
window.fitText = function (el, kompressor, options) {
|
||||
|
||||
var settings = extend({
|
||||
'minFontSize': -1 / 0,
|
||||
'maxFontSize': 1 / 0
|
||||
}, options);
|
||||
|
||||
var fit = function (el) {
|
||||
var compressor = kompressor || 1;
|
||||
|
||||
var resizer = function () {
|
||||
el.style.fontSize = Math.max(Math.min(el.clientWidth / (compressor * 10), parseFloat(settings.maxFontSize)), parseFloat(settings.minFontSize)) + 'px';
|
||||
};
|
||||
|
||||
// Call once to set.
|
||||
resizer();
|
||||
|
||||
// Bind events
|
||||
// If you have any js library which support Events, replace this part
|
||||
// and remove addEvent function (or use original jQuery version)
|
||||
addEvent(window, 'resize', resizer);
|
||||
addEvent(window, 'orientationchange', resizer);
|
||||
};
|
||||
|
||||
if (el.length)
|
||||
for (var i = 0; i < el.length; i++)
|
||||
fit(el[i]);
|
||||
else
|
||||
fit(el);
|
||||
|
||||
// return set of elements
|
||||
return el;
|
||||
};
|
||||
})();
|
6
app/res/js/vendor/vue.min.js
vendored
Normal file
29
app/res/js/vue/dvb-date.js
Normal file
@ -0,0 +1,29 @@
|
||||
Vue.component('dvb-date', {
|
||||
template: `<div id="date">{{cur}}</div>`,
|
||||
data() {
|
||||
return {
|
||||
hz: 3,
|
||||
cur: '...',
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
update() {
|
||||
this.cur = new Date().toLocaleDateString('de-DE', {
|
||||
weekday: 'short',
|
||||
year: 'numeric',
|
||||
month: 'numeric',
|
||||
day: 'numeric'
|
||||
})
|
||||
},
|
||||
startWatcher() {
|
||||
this.update()
|
||||
this.watcherId = setInterval(this.update, this.hz * 1000)
|
||||
},
|
||||
stopWatcher() {
|
||||
clearInterval(this.watcherId)
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.startWatcher()
|
||||
},
|
||||
})
|
86
app/res/js/vue/dvb-line.js
Normal file
@ -0,0 +1,86 @@
|
||||
Vue.component('dvb-line', {
|
||||
template: '#tmpl-dvb-line',
|
||||
data() {
|
||||
return {
|
||||
hz: 30,
|
||||
departures: null,
|
||||
directions: {},
|
||||
stopName: '',
|
||||
lineNumber: ''
|
||||
}
|
||||
},
|
||||
props: {
|
||||
stopId: Number,
|
||||
line: Number
|
||||
},
|
||||
filters: {
|
||||
formatTime(d) {
|
||||
if (d === undefined) return
|
||||
d = this.parseTime(d)
|
||||
let
|
||||
h = d.getHours(),
|
||||
m = d.getMinutes()
|
||||
h = h < 10 ? `0${h}` : h
|
||||
m = m < 10 ? `0${m}` : m
|
||||
return `${h}:${m}`
|
||||
},
|
||||
timeInterval(a, b) {
|
||||
a = this.parseTime(a)
|
||||
b = this.parseTime(b)
|
||||
return ((b - a) / 1000 / 60) | 0
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
startWatcher() {
|
||||
this.update()
|
||||
this.watcherId = setInterval(this.update, 1000 * this.hz)
|
||||
},
|
||||
stopWatcher() {
|
||||
clearInterval(this.watcherId)
|
||||
},
|
||||
update() {
|
||||
console.log('Updating...')
|
||||
fetch('https://webapi.vvo-online.de/dm', {
|
||||
mode: 'cors',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
'stopid': this.stopId
|
||||
})
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
this.stopName = res.Name
|
||||
this.departures = res.Departures.filter(departure => departure.LineName === this.line)
|
||||
this.formatDirections()
|
||||
})
|
||||
},
|
||||
formatDirections() {
|
||||
// Reset names & data
|
||||
this.directions = {}
|
||||
|
||||
for (var i of this.departures) {
|
||||
// Initial directions if null
|
||||
if (!(i.Direction in this.directions))
|
||||
this.directions[i.Direction] = []
|
||||
|
||||
// Inset departure into array
|
||||
this.directions[i.Direction].push(i)
|
||||
}
|
||||
|
||||
this.directions = Object.keys(this.directions)
|
||||
.sort().reduce((a, v) => {
|
||||
a[v] = this.directions[v];
|
||||
return a;
|
||||
}, {});
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.lineNumber = this.line
|
||||
this.stopId = this.stopId
|
||||
this.startWatcher()
|
||||
},
|
||||
})
|
31
app/res/js/vue/dvb-time.js
Normal file
@ -0,0 +1,31 @@
|
||||
Vue.component('dvb-time', {
|
||||
template: `<div id="time">{{cur}}</div>`,
|
||||
data() {
|
||||
return {
|
||||
hz: 0.5,
|
||||
cur: '...',
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
update() {
|
||||
this.cur = new Date().toLocaleDateString('de-DE', {
|
||||
formatMatcher: 'best fit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
})
|
||||
.split(',')[1]
|
||||
.trim()
|
||||
},
|
||||
startWatcher() {
|
||||
this.update()
|
||||
this.watcherId = setInterval(this.update, this.hz * 1000)
|
||||
},
|
||||
stopWatcher() {
|
||||
clearInterval(this.watcherId)
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.startWatcher()
|
||||
},
|
||||
})
|
63
app/res/js/vue/dvb-weather.js
Normal file
@ -0,0 +1,63 @@
|
||||
Vue.component('dvb-weather', {
|
||||
template: '#tmpl-dvb-weather',
|
||||
props: {
|
||||
offset: ['Number']
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hz: 10,
|
||||
cur: '...',
|
||||
images: {
|
||||
'sunny': 11,
|
||||
'rain': 4,
|
||||
'snow': 6,
|
||||
'clouds': 10,
|
||||
'storm': 1,
|
||||
'': 9,
|
||||
},
|
||||
states: {
|
||||
'fog': 2,
|
||||
'wind': 3,
|
||||
'frosty': 8,
|
||||
'wet': 12,
|
||||
'cold': 7,
|
||||
},
|
||||
static: {
|
||||
'0': {
|
||||
image: 11,
|
||||
temp: '23'
|
||||
},
|
||||
'14400': {
|
||||
image: 4,
|
||||
temp: '11'
|
||||
},
|
||||
'28800': {
|
||||
image: 6,
|
||||
temp: '-1'
|
||||
},
|
||||
'43200': {
|
||||
image: 1,
|
||||
temp: '7'
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
startWatcher() {
|
||||
this.update()
|
||||
this.watcherId = setInterval(this.update, this.hz * 1000)
|
||||
},
|
||||
stopWatcher() {
|
||||
clearInterval(this.watcherId)
|
||||
},
|
||||
update() {},
|
||||
},
|
||||
created() {
|
||||
this.startWatcher()
|
||||
},
|
||||
mounted() {
|
||||
const data = this.static[this.offset]
|
||||
this.$el.style.backgroundImage = `url('res/img/${data.image}.png')`
|
||||
this.cur = data.temp
|
||||
},
|
||||
})
|
BIN
design/DVB_Anzeige.pdf
Normal file
BIN
design/icons/single/0.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
design/icons/single/1.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
design/icons/single/10.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
design/icons/single/11.png
Normal file
After Width: | Height: | Size: 29 KiB |
BIN
design/icons/single/12.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
design/icons/single/2.png
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
design/icons/single/3.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
design/icons/single/4.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
design/icons/single/6.png
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
design/icons/single/7.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
design/icons/single/8.png
Normal file
After Width: | Height: | Size: 35 KiB |
BIN
design/icons/single/9.png
Normal file
After Width: | Height: | Size: 19 KiB |