├── .gitignore ├── LICENSE.md ├── README.md ├── bin ├── index.js └── lib │ ├── card-generator.js │ └── find-image.js ├── package.json └── src ├── Bahnhofsquartett.jpg ├── backside.svg ├── data ├── images.yaml └── stations.json └── fonts ├── FiraSans-Book.ttf └── FiraSans-Light.ttf /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/node 3 | 4 | ### Node ### 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # node-waf configuration 25 | .lock-wscript 26 | 27 | # Compiled binary addons (http://nodejs.org/api/addons.html) 28 | build/Release 29 | 30 | # Dependency directories 31 | node_modules 32 | jspm_packages 33 | 34 | # Optional npm cache directory 35 | .npm 36 | 37 | # Optional REPL history 38 | .node_repl_history 39 | 40 | # bahnhofsquartett 41 | dist/ 42 | .cache/ 43 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2015 [Philipp Bock](http://philippbock.de/) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | **The Software is provided "as is", without warranty of any kind, express or 14 | implied, including but not limited to the warranties of merchantability, 15 | fitness for a particular purpose and noninfringement. In no event shall the 16 | authors or copyright holders be liable for any claim, damages or other 17 | liability, whether in an action of contract, tort or otherwise, arising from, 18 | out of or in connection with the Software or the use or other dealings in the 19 | Software.** 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bahnhofsquartett 2 | 3 | Bahnhofsquartett is an automatically-generated "Quartett" game based on 4 | open data from Deutsche Bahn and Wikimedia Commons. 5 | 6 |  7 | 8 | Most of the code came to life between 11pm and 5am at #dbhackathon in December 9 | 2015, and it certainly looks the part. Still, if you're brave enough to want 10 | to customise it, the values on the cards are generated in `bin/index.js` and 11 | the drawing happens in `bin/lib/card-generator.js`. 12 | 13 | Code by [@bockph](https://twitter.com/bockph), 14 | with contributions from [@tbsprs](https://twitter.com/tbsprs) and 15 | [@ubahnverleih](https://twitter.com/ubahnverleih) and 16 | [Falco Nogatz](https://github.com/fnogatz). 17 | 18 | ## Requirements 19 | 20 | node.js >= 5.0.0 (might work with >=4.0.0, but not tested) 21 | 22 | ## Installing 23 | 24 | ```sh 25 | npm install 26 | ``` 27 | 28 | ## Building 29 | 30 | ```sh 31 | npm run build 32 | ``` 33 | 34 | ## Licence 35 | 36 | The code is licensed under the [MIT License](LICENSE.md). 37 | 38 | [Fira Sans](https://github.com/mozilla/Fira) is Copyright 2012-2015, 39 | The Mozilla Foundation and Telefonica S.A.; licensed under the [SIL Open Font 40 | License](https://github.com/mozilla/Fira/blob/master/LICENSE). 41 | 42 | The data was extracted from the data sets published at 43 | [data.deutschebahn.com](http://data.deutschebahn.com), licensed under 44 | [CC-BY 4.0](https://creativecommons.org/licenses/by/4.0/), 45 | as well as data from the OpenStreetMap contributors, 46 | licensed under the [ODbL](http://opendatacommons.org/licenses/odbl/). 47 | -------------------------------------------------------------------------------- /bin/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const async = require('async'); 4 | const pr = require('path').resolve; 5 | const fs = require('fs'); 6 | const _ = require('lodash'); 7 | 8 | const pdf = require('./lib/card-generator')(); 9 | 10 | const SRC_DIR = pr(__dirname, '../src'); 11 | const DATA_DIR = pr(SRC_DIR, 'data'); 12 | const DEST_DIR = pr(__dirname, '../dist'); 13 | 14 | const ALPHABET = 'ABCDEFGHIJKMNOPQRSTUVWXYZ'; 15 | 16 | try { 17 | fs.mkdirSync(DEST_DIR); 18 | } catch (e) { 19 | if (e.code !== 'EEXIST') throw e; 20 | } 21 | 22 | let stations = JSON.parse(fs.readFileSync(pr(DATA_DIR, 'stations.json'))); 23 | let categories = [ 24 | { 25 | name: 'Anzahl der Bahnsteige', 26 | find: station => station.platforms.length, 27 | reverse: true, 28 | }, 29 | { 30 | name: 'Längster Bahnsteig', 31 | find: station => _(station.platforms).map('length').max() || 0, 32 | format: n => n.toFixed(2).replace('.', ',') + ' m', 33 | reverse: true, 34 | }, 35 | { 36 | name: 'Höchster Bahnsteig', 37 | find: station => _(station.platforms).map('height').max() || 0, 38 | format: n => n.toFixed(2).replace('.', ',') + ' m', 39 | reverse: true, 40 | }, 41 | { 42 | name: 'Bahnhofskategorie', 43 | find: n => n.category, 44 | }, 45 | { 46 | name: 'Anzahl der Aufzüge', 47 | find: n => n.elevators.length, 48 | reverse: true, 49 | }, 50 | { 51 | name: 'Ältester Aufzug', 52 | find: n => _(n.elevators).map(e => +e.year).filter().min(), 53 | filter: n => n.elevators.length, 54 | format: n => (n === Infinity || n === undefined) ? '—' : n, 55 | }, 56 | { 57 | name: 'Größte Aufzugskabine', 58 | find: n => _(n.elevators).map(e => e.cabin.width * e.cabin.depth * e.cabin.height / 1e9).filter(n => n < 100).max(), 59 | reverse: true, 60 | format: n => (n > 0) ? n.toFixed(1).replace('.', ',') + ' m³' : '—', 61 | }, 62 | { 63 | name: 'Anschluss an eine Fähre', 64 | find: n => n.ferryNearby, 65 | reverse: true, 66 | format: n => n ? 'ja' : 'nein', 67 | } 68 | /*{ 69 | name: 'Höchster Aufzugschacht', 70 | find: n => _(n.elevators).map(e => e.wellHeight).filter().max(), 71 | format: n => (n > 0) ? n.toFixed(2).replace('.', ',') + ' m' : '—', 72 | reverse: true, 73 | }, 74 | { 75 | name: 'Höchster Aufzugschacht', 76 | find: n => _(n.elevators).map(e => +e.wellHeight).filter().max(), 77 | reverse: true, 78 | format: n => n === -Infinity ? '—' : n.toFixed(2).replace('.', ',') + ' m', 79 | },*/ 80 | ]; 81 | 82 | let cards = new Set(); 83 | 84 | let potentialCards = categories.map(category => { 85 | let filter = category.filter || (() => true); 86 | let results = _(stations).filter(filter).sortBy(category.find); 87 | if (category.reverse) results = results.reverse(); 88 | return results.value(); 89 | }); 90 | 91 | while (cards.size < potentialCards.length * 4) { 92 | let group = cards.size % potentialCards.length; 93 | let card = potentialCards[group].shift(); 94 | cards.add(card); 95 | } 96 | 97 | cards = _(Array.from(cards)).sortBy(s => s.state).value(); 98 | 99 | let i = 0; 100 | async.eachLimit(cards, 1, (station, cardDone) => { 101 | let cardID = ALPHABET[i / 4 | 0] + (i % 4 + 1); 102 | console.log(cardID, station.name); 103 | let card = { 104 | name: station.name, 105 | id: cardID, 106 | values: [], 107 | }; 108 | categories.forEach(category => { 109 | let format = category.format || _.identity; 110 | card.values.push({ name: category.name, value: format(category.find(station)) }); 111 | }); 112 | 113 | pdf.add(card).then(cardDone) 114 | .catch((err) => { 115 | console.error(err); 116 | cardDone(err); 117 | }); 118 | i++; 119 | }, function () { 120 | pdf.end(); 121 | pdf.doc.pipe(fs.createWriteStream(pr(DEST_DIR, 'output.pdf'))); 122 | }); 123 | -------------------------------------------------------------------------------- /bin/lib/card-generator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const PDFDocument = require('pdfkit'); 4 | const cheerio = require('cheerio'); 5 | const pr = require('path').resolve; 6 | const fs = require('fs'); 7 | 8 | const findImage = require('./find-image'); 9 | 10 | const BLEED = 3 11 | const WIDTH = 170 + 2 * BLEED; 12 | const HEIGHT = 255 + 2 * BLEED; 13 | const MARGIN = 10 + 2 * BLEED; 14 | const LINE_HEIGHT = 14; 15 | 16 | const VERKEHRSROT = [ 0, 100, 100, 10 ]; 17 | const LICHTGRAU = [ 0, 0, 0, 20 ]; 18 | const WHITE = [ 0, 0, 0, 0 ]; 19 | const BLACK = [ 0, 0, 0, 100 ]; 20 | 21 | let $ = cheerio.load(fs.readFileSync(pr(__dirname, '../../src/backside.svg'))); 22 | function drawBacks(doc) { 23 | for (let i = 0; i < 9; i++) { 24 | let pageX = 595 - (1 + (i / 3 | 0) % 3) * (WIDTH + MARGIN); 25 | let pageY = MARGIN + (i % 3) * (HEIGHT + MARGIN); 26 | drawBack(doc, pageX, pageY); 27 | } 28 | } 29 | function drawBack(doc, pageX, pageY) { 30 | doc.save().translate(pageX, pageY); 31 | let first = true; 32 | $('path').each((i, path) => { 33 | let d = $(path).attr('d'); 34 | doc.path(d); 35 | if (first) { 36 | first = false; 37 | doc.fill(VERKEHRSROT); 38 | } else { 39 | doc.strokeOpacity(0.5).lineWidth(0.5).stroke(WHITE); 40 | } 41 | }); 42 | $('line').each((i, line) => { 43 | let $line = $(line); 44 | doc.moveTo($line.attr('x1'), $line.attr('y1')) 45 | .lineTo($line.attr('x2'), $line.attr('y2')) 46 | .strokeOpacity(0.5).lineWidth(0.5).stroke(WHITE); 47 | }); 48 | doc.fontSize(8).fill(WHITE).text('v0.4', MARGIN, MARGIN); 49 | doc.restore(); 50 | } 51 | 52 | function makePDF(card) { 53 | let doc = new PDFDocument({ size: 'a4', margin: 15 }); 54 | let i = 0; 55 | return { 56 | doc: doc, 57 | end: () => { 58 | if ((i + 1) % 9 !== 0) { 59 | doc.addPage(); 60 | drawBacks(doc); 61 | } 62 | return doc.end(); 63 | }, 64 | add: (card) => new Promise((resolve, reject) => { 65 | let pageX = MARGIN + ((i / 3 | 0) % 3) * (WIDTH + MARGIN); 66 | let pageY = MARGIN + (i % 3) * (HEIGHT + MARGIN); 67 | 68 | findImage(card).then(image => { 69 | let y = 0; 70 | doc.rect(pageX + 0, pageY + 0, WIDTH, HEIGHT / 2.5); 71 | y = HEIGHT / 2.2; 72 | 73 | // Declare fonts 74 | doc.font(pr(__dirname, '../../src/fonts/FiraSans-Light.ttf'), 'Light'); 75 | doc.font(pr(__dirname, '../../src/fonts/FiraSans-Book.ttf'), 'Regular'); 76 | 77 | let placeholderAspectRatio = WIDTH / (HEIGHT / 2.5); 78 | 79 | if (image) { 80 | // Find out which dimension we need to pass to PDFKit to make sure 81 | // that the image covers its placeholder. 82 | let imageSize; 83 | let imageAspectRatio = image.dimensions.width / image.dimensions.height; 84 | if (imageAspectRatio > placeholderAspectRatio) { 85 | imageSize = { height: HEIGHT / 2.5 }; 86 | } else { 87 | imageSize = { width: WIDTH }; 88 | } 89 | doc.save() 90 | .clip() 91 | .image(image.image, pageX + 0, pageY + 0, imageSize) 92 | .restore(); 93 | 94 | // Attribution 95 | doc.fontSize(5) 96 | .fill(WHITE) 97 | .text( 98 | image.metadata.Author || image.metadata.url, 99 | pageX + MARGIN / 2 + 2, 100 | pageY + HEIGHT / 2.5 - 8); 101 | } else { 102 | doc.fill(LICHTGRAU); 103 | } 104 | 105 | doc.moveTo(pageX + 0, pageY + HEIGHT / 2.5 + 4) 106 | .lineTo(pageX + WIDTH, pageY + HEIGHT / 2.5 + 4) 107 | .lineWidth(3) 108 | .strokeOpacity(1) 109 | .stroke(VERKEHRSROT); 110 | 111 | doc.fill(BLACK); 112 | doc.circle(pageX + WIDTH - MARGIN - 5, pageY + MARGIN + 7, 10).fill(BLACK); 113 | doc.fontSize(12); 114 | doc.font('Light').fill(WHITE).text(card.id, pageX - 2 * (MARGIN + BLEED) + WIDTH - 23, pageY + MARGIN, { width: 80, align: 'center' }); 115 | 116 | doc.fontSize(9); 117 | doc.font('Regular').fill(BLACK).text(card.name, pageX + MARGIN, pageY + y); 118 | y += LINE_HEIGHT * 1.5; 119 | 120 | doc.fontSize(8); 121 | card.values.forEach(category => { 122 | doc.font('Light').text(category.name, pageX + MARGIN, pageY + y); 123 | doc.font('Regular').text(category.value, pageX + MARGIN, pageY + y, { width: WIDTH - 2 * MARGIN, align: 'right' }); 124 | y += LINE_HEIGHT; 125 | }); 126 | 127 | doc.fontSize(8); 128 | 129 | doc.lineWidth(0.5); 130 | 131 | // Draw cutting marks 132 | doc.moveTo(pageX + BLEED, pageY - 6) 133 | .lineTo(pageX + BLEED, pageY + BLEED) 134 | .lineTo(pageX - 6, pageY + BLEED) 135 | .stroke(BLACK); 136 | doc.moveTo(pageX - BLEED + WIDTH, pageY - 6) 137 | .lineTo(pageX - BLEED + WIDTH, pageY + BLEED) 138 | .lineTo(pageX + WIDTH + 6, pageY + BLEED) 139 | .stroke(); 140 | doc.moveTo(pageX - BLEED + WIDTH, pageY + BLEED + HEIGHT + 6) 141 | .lineTo(pageX - BLEED + WIDTH, pageY + HEIGHT) 142 | .lineTo(pageX + WIDTH + 6, pageY + HEIGHT) 143 | .stroke(); 144 | doc.moveTo(pageX + BLEED, pageY + BLEED + HEIGHT + 6) 145 | .lineTo(pageX + BLEED, pageY + HEIGHT) 146 | .lineTo(pageX - 6, pageY + HEIGHT) 147 | .stroke(); 148 | 149 | doc.rect(pageX + BLEED, pageY + BLEED, WIDTH - 2 * BLEED, HEIGHT - 2 * BLEED + 3) 150 | .lineWidth(0.25) 151 | .stroke(BLACK); 152 | 153 | i++; 154 | if (i % 9 === 0) { 155 | doc.addPage(); 156 | drawBacks(doc); 157 | doc.addPage(); 158 | } 159 | 160 | resolve(); 161 | }).catch(reject); 162 | }), 163 | } 164 | } 165 | 166 | module.exports = makePDF; 167 | -------------------------------------------------------------------------------- /bin/lib/find-image.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const sizeOf = require('image-size'); 4 | const request = require('request'); 5 | const cheerio = require('cheerio'); 6 | const YAML = require('js-yaml'); 7 | const slug = require('slug'); 8 | const pr = require('path').resolve; 9 | const fs = require('fs'); 10 | 11 | const DATA_DIR = pr(__dirname, '../../src/data'); 12 | let images = YAML.safeLoad(fs.readFileSync(pr(DATA_DIR, 'images.yaml'))); 13 | 14 | const CACHE_DIR = pr(__dirname, '../../.cache'); 15 | try { 16 | fs.mkdirSync(CACHE_DIR); 17 | } catch (e) { 18 | if (e.code !== 'EEXIST') throw e; 19 | } 20 | 21 | function findImage(station) { 22 | return new Promise((resolve, reject) => { 23 | let name = station.name; 24 | let url = images[name]; 25 | if (!url) return resolve(null); 26 | url = url.replace(/uselang=..&?/, 'uselang=en'); 27 | console.log(url); 28 | 29 | let metadataPath = pr(CACHE_DIR, slug(name) + '.json'); 30 | let imagePath = pr(CACHE_DIR, slug(name) + '.jpg'); 31 | 32 | // See if we have stuff in the cache 33 | try { 34 | let imageBuffer = fs.readFileSync(imagePath); 35 | let metadataBuffer = fs.readFileSync(metadataPath); 36 | if (imageBuffer && metadataBuffer) { 37 | resolve({ 38 | image: imageBuffer, 39 | metadata: JSON.parse(metadataBuffer), 40 | dimensions: sizeOf(imageBuffer), 41 | }); 42 | return; 43 | } 44 | } catch (e) { 45 | if (e.code !== 'ENOENT') return reject(e); 46 | console.log('Image not found in cache'); 47 | } 48 | 49 | request(url, function (error, response, body) { 50 | if (error || response.statusCode !== 200) { 51 | return reject(error); 52 | } 53 | let $ = cheerio.load(body); 54 | let $rows = $('.commons-file-information-table tr'); 55 | let metadata = {}; 56 | $rows.each((i, row) => { 57 | let $row = $(row); 58 | let key = $row.find('td:first-child').text().trim(); 59 | let value = $row.find('td:not(:first-child)').text().trim(); 60 | metadata[key] = value; 61 | }); 62 | metadata.url = url; 63 | fs.writeFileSync(metadataPath, JSON.stringify(metadata)); 64 | 65 | console.log('Fetching image from %s', url); 66 | let imageUrl = $('.fullImageLink a').attr('href'); 67 | request(imageUrl, { encoding: null }, function (error, res, buffer) { 68 | if (error || res.statusCode !== 200) { 69 | return reject(error); 70 | } 71 | console.log('Fetched image from %s', url); 72 | let imageBuffer = buffer; 73 | fs.writeFileSync(imagePath, buffer); 74 | resolve({ 75 | image: imageBuffer, 76 | metadata: metadata, 77 | dimensions: sizeOf(imageBuffer), 78 | }); 79 | }); 80 | }); 81 | }); 82 | } 83 | 84 | module.exports = findImage; 85 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bahnhofquartett", 3 | "version": "0.1.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "node bin/index.js" 9 | }, 10 | "author": "Philipp Bock
(http://philippbock.de/)", 11 | "license": "MIT", 12 | "dependencies": { 13 | "async": "^1.5.0", 14 | "cheerio": "^0.22.0", 15 | "image-size": "^0.4.0", 16 | "js-yaml": "^3.4.6", 17 | "lodash": "^4.17.15", 18 | "pdfkit": "^0.7.1", 19 | "request": "^2.67.0", 20 | "slug": "^0.9.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Bahnhofsquartett.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbock/bahnhofsquartett/2d4b34a926a5ce1a9bcc860c2ac9be46a3e300bc/src/Bahnhofsquartett.jpg -------------------------------------------------------------------------------- /src/backside.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 68 | -------------------------------------------------------------------------------- /src/data/images.yaml: -------------------------------------------------------------------------------- 1 | Bernau (b Berlin): https://commons.wikimedia.org/wiki/File:Bahnhof_Bernau_(2009).jpg 2 | München Hbf: https://commons.wikimedia.org/wiki/File:Munich_Main_Railway_Station_-_aerial_view.JPG?uselang=de 3 | Essen Hbf: https://commons.wikimedia.org/wiki/File:Essen_Hbf_13_%C3%9Cbersicht.jpg 4 | Griebnitzsee: https://commons.wikimedia.org/wiki/File:S-Bahn_Berlin_Potsdam_Griebnitzsee_Entrance.jpg 5 | Frankfurt (Main) Hbf: https://commons.wikimedia.org/wiki/File:Hauptbahnhof_Frankfurt.jpg?uselang=de 6 | Leipzig Hbf: https://commons.wikimedia.org/wiki/File:Hauptbahnhof_leipzig,_Top_View.jpg 7 | Mainz Hbf: https://commons.wikimedia.org/wiki/File:Mainz_Hauptbahnhof_Gleisanlage_20101008.jpg 8 | Hohen Neuendorf West: https://commons.wikimedia.org/wiki/File:Bahnhof_Hohen_Neuendorf_West_05_Empfangsgeb%C3%A4ude.JPG 9 | Hamburg Hbf: https://commons.wikimedia.org/wiki/File:2013-06-08_Highflyer_HP_L4732.JPG 10 | Nürnberg Hbf: https://commons.wikimedia.org/wiki/File:Nuremberg_Aerial_Hauptbahnhof.JPG 11 | Andernach: https://commons.wikimedia.org/wiki/File:Bahnhof_Andernach.jpg 12 | Hennigsdorf (b Berlin): https://commons.wikimedia.org/wiki/File:Bahnhof_Hennigsdorf.JPG 13 | Hamburg-Altona: https://commons.wikimedia.org/wiki/File:Bahnhof_Altona_(Einfahrt).jpg 14 | Stuttgart Hbf: https://commons.wikimedia.org/wiki/File:Stuttgart21luft.jpg?uselang=de 15 | Erfurt Hbf: https://commons.wikimedia.org/wiki/File:Erfurt_Hbf_Front.JPG 16 | Hannover Hbf: https://commons.wikimedia.org/wiki/File:Hannover_Hauptbahnhof_Vorplatz.jpg 17 | Hagen Hbf: https://commons.wikimedia.org/wiki/File:Bahnhof_Hagen_Hbf_02_Empfangsgeb%C3%A4ude.jpg 18 | Saarbrücken Hbf: https://commons.wikimedia.org/wiki/File:20110906Hauptbahnhof_Saarbruecken.jpg 19 | Königs Wusterhausen: https://commons.wikimedia.org/wiki/File:Koenigswusterhausen_bahnhof.JPG 20 | Berlin Hauptbahnhof: https://commons.wikimedia.org/wiki/File:150524_Berlin_Hauptbahnhof_Ostseite.jpg 21 | Dresden Hbf: https://commons.wikimedia.org/wiki/File:Dresden-Germany-Main_station.jpg 22 | Köln Hbf: https://commons.wikimedia.org/wiki/File:Koeln_Hauptbahnhof_Luftaufnahme.jpg 23 | Düsseldorf Hbf: https://commons.wikimedia.org/wiki/File:Hauptbahnhof_in_Duesseldorf-Stadtmitte,_von_Westen.jpg 24 | Karlsruhe Hbf: https://commons.wikimedia.org/wiki/File:Karlsruhe_Railway_station.jpg 25 | Berlin-Spandau: https://commons.wikimedia.org/wiki/File:2013-08_View_from_Rathaus_Spandau_08.jpg 26 | Berlin Ostbahnhof: https://commons.wikimedia.org/wiki/File:Berlin_Ostbahnhof2.JPG 27 | Dortmund Hbf: https://commons.wikimedia.org/wiki/File:Dortmund-Hauptbahnhof-Abends-2013.jpg 28 | Kaiserslautern Hbf: https://commons.wikimedia.org/wiki/File:Hbf_Kaiserslautern.jpg 29 | Berlin-Lichterfelde Ost: https://commons.wikimedia.org/wiki/File:Lichterfelde_Lankwitzer_Straße_Bahnhof.JPG 30 | Berlin Südkreuz: https://commons.wikimedia.org/wiki/File:Bahnhof_Berlin_S%C3%BCdkreuz_denis_apel.JPG 31 | Duisburg Hbf: https://commons.wikimedia.org/wiki/File:Duisburg_Hauptbahnhof_Panorama.jpg 32 | Yorckstraße: https://commons.wikimedia.org/wiki/File:Berlin_YorckGro%C3%9Fg%C3%B6rschenstra%C3%9Fe_entrance.jpg 33 | Berlin-Gesundbrunnen: https://commons.wikimedia.org/wiki/File:Berlin_-_Bahnhof_Gesundbrunnen_(7357167108).jpg 34 | Schöna: https://commons.wikimedia.org/wiki/File:Sch%C3%B6na_Bahnhof.JPG 35 | Wulfen (Westf): https://commons.wikimedia.org/wiki/File:Bahnhof_Wulfen_(Westf)_2011-07-17_07.jpg 36 | Opladen: https://commons.wikimedia.org/wiki/File:Bahnhof_Opladen_Umbauzustand_09-2015.JPG 37 | Westbevern: https://commons.wikimedia.org/wiki/File:Bahnhof_Westbevern.jpg 38 | Meinerzhagen: https://commons.wikimedia.org/wiki/File:Bahnsteig_Meinerzhagen_2013.JPG 39 | Telgte: https://commons.wikimedia.org/wiki/File:Telgte_Bahnhof_1331.jpg 40 | Radolfzell: https://commons.wikimedia.org/wiki/File:Seehaesle_Ringzug.jpg 41 | Rüdesheim am Rhein: https://commons.wikimedia.org/wiki/File:BahnhofR%C3%BCdesheimAmRhein2011-3.JPG 42 | Norddeich Mole: https://commons.wikimedia.org/wiki/File:2013-05-03_Fotoflug_Leer_Papenburg_DSCF7321.jpg 43 | Sassnitz: https://commons.wikimedia.org/wiki/File:2015_Faehrhafen_Sassnitz_und_Wostevitzer_Teiche.JPG?uselang=de 44 | Starnberg: https://commons.wikimedia.org/wiki/File:StarnbergBahnhof-01.jpg 45 | Würzburg Hbf: https://commons.wikimedia.org/wiki/File:Würzburg_Hauptbahnhof_Empfangsgebäude_0516.jpg 46 | Kempten (Allgäu) Hbf: https://commons.wikimedia.org/wiki/File:Kempten_(Allgau)_Hbf.JPG 47 | München-Lochhausen: https://commons.wikimedia.org/wiki/File:Bahnhof_München-Lochhausen.JPG 48 | Bochum Hbf: https://commons.wikimedia.org/wiki/File:Bochum_-_Kurt-Schumacher-Platz_-_Hbf_01_ies.jpg 49 | Jannowitzbrücke: https://commons.wikimedia.org/wiki/File:Berlin_-_S-Bahnhof_Jannowitzbruecke.jpg 50 | -------------------------------------------------------------------------------- /src/fonts/FiraSans-Book.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbock/bahnhofsquartett/2d4b34a926a5ce1a9bcc860c2ac9be46a3e300bc/src/fonts/FiraSans-Book.ttf -------------------------------------------------------------------------------- /src/fonts/FiraSans-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbock/bahnhofsquartett/2d4b34a926a5ce1a9bcc860c2ac9be46a3e300bc/src/fonts/FiraSans-Light.ttf --------------------------------------------------------------------------------