Files
osm-import-tools/web/templates/index.html
T

654 lines
21 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OSM Import Tools</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #eef0f3;
color: #222;
}
/* ── Page shell ── */
.page {
height: 100%;
display: flex;
flex-direction: column;
}
/* ── Top bar ── */
.topbar {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 16px;
background: #1a1a2e;
flex-shrink: 0;
}
.topbar h1 {
font-size: 16px;
font-weight: 700;
color: #fff;
margin-right: 4px;
}
.topbar-sep {
width: 1px;
height: 20px;
background: #ffffff30;
}
.btn-topbar {
padding: 5px 12px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 12px;
font-weight: 600;
background: #ffffff18;
color: #ddd;
text-decoration: none;
}
.btn-topbar:hover { background: #ffffff30; }
.btn-topbar.purple {
background: #6f42c1;
color: white;
}
.btn-topbar.purple:hover { background: #5a32a3; }
/* ── Main two-panel layout ── */
.main {
flex: 1;
display: flex;
overflow: hidden;
}
/* ── Left panel (workflows) ── */
.left {
width: 340px;
flex-shrink: 0;
display: flex;
flex-direction: column;
border-right: 1px solid #d8dbe0;
overflow: hidden;
}
/* County tabs */
.county-tabs {
display: flex;
flex-shrink: 0;
border-bottom: 1px solid #d8dbe0;
}
.county-tab {
flex: 1;
padding: 10px;
border: none;
background: #f4f5f7;
cursor: pointer;
font-size: 13px;
font-weight: 600;
color: #888;
border-bottom: 3px solid transparent;
transition: color 0.15s, border-color 0.15s;
}
.county-tab:hover { color: #444; }
.county-tab.active.lake { color: #1a7a33; border-bottom-color: #28a745; background: white; }
.county-tab.active.sumter { color: #0d7a8f; border-bottom-color: #17a2b8; background: white; }
/* Scrollable workflow area */
.workflows {
flex: 1;
overflow-y: auto;
padding: 14px 16px;
display: flex;
flex-direction: column;
gap: 16px;
}
/* Workflow group (Roads / Paths / Addresses) */
.wf-group {
background: white;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.07);
overflow: hidden;
}
.wf-title {
padding: 8px 14px;
font-size: 11px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.7px;
color: #555;
background: #f8f9fa;
border-bottom: 1px solid #eee;
}
/* Steps */
.step {
display: flex;
align-items: center;
gap: 10px;
padding: 9px 14px;
border-bottom: 1px solid #f3f3f3;
}
.step:last-child { border-bottom: none; }
.step-num {
width: 20px;
height: 20px;
border-radius: 50%;
background: #e8eaf6;
color: #3949ab;
font-size: 10px;
font-weight: 800;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.step-label {
flex: 1;
font-size: 12px;
color: #444;
}
.step-actions {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
}
/* Run buttons */
.btn-run {
padding: 5px 12px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 12px;
font-weight: 600;
color: white;
transition: filter 0.15s;
}
.btn-run:hover { filter: brightness(0.88); }
.btn-run:disabled { filter: brightness(0.5); cursor: not-allowed; }
.lake-active .btn-run { background: #28a745; }
.sumter-active .btn-run { background: #17a2b8; }
/* Force redownload */
.force-label {
font-size: 10px;
color: #aaa;
display: flex;
align-items: center;
gap: 3px;
cursor: pointer;
white-space: nowrap;
}
/* Upload (paths) */
.upload-input {
font-size: 11px;
width: 120px;
color: #555;
}
/* N/A badge */
.na {
font-size: 10px;
color: #ccc;
font-style: italic;
}
/* Data files strip */
.files-strip {
flex-shrink: 0;
border-top: 1px solid #d8dbe0;
padding: 8px 14px;
background: #f8f9fa;
}
.files-strip-title {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.4px;
color: #aaa;
margin-bottom: 5px;
}
.file-links {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.file-link {
font-size: 10px;
padding: 2px 7px;
border-radius: 3px;
color: white;
text-decoration: none;
font-weight: 500;
}
.file-link.lake { background: #28a745; }
.file-link.sumter { background: #17a2b8; }
/* ── Right panel (console) ── */
.right {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
background: #1e1e1e;
}
.console-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 14px;
background: #111;
flex-shrink: 0;
}
.console-header span {
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.6px;
color: #666;
}
.btn-cancel {
padding: 3px 10px;
background: #c0392b;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 11px;
font-weight: 600;
display: none;
}
.btn-cancel.active { display: inline-block; }
#status {
padding: 7px 14px;
font-size: 12px;
flex-shrink: 0;
display: none;
}
#status.success { background: #1e3a24; color: #6fcf7f; }
#status.error { background: #3a1e1e; color: #e07070; }
#status.info { background: #1e2e3a; color: #70b8e0; }
#logs {
flex: 1;
overflow-y: auto;
padding: 12px 14px;
font-family: 'Courier New', monospace;
font-size: 12px;
color: #d4d4d4;
white-space: pre-wrap;
word-wrap: break-word;
}
.log-placeholder {
color: #444;
font-style: italic;
}
</style>
</head>
<body>
<div class="page">
<!-- Top bar -->
<div class="topbar">
<h1>OSM Import Tools</h1>
<div class="topbar-sep"></div>
<button class="btn-topbar" onclick="runScript('make-new-latest', '')">New Latest</button>
<button class="btn-topbar" onclick="runScript('ls', '')">List Files</button>
<div style="flex:1"></div>
<a href="/exceptions" class="btn-topbar">Street Exceptions</a>
<a href="/map" class="btn-topbar purple">Open Map Viewer</a>
</div>
<div class="main">
<!-- Left: workflows -->
<div class="left" id="leftPanel">
<div class="county-tabs">
<button class="county-tab lake active" onclick="selectCounty('lake')">Lake County</button>
<button class="county-tab sumter" onclick="selectCounty('sumter')">Sumter County</button>
</div>
<div class="workflows">
<!-- Roads -->
<div class="wf-group">
<div class="wf-title">Roads</div>
<div class="step">
<div class="step-num">1</div>
<div class="step-label">Download county data</div>
<div class="step-actions">
<button class="btn-run" onclick="runScript('download-county-roads')">Run</button>
</div>
</div>
<div class="step">
<div class="step-num">2</div>
<div class="step-label">Download OSM roads</div>
<div class="step-actions">
<button class="btn-run" onclick="runScript('download-osm-roads')">Run</button>
</div>
</div>
<div class="step">
<div class="step-num">3</div>
<div class="step-label">Convert to GeoJSON</div>
<div class="step-actions">
<button class="btn-run" onclick="runScript('convert-roads')">Run</button>
</div>
</div>
<div class="step">
<div class="step-num">4</div>
<div class="step-label">Diff vs OSM</div>
<div class="step-actions">
<button class="btn-run" onclick="runScript('diff-roads')">Run</button>
</div>
</div>
</div>
<!-- Paths (Sumter only) -->
<div class="wf-group" id="paths-group">
<div class="wf-title">Paths</div>
<div class="step" id="paths-step-1">
<div class="step-num">1</div>
<div class="step-label">Upload county paths</div>
<div class="step-actions" id="paths-step-1-actions">
<!-- filled by selectCounty() -->
</div>
</div>
<div class="step">
<div class="step-num">2</div>
<div class="step-label">Download OSM paths</div>
<div class="step-actions" id="paths-step-2-actions">
<!-- filled by selectCounty() -->
</div>
</div>
<div class="step">
<div class="step-num">3</div>
<div class="step-label">Convert to GeoJSON</div>
<div class="step-actions" id="paths-step-3-actions">
<!-- filled by selectCounty() -->
</div>
</div>
<div class="step">
<div class="step-num">4</div>
<div class="step-label">Diff vs OSM</div>
<div class="step-actions" id="paths-step-4-actions">
<!-- filled by selectCounty() -->
</div>
</div>
</div>
<!-- Addresses -->
<div class="wf-group">
<div class="wf-title">Addresses</div>
<div class="step">
<div class="step-num">1</div>
<div class="step-label">Download county data</div>
<div class="step-actions">
<button class="btn-run" onclick="runScript('download-county-addresses')">Run</button>
</div>
</div>
<div class="step">
<div class="step-num">2</div>
<div class="step-label">Download OSM addresses</div>
<div class="step-actions">
<button class="btn-run" onclick="runScript('download-osm-addresses')">Run</button>
</div>
</div>
<div class="step">
<div class="step-num">3</div>
<div class="step-label">Convert to GeoJSON</div>
<div class="step-actions">
<button class="btn-run" onclick="runScript('convert-addresses')">Run</button>
</div>
</div>
<div class="step">
<div class="step-num">4</div>
<div class="step-label">Diff vs OSM</div>
<div class="step-actions">
<button class="btn-run" onclick="runScript('diff-addresses')">Run</button>
<label class="force-label">
<input type="checkbox" id="force-redownload"> Force redownload
</label>
</div>
</div>
</div>
</div><!-- /workflows -->
<!-- Data files strip -->
{% if data_files %}
<div class="files-strip">
<div class="files-strip-title">Downloaded files</div>
<div class="file-links">
{% for county, file_names in data_files.items() %}
{% for file_name in file_names %}
<a class="file-link {% if 'lake' in county %}lake{% else %}sumter{% endif %}"
href="/data/latest/{{ county }}/{{ file_name }}"
title="{{ county }}/{{ file_name }}">{{ file_name }}</a>
{% endfor %}
{% endfor %}
</div>
</div>
{% endif %}
</div><!-- /left -->
<!-- Right: console -->
<div class="right">
<div class="console-header">
<span>Output</span>
<button id="cancelButton" class="btn-cancel" onclick="cancelJob()">Cancel</button>
</div>
<div id="status"></div>
<div id="logs"><span class="log-placeholder">Run a step to see output here.</span></div>
</div>
</div><!-- /main -->
</div><!-- /page -->
<script>
let activeCounty = 'lake';
let currentJobId = null;
let logCheckInterval = null;
function selectCounty(county) {
activeCounty = county;
// Update tab styles
document.querySelectorAll('.county-tab').forEach(t => t.classList.remove('active'));
document.querySelector(`.county-tab.${county}`).classList.add('active');
// Update left panel class for button color theming
const panel = document.getElementById('leftPanel');
panel.classList.toggle('lake-active', county === 'lake');
panel.classList.toggle('sumter-active', county === 'sumter');
// Update paths section availability
renderPathsActions(county);
}
function renderPathsActions(county) {
if (county === 'lake') {
['1','2','3','4'].forEach(n => {
document.getElementById(`paths-step-${n}-actions`).innerHTML =
'<span class="na">Not available for Lake</span>';
});
} else {
// Step 1: upload
document.getElementById('paths-step-1-actions').innerHTML =
'<input type="file" id="upload-sumter-paths" class="upload-input" accept=".zip">' +
'<button class="btn-run" onclick="uploadFile(\'upload-sumter-paths\', \'/data/latest/sumter/paths.shp.zip\')">Upload</button>';
// Steps 2-4: run buttons
[
['2', 'download-osm-paths'],
['3', 'convert-paths'],
['4', 'diff-paths'],
].forEach(([n, script]) => {
document.getElementById(`paths-step-${n}-actions`).innerHTML =
`<button class="btn-run" onclick="runScript('${script}')">Run</button>`;
});
}
}
// Init
selectCounty('lake');
/* ── Script runner ── */
function runScript(scriptName) {
const logsEl = document.getElementById('logs');
logsEl.textContent = '';
showStatus(`Running: ${scriptName} (${activeCounty})`, 'info');
disableButtons();
document.getElementById('cancelButton').classList.add('active');
let forceDownload = false;
if (scriptName === 'diff-addresses') {
const cb = document.getElementById('force-redownload');
if (cb) forceDownload = cb.checked;
}
fetch('/api/run-script', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ script: scriptName, county: activeCounty, forceDownload })
})
.then(r => r.json())
.then(data => {
if (data.error) {
showStatus(`Error: ${data.error}`, 'error');
enableButtons();
document.getElementById('cancelButton').classList.remove('active');
return;
}
currentJobId = data.job_id;
if (logCheckInterval) clearInterval(logCheckInterval);
logCheckInterval = setInterval(checkJobStatus, 1000);
})
.catch(err => {
showStatus(`Error: ${err.message}`, 'error');
enableButtons();
document.getElementById('cancelButton').classList.remove('active');
});
}
function cancelJob() {
if (!currentJobId || !confirm('Cancel the running job?')) return;
fetch('/api/cancel-job', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ job_id: currentJobId })
})
.then(r => r.json())
.then(data => {
if (data.success) {
showStatus('Cancelled.', 'info');
clearInterval(logCheckInterval);
enableButtons();
document.getElementById('cancelButton').classList.remove('active');
currentJobId = null;
} else {
showStatus(`Cancel failed: ${data.error}`, 'error');
}
});
}
function checkJobStatus() {
if (!currentJobId) return;
fetch(`/api/job-status/${currentJobId}`)
.then(r => r.json())
.then(data => {
const logsEl = document.getElementById('logs');
logsEl.textContent = data.logs.join('');
logsEl.scrollTop = logsEl.scrollHeight;
if (!data.running) {
clearInterval(logCheckInterval);
showStatus('Done.', 'success');
enableButtons();
document.getElementById('cancelButton').classList.remove('active');
currentJobId = null;
}
})
.catch(err => console.error('Status check error:', err));
}
function uploadFile(inputId, dest) {
const input = document.getElementById(inputId);
if (!input || !input.files.length) {
showStatus('Please select a file first.', 'error');
return;
}
const file = input.files[0];
const formData = new FormData();
formData.append('file', file);
formData.append('dest', dest);
showStatus(`Uploading ${file.name}...`, 'info');
disableButtons();
fetch('/api/upload-file', { method: 'POST', body: formData })
.then(r => r.json())
.then(data => {
if (data.error) {
showStatus(`Error: ${data.error}`, 'error');
} else {
showStatus(`Uploaded to ${data.path}`, 'success');
input.value = '';
}
})
.catch(err => showStatus(`Error: ${err.message}`, 'error'))
.finally(() => enableButtons());
}
function showStatus(msg, type) {
const el = document.getElementById('status');
el.textContent = msg;
el.className = type;
el.style.display = 'block';
}
function disableButtons() {
document.querySelectorAll('.btn-run').forEach(b => b.disabled = true);
}
function enableButtons() {
document.querySelectorAll('.btn-run').forEach(b => b.disabled = false);
}
</script>
</body>
</html>