pwaLUPMIS2/index.html
2026-03-04 12:59:40 +01:00

1215 lines
35 KiB
HTML
Raw 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="#005eb8">
<meta name="description" content="LUPMIS2 Drawing Tools">
<!-- PWA Manifest -->
<link rel="manifest" href="/manifest.json">
<link rel="apple-touch-icon" href="/icons/luspa.icon">
<link rel="icon" href="/icons/luspa.icon">
<!-- LUSPA Design System Fonts: Bebas Neue (display) + Exo (body) -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Exo:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<title>LUPMIS2 Drawing Tools</title>
<style>
/* LUSPA Design System Tokens
* Source: lupmis.ammonvictor.com design guidelines
*
* Brand Palette:
* --brand-navy: #1e1a4b (foreground text)
* --brand-blue-gray: #333f48 (charcoal)
* --brand-blue-strong: #005eb8 (primary action)
* --brand-blue-light: #b9d9eb (accent/highlight)
* --brand-gray-cool: #f2f4f7 (muted background)
* --brand-green-deep: #006b3f (success)
* --brand-green-bright: #41b6a6 (teal accent)
* --brand-brown-muted: #8b6f47 (earth tone)
* --brand-orange-warm: #ff9e1b (warning)
* --brand-gray-medium: #7a7a7a (muted text)
*/
:root {
/* Brand palette */
--brand-navy: #1e1a4b;
--brand-blue-gray: #333f48;
--brand-blue-strong: #005eb8;
--brand-blue-light: #b9d9eb;
--brand-gray-cool: #f2f4f7;
--brand-green-deep: #006b3f;
--brand-green-bright: #41b6a6;
--brand-brown-muted: #8b6f47;
--brand-orange-warm: #ff9e1b;
--brand-gray-medium: #7a7a7a;
/* Semantic tokens */
--primary: var(--brand-blue-strong);
--primary-foreground: #fff;
--primary-hover: #004a92;
--foreground: var(--brand-navy);
--background: #fff;
--card: #fff;
--card-foreground: var(--brand-navy);
--muted: var(--brand-gray-cool);
--muted-foreground: var(--brand-gray-medium);
--accent: var(--brand-blue-light);
--accent-foreground: var(--brand-navy);
--success: var(--brand-green-deep);
--success-foreground: #fff;
--warning: var(--brand-orange-warm);
--warning-foreground: var(--brand-navy);
--destructive: #d4183d;
--destructive-foreground: #fff;
--border: #1e1a4b1f;
--ring: var(--brand-blue-strong);
/* Typography */
--font-display: "Bebas Neue", sans-serif;
--font-body: "Exo", sans-serif;
/* Bootstrap overrides using semantic tokens */
--bs-primary: var(--brand-blue-strong);
--bs-primary-rgb: 0, 94, 184;
--bs-secondary: var(--brand-green-deep);
--bs-secondary-rgb: 0, 107, 63;
--bs-body-color: var(--brand-navy);
/* Shorthand overrides for component styling */
--bsOverwrite-primary: var(--brand-blue-strong);
--bsOverwrite-primary-light: var(--brand-blue-light);
--bsOverwrite-primary-dark: var(--primary-hover);
--bsOverwrite-secondary: var(--brand-green-deep);
--bsOverwrite-text: var(--brand-navy);
--bsOverwrite-on-primary: #fff;
/* Legacy named tokens (mapped to new palette) */
--luspa-charcoal: var(--brand-blue-gray);
--luspa-navy: var(--brand-navy);
--luspa-blue: var(--brand-blue-strong);
--luspa-blue-light: var(--brand-blue-light);
--luspa-lavender: var(--brand-gray-cool);
--luspa-green: var(--brand-green-deep);
--luspa-lime: var(--brand-green-bright);
--luspa-tan: var(--brand-brown-muted);
--luspa-gold: var(--brand-orange-warm);
/* Border radius tokens */
--radius-sm: 0.25rem;
--radius-md: 0.375rem;
--radius-lg: 0.5rem;
--radius-xl: 0.75rem;
--radius-2xl: 1rem;
}
/* Full height layout */
html, body {
height: 100%;
overflow: hidden;
color: var(--foreground);
font-family: var(--font-body);
}
/* 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-on-primary);
--bs-btn-hover-color: var(--bsOverwrite-on-primary);
--bs-btn-active-color: var(--bsOverwrite-on-primary);
}
.btn-outline-primary {
--bs-btn-color: var(--bsOverwrite-primary);
--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-on-primary);
--bs-btn-active-bg: var(--bsOverwrite-primary);
--bs-btn-active-border-color: var(--bsOverwrite-primary);
--bs-btn-active-color: var(--bsOverwrite-on-primary);
}
.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-on-primary) !important;
}
.text-primary {
color: var(--bsOverwrite-primary) !important;
}
.border-primary {
border-color: var(--bsOverwrite-primary) !important;
}
/* Navbar styling — white background with navy text and blue-strong accent */
.navbar {
background-color: var(--background) !important;
border-bottom: 3px solid var(--primary);
box-shadow: 0 1px 4px rgba(0,0,0,0.08);
}
.navbar .navbar-brand {
color: var(--foreground) !important;
font-family: var(--font-display);
font-size: 1.4rem;
letter-spacing: 0.04em;
text-transform: uppercase;
}
/* Card headers */
.card-header.bg-primary {
background-color: var(--primary) !important;
color: var(--primary-foreground) !important;
}
.card-header.bg-primary h6 {
color: var(--primary-foreground) !important;
font-family: var(--font-body);
font-weight: 600;
}
/* Modal header */
.modal-header.bg-primary {
background-color: var(--primary) !important;
color: var(--primary-foreground) !important;
}
.modal-header.bg-primary .modal-title {
color: var(--primary-foreground) !important;
font-family: var(--font-display);
font-weight: 400;
letter-spacing: 0.025em;
text-transform: uppercase;
}
.modal-header.bg-primary .btn-close-white {
filter: none;
}
/* Focus states */
.form-control:focus, .form-select:focus {
border-color: var(--primary);
box-shadow: 0 0 0 0.25rem rgba(0, 94, 184, 0.2);
}
/* Form labels */
.form-label {
color: var(--foreground);
font-family: var(--font-body);
}
/* 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(--foreground);
}
.location-item:hover {
background-color: var(--muted);
}
.location-item h6 {
color: var(--foreground);
}
/* 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(--primary) !important;
color: var(--foreground);
}
/* Badge in navbar and cards */
.badge.bg-primary {
background-color: var(--primary) !important;
color: var(--primary-foreground) !important;
}
/* Headings and text — LUSPA Design System typography */
h1, h2 {
font-family: var(--font-display);
font-weight: 400;
line-height: 1.25;
letter-spacing: 0.025em;
text-transform: uppercase;
color: var(--foreground);
}
h3, h4, h5, h6 {
font-family: var(--font-body);
font-weight: 600;
line-height: 1.375;
color: var(--foreground);
}
/* 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(--primary);
border-radius: 3px;
}
.offcanvas-body::-webkit-scrollbar-thumb:hover,
.locations-list::-webkit-scrollbar-thumb:hover {
background: var(--primary-hover);
}
/* Offcanvas toggle buttons on map */
.offcanvas-toggle {
position: absolute;
z-index: 500;
background-color: var(--card);
border: 2px solid var(--primary);
color: var(--primary);
min-width: 44px;
min-height: 44px;
padding: 10px 14px;
border-radius: var(--radius-lg);
cursor: pointer;
font-size: 1.2rem;
box-shadow: 0 2px 6px rgba(0,0,0,0.15);
transition: all 0.15s ease;
/* Touch-friendly */
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
}
.offcanvas-toggle:hover {
background-color: var(--primary);
color: var(--primary-foreground);
transform: scale(1.05);
}
.offcanvas-toggle:active {
background-color: var(--primary-hover);
color: var(--primary-foreground);
transform: scale(0.95);
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
}
.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 — white card style with blue-strong accent */
.bottom-dock {
position: absolute;
bottom: 0;
left: 0;
right: 0;
z-index: 600;
background-color: var(--card);
border-top: 3px solid var(--primary);
padding: 8px 16px;
display: flex;
justify-content: space-around;
align-items: center;
box-shadow: 0 -2px 10px rgba(0,0,0,0.08);
}
.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: 1px solid var(--primary);
color: var(--foreground);
font-size: 1.4rem;
cursor: pointer;
border-radius: var(--radius-lg);
transition: all 0.15s ease;
/* Touch-friendly */
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
}
.dock-btn:hover {
background-color: var(--muted);
}
.dock-btn:active {
background-color: var(--accent);
transform: scale(0.92);
}
.dock-btn.active {
background-color: var(--primary);
color: var(--primary-foreground);
border-color: var(--primary);
}
.dock-btn.active:hover {
background-color: var(--primary-hover);
color: var(--primary-foreground);
}
.dock-btn-label {
font-size: 0.65rem;
margin-top: 2px;
font-weight: 500;
font-family: var(--font-body);
}
/* EditBar colour picker inline styling */
.ol-editbar .ol-colorpicker button {
padding: 2px;
display: flex;
align-items: center;
justify-content: center;
}
/* 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(--foreground);
min-height: 60px;
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
}
.location-item:hover {
background-color: var(--muted);
}
.location-item:active {
background-color: var(--accent);
}
.location-item h6 {
color: var(--foreground);
}
/* ol-ext GeolocationButton styling */
.ol-geobt {
top: auto !important;
bottom: 90px !important;
right: 10px !important;
left: auto !important;
}
.ol-geobt button {
background-color: var(--primary) !important;
color: var(--primary-foreground) !important;
min-width: 44px;
min-height: 44px;
}
.ol-geobt button:hover {
background-color: var(--primary-hover) !important;
}
/* ol-ext SearchNominatim styling */
.ol-search {
top: 10px !important;
left: 40px !important;
right: auto !important;
}
.ol-search button {
background-color: var(--primary) !important;
color: var(--primary-foreground) !important;
min-width: 24px;
min-height: 24px;
border: none;
}
.ol-search button:hover {
background-color: var(--primary-hover) !important;
}
.ol-search input {
border: 2px solid var(--primary) !important;
border-radius: var(--radius-md);
padding: 8px 12px;
font-size: 14px;
font-family: var(--font-body);
min-height: 40px;
color: var(--foreground);
width: 200px;
}
@media (min-width: 500px) {
.ol-search input {
width: 280px;
}
}
.ol-search input:focus {
outline: none;
border-color: var(--primary) !important;
box-shadow: 0 0 0 2px rgba(0, 94, 184, 0.2);
}
.ol-search input::placeholder {
color: var(--muted-foreground);
}
.ol-search ul.autocomplete {
background: var(--card);
border: 2px solid var(--primary);
border-top: none;
border-radius: 0 0 var(--radius-lg) var(--radius-lg);
max-height: 300px;
overflow-y: auto;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.ol-search ul.autocomplete li {
padding: 10px 14px;
cursor: pointer;
border-bottom: 1px solid var(--border);
color: var(--foreground);
font-size: 13px;
font-family: var(--font-body);
min-height: 44px;
display: flex;
align-items: center;
}
.ol-search ul.autocomplete li:last-child {
border-bottom: none;
}
.ol-search ul.autocomplete li:hover,
.ol-search ul.autocomplete li.select {
background-color: var(--muted) !important;
}
.ol-search ul.autocomplete li b {
color: var(--foreground);
font-weight: 600;
}
/* Add Location Popup Form on Map */
.map-add-location-popup {
background: var(--card);
border-radius: var(--radius-xl);
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
min-width: 280px;
max-width: 320px;
font-family: var(--font-body);
font-size: 14px;
z-index: 1000;
border: 2px solid var(--primary);
overflow: hidden;
}
.add-location-popup-header {
background-color: var(--primary);
color: var(--primary-foreground);
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(--primary-foreground);
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(--foreground);
}
.add-location-popup-field input,
.add-location-popup-field select,
.add-location-popup-field textarea {
width: 100%;
padding: 8px 10px;
border: 1px solid var(--border);
border-radius: var(--radius-md);
font-size: 14px;
font-family: var(--font-body);
min-height: 40px;
color: var(--foreground);
}
.add-location-popup-field input:focus,
.add-location-popup-field select:focus,
.add-location-popup-field textarea:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 2px rgba(0, 94, 184, 0.2);
}
.add-location-popup-field textarea {
resize: vertical;
min-height: 60px;
}
.add-location-popup-coords {
margin-bottom: 12px;
padding: 8px 10px;
background: var(--muted);
border-radius: var(--radius-md);
color: var(--muted-foreground);
font-family: monospace;
}
.add-location-popup-submit {
width: 100%;
padding: 10px 16px;
background-color: var(--primary);
color: var(--primary-foreground);
border: none;
border-radius: var(--radius-md);
font-size: 14px;
font-family: var(--font-body);
font-weight: 600;
cursor: pointer;
min-height: 44px;
transition: background-color 0.15s;
}
.add-location-popup-submit:hover {
background-color: var(--primary-hover);
}
.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 — white card with blue-strong header */
.offcanvas {
background-color: var(--background) !important;
color: var(--foreground);
}
.offcanvas-header {
background-color: var(--primary);
border-bottom: none;
}
.offcanvas-title {
color: var(--primary-foreground);
font-family: var(--font-display);
font-weight: 400;
letter-spacing: 0.025em;
text-transform: uppercase;
font-size: 1.25rem;
}
.offcanvas-header .btn-close {
filter: invert(1) grayscale(100%) brightness(200%);
}
.offcanvas-body {
color: var(--foreground);
}
/* 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(--primary);
color: var(--primary-foreground);
}
.offcanvas-end .offcanvas-body {
background-color: var(--background);
color: var(--foreground);
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;
}
/* ================================
Map Tools - Measurement Tooltips
================================ */
.measure-tooltip {
position: relative;
background: rgba(255, 255, 255, 0.95);
border-radius: var(--radius-md);
padding: 6px 10px;
white-space: nowrap;
font-size: 13px;
font-weight: 500;
font-family: var(--font-body);
color: var(--foreground);
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
border: 2px solid var(--primary);
}
.measure-tooltip-static {
background-color: var(--primary);
color: var(--primary-foreground);
border-color: var(--primary);
}
.measure-tooltip-static::before {
border-top-color: var(--primary);
}
/* Arrow pointing to measurement point */
.measure-tooltip::before {
content: '';
position: absolute;
left: -10px;
top: 50%;
transform: translateY(-50%);
border: 5px solid transparent;
border-right-color: var(--primary);
}
/* ================================
Map Tools - Control Bar Styling
================================ */
.map-tools-bar {
position: absolute;
top: 70px;
left: 10px;
z-index: 100;
}
.map-tools-bar .ol-bar {
background: var(--card);
border-radius: var(--radius-lg);
box-shadow: 0 2px 8px rgba(0,0,0,0.12);
border: 2px solid var(--primary);
padding: 4px;
}
.map-tools-bar button {
background: var(--card);
border: none;
min-width: 40px;
min-height: 40px;
border-radius: var(--radius-md);
cursor: pointer;
transition: all 0.15s;
display: flex;
align-items: center;
justify-content: center;
}
.map-tools-bar button:hover {
background: var(--muted);
}
.map-tools-bar button.ol-active,
.map-tools-bar button:active {
background: var(--primary);
}
.map-tools-bar .tool-icon {
font-size: 18px;
}
/* ScaleLine - position above the bottom dock */
.ol-scale-line {
bottom: 76px !important;
left: 10px !important;
}
.ol-scale-line-inner {
border-color: var(--foreground) !important;
color: var(--foreground) !important;
font-family: var(--font-body) !important;
font-size: 11px !important;
}
/* ol-ext Bar overrides */
.ol-control.ol-bar {
padding: 0;
background: transparent;
}
.ol-control.ol-bar .ol-bar {
display: flex;
gap: 2px;
}
.ol-bar.ol-group {
display: flex;
gap: 2px;
}
</style>
</head>
<body>
<div class="app-container">
<!-- Navbar - visible on all screens -->
<nav class="navbar py-2">
<div class="container-fluid">
<span class="navbar-brand mb-0 h1">
<span class="me-2">🌍</span>LUPMIS2 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" style="background-color: var(--primary); color: var(--primary-foreground);" 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 active" type="button" id="dock-btn-add-location" title="Add Location Mode">
<span>📍</span>
<span class="dock-btn-label">Add</span>
</button>
<button class="dock-btn" type="button" id="dock-btn-measure-circle" title="Measure Circle">
<span></span>
<span class="dock-btn-label">Circle</span>
</button>
<button class="dock-btn" type="button" id="dock-btn-measure-line" title="Measure Distance">
<span>📏</span>
<span class="dock-btn-label">Distance</span>
</button>
<button class="dock-btn" type="button" id="dock-btn-measure-area" title="Measure Area">
<span></span>
<span class="dock-btn-label">Area</span>
</button>
<button class="dock-btn" type="button" id="dock-btn-draw" title="Draw / Edit Features">
<span>✏️</span>
<span class="dock-btn-label">Draw</span>
</button>
<button class="dock-btn" type="button" id="dock-btn-clear" title="Clear Measurements">
<span>🗑️</span>
<span class="dock-btn-label">Clear</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>
<!-- Table Content Modal -->
<div class="modal fade" id="tableContentModal" tabindex="-1" aria-labelledby="tableContentModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header bg-primary">
<h5 class="modal-title" id="tableContentModalLabel"><i class="bi bi-table me-2"></i>Table</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-0">
<div id="table-content-body">
<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">
<small class="text-muted me-auto" id="table-content-info"></small>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<!-- Left Offcanvas -->
<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>Tools</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
<button type="button" class="btn btn-outline-primary w-100 mb-3" id="local-data-btn">
<i class="bi bi-database me-2"></i>Local Data
</button>
<div id="local-data-stats" class="d-none">
<div class="card">
<div class="card-header bg-primary py-2">
<h6 class="mb-0"><i class="bi bi-database me-2"></i>Local Database Tables</h6>
</div>
<div class="card-body p-0">
<table class="table table-sm table-striped mb-0">
<thead>
<tr>
<th class="ps-3">Table</th>
<th class="text-end pe-3">Records</th>
</tr>
</thead>
<tbody id="local-data-tbody">
</tbody>
</table>
</div>
</div>
</div>
</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>
<!-- Polyfill crypto.randomUUID for non-secure contexts (HTTP) -->
<!-- Must run before module imports (SQLocal/coincident require it) -->
<script>
if (typeof crypto !== 'undefined' && !crypto.randomUUID) {
crypto.randomUUID = function () {
return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, function (c) {
return (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16);
});
};
}
</script>
<!-- Main application script -->
<script type="module" src="/main.js"></script>
</body>
</html>