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:
@@ -70,6 +70,7 @@ RewriteRule ^instapage$ src/pages/events/instapage.php [L]
|
|||||||
RewriteRule ^about$ src/pages/other/about.php [L]
|
RewriteRule ^about$ src/pages/other/about.php [L]
|
||||||
RewriteRule ^contact$ src/pages/other/contact.php [L]
|
RewriteRule ^contact$ src/pages/other/contact.php [L]
|
||||||
RewriteRule ^privacy_policy$ src/pages/other/privacy_policy.php [L]
|
RewriteRule ^privacy_policy$ src/pages/other/privacy_policy.php [L]
|
||||||
|
RewriteRule ^track-map$ src/pages/track-map.php [L]
|
||||||
RewriteRule ^404$ src/pages/other/404.php [L]
|
RewriteRule ^404$ src/pages/other/404.php [L]
|
||||||
RewriteRule ^account_settings$ src/pages/other/account_settings.php [L]
|
RewriteRule ^account_settings$ src/pages/other/account_settings.php [L]
|
||||||
RewriteRule ^rescue_recovery$ src/pages/other/rescue_recovery.php [L]
|
RewriteRule ^rescue_recovery$ src/pages/other/rescue_recovery.php [L]
|
||||||
|
|||||||
Binary file not shown.
BIN
assets/images/obstacles/01_03.png
Normal file
BIN
assets/images/obstacles/01_03.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.6 MiB |
BIN
assets/images/track-aerial.jpg
Normal file
BIN
assets/images/track-aerial.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 MiB |
115
assets/images/track-route.svg
Normal file
115
assets/images/track-route.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 24 KiB |
13126
assets/images/track-route2.svg
Normal file
13126
assets/images/track-route2.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 2.6 MiB |
66
assets/js/map.js
Normal file
66
assets/js/map.js
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
/**
|
||||||
|
* TRACK MAP WITH LEAFLET.JS
|
||||||
|
*
|
||||||
|
* Basic Leaflet map test
|
||||||
|
*/
|
||||||
|
|
||||||
|
console.log('Track map script loaded2');
|
||||||
|
|
||||||
|
// 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 = 2876;
|
||||||
|
const imageHeight = 2035;
|
||||||
|
|
||||||
|
// 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: -2,
|
||||||
|
maxZoom: 2,
|
||||||
|
center: [imageHeight / 2, imageWidth / 2],
|
||||||
|
zoom: -1
|
||||||
|
});
|
||||||
|
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: 0.8,
|
||||||
|
interactive: false
|
||||||
|
}).addTo(map);
|
||||||
|
console.log('SVG route overlay added');
|
||||||
|
|
||||||
|
// Fit map to image bounds
|
||||||
|
map.fitBounds(bounds);
|
||||||
|
|
||||||
|
console.log('Map initialized successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error initializing map:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
25
docs/migrations/005_create_track_obstacles_table.sql
Normal file
25
docs/migrations/005_create_track_obstacles_table.sql
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- MIGRATION: Create Track Obstacles Table
|
||||||
|
-- Date: 2025-12-12
|
||||||
|
-- Description: Create table to store 4x4 track obstacles with positioning and details
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS track_obstacles (
|
||||||
|
obstacle_id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
name VARCHAR(100) NOT NULL COMMENT 'Obstacle name (e.g., "Rock Crawl", "Water Crossing")',
|
||||||
|
x_position INT NOT NULL COMMENT 'X pixel position on the track map',
|
||||||
|
y_position INT NOT NULL COMMENT 'Y pixel position on the track map',
|
||||||
|
difficulty VARCHAR(20) NOT NULL COMMENT 'Difficulty level (easy, medium, hard, extreme)',
|
||||||
|
description TEXT COMMENT 'Detailed description of the obstacle',
|
||||||
|
image_path VARCHAR(255) COMMENT 'Path to obstacle image (e.g., assets/images/obstacles/obstacle1.jpg)',
|
||||||
|
marker_color VARCHAR(20) NOT NULL COMMENT 'Marker color: red, green, black, or split (red-green)',
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_name (name),
|
||||||
|
INDEX idx_difficulty (difficulty)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- ROLLBACK INSTRUCTIONS (if needed)
|
||||||
|
-- ============================================================================
|
||||||
|
-- DROP TABLE IF EXISTS track_obstacles;
|
||||||
@@ -258,6 +258,7 @@ if ($headerStyle === 'light') {
|
|||||||
<ul class="navigation clearfix">
|
<ul class="navigation clearfix">
|
||||||
<li><a href="index">Home</a></li>
|
<li><a href="index">Home</a></li>
|
||||||
<li><a href="about">About</a></li>
|
<li><a href="about">About</a></li>
|
||||||
|
<li><a href="track-map">Track Map</a></li>
|
||||||
<li><a href="trips">Trips</a>
|
<li><a href="trips">Trips</a>
|
||||||
<?php if ($headerStyle === 'dark'): ?>
|
<?php if ($headerStyle === 'dark'): ?>
|
||||||
<ul>
|
<ul>
|
||||||
|
|||||||
@@ -687,6 +687,10 @@ if (countUpcomingTrips() > 0) { ?>
|
|||||||
<h2>✨ What's New</h2>
|
<h2>✨ What's New</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="updates-modal-body">
|
<div class="updates-modal-body">
|
||||||
|
<div class="update-item">
|
||||||
|
<h3><i class="fas fa-images" style="margin-right: 10px; color: #e90000;"></i>Track Map</h3>
|
||||||
|
<p>Interactive map of the BASE4 4x4 Training Track.</p>
|
||||||
|
</div>
|
||||||
<div class="update-item">
|
<div class="update-item">
|
||||||
<h3><i class="fas fa-images" style="margin-right: 10px; color: #e90000;"></i>Photo Gallery</h3>
|
<h3><i class="fas fa-images" style="margin-right: 10px; color: #e90000;"></i>Photo Gallery</h3>
|
||||||
<p>Explore and share memories from club events and trips. Members can now upload and view photos from past adventures.</p>
|
<p>Explore and share memories from club events and trips. Members can now upload and view photos from past adventures.</p>
|
||||||
|
|||||||
@@ -1,107 +0,0 @@
|
|||||||
<?php
|
|
||||||
// Migration runner - creates membership linking tables if they don't exist
|
|
||||||
require_once __DIR__ . '/vendor/autoload.php';
|
|
||||||
require_once __DIR__ . '/src/config/env.php';
|
|
||||||
require_once __DIR__ . '/src/config/functions.php';
|
|
||||||
|
|
||||||
$conn = openDatabaseConnection();
|
|
||||||
|
|
||||||
if (!$conn) {
|
|
||||||
die("Database connection failed\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "Connected to database successfully.\n\n";
|
|
||||||
|
|
||||||
// Check if membership_links table exists
|
|
||||||
$checkTable = $conn->query("SHOW TABLES LIKE 'membership_links'");
|
|
||||||
if ($checkTable->num_rows > 0) {
|
|
||||||
echo "✓ membership_links table already exists\n";
|
|
||||||
} else {
|
|
||||||
echo "Creating membership_links table...\n";
|
|
||||||
|
|
||||||
$createLink = $conn->query("
|
|
||||||
CREATE TABLE IF NOT EXISTS `membership_links` (
|
|
||||||
`link_id` INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
`primary_user_id` INT NOT NULL,
|
|
||||||
`secondary_user_id` INT NOT NULL,
|
|
||||||
`relationship` VARCHAR(50) NOT NULL DEFAULT 'spouse',
|
|
||||||
`linked_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
|
|
||||||
CONSTRAINT `fk_membership_links_primary` FOREIGN KEY (`primary_user_id`)
|
|
||||||
REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE,
|
|
||||||
CONSTRAINT `fk_membership_links_secondary` FOREIGN KEY (`secondary_user_id`)
|
|
||||||
REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE,
|
|
||||||
|
|
||||||
INDEX `idx_primary_user` (`primary_user_id`),
|
|
||||||
INDEX `idx_secondary_user` (`secondary_user_id`),
|
|
||||||
|
|
||||||
UNIQUE KEY `unique_link` (`primary_user_id`, `secondary_user_id`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
|
||||||
");
|
|
||||||
|
|
||||||
if ($createLink) {
|
|
||||||
echo "✓ membership_links table created successfully\n";
|
|
||||||
} else {
|
|
||||||
echo "✗ Error creating membership_links table: " . $conn->error . "\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if membership_permissions table exists
|
|
||||||
$checkTable = $conn->query("SHOW TABLES LIKE 'membership_permissions'");
|
|
||||||
if ($checkTable->num_rows > 0) {
|
|
||||||
echo "✓ membership_permissions table already exists\n";
|
|
||||||
} else {
|
|
||||||
echo "Creating membership_permissions table...\n";
|
|
||||||
|
|
||||||
$createPerm = $conn->query("
|
|
||||||
CREATE TABLE IF NOT EXISTS `membership_permissions` (
|
|
||||||
`permission_id` INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
`link_id` INT NOT NULL,
|
|
||||||
`permission_name` VARCHAR(100) NOT NULL,
|
|
||||||
`granted_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
|
|
||||||
CONSTRAINT `fk_membership_permissions_link` FOREIGN KEY (`link_id`)
|
|
||||||
REFERENCES `membership_links`(`link_id`) ON DELETE CASCADE ON UPDATE CASCADE,
|
|
||||||
|
|
||||||
INDEX `idx_link` (`link_id`),
|
|
||||||
|
|
||||||
UNIQUE KEY `unique_permission` (`link_id`, `permission_name`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
|
||||||
");
|
|
||||||
|
|
||||||
if ($createPerm) {
|
|
||||||
echo "✓ membership_permissions table created successfully\n";
|
|
||||||
} else {
|
|
||||||
echo "✗ Error creating membership_permissions table: " . $conn->error . "\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create or replace the view
|
|
||||||
echo "\nCreating linked_membership_users view...\n";
|
|
||||||
$createView = $conn->query("
|
|
||||||
CREATE OR REPLACE VIEW `linked_membership_users` AS
|
|
||||||
SELECT
|
|
||||||
primary_user_id,
|
|
||||||
secondary_user_id,
|
|
||||||
relationship,
|
|
||||||
linked_at
|
|
||||||
FROM membership_links
|
|
||||||
UNION ALL
|
|
||||||
SELECT
|
|
||||||
primary_user_id,
|
|
||||||
primary_user_id as secondary_user_id,
|
|
||||||
'primary' as relationship,
|
|
||||||
linked_at
|
|
||||||
FROM membership_links
|
|
||||||
");
|
|
||||||
|
|
||||||
if ($createView) {
|
|
||||||
echo "✓ View created successfully\n";
|
|
||||||
} else {
|
|
||||||
echo "✗ Error creating view: " . $conn->error . "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
$conn->close();
|
|
||||||
echo "\n✓ Migration completed successfully!\n";
|
|
||||||
?>
|
|
||||||
@@ -15,6 +15,11 @@
|
|||||||
<lastmod>2025-04-10T11:24:41+00:00</lastmod>
|
<lastmod>2025-04-10T11:24:41+00:00</lastmod>
|
||||||
<priority>0.80</priority>
|
<priority>0.80</priority>
|
||||||
</url>
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://4wdcsa.co.za/track-map</loc>
|
||||||
|
<lastmod>2025-12-12T00:00:00+00:00</lastmod>
|
||||||
|
<priority>0.80</priority>
|
||||||
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://4wdcsa.co.za/trips.php</loc>
|
<loc>https://4wdcsa.co.za/trips.php</loc>
|
||||||
<lastmod>2025-04-10T11:24:41+00:00</lastmod>
|
<lastmod>2025-04-10T11:24:41+00:00</lastmod>
|
||||||
|
|||||||
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