Files
geograsim/App/lieferketten.html
T
Adminator f22c5ebbfe Stand 2026-04-13: PHP/MySQL Infrastruktur, Flussmanagement, Stadt-Prototyp
- PHP/MySQL Backend (XAMPP + Produktionsserver)
- Front-Controller, API-Endpunkte, Session-Management
- Flussmanagement-Simulation (Echtzeit, Punkt-basierter Fluss)
- Stadt & Raumplanung (Prototyp, Top-Down Kachelsystem)
- Klimawaechter 3D: Deiche kleiner, Baeume kippen, Budget angepasst
- persistence.ts: Dualer Speicher (localStorage + Server-API)
- 6 Unit-Test-Dateien fuer bestehende Simulationen

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 16:43:42 +02:00

546 lines
22 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GeoGraSim — Lieferketten-Planer*in</title>
<link rel="icon" type="image/png" href="assets/img/logo.png">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { margin:0; padding:0; box-sizing:border-box; }
:root {
--bg: #f7f6f3;
--text: #1a1a1a; --text2: #4a4a4a; --text3: #8a8a8a;
--fjord: #4a7c8a; --fjord-l: #dae8ec; --fjord-d: #3a6470;
--moss: #5a8a5e; --moss-l: #dceadd;
--sand: #c4a35a; --sand-l: #f2eacc;
--coral: #c07a6b; --coral-l: #f2ddd8;
--danger: #b04a3a;
--glass-bg: rgba(255,255,255,.85);
--glass-bg2: rgba(255,255,255,.92);
--glass-bord: rgba(255,255,255,.55);
--glass-shadow: 0 8px 32px rgba(20,40,60,.18);
--topbar-h: 2.8rem;
--side-w: 220px;
--pad: .65rem;
}
html, body { height:100%; overflow:hidden; }
body { font-family:'Inter',system-ui,sans-serif; background:#dceaf0; color:var(--text); line-height:1.5; }
#canvas-wrap { position:fixed; inset:0; z-index:0; }
/* === FLOATING PANELS === */
.floating {
position:fixed; z-index:5;
backdrop-filter:blur(14px) saturate(140%); -webkit-backdrop-filter:blur(14px) saturate(140%);
background:var(--glass-bg); border:1px solid var(--glass-bord);
box-shadow:var(--glass-shadow); border-radius:14px;
}
/* === TOPBAR === */
.topbar {
top:0; left:0; right:0;
padding:.35rem .7rem;
display:flex; justify-content:space-between; align-items:center; gap:.8rem;
border-radius:0; border-bottom:1px solid rgba(0,0,0,.08);
}
.topbar .brand {
display:flex; align-items:center; gap:.5rem; font-weight:800; font-size:.92rem;
color:var(--fjord); text-decoration:none; white-space:nowrap;
}
.topbar .brand img { width:24px; height:24px; border-radius:6px; }
.topbar-controls { display:flex; gap:.5rem; align-items:center; }
.icon-btn {
width:32px; height:32px; border-radius:8px;
border:1px solid rgba(0,0,0,.08); background:rgba(255,255,255,.55);
color:var(--text2); font-size:.95rem; cursor:pointer;
display:flex; align-items:center; justify-content:center; padding:0;
text-decoration:none; transition:all .15s;
}
.icon-btn:hover { background:var(--fjord-l); border-color:var(--fjord); color:var(--fjord); }
.ctrl-group {
display:flex; gap:.2rem; padding:.2rem;
background:rgba(255,255,255,.45); border-radius:9px;
border:1px solid rgba(0,0,0,.06);
}
.ctrl-group .icon-btn {
border:none; background:transparent;
width:30px; height:30px; font-size:.8rem; font-weight:700;
}
.ctrl-group .icon-btn:hover { background:rgba(0,0,0,.06); }
.ctrl-group .icon-btn.active { background:var(--fjord); color:#fff; }
.topbar-sep { width:1px; height:24px; background:rgba(0,0,0,.08); }
/* === STATUS-PANEL (LINKS) === */
.panel-h {
font-size:.58rem; font-weight:800; color:var(--fjord);
text-transform:uppercase; letter-spacing:.07em; margin-bottom:.4rem;
}
.panel-h-sub { margin-top:.9rem; padding-top:.6rem; border-top:1px solid rgba(0,0,0,.08); }
.panel-status {
top: calc(var(--topbar-h) + var(--pad));
left: var(--pad);
width: var(--side-w);
padding: .6rem .75rem;
bottom: var(--pad);
overflow-y: auto;
}
.resource {
display:flex; align-items:flex-start; gap:.5rem; padding:.3rem .1rem;
border-bottom:1px solid rgba(0,0,0,.06);
}
.resource:last-child { border-bottom:none; }
.resource-icon { font-size:1.15rem; width:22px; text-align:center; padding-top:.1rem; }
.resource-info { flex:1; min-width:0; }
.resource-name { font-size:.58rem; color:var(--text3); font-weight:500; line-height:1.1; }
.resource-val { font-size:.85rem; font-weight:800; color:var(--text); line-height:1.2; }
/* === GOALS === */
.goal { padding:.3rem 0; border-bottom:1px solid rgba(0,0,0,.06); }
.goal:last-child { border:0; }
.goal-row { display:flex; align-items:center; gap:.35rem; font-size:.7rem; }
.goal-check {
width:12px; height:12px; border:1.5px solid var(--text3);
border-radius:3px; display:flex; align-items:center; justify-content:center;
font-size:.52rem; flex-shrink:0;
}
.goal-check.done { background:var(--moss); border-color:var(--moss); color:#fff; }
.goal-bar { height:3px; background:rgba(0,0,0,.08); border-radius:2px; margin-top:.2rem; overflow:hidden; }
.goal-bar-fill { height:100%; background:var(--moss); transition:width .3s; }
/* === BESTELL-FORMULAR (RECHTS) === */
.panel-order {
top: calc(var(--topbar-h) + var(--pad));
right: var(--pad);
bottom: var(--pad);
width: 300px;
padding: .85rem 1rem 1rem;
overflow-y: auto;
}
.order-section { margin-bottom: 1rem; }
.order-section h4 {
font-size: .65rem; font-weight: 800; color: var(--fjord);
text-transform: uppercase; letter-spacing: .07em;
margin-bottom: .35rem;
}
.order-options { display: flex; flex-direction: column; gap: .25rem; }
.order-option {
display: flex; align-items: center; gap: .4rem;
padding: .35rem .5rem; border-radius: 8px;
background: rgba(255,255,255,.55);
border: 1.5px solid rgba(0,0,0,.06);
cursor: pointer; transition: all .12s;
font-family: inherit; text-align: left;
}
.order-option:hover { border-color: var(--fjord); background: var(--fjord-l); }
.order-option.selected { border-color: var(--moss); background: var(--moss-l); }
.oo-flag { font-size: .9rem; flex-shrink: 0; }
.oo-info { flex: 1; min-width: 0; }
.oo-name { font-size: .68rem; font-weight: 700; line-height: 1.15; }
.oo-stats {
font-size: .55rem; color: var(--text3); line-height: 1.2;
font-variant-numeric: tabular-nums;
}
.oo-stats .oo-stat { display: inline-block; margin-right: .35rem; }
.oo-stat-good { color: var(--moss); }
.oo-stat-bad { color: var(--danger); }
.transport-row { display: flex; gap: .25rem; }
.transport-btn {
flex: 1; padding: .4rem .25rem; border-radius: 8px;
background: rgba(255,255,255,.55); border: 1.5px solid rgba(0,0,0,.06);
cursor: pointer; font-family: inherit;
display: flex; flex-direction: column; align-items: center; gap: .1rem;
transition: all .12s;
}
.transport-btn:hover { border-color: var(--fjord); background: var(--fjord-l); }
.transport-btn.selected { border-color: var(--moss); background: var(--moss-l); }
.transport-btn .tb-emoji { font-size: 1.1rem; }
.transport-btn .tb-name { font-size: .55rem; font-weight: 700; }
.order-summary {
margin-top: .8rem; padding: .65rem .75rem;
background: rgba(0,0,0,.04); border-radius: 9px;
font-size: .68rem; line-height: 1.5;
}
.order-summary .os-row {
display: flex; justify-content: space-between;
font-variant-numeric: tabular-nums;
}
.order-summary .os-row strong { font-weight: 800; }
.os-good { color: var(--moss); }
.os-bad { color: var(--danger); }
.order-btn {
margin-top: .7rem; width: 100%;
padding: .65rem; border: none; border-radius: 9px;
background: var(--fjord); color: #fff;
font-weight: 800; font-size: .85rem; cursor: pointer;
font-family: inherit; transition: all .15s;
}
.order-btn:hover { background: var(--fjord-d); }
.order-btn:disabled { background: #c0c0c0; cursor: not-allowed; }
/* === EVENTS-STRIP (unten links neben dem Status) === */
.panel-events-strip {
position: fixed;
left: calc(var(--side-w) + var(--pad) * 2);
bottom: var(--pad);
width: 280px;
max-height: 90px;
padding: .35rem .55rem .45rem;
overflow: hidden;
cursor: pointer;
transition: max-height .38s cubic-bezier(.22,1,.36,1);
z-index: 12;
display: flex; flex-direction: column; gap: .25rem;
}
.panel-events-strip.open {
max-height: calc(100vh - var(--topbar-h) - 4 * var(--pad));
box-shadow: 0 18px 50px rgba(20,40,60,.28);
cursor: zoom-out;
}
.events-strip-header {
display: flex; justify-content: space-between; align-items: center;
font-size: .58rem; font-weight: 800; color: var(--fjord);
text-transform: uppercase; letter-spacing: .07em;
flex-shrink: 0;
}
.esh-toggle { transition: transform .35s ease-out; font-size: .65rem; color: var(--text3); }
.panel-events-strip.open .esh-toggle { transform: rotate(180deg); }
.events-strip-list {
overflow-y: auto;
flex: 1; min-height: 0;
}
.panel-events-strip:not(.open) .events-strip-list .event:not(:first-child) {
display: none;
}
.panel-events-strip:not(.open) .events-strip-list .event {
margin-bottom: 0;
}
.event {
padding:.3rem .45rem; border-radius:7px; margin-bottom:.25rem;
font-size:.65rem; line-height:1.3;
display:flex; gap:.3rem; align-items:flex-start;
}
.event-text { flex:1; }
.event-info { background:rgba(0,0,0,.05); color:var(--text2); }
.event-success { background:var(--moss-l); color:var(--moss); }
.event-warning { background:var(--sand-l); color:var(--sand); }
.event-danger { background:var(--coral-l); color:var(--danger); }
/* === TUTORIAL === */
.tutorial-overlay {
position:fixed; inset:0; background:rgba(15,30,40,.85);
backdrop-filter:blur(8px); display:flex; align-items:center;
justify-content:center; z-index:50; padding:1.5rem;
}
.tutorial-card {
background:#fff; border-radius:18px; padding:1.6rem 1.9rem;
max-width:520px; box-shadow:0 30px 80px rgba(0,0,0,.4);
}
.tutorial-card h3 { font-size:1.25rem; font-weight:800; margin-bottom:.6rem; color:var(--fjord); }
.tutorial-card p { font-size:.9rem; color:var(--text2); white-space:pre-line; margin-bottom:1rem; }
.progress-dots { display:flex; gap:.3rem; justify-content:center; margin:.8rem 0; }
.dot { width:8px; height:8px; border-radius:50%; background:#dadad0; }
.dot.active { background:var(--fjord); }
.tut-btn {
width:100%; padding:.75rem; background:var(--fjord); color:#fff;
border:none; border-radius:10px; font-weight:700; font-size:.95rem; cursor:pointer;
}
.tut-btn:hover { background:var(--fjord-d); }
/* === END SCREEN === */
.end-screen {
position:fixed; inset:0; background:rgba(15,30,40,.9);
backdrop-filter:blur(10px); display:flex; align-items:center;
justify-content:center; z-index:100; padding:2rem;
}
.end-card { background:#fff; border-radius:20px; padding:2rem 2.5rem; max-width:500px; text-align:center; box-shadow:0 30px 80px rgba(0,0,0,.4); }
.end-icon { font-size:4rem; margin-bottom:.5rem; }
.end-card h2 { font-size:1.8rem; font-weight:900; letter-spacing:-.02em; margin-bottom:.3rem; }
.end-card .end-summary { color:var(--text2); margin-bottom:1rem; }
.end-stats { display:grid; grid-template-columns:1fr 1fr; gap:.8rem; margin:1rem 0; }
.end-stat { background:#f7f6f3; border-radius:10px; padding:.8rem; }
.end-stat-val { font-size:1.4rem; font-weight:800; color:var(--fjord); }
.end-stat-lbl { font-size:.7rem; color:var(--text3); text-transform:uppercase; }
.end-goals { display:flex; flex-direction:column; gap:.3rem; margin:.8rem 0 1rem; text-align:left; }
.end-goal { display:flex; align-items:center; gap:.55rem; font-size:.82rem; padding:.35rem .55rem; border-radius:7px; background:#f7f6f3; }
.end-goal.done { color:var(--moss); background:var(--moss-l); }
.end-goal.fail { color:var(--danger); background:var(--coral-l); }
.end-goal-icon { font-weight:900; font-size:.95rem; width:1rem; text-align:center; }
</style>
</head>
<body>
<div id="canvas-wrap"></div>
<!-- TOP BAR -->
<nav class="floating topbar">
<a href="index.html" class="brand">
<img src="assets/img/logo.png" alt="">
Lieferketten-Planer*in
</a>
<div class="topbar-controls">
<div class="ctrl-group" title="Simulationsgeschwindigkeit">
<button class="icon-btn" data-speed="0" title="Pause"></button>
<button class="icon-btn active" data-speed="1" title="Normal (×1)">×1</button>
<button class="icon-btn" data-speed="2" title="Schnell (×2)">×2</button>
<button class="icon-btn" data-speed="4" title="Sehr schnell (×4)">×4</button>
</div>
<div class="topbar-sep"></div>
<button class="icon-btn" id="btn-save" title="Stand speichern">💾</button>
<button class="icon-btn" id="btn-load" title="Stand laden">📂</button>
<button class="icon-btn" id="btn-finish" title="Simulation beenden"></button>
<button class="icon-btn" id="btn-reset" title="Zurücksetzen">🔄</button>
<a class="icon-btn" href="index.html" title="Zurück zur Übersicht">🏠</a>
</div>
</nav>
<!-- LINKS: Status + Ziele -->
<aside class="floating panel-status">
<div class="panel-h">📊 Status</div>
<div id="resources"></div>
<div class="panel-h panel-h-sub">🎯 Ziele</div>
<div id="status-goals"></div>
</aside>
<!-- LINKS DANEBEN: Ereignisse-Strip -->
<aside class="floating panel-events-strip" id="events-strip">
<div class="events-strip-header">
<span>📰 Ereignisse</span>
<span class="esh-toggle"></span>
</div>
<div id="events" class="events-strip-list"></div>
</aside>
<!-- RECHTS: Bestell-Formular -->
<aside class="floating panel-order">
<div class="order-section">
<h4>🌾 Baumwolle</h4>
<div class="order-options" id="cotton-options"></div>
</div>
<div class="order-section">
<h4>🚚 Transport zur Näherei</h4>
<div class="transport-row" id="transport1-row"></div>
</div>
<div class="order-section">
<h4>🏭 Näherei</h4>
<div class="order-options" id="factory-options"></div>
</div>
<div class="order-section">
<h4>🚚 Transport nach Wien</h4>
<div class="transport-row" id="transport2-row"></div>
</div>
<div class="order-summary" id="order-summary"></div>
<button class="order-btn" id="order-btn" disabled>Bestellen</button>
</aside>
<!-- TUTORIAL -->
<div class="tutorial-overlay" id="tutorial">
<div class="tutorial-card">
<h3 id="tut-title"></h3>
<p id="tut-text"></p>
<div class="progress-dots" id="tut-dots"></div>
<button class="tut-btn" id="tut-next">Weiter →</button>
</div>
</div>
<div id="end-screen-host"></div>
<div id="citizen-event-host"></div>
<div id="info-overlay-host"></div>
<script type="module">
import {
LieferkettenGame, COTTON_SOURCES, FACTORIES, TRANSPORT_MODES, DEFAULT_CONFIG,
} from './src/sims/sim-10-lieferketten/game.ts'
import { LieferkettenRenderer } from './src/sims/sim-10-lieferketten/renderer.ts'
import { GameUI } from './src/ui/game-ui.ts'
import { installTooltips } from './src/ui/info-overlay.ts'
installTooltips()
// === Lehrer-Konfiguration via URL-Parameter ===
// ?config=<json> erlaubt der Lehrperson, die drei Hauptparameter zu setzen.
function readConfig() {
try {
const raw = new URLSearchParams(location.search).get('config')
if (raw) return JSON.parse(decodeURIComponent(raw))
} catch {}
return {}
}
const userConfig = readConfig()
const game = new LieferkettenGame(userConfig)
// === GameUI ohne Shop (wir bauen unser eigenes Bestell-Formular) ===
// Wir geben einen leeren shopItems-Array, damit GameUI nichts rendert.
const ui = new GameUI({
game,
Renderer: LieferkettenRenderer,
shopItems: [],
onBuy: () => false,
graphs: [],
saveKey: 'lieferketten-save',
yearOffset: 2025,
})
// === Bestell-Formular bauen ===
const filterByConfig = (list, allowed) => list.filter(x => allowed.includes(x.id))
const cottonAvailable = filterByConfig(COTTON_SOURCES, game.getConfig().enabledOptions.cotton)
const factoryAvailable = filterByConfig(FACTORIES, game.getConfig().enabledOptions.factory)
const transportAvailable = TRANSPORT_MODES.filter(t => game.getConfig().enabledOptions.transport.includes(t.id))
const currentOrder = {
cottonId: null,
factoryId: null,
transport1: null,
transport2: null,
}
function ratingClass(value, goodAbove, badBelow) {
if (value >= goodAbove) return 'oo-stat-good'
if (value < badBelow) return 'oo-stat-bad'
return ''
}
function renderCottonOptions() {
const el = document.getElementById('cotton-options')
el.innerHTML = cottonAvailable.map(c => `
<button class="order-option ${currentOrder.cottonId === c.id ? 'selected' : ''}" data-id="${c.id}">
<span class="oo-flag">${c.flag}</span>
<div class="oo-info">
<div class="oo-name">${c.country}</div>
<div class="oo-stats">
<span class="oo-stat">€${c.pricePerShirt.toFixed(2)}</span>
<span class="oo-stat ${ratingClass(-c.co2PerShirt, -1.5, -2.4)}">${c.co2PerShirt.toFixed(1)} kg CO₂</span>
<span class="oo-stat ${ratingClass(c.ethicsScore, 7, 5)}">⚖️ ${c.ethicsScore}</span>
</div>
</div>
</button>
`).join('')
el.querySelectorAll('.order-option').forEach(btn => {
btn.addEventListener('click', () => {
currentOrder.cottonId = btn.dataset.id
renderCottonOptions(); updatePreview()
})
})
}
function renderFactoryOptions() {
const el = document.getElementById('factory-options')
el.innerHTML = factoryAvailable.map(f => `
<button class="order-option ${currentOrder.factoryId === f.id ? 'selected' : ''}" data-id="${f.id}">
<span class="oo-flag">${f.flag}</span>
<div class="oo-info">
<div class="oo-name">${f.country}</div>
<div class="oo-stats">
<span class="oo-stat">€${f.pricePerShirt.toFixed(2)}</span>
<span class="oo-stat ${ratingClass(-f.co2PerShirt, -1.0, -1.5)}">${f.co2PerShirt.toFixed(1)} kg CO₂</span>
<span class="oo-stat ${ratingClass(f.ethicsScore, 7, 5)}">⚖️ ${f.ethicsScore}</span>
</div>
</div>
</button>
`).join('')
el.querySelectorAll('.order-option').forEach(btn => {
btn.addEventListener('click', () => {
currentOrder.factoryId = btn.dataset.id
renderFactoryOptions(); updatePreview()
})
})
}
function renderTransportRow(rowId, key) {
const el = document.getElementById(rowId)
el.innerHTML = transportAvailable.map(t => `
<button class="transport-btn ${currentOrder[key] === t.id ? 'selected' : ''}" data-id="${t.id}">
<span class="tb-emoji">${t.emoji}</span>
<span class="tb-name">${t.name.split(' ')[0]}</span>
</button>
`).join('')
el.querySelectorAll('.transport-btn').forEach(btn => {
btn.addEventListener('click', () => {
currentOrder[key] = btn.dataset.id
renderTransportRow(rowId, key); updatePreview()
})
})
}
function updatePreview() {
// Renderer-Vorschau aktualisieren
const r = ui.getRenderer()
if (r && typeof r.setPreviewOrder === 'function') {
r.setPreviewOrder(currentOrder)
}
// Order-Summary berechnen
const summary = document.getElementById('order-summary')
const btn = document.getElementById('order-btn')
const complete = currentOrder.cottonId && currentOrder.factoryId && currentOrder.transport1 && currentOrder.transport2
if (!complete) {
summary.innerHTML = '<div style="color:var(--text3);text-align:center;font-size:.7rem">Wähle alle 4 Stationen aus.</div>'
btn.disabled = true
return
}
// Vorschau-Berechnung lokal (mirror der placeOrder-Logik)
const c = COTTON_SOURCES.find(x => x.id === currentOrder.cottonId)
const f = FACTORIES.find(x => x.id === currentOrder.factoryId)
const t1 = TRANSPORT_MODES.find(x => x.id === currentOrder.transport1)
const t2 = TRANSPORT_MODES.find(x => x.id === currentOrder.transport2)
// Sehr grobe Schätzung — wir nutzen die Distanz-Mittelwerte (nicht die Effekte)
const dist1 = 5 // ~5000 km Schätzwert für Vorschau
const dist2 = 3 // ~3000 km Schätzwert
const t1Cost = t1.costPer1000km * dist1
const t2Cost = t2.costPer1000km * dist2
const t1CO2 = t1.co2Per1000km * dist1
const t2CO2 = t2.co2Per1000km * dist2
const costPerShirt = c.pricePerShirt + f.pricePerShirt + t1Cost + t2Cost
const co2PerShirt = c.co2PerShirt + f.co2PerShirt + t1CO2 + t2CO2
const ethics = (c.ethicsScore + f.ethicsScore) / 2
const profitPerShirt = 12 - costPerShirt
const profitMio = (5000 * profitPerShirt) / 1_000_000
const profitClass = profitMio >= 0 ? 'os-good' : 'os-bad'
const co2Class = co2PerShirt < 6 ? 'os-good' : 'os-bad'
const ethicsClass = ethics >= 5 ? 'os-good' : 'os-bad'
summary.innerHTML = `
<div class="os-row"><span>Kosten/Stück</span><strong>€${costPerShirt.toFixed(2)}</strong></div>
<div class="os-row"><span>Gewinn/Charge</span><strong class="${profitClass}">${profitMio >= 0 ? '+' : ''}${profitMio.toFixed(2)} Mio €</strong></div>
<div class="os-row"><span>CO₂/Stück</span><strong class="${co2Class}">${co2PerShirt.toFixed(1)} kg</strong></div>
<div class="os-row"><span>Ø Ethik</span><strong class="${ethicsClass}">${ethics.toFixed(1)} / 10</strong></div>
`
btn.disabled = false
}
document.getElementById('order-btn').addEventListener('click', () => {
if (!currentOrder.cottonId || !currentOrder.factoryId || !currentOrder.transport1 || !currentOrder.transport2) return
game.placeOrder({ ...currentOrder })
// Sprite-Animation auf der Karte
const r = ui.getRenderer()
if (r && typeof r.animateOrder === 'function') r.animateOrder(currentOrder)
})
// Events-Strip toggle
const eventsStrip = document.getElementById('events-strip')
eventsStrip.addEventListener('click', (ev) => {
ev.stopPropagation()
eventsStrip.classList.toggle('open')
})
document.addEventListener('click', (ev) => {
if (!eventsStrip.classList.contains('open')) return
if (eventsStrip.contains(ev.target)) return
eventsStrip.classList.remove('open')
})
// Initial render
renderCottonOptions()
renderFactoryOptions()
renderTransportRow('transport1-row', 'transport1')
renderTransportRow('transport2-row', 'transport2')
updatePreview()
</script>
</body>
</html>