pwa-drawing-tools/index.html
ekke 923d353fec Initial upload of pwa for the LUPMIS2 Drawing tools
the files are supposed to be in the root of the installation
2026-01-27 09:49:19 +00:00

825 lines
23 KiB
HTML
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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="en" data-bs-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#2d5016">
<meta name="description" content="LUPMIS2 Drawing Tools">
<!-- PWA Manifest -->
<link rel="manifest" href="/manifest.json">
<link rel="apple-touch-icon" href="/icons/icon-192.png">
<title>LUPMIS Drawing Tools</title>
<style>
/* Custom theme colors for Bootstrap */
/*
--bs-primary: #2d5016;
--bs-primary-rgb: 45, 80, 22;
--lupmis-green: #2d5016;
--lupmis-green-light: #3d6b1e;
--lupmis-green-dark: #1e3a0f;
*/
:root {
--bs-primary: #6CCB2D;
--bs-primary-rgb: 144, 207, 84;
--bs-secondary: #6CCB2D;
--bs-secondary-rgb: 45, 80, 22;
--bs-body-color: #8B008B;
--bsOverwrite-primary: rgb(96, 127, 35, 0.6);
--bsOverwrite-primary-light: #B6ED74;
--bsOverwrite-primary-dark: #1E3A0F;
--bsOverwrite-secondary: #556B2F;
--bsOverwrite-text: #5C728F;
}
/* Full height layout */
html, body {
height: 100%;
overflow: hidden;
color: var(--bsOverwrite-text);
}
/* Override Bootstrap primary color */
.btn-primary {
--bs-btn-bg: var(--bsOverwrite-primary);
--bs-btn-border-color: var(--bsOverwrite-primary);
--bs-btn-hover-bg: var(--bsOverwrite-primary-dark);
--bs-btn-hover-border-color: var(--bsOverwrite-primary-dark);
--bs-btn-active-bg: var(--bsOverwrite-primary-dark);
--bs-btn-active-border-color: var(--bsOverwrite-primary-dark);
--bs-btn-color: var(--bsOverwrite-text);
--bs-btn-hover-color: var(--bsOverwrite-text);
--bs-btn-active-color: var(--bsOverwrite-text);
}
.btn-outline-primary {
--bs-btn-color: var(--bsOverwrite-text);
--bs-btn-border-color: var(--bsOverwrite-primary);
--bs-btn-hover-bg: var(--bsOverwrite-primary);
--bs-btn-hover-border-color: var(--bsOverwrite-primary);
--bs-btn-hover-color: var(--bsOverwrite-text);
--bs-btn-active-bg: var(--bsOverwrite-primary);
--bs-btn-active-border-color: var(--bsOverwrite-primary);
--bs-btn-active-color: var(--bsOverwrite-text);
}
.btn-secondary {
--bs-btn-bg: var(--bsOverwrite-secondary);
--bs-btn-border-color: var(--bsOverwrite-secondary);
--bs-btn-color: #fff;
}
.bg-primary {
background-color: var(--bsOverwrite-primary) !important;
color: var(--bsOverwrite-text) !important;
}
.text-primary {
color: var(--bsOverwrite-text) !important;
}
.border-primary {
border-color: var(--bsOverwrite-primary) !important;
}
/* Navbar styling */
.navbar.bg-primary {
background-color: var(--bsOverwrite-primary) !important;
}
.navbar.bg-primary .navbar-brand {
color: var(--bsOverwrite-text) !important;
}
/* Card headers */
.card-header.bg-primary {
background-color: var(--bsOverwrite-primary) !important;
color: var(--bsOverwrite-text) !important;
}
.card-header.bg-primary h6 {
color: var(--bsOverwrite-text) !important;
}
/* Modal header */
.modal-header.bg-primary {
background-color: var(--bsOverwrite-primary) !important;
color: var(--bsOverwrite-text) !important;
}
.modal-header.bg-primary .modal-title {
color: var(--bsOverwrite-text) !important;
}
.modal-header.bg-primary .btn-close-white {
filter: none;
}
/* Focus states */
.form-control:focus, .form-select:focus {
border-color: var(--bsOverwrite-primary);
box-shadow: 0 0 0 0.25rem rgba(255, 233, 106, 0.25);
}
/* Form labels */
.form-label {
color: var(--bsOverwrite-text);
}
/* Main container - full height */
.app-container {
height: 100vh;
display: flex;
flex-direction: column;
}
/* Map and sidebar row */
.main-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
}
/* Map container - now takes full space */
.map-container {
flex: 1;
position: relative;
min-height: 250px;
width: 100%;
height: 100%;
}
#map {
width: 100%;
height: 100%;
}
/* Offline indicator */
#offline-indicator {
display: none;
position: absolute;
top: 10px;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
}
body.is-offline #offline-indicator {
display: block;
}
/* Locations list */
.locations-list {
max-height: 200px;
overflow-y: auto;
}
@media (min-width: 768px) {
.locations-list {
max-height: 300px;
}
}
.location-item {
cursor: pointer;
transition: background-color 0.15s;
color: var(--bsOverwrite-text);
}
.location-item:hover {
background-color: var(--bsOverwrite-primary-light);
}
.location-item h6 {
color: var(--bsOverwrite-text);
}
/* Category badges */
.badge-water { background-color: #3b82f6 !important; }
.badge-school { background-color: #f59e0b !important; }
.badge-health { background-color: #ef4444 !important; }
.badge-market { background-color: #8b5cf6 !important; }
.badge-default { background-color: var(--bsOverwrite-secondary) !important; }
.badge-other { background-color: #6b7280 !important; }
/* Install button */
#install-btn {
display: none;
position: fixed;
bottom: 20px;
right: 20px;
z-index: 1050;
}
/* Fix ol-ext LayerSwitcher z-index */
.ol-layerswitcher {
z-index: 100;
}
/* Alert hint box */
.alert-light.border-primary {
border-color: var(--bsOverwrite-primary) !important;
color: var(--bsOverwrite-text);
}
/* Badge in navbar and cards */
.badge.bg-primary {
background-color: var(--bsOverwrite-primary) !important;
color: var(--bsOverwrite-text) !important;
}
/* Headings and text */
h1, h2, h3, h4, h5, h6 {
color: var(--bsOverwrite-text);
}
/* Scrollbar styling */
.offcanvas-body::-webkit-scrollbar,
.locations-list::-webkit-scrollbar {
width: 6px;
}
.offcanvas-body::-webkit-scrollbar-track,
.locations-list::-webkit-scrollbar-track {
background: #f1f1f1;
}
.offcanvas-body::-webkit-scrollbar-thumb,
.locations-list::-webkit-scrollbar-thumb {
background: var(--bsOverwrite-secondary);
border-radius: 3px;
}
.offcanvas-body::-webkit-scrollbar-thumb:hover,
.locations-list::-webkit-scrollbar-thumb:hover {
background: var(--bsOverwrite-text);
}
/* Offcanvas toggle buttons on map */
.offcanvas-toggle {
position: absolute;
z-index: 500;
background-color: var(--bsOverwrite-primaryRGBtranslucent);
border: 2px solid var(--bsOverwrite-text);
color: var(--bsOverwrite-text);
min-width: 44px;
min-height: 44px;
padding: 10px 14px;
border-radius: 8px;
cursor: pointer;
font-size: 1.2rem;
box-shadow: 0 2px 6px rgba(0,0,0,0.3);
transition: all 0.15s ease;
/* Touch-friendly */
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
}
.offcanvas-toggle:hover {
background-color: var(--bsOverwrite-primary-dark);
transform: scale(1.05);
}
.offcanvas-toggle:active {
background-color: var(--bsOverwrite-primary-dark);
transform: scale(0.95);
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
}
.offcanvas-toggle-left {
left: 10px;
top: 50%;
transform: translateY(-50%);
}
.offcanvas-toggle-left:hover {
transform: translateY(-50%) scale(1.05);
}
.offcanvas-toggle-left:active {
transform: translateY(-50%) scale(0.95);
}
.offcanvas-toggle-right {
right: 10px;
top: 50%;
transform: translateY(-50%);
}
.offcanvas-toggle-right:hover {
transform: translateY(-50%) scale(1.05);
}
.offcanvas-toggle-right:active {
transform: translateY(-50%) scale(0.95);
}
.offcanvas-toggle-bottom {
bottom: 80px; /* Above the dock */
left: 50%;
transform: translateX(-50%);
}
.offcanvas-toggle-bottom:hover {
transform: translateX(-50%) scale(1.05);
}
.offcanvas-toggle-bottom:active {
transform: translateX(-50%) scale(0.95);
}
/* Bottom Dock */
.bottom-dock {
position: absolute;
bottom: 0;
left: 0;
right: 0;
z-index: 600;
background-color: var(--bsOverwrite-primary);
border-top: 2px solid var(--bsOverwrite-text);
padding: 8px 16px;
display: flex;
justify-content: space-around;
align-items: center;
box-shadow: 0 -2px 10px rgba(0,0,0,0.15);
}
.dock-btn {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-width: 60px;
min-height: 50px;
padding: 6px 12px;
background-color: transparent;
border: none;
color: var(--bsOverwrite-text);
font-size: 1.4rem;
cursor: pointer;
border-radius: 8px;
transition: all 0.15s ease;
/* Touch-friendly */
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
}
.dock-btn:hover {
background-color: var(--bsOverwrite-primary-dark);
}
.dock-btn:active {
background-color: var(--bsOverwrite-primary-dark);
transform: scale(0.92);
}
.dock-btn-label {
font-size: 0.65rem;
margin-top: 2px;
font-weight: 500;
}
/* Touch-friendly improvements for forms and buttons */
.form-control, .form-select {
min-height: 44px;
font-size: 16px; /* Prevents iOS zoom on focus */
}
.btn {
min-height: 44px;
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
}
.btn:active {
transform: scale(0.97);
}
/* Location list items - larger touch targets */
.location-item {
cursor: pointer;
transition: background-color 0.15s;
color: var(--bsOverwrite-text);
min-height: 60px;
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
}
.location-item:hover {
background-color: var(--bsOverwrite-primary-light);
}
.location-item:active {
background-color: var(--bsOverwrite-primary);
}
.location-item h6 {
color: var(--bsOverwrite-text);
}
/* ol-ext GeolocationButton styling */
.ol-geobt {
top: auto !important;
bottom: 90px !important;
right: 10px !important;
left: auto !important;
}
.ol-geobt button {
background-color: var(--bsOverwrite-primary) !important;
color: var(--bsOverwrite-text) !important;
min-width: 44px;
min-height: 44px;
}
.ol-geobt button:hover {
background-color: var(--bsOverwrite-primary-dark) !important;
}
/* Add Location Popup Form on Map */
.map-add-location-popup {
background: white;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0,0,0,0.25);
min-width: 280px;
max-width: 320px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 14px;
z-index: 1000;
border: 2px solid var(--bsOverwrite-primary);
overflow: hidden;
}
.add-location-popup-header {
background-color: var(--bsOverwrite-primary);
color: var(--bsOverwrite-text);
padding: 10px 14px;
font-weight: 600;
display: flex;
justify-content: space-between;
align-items: center;
}
.add-location-popup-close {
background: none;
border: none;
font-size: 1.5rem;
line-height: 1;
color: var(--bsOverwrite-text);
cursor: pointer;
padding: 0 4px;
opacity: 0.7;
}
.add-location-popup-close:hover {
opacity: 1;
}
#map-add-location-form {
padding: 14px;
}
.add-location-popup-field {
margin-bottom: 12px;
}
.add-location-popup-field label {
display: block;
margin-bottom: 4px;
font-size: 13px;
font-weight: 500;
color: var(--bsOverwrite-text);
}
.add-location-popup-field input,
.add-location-popup-field select,
.add-location-popup-field textarea {
width: 100%;
padding: 8px 10px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
min-height: 40px;
color: var(--bsOverwrite-text);
}
.add-location-popup-field input:focus,
.add-location-popup-field select:focus,
.add-location-popup-field textarea:focus {
outline: none;
border-color: var(--bsOverwrite-primary);
box-shadow: 0 0 0 2px rgba(255, 233, 106, 0.3);
}
.add-location-popup-field textarea {
resize: vertical;
min-height: 60px;
}
.add-location-popup-coords {
margin-bottom: 12px;
padding: 8px 10px;
background: #f8f9fa;
border-radius: 6px;
color: #666;
font-family: monospace;
}
.add-location-popup-submit {
width: 100%;
padding: 10px 16px;
background-color: var(--bsOverwrite-primary);
color: var(--bsOverwrite-text);
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
min-height: 44px;
transition: background-color 0.15s;
}
.add-location-popup-submit:hover {
background-color: var(--bsOverwrite-primary-dark);
}
.add-location-popup-submit:active {
transform: scale(0.98);
}
/* Navbar location count as clickable button */
.location-count-btn {
background: none;
border: none;
padding: 0;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.location-count-btn .badge {
transition: transform 0.15s;
}
.location-count-btn:hover .badge {
transform: scale(1.1);
}
.location-count-btn:active .badge {
transform: scale(0.95);
}
/* Offcanvas styling */
.offcanvas {
background-color: var(--bsOverwrite-primary) !important;
color: var(--bsOverwrite-text);
}
.offcanvas-header {
border-bottom: 2px solid var(--bsOverwrite-text);
}
.offcanvas-title {
color: var(--bsOverwrite-text);
font-weight: 600;
}
.offcanvas-body {
color: var(--bsOverwrite-text);
}
/* Right offcanvas - Data Entry panel */
.offcanvas-end {
width: 320px !important;
background-color: var(--bs-body-bg) !important;
}
@media (min-width: 400px) {
.offcanvas-end {
width: 380px !important;
}
}
.offcanvas-end .offcanvas-header {
background-color: var(--bsOverwrite-primary);
}
.offcanvas-end .offcanvas-body {
background-color: var(--bs-body-bg);
padding: 1rem;
}
/* Locations list in offcanvas - can be taller now without form */
.offcanvas-end .locations-list {
max-height: calc(100vh - 280px);
overflow-y: auto;
}
/* Bottom offcanvas height */
.offcanvas-bottom {
height: 40vh;
max-height: 400px;
}
</style>
</head>
<body>
<div class="app-container">
<!-- Navbar - visible on all screens -->
<nav class="navbar bg-primary py-2">
<div class="container-fluid">
<span class="navbar-brand mb-0 h1">
<span class="me-2">🌍</span>LUPMIS Drawing Tools
</span>
<button type="button"
class="location-count-btn"
data-bs-toggle="offcanvas"
data-bs-target="#offcanvasRight"
aria-controls="offcanvasRight"
title="View saved locations">
<span class="badge bg-light text-dark" id="location-count-mobile">0</span>
</button>
</div>
</nav>
<!-- Main content area -->
<div class="main-content">
<!-- Map -->
<div class="map-container">
<div id="offline-indicator">
<span class="badge bg-danger fs-6">
📴 OFFLINE
</span>
</div>
<div id="map"></div>
<!-- Offcanvas toggle buttons -->
<button class="offcanvas-toggle offcanvas-toggle-left"
type="button"
data-bs-toggle="offcanvas"
data-bs-target="#offcanvasLeft"
aria-controls="offcanvasLeft"
title="Open left panel">
<i class="bi bi-chevron-left"></i>
</button>
<button class="offcanvas-toggle offcanvas-toggle-right"
type="button"
data-bs-toggle="offcanvas"
data-bs-target="#offcanvasRight"
aria-controls="offcanvasRight"
title="Open right panel">
<i class="bi bi-chevron-right"></i>
</button>
<button class="offcanvas-toggle offcanvas-toggle-bottom"
type="button"
data-bs-toggle="offcanvas"
data-bs-target="#offcanvasBottom"
aria-controls="offcanvasBottom"
title="Open bottom panel">
<i class="bi bi-chevron-down"></i>
</button>
<!-- Bottom Dock -->
<div class="bottom-dock">
<button class="dock-btn" type="button" id="dock-btn-1" title="Button 1">
<i class="bi bi-circle"></i>
<span class="dock-btn-label">Button 1</span>
</button>
<button class="dock-btn" type="button" id="dock-btn-2" title="Button 2">
<i class="bi bi-circle"></i>
<span class="dock-btn-label">Button 2</span>
</button>
<button class="dock-btn" type="button" id="dock-btn-3" title="Button 3">
<i class="bi bi-circle"></i>
<span class="dock-btn-label">Button 3</span>
</button>
<button class="dock-btn" type="button" id="dock-btn-4" title="Button 4">
<i class="bi bi-circle"></i>
<span class="dock-btn-label">Button 4</span>
</button>
</div>
</div>
</div>
</div>
<!-- Install PWA button -->
<button id="install-btn" class="btn btn-primary btn-lg shadow">
📲 Install App
</button>
<!-- Status Modal -->
<div class="modal fade" id="statusModal" tabindex="-1" aria-labelledby="statusModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-primary">
<h5 class="modal-title" id="statusModalLabel"> Database Status</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div id="status-content">
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<!-- Left Offcanvas - Placeholder -->
<div class="offcanvas offcanvas-start" tabindex="-1" id="offcanvasLeft" aria-labelledby="offcanvasLeftLabel">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="offcanvasLeftLabel"><i class="bi bi-chevron-left me-2"></i>Left Panel</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
<p>This is the left offcanvas panel.</p>
<p>You can add navigation, filters, or other controls here.</p>
</div>
</div>
<!-- Right Offcanvas - Saved Locations Panel -->
<div class="offcanvas offcanvas-end" tabindex="-1" id="offcanvasRight" aria-labelledby="offcanvasRightLabel">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="offcanvasRightLabel"><i class="bi bi-geo-alt me-2"></i>Saved Locations</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
<!-- Alert messages -->
<div id="error-message" class="alert alert-danger alert-dismissible fade show d-none" role="alert">
<span class="message-text"></span>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<div id="success-message" class="alert alert-success alert-dismissible fade show d-none" role="alert">
<span class="message-text"></span>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<!-- Tip -->
<div class="alert alert-light border-start border-4 border-primary py-2 mb-3" role="alert">
<small>💡 Tap on the map to add a new location.</small>
</div>
<!-- Saved Locations Card -->
<div class="card">
<div class="card-header bg-primary py-2 d-flex justify-content-between align-items-center">
<h6 class="mb-0">📍 Locations</h6>
<span class="badge bg-light text-dark" id="location-count">0</span>
</div>
<div class="card-body p-0">
<div id="locations-list" class="locations-list list-group list-group-flush">
<div class="text-center text-muted py-4">
<div class="spinner-border spinner-border-sm me-2" role="status">
<span class="visually-hidden">Loading...</span>
</div>
Loading...
</div>
</div>
</div>
<div class="card-footer bg-transparent">
<div class="btn-group btn-group-sm w-100" role="group">
<button type="button" class="btn btn-outline-secondary" id="fit-btn" title="Fit map to all markers">
🔍 Fit All
</button>
<button type="button" class="btn btn-outline-secondary" id="export-btn" title="Export database">
📥 Export
</button>
<button type="button" class="btn btn-outline-secondary" id="exportGeoJSON-btn" title="Export GeoJSON">
📥 Export
</button>
<button type="button" class="btn btn-outline-secondary" id="status-btn" title="Show database status">
Status
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Bottom Offcanvas -->
<div class="offcanvas offcanvas-bottom" tabindex="-1" id="offcanvasBottom" aria-labelledby="offcanvasBottomLabel">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="offcanvasBottomLabel"><i class="bi bi-chevron-down me-2"></i>Bottom Panel</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
<p>This is the bottom offcanvas panel.</p>
<p>You can add a data table, charts, or other wide content here.</p>
</div>
</div>
<!-- Main application script -->
<script type="module" src="/main.js"></script>
</body>
</html>