Add interactive Base 4 track map with Leaflet.js
- Created new track-map page with aerial image and SVG overlay - Implemented custom rotated square markers with obstacle numbers - Added admin edit mode for placing and repositioning markers - Database migration for track_obstacles table - Modal form for adding new obstacles (replaces browser alerts) - Drag-to-reposition functionality with auto-save - Color-coded markers (green/red/black/split) for difficulty levels - Clickable popups showing obstacle details - Added track-map to navigation menu and sitemap - URL rewrite rule for clean /track-map URL
This commit is contained in:
46
src/api/track/get_obstable.php
Normal file
46
src/api/track/get_obstable.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
// /api/track/get_obstacle.php
|
||||
header('Content-Type: text/html; charset=utf-8');
|
||||
|
||||
// Read JSON POST body
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$id = $input['id'] ?? '';
|
||||
|
||||
if (!$id) {
|
||||
http_response_code(400);
|
||||
echo "<h3>Error</h3><p>Invalid obstacle id.</p>";
|
||||
exit;
|
||||
}
|
||||
|
||||
// TODO: Replace this with DB lookup (mysqli) by id.
|
||||
// For demo return stubbed content:
|
||||
$fake = [
|
||||
'obst-camp' => [
|
||||
'title' => 'Base Camp',
|
||||
'img' => '/assets/images/camp.jpg',
|
||||
'difficulty' => 'easy',
|
||||
'desc' => 'Flat campsite with shade and water point.'
|
||||
],
|
||||
'obst-water' => [
|
||||
'title' => 'Water Crossing',
|
||||
'img' => '/assets/images/water.jpg',
|
||||
'difficulty' => 'hard',
|
||||
'desc' => 'Deep crossing after heavy rain, check depth first.'
|
||||
]
|
||||
];
|
||||
|
||||
$data = $fake[$id] ?? null;
|
||||
|
||||
if (!$data) {
|
||||
http_response_code(404);
|
||||
echo "<h3>Not found</h3><p>No details for '{$id}'.</p>";
|
||||
exit;
|
||||
}
|
||||
|
||||
// render HTML snippet for Magnific
|
||||
?>
|
||||
<img src="<?= htmlspecialchars($data['img']) ?>" alt="<?= htmlspecialchars($data['title']) ?>" style="width:100%; height:220px; object-fit:cover; border-radius:6px; margin-bottom:12px;">
|
||||
<h3><?= htmlspecialchars($data['title']) ?></h3>
|
||||
<span class="difficulty-badge <?= htmlspecialchars($data['difficulty']) ?>"><?= htmlspecialchars(ucfirst($data['difficulty'])) ?></span>
|
||||
<div class="description" style="margin-top:10px;"><?= htmlspecialchars($data['desc']) ?></div>
|
||||
<?php
|
||||
660
src/pages/track-map.php
Normal file
660
src/pages/track-map.php
Normal file
@@ -0,0 +1,660 @@
|
||||
<?php
|
||||
$headerStyle = 'light';
|
||||
$rootPath = dirname(dirname(__DIR__));
|
||||
include_once($rootPath . '/header.php');
|
||||
?>
|
||||
|
||||
<!-- Leaflet CSS -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
||||
crossorigin=""/>
|
||||
|
||||
<style>
|
||||
.track-map-section {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.track-info-box {
|
||||
background: #f9f9f9;
|
||||
padding: 30px;
|
||||
margin: 20px auto;
|
||||
max-width: 1200px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.track-info-box h3 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.track-info-box p {
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.legend {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 30px;
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #ddd;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* Shared marker styling for both legend and map obstacles */
|
||||
.legend-marker,
|
||||
.obstacle-marker {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.legend-marker span,
|
||||
.obstacle-marker span {
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
.legend-marker.red,
|
||||
.obstacle-marker.red {
|
||||
background: #e61e25;
|
||||
}
|
||||
|
||||
.legend-marker.green,
|
||||
.obstacle-marker.green {
|
||||
background: #28a745;
|
||||
}
|
||||
|
||||
.legend-marker.black,
|
||||
.obstacle-marker.black {
|
||||
background: #343a40;
|
||||
}
|
||||
|
||||
.legend-marker.split,
|
||||
.obstacle-marker.split {
|
||||
background: linear-gradient(45deg, #e61e25 50%, #28a745 50%);
|
||||
}
|
||||
|
||||
.obstacle-marker {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Leaflet marker container */
|
||||
.custom-marker-container {
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
#map {
|
||||
width: 100%;
|
||||
height: 700px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.obstacle-popup h4 {
|
||||
margin: 0 0 10px 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.obstacle-popup .difficulty-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 10px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.obstacle-popup .difficulty-badge.easy {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.obstacle-popup .difficulty-badge.medium {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.obstacle-popup .difficulty-badge.hard {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.obstacle-popup .difficulty-badge.extreme {
|
||||
background: #d1ecf1;
|
||||
color: #0c5460;
|
||||
}
|
||||
|
||||
.obstacle-popup img {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
margin: 10px 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.obstacle-popup .description {
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.obstacle-marker:hover {
|
||||
transform: rotate(45deg) scale(1.1);
|
||||
}
|
||||
|
||||
.obstacle-marker span {
|
||||
transform: rotate(-45deg);
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
text-shadow: 1px 1px 2px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
/* Obstacle Form Modal */
|
||||
.obstacle-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.5);
|
||||
z-index: 10000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.obstacle-modal.show {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.obstacle-modal-content {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.obstacle-modal-content h3 {
|
||||
margin: 0 0 20px 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.obstacle-modal-content .form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.obstacle-modal-content label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 600;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.obstacle-modal-content input,
|
||||
.obstacle-modal-content select {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.obstacle-modal-content .btn-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.obstacle-modal-content .btn-group button {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.alert-message {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 15px 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
z-index: 10001;
|
||||
display: none;
|
||||
animation: slideIn 0.3s ease;
|
||||
}
|
||||
|
||||
.alert-message.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.alert-message.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.alert-message.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<?php
|
||||
$pageTitle = 'BASE4 4x4 Training Track';
|
||||
$breadcrumbs = [['Home' => 'index.php']];
|
||||
require_once($rootPath . '/components/banner.php');
|
||||
?>
|
||||
|
||||
<!-- Track Map Section -->
|
||||
<section class="track-map-section">
|
||||
<div class="container">
|
||||
<div class="track-info-box">
|
||||
<h3>BASE4 4x4 Training Track</h3>
|
||||
<p>The training track at BASE4 was first created when the property was acquired in 2000. It has since been developed to provide a variety of obstacles and terrain challenges suitable for all skill levels. Open to all members. Join us on our next Driver Training Course to enhance your off-road skills and confidence and put your vehicle to the test.</p>
|
||||
|
||||
<?php if ($role === 'superadmin'): ?>
|
||||
<div style="margin: 20px 0; padding: 15px; background: #fff3cd; border-radius: 8px; border-left: 4px solid #ffc107;">
|
||||
<button id="toggleEditMode" class="btn btn-warning" style="margin-bottom: 10px;">
|
||||
🔧 Enable Edit Mode
|
||||
</button>
|
||||
<p id="editModeStatus" style="margin: 0; color: #856404; font-weight: bold; display: none;">
|
||||
✏️ Edit Mode Active - Click on map to place new markers, drag to reposition
|
||||
</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="legend">
|
||||
<div class="legend-item">
|
||||
<div class="legend-marker green"><span></span></div>
|
||||
<span>Beginner</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-marker red"><span></span></div>
|
||||
<span>Intermediate</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-marker black"><span></span></div>
|
||||
<span>Advanced</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="map"></div>
|
||||
</section>
|
||||
|
||||
<!-- Obstacle Form Modal -->
|
||||
<div id="obstacleModal" class="obstacle-modal">
|
||||
<div class="obstacle-modal-content">
|
||||
<h3>Add New Obstacle</h3>
|
||||
<form id="obstacleForm">
|
||||
<input type="hidden" id="clickedLat" name="clickedLat">
|
||||
<input type="hidden" id="clickedLng" name="clickedLng">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="obstacleNumber">Obstacle Number *</label>
|
||||
<input type="number" id="obstacleNumber" name="obstacleNumber" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="markerColor">Marker Color *</label>
|
||||
<select id="markerColor" name="markerColor" required>
|
||||
<option value="green">Green (Beginner)</option>
|
||||
<option value="red">Red (Intermediate)</option>
|
||||
<option value="black">Black (Advanced)</option>
|
||||
<option value="split">Split (Mixed)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="obstacleName">Name</label>
|
||||
<input type="text" id="obstacleName" name="obstacleName" value="New Obstacle">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="obstacleDifficulty">Difficulty</label>
|
||||
<select id="obstacleDifficulty" name="obstacleDifficulty">
|
||||
<option value="Easy">Easy</option>
|
||||
<option value="Medium" selected>Medium</option>
|
||||
<option value="Hard">Hard</option>
|
||||
<option value="Extreme">Extreme</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-secondary" onclick="closeObstacleModal()">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Save Obstacle</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alert Message -->
|
||||
<div id="alertMessage" class="alert-message"></div>
|
||||
|
||||
<?php
|
||||
require_once($rootPath . '/components/insta_footer.php');
|
||||
?>
|
||||
|
||||
<!-- Leaflet JS -->
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
||||
crossorigin=""></script>
|
||||
|
||||
<!-- Track Map JavaScript -->
|
||||
<script>
|
||||
console.log('Track map script loaded');
|
||||
|
||||
// Check if Leaflet is available
|
||||
if (typeof L === 'undefined') {
|
||||
console.error('Leaflet library not loaded!');
|
||||
} else {
|
||||
console.log('Leaflet library is available, version:', L.version);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
console.log('DOM loaded, initializing map...');
|
||||
|
||||
const mapElement = document.getElementById('map');
|
||||
console.log('Map element:', mapElement);
|
||||
|
||||
if (!mapElement) {
|
||||
console.error('Map element not found!');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Map element dimensions:', mapElement.offsetWidth, 'x', mapElement.offsetHeight);
|
||||
|
||||
try {
|
||||
// Image dimensions: 2876 x 2035 pixels
|
||||
const imageWidth = 7942;
|
||||
const imageHeight = 3913;
|
||||
|
||||
// Create map with simple CRS (pixel coordinates)
|
||||
// Note: Leaflet uses [y, x] format, so bounds are [[0, 0], [height, width]]
|
||||
const bounds = [[0, 0], [imageHeight, imageWidth]];
|
||||
const map = L.map('map', {
|
||||
crs: L.CRS.Simple,
|
||||
minZoom: -1.5,
|
||||
maxZoom: 2,
|
||||
center: [imageHeight / 2, imageWidth / 2],
|
||||
zoom: -1,
|
||||
maxBounds: bounds,
|
||||
maxBoundsViscosity: 1.0
|
||||
});
|
||||
console.log('Map object created with CRS.Simple:', map);
|
||||
|
||||
// Add aerial image overlay
|
||||
const imageUrl = '/assets/images/track-aerial.jpg';
|
||||
L.imageOverlay(imageUrl, bounds).addTo(map);
|
||||
console.log('Aerial image overlay added');
|
||||
|
||||
// Add SVG overlay
|
||||
const svgUrl = '/assets/images/track-route.svg';
|
||||
L.imageOverlay(svgUrl, bounds, {
|
||||
opacity: 1,
|
||||
interactive: false
|
||||
}).addTo(map);
|
||||
console.log('SVG route overlay added');
|
||||
|
||||
// Fit map to image bounds
|
||||
map.fitBounds(bounds);
|
||||
|
||||
console.log('Map initialized successfully');
|
||||
|
||||
// Edit mode state
|
||||
let editMode = false;
|
||||
let markers = [];
|
||||
|
||||
// Edit mode toggle (only for admins)
|
||||
const toggleBtn = document.getElementById('toggleEditMode');
|
||||
const statusText = document.getElementById('editModeStatus');
|
||||
|
||||
if (toggleBtn) {
|
||||
toggleBtn.addEventListener('click', () => {
|
||||
editMode = !editMode;
|
||||
if (editMode) {
|
||||
toggleBtn.textContent = '🔒 Disable Edit Mode';
|
||||
toggleBtn.classList.remove('btn-warning');
|
||||
toggleBtn.classList.add('btn-success');
|
||||
statusText.style.display = 'block';
|
||||
|
||||
// Make existing markers draggable
|
||||
markers.forEach(m => m.marker.dragging.enable());
|
||||
} else {
|
||||
toggleBtn.textContent = '🔧 Enable Edit Mode';
|
||||
toggleBtn.classList.remove('btn-success');
|
||||
toggleBtn.classList.add('btn-warning');
|
||||
statusText.style.display = 'none';
|
||||
|
||||
// Disable dragging
|
||||
markers.forEach(m => m.marker.dragging.disable());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Click on map to add new marker (only in edit mode)
|
||||
map.on('click', (e) => {
|
||||
if (!editMode) return;
|
||||
|
||||
const coords = e.latlng;
|
||||
|
||||
// Store clicked coordinates and show modal
|
||||
document.getElementById('clickedLat').value = coords.lat;
|
||||
document.getElementById('clickedLng').value = coords.lng;
|
||||
document.getElementById('obstacleModal').classList.add('show');
|
||||
});
|
||||
|
||||
// Modal functions
|
||||
window.closeObstacleModal = function() {
|
||||
document.getElementById('obstacleModal').classList.remove('show');
|
||||
document.getElementById('obstacleForm').reset();
|
||||
};
|
||||
|
||||
window.showAlert = function(message, type = 'success') {
|
||||
const alertDiv = document.getElementById('alertMessage');
|
||||
alertDiv.textContent = message;
|
||||
alertDiv.className = 'alert-message ' + type + ' show';
|
||||
|
||||
setTimeout(() => {
|
||||
alertDiv.classList.remove('show');
|
||||
}, 4000);
|
||||
};
|
||||
|
||||
// Handle form submission
|
||||
document.getElementById('obstacleForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const lat = parseFloat(document.getElementById('clickedLat').value);
|
||||
const lng = parseFloat(document.getElementById('clickedLng').value);
|
||||
const obstacleNumber = document.getElementById('obstacleNumber').value;
|
||||
const markerColor = document.getElementById('markerColor').value;
|
||||
const name = document.getElementById('obstacleName').value;
|
||||
const difficulty = document.getElementById('obstacleDifficulty').value;
|
||||
|
||||
// Create temporary marker
|
||||
const markerHtml = `
|
||||
<div class="obstacle-marker ${markerColor}">
|
||||
<span>${obstacleNumber}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const customIcon = L.divIcon({
|
||||
html: markerHtml,
|
||||
className: 'custom-marker-container',
|
||||
iconSize: [40, 40],
|
||||
iconAnchor: [20, 20]
|
||||
});
|
||||
|
||||
const marker = L.marker([lat, lng], {
|
||||
icon: customIcon,
|
||||
draggable: true
|
||||
}).addTo(map);
|
||||
|
||||
// Save to database
|
||||
const obstacleData = {
|
||||
obstacle_number: obstacleNumber,
|
||||
x_position: Math.round(lng),
|
||||
y_position: Math.round(lat),
|
||||
marker_color: markerColor,
|
||||
name: name,
|
||||
difficulty: difficulty,
|
||||
description: 'New obstacle - edit details in admin panel'
|
||||
};
|
||||
|
||||
saveObstacle(obstacleData, marker);
|
||||
closeObstacleModal();
|
||||
});
|
||||
|
||||
function saveObstacle(data, marker) {
|
||||
fetch('/src/processors/track-obstacles.php?action=create', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.status === 'success') {
|
||||
showAlert('Obstacle #' + data.obstacle_number + ' created successfully!', 'success');
|
||||
marker.obstacleId = result.obstacle_id;
|
||||
markers.push({ marker, data: {...data, obstacle_id: result.obstacle_id} });
|
||||
|
||||
// Add dragend event to update position
|
||||
marker.on('dragend', function() {
|
||||
const pos = marker.getLatLng();
|
||||
updateObstaclePosition(marker.obstacleId, Math.round(pos.lng), Math.round(pos.lat));
|
||||
});
|
||||
} else {
|
||||
showAlert('Error: ' + result.message, 'error');
|
||||
map.removeLayer(marker);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showAlert('Error creating obstacle: ' + error, 'error');
|
||||
map.removeLayer(marker);
|
||||
});
|
||||
}
|
||||
|
||||
function updateObstaclePosition(obstacleId, x, y) {
|
||||
fetch('/src/processors/track-obstacles.php?action=updatePosition', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
obstacle_id: obstacleId,
|
||||
x_position: x,
|
||||
y_position: y
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.status === 'success') {
|
||||
showAlert('Position updated', 'success');
|
||||
} else {
|
||||
showAlert('Error updating position: ' + result.message, 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch and add obstacle markers
|
||||
fetch('/src/processors/track-obstacles.php?action=getAll')
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
console.log('Obstacles data:', result);
|
||||
|
||||
if (result.status === 'success' && result.data) {
|
||||
result.data.forEach((obstacle, index) => {
|
||||
// Leaflet uses [y, x] format for coordinates
|
||||
const position = [obstacle.y_position, obstacle.x_position];
|
||||
|
||||
// Create custom marker HTML
|
||||
const markerHtml = `
|
||||
<div class="obstacle-marker ${obstacle.marker_color}">
|
||||
<span>${obstacle.obstacle_number}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Create custom icon
|
||||
const customIcon = L.divIcon({
|
||||
html: markerHtml,
|
||||
className: 'custom-marker-container',
|
||||
iconSize: [40, 40],
|
||||
iconAnchor: [20, 20]
|
||||
});
|
||||
|
||||
// Create popup content
|
||||
const popupContent = `
|
||||
<div class="obstacle-popup">
|
||||
<h4>${obstacle.name}</h4>
|
||||
<span class="difficulty-badge ${obstacle.difficulty.toLowerCase()}">${obstacle.difficulty}</span>
|
||||
${obstacle.image_path ? `<img src="${obstacle.image_path}" alt="${obstacle.name}" style="width: 100%; max-width: 300px; margin: 10px 0; border-radius: 8px;">` : ''}
|
||||
<p>${obstacle.description}</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add marker to map
|
||||
const marker = L.marker(position, {
|
||||
icon: customIcon,
|
||||
draggable: false
|
||||
})
|
||||
.addTo(map)
|
||||
.bindPopup(popupContent, {
|
||||
maxWidth: 350,
|
||||
className: 'obstacle-popup-container'
|
||||
});
|
||||
|
||||
marker.obstacleId = obstacle.obstacle_id;
|
||||
markers.push({ marker, data: obstacle });
|
||||
|
||||
// Add dragend event for position updates
|
||||
marker.on('dragend', function() {
|
||||
const pos = marker.getLatLng();
|
||||
updateObstaclePosition(obstacle.obstacle_id, Math.round(pos.lng), Math.round(pos.lat));
|
||||
});
|
||||
});
|
||||
|
||||
console.log('Added ' + result.data.length + ' obstacle markers');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading obstacles:', error);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error initializing map:', error);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php ob_end_flush(); ?>
|
||||
164
src/processors/track-obstacles.php
Normal file
164
src/processors/track-obstacles.php
Normal file
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
/**
|
||||
* TRACK OBSTACLES API ENDPOINT
|
||||
*
|
||||
* Returns all track obstacles as JSON for the interactive map.
|
||||
*
|
||||
* Usage:
|
||||
* GET /src/processors/track-obstacles.php?action=getAll
|
||||
*
|
||||
* Response:
|
||||
* {
|
||||
* "status": "success",
|
||||
* "data": [
|
||||
* {
|
||||
* "obstacle_id": 1,
|
||||
* "name": "Rock Crawl",
|
||||
* "x_position": 150,
|
||||
* "y_position": 200,
|
||||
* "difficulty": "medium",
|
||||
* "description": "Navigate through rocky terrain...",
|
||||
* "image_path": "assets/images/obstacles/obstacle1.jpg",
|
||||
* "marker_color": "green"
|
||||
* },
|
||||
* ...
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
|
||||
// Set headers for JSON response
|
||||
header('Content-Type: application/json');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
|
||||
// Load configuration and database
|
||||
$rootPath = dirname(dirname(__DIR__));
|
||||
require_once($rootPath . "/src/config/env.php");
|
||||
require_once($rootPath . "/src/config/connection.php");
|
||||
require_once($rootPath . "/src/config/functions.php");
|
||||
require_once($rootPath . "/classes/DatabaseService.php");
|
||||
|
||||
// Get database instance
|
||||
$db = new DatabaseService($conn);
|
||||
|
||||
try {
|
||||
// Get action from query string
|
||||
$action = $_GET['action'] ?? 'getAll';
|
||||
|
||||
if ($action === 'getAll') {
|
||||
// Fetch all obstacles from the database
|
||||
$sql = "SELECT
|
||||
obstacle_id,
|
||||
obstacle_number,
|
||||
name,
|
||||
x_position,
|
||||
y_position,
|
||||
difficulty,
|
||||
description,
|
||||
image_path,
|
||||
marker_color
|
||||
FROM track_obstacles
|
||||
ORDER BY obstacle_id ASC";
|
||||
|
||||
$result = $conn->query($sql);
|
||||
|
||||
if ($result === false) {
|
||||
throw new Exception("Database query failed: " . $conn->error);
|
||||
}
|
||||
|
||||
$obstacles = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$obstacles[] = $row;
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'data' => $obstacles
|
||||
]);
|
||||
|
||||
} elseif ($action === 'create') {
|
||||
// Create new obstacle (superadmin only)
|
||||
$role = getUserRole();
|
||||
if ($role !== 'superadmin') {
|
||||
http_response_code(403);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
$sql = "INSERT INTO track_obstacles
|
||||
(obstacle_number, name, x_position, y_position, difficulty, description, marker_color)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)";
|
||||
|
||||
$insertId = $db->insert($sql, [
|
||||
$input['obstacle_number'],
|
||||
$input['name'],
|
||||
$input['x_position'],
|
||||
$input['y_position'],
|
||||
$input['difficulty'],
|
||||
$input['description'],
|
||||
$input['marker_color']
|
||||
], 'ssiisss');
|
||||
|
||||
if ($insertId) {
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'message' => 'Obstacle created',
|
||||
'obstacle_id' => $insertId
|
||||
]);
|
||||
} else {
|
||||
throw new Exception("Failed to create obstacle: " . $db->getLastError());
|
||||
}
|
||||
|
||||
} elseif ($action === 'updatePosition') {
|
||||
// Update obstacle position (superadmin only)
|
||||
$role = getUserRole();
|
||||
if ($role !== 'superadmin') {
|
||||
http_response_code(403);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
$sql = "UPDATE track_obstacles
|
||||
SET x_position = ?, y_position = ?
|
||||
WHERE obstacle_id = ?";
|
||||
|
||||
$result = $db->update($sql, [
|
||||
$input['x_position'],
|
||||
$input['y_position'],
|
||||
$input['obstacle_id']
|
||||
], 'iii');
|
||||
|
||||
if ($result !== false) {
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'message' => 'Position updated'
|
||||
]);
|
||||
} else {
|
||||
throw new Exception("Failed to update position: " . $db->getLastError());
|
||||
}
|
||||
|
||||
} else {
|
||||
// Invalid action
|
||||
http_response_code(400);
|
||||
echo json_encode([
|
||||
'status' => 'error',
|
||||
'message' => 'Invalid action specified'
|
||||
]);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
// Return error response
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'status' => 'error',
|
||||
'message' => 'Server error: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
|
||||
exit();
|
||||
?>
|
||||
Reference in New Issue
Block a user