685 lines
38 KiB
HTML
685 lines
38 KiB
HTML
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Fence & Perimeter Calculator (Google Maps)</title>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
<!-- Firebase SDK -->
|
|
<script src="https://www.gstatic.com/firebasejs/9.22.0/firebase-app-compat.js"></script>
|
|
<script src="https://www.gstatic.com/firebasejs/9.22.0/firebase-firestore-compat.js"></script>
|
|
<!-- SheetJS (xlsx) for Excel import -->
|
|
<script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
|
|
<style>
|
|
body {
|
|
font-family: 'Inter', sans-serif;
|
|
}
|
|
#map {
|
|
height: 100%;
|
|
}
|
|
.control-button {
|
|
transition: all 0.2s ease-in-out;
|
|
}
|
|
.control-button:hover {
|
|
transform: scale(1.05);
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
}
|
|
.control-button:active {
|
|
transform: scale(0.98);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="bg-gray-50 text-gray-800">
|
|
|
|
<div class="container mx-auto p-4 md:p-8 max-w-7xl">
|
|
<header class="text-center mb-8">
|
|
<h1 class="text-4xl md:text-5xl font-bold text-gray-900">Fence & Perimeter Calculator</h1>
|
|
<p class="text-lg text-gray-600 mt-2">Find an address, draw with arrows, or use the pencil for freehand mapping.</p>
|
|
</header>
|
|
|
|
<!-- Tab Navigation -->
|
|
<div class="flex justify-center mb-8">
|
|
<div class="inline-flex rounded-md shadow-sm" role="group">
|
|
<button id="calculatorTab" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-blue-600 rounded-l-lg hover:bg-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-white">
|
|
Calculator
|
|
</button>
|
|
<button id="materialsTab" class="px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-r-lg hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700">
|
|
Materials Database
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Calculator Section -->
|
|
<div id="calculatorSection" class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
<!-- Left Column: Map and Controls -->
|
|
<div class="lg:col-span-2 bg-white p-6 rounded-2xl shadow-lg flex flex-col">
|
|
<div id="map" class="w-full h-96 md:h-[500px] flex-grow bg-gray-100 rounded-lg border-2 border-gray-200"></div>
|
|
<div class="mt-6 flex flex-col items-center flex-shrink-0">
|
|
<p class="text-gray-600 mb-2">Use the controls below to map your fence line</p>
|
|
<div class="grid grid-cols-3 gap-2 w-48 mb-4">
|
|
<div></div>
|
|
<button id="up" class="control-button bg-blue-500 text-white p-3 rounded-md shadow-md hover:bg-blue-600">↑</button>
|
|
<div></div>
|
|
<button id="left" class="control-button bg-blue-500 text-white p-3 rounded-md shadow-md hover:bg-blue-600">←</button>
|
|
<button id="down" class="control-button bg-blue-500 text-white p-3 rounded-md shadow-md hover:bg-blue-600">↓</button>
|
|
<button id="right" class="control-button bg-blue-500 text-white p-3 rounded-md shadow-md hover:bg-blue-600">→</button>
|
|
</div>
|
|
<div class="flex flex-wrap justify-center gap-4">
|
|
<button id="pencil" class="control-button bg-indigo-500 text-white py-2 px-4 rounded-md shadow-md hover:bg-indigo-600">Pencil</button>
|
|
<button id="place-gate" class="control-button bg-teal-500 text-white py-2 px-4 rounded-md shadow-md hover:bg-teal-600">Place Gate</button>
|
|
<button id="set-start-point" class="control-button bg-purple-500 text-white py-2 px-4 rounded-md shadow-md hover:bg-purple-600">Set Start Point</button>
|
|
<button id="undo" class="control-button bg-yellow-500 text-white py-2 px-4 rounded-md shadow-md hover:bg-yellow-600">Undo</button>
|
|
<button id="reset" class="control-button bg-red-500 text-white py-2 px-4 rounded-md shadow-md hover:bg-red-600">Reset</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right Column: Calculations -->
|
|
<div class="bg-white p-6 rounded-2xl shadow-lg">
|
|
<h2 class="text-2xl font-bold mb-4 border-b pb-2">Tools & Calculations</h2>
|
|
|
|
<div class="mb-6">
|
|
<h3 class="text-lg font-semibold mb-2">Find Address</h3>
|
|
<div class="flex flex-col space-y-2">
|
|
<input type="text" id="address-input" placeholder="Enter an address or place" class="w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-6">
|
|
<h3 class="text-lg font-semibold mb-2">Mapped Perimeter</h3>
|
|
<div class="bg-blue-50 p-4 rounded-lg text-center">
|
|
<span id="perimeterDisplay" class="text-3xl font-bold text-blue-800">0</span>
|
|
<span class="text-gray-600">linear ft.</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-6">
|
|
<h3 class="text-lg font-semibold mb-2">Post Calculation</h3>
|
|
<div class="space-y-2 bg-blue-50 p-4 rounded-lg">
|
|
<div class="flex justify-between items-center">
|
|
<span class="text-gray-600">End Posts:</span>
|
|
<span id="endPosts" class="text-xl font-bold text-blue-800">0</span>
|
|
</div>
|
|
<div class="flex justify-between items-center">
|
|
<span class="text-gray-600">Corner Posts:</span>
|
|
<span id="cornerPosts" class="text-xl font-bold text-blue-800">0</span>
|
|
</div>
|
|
<div class="flex justify-between items-center">
|
|
<span class="text-gray-600">Line Posts:</span>
|
|
<span id="linePosts" class="text-xl font-bold text-blue-800">0</span>
|
|
</div>
|
|
<div class="flex justify-between items-center">
|
|
<span class="text-gray-600">Gate Posts:</span>
|
|
<span id="gatePosts" class="text-xl font-bold text-blue-800">0</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-6">
|
|
<h3 class="text-lg font-semibold mb-2">Gates</h3>
|
|
<div class="space-y-2">
|
|
<label for="gate-width-input" class="block text-sm font-medium text-gray-700">Gate Width (ft)</label>
|
|
<input type="number" id="gate-width-input" value="4" class="w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm">
|
|
<div id="gate-list" class="mt-2 space-y-1 max-h-24 overflow-y-auto"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<h3 class="text-lg font-semibold mb-2">Fence Section Estimation</h3>
|
|
<div class="space-y-4">
|
|
<div>
|
|
<label for="productSelect" class="block text-sm font-medium text-gray-700">Fence Type</label>
|
|
<select id="productSelect" class="mt-1 block w-full py-2 px-3 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
|
<option>Wood Panel</option>
|
|
<option>Chain Link</option>
|
|
<option>Vinyl Panel</option>
|
|
<option>Wrought Iron</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label for="coverageInput" class="block text-sm font-medium text-gray-700">Length per Fence Section (ft.)</label>
|
|
<input type="number" id="coverageInput" value="8" class="mt-1 block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
|
</div>
|
|
<button id="calculateMaterial" class="w-full bg-green-500 text-white py-3 px-4 rounded-md shadow-md hover:bg-green-600 font-bold text-lg control-button">Calculate Sections</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="result" class="mt-6 p-4 bg-green-50 rounded-lg hidden">
|
|
<h4 class="text-lg font-bold text-green-900 mb-2 text-center">Required Materials</h4>
|
|
<div id="result-list" class="space-y-2"></div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Materials Database Section -->
|
|
<div id="materialsSection" class="container mx-auto p-4 md:p-8 max-w-7xl hidden">
|
|
<div class="bg-white p-6 rounded-2xl shadow-lg">
|
|
<h2 class="text-2xl font-bold mb-6 border-b pb-2">Materials Database</h2>
|
|
|
|
<!-- Materials Table with Refresh Button -->
|
|
<div class="mb-8">
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h3 class="text-lg font-semibold">Materials List</h3>
|
|
<button id="refreshMaterials" class="py-2 px-4 bg-indigo-600 text-white rounded-md shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
|
<span class="inline-block mr-1">🔄</span> Refresh Materials
|
|
</button>
|
|
</div>
|
|
<div class="overflow-x-auto">
|
|
<table class="min-w-full bg-white border border-gray-200">
|
|
<thead>
|
|
<tr>
|
|
<th class="py-2 px-4 border-b border-gray-200 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Part Number</th>
|
|
<th class="py-2 px-4 border-b border-gray-200 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Description</th>
|
|
<th class="py-2 px-4 border-b border-gray-200 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Old Style</th>
|
|
<th class="py-2 px-4 border-b border-gray-200 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">New Style</th>
|
|
<th class="py-2 px-4 border-b border-gray-200 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Color</th>
|
|
<th class="py-2 px-4 border-b border-gray-200 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Price ($)</th>
|
|
<th class="py-2 px-4 border-b border-gray-200 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="materialsTableBody">
|
|
<!-- Materials will be loaded here dynamically -->
|
|
<tr>
|
|
<td class="py-4 px-4 border-b border-gray-200 text-sm" colspan="7">Loading materials...</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Excel Import Section -->
|
|
<div class="bg-blue-50 p-6 rounded-lg mb-6">
|
|
<h3 class="text-lg font-semibold mb-4">Import Materials from Excel</h3>
|
|
<p class="text-sm text-gray-600 mb-4">
|
|
Your Excel file should have columns for: Part Number, Description, Old Style, New Style, Color, and Price.
|
|
The first row should be headers.
|
|
</p>
|
|
<div class="flex flex-col space-y-4">
|
|
<input type="file" id="excelFileInput" accept=".xlsx, .xls, .csv" class="block w-full text-sm text-gray-500
|
|
file:mr-4 file:py-2 file:px-4
|
|
file:rounded-md file:border-0
|
|
file:text-sm file:font-semibold
|
|
file:bg-blue-50 file:text-blue-700
|
|
hover:file:bg-blue-100">
|
|
<button id="importExcelBtn" class="py-2 px-4 bg-blue-600 text-white rounded-md shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
|
Import Materials
|
|
</button>
|
|
<div id="importStatus" class="text-sm hidden"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add/Edit Material Form -->
|
|
<div class="bg-gray-50 p-6 rounded-lg">
|
|
<h3 class="text-lg font-semibold mb-4" id="formTitle">Add New Material</h3>
|
|
<form id="materialForm" class="space-y-4">
|
|
<input type="hidden" id="materialId">
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div>
|
|
<label for="partNumber" class="block text-sm font-medium text-gray-700">Part Number</label>
|
|
<input type="text" id="partNumber" class="mt-1 block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" required>
|
|
</div>
|
|
<div>
|
|
<label for="description" class="block text-sm font-medium text-gray-700">Description</label>
|
|
<input type="text" id="description" class="mt-1 block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" required>
|
|
</div>
|
|
<div>
|
|
<label for="oldStyle" class="block text-sm font-medium text-gray-700">Old Style</label>
|
|
<input type="text" id="oldStyle" class="mt-1 block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" required>
|
|
</div>
|
|
<div>
|
|
<label for="newStyle" class="block text-sm font-medium text-gray-700">New Style</label>
|
|
<input type="text" id="newStyle" class="mt-1 block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" required>
|
|
</div>
|
|
<div>
|
|
<label for="color" class="block text-sm font-medium text-gray-700">Color</label>
|
|
<input type="text" id="color" class="mt-1 block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" required>
|
|
</div>
|
|
<div>
|
|
<label for="price" class="block text-sm font-medium text-gray-700">Price ($)</label>
|
|
<input type="number" id="price" step="0.01" min="0" class="mt-1 block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" required>
|
|
</div>
|
|
</div>
|
|
<div class="flex justify-end space-x-3">
|
|
<button type="button" id="cancelEdit" class="py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 hidden">
|
|
Cancel
|
|
</button>
|
|
<button type="submit" class="py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
|
Save Material
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Firebase Configuration
|
|
// ⚠️ IMPORTANT: You MUST replace this with your own Firebase configuration ⚠️
|
|
// The placeholder values below will cause 400 errors when trying to use the database
|
|
//
|
|
// Instructions to get your Firebase config:
|
|
// 1. Go to https://console.firebase.google.com/
|
|
// 2. Click "Add project" or select your existing project
|
|
// 3. After project creation, click the web icon (</>) to add a web app
|
|
// 4. Register your app with a nickname (e.g., "Fence Estimator")
|
|
// 5. Copy the firebaseConfig object shown after registration
|
|
// 6. Replace this entire configuration object with yours
|
|
//
|
|
// If you're seeing 400 errors, click the "Debug Firebase" button at the bottom right
|
|
// of the screen for troubleshooting help.
|
|
const firebaseConfig = {
|
|
apiKey: "AIzaSyAfG-uAp2ePfOntuLUcgUB7rDNbUPi1dro", // ⚠️ REPLACE THIS
|
|
authDomain: "fence-estimator-a8923.firebaseapp.com", // ⚠️ REPLACE THIS
|
|
projectId: "fence-estimator-a8923", // ⚠️ REPLACE THIS
|
|
storageBucket: "fence-estimator-a8923.firebasestorage.app", // ⚠️ REPLACE THIS
|
|
messagingSenderId: "635809080450", // ⚠️ REPLACE THIS
|
|
appId: "1:635809080450:web:4997e21d8df34a6c61809b", // ⚠️ REPLACE THIS
|
|
measurementId: "G-F2E2R3JTMR"
|
|
|
|
};
|
|
|
|
// Declare Firebase variables in global scope and explicitly attach to window
|
|
window.db = null;
|
|
window.materialsCollection = null;
|
|
|
|
// Initialize Firebase with error handling
|
|
try {
|
|
firebase.initializeApp(firebaseConfig);
|
|
window.db = firebase.firestore();
|
|
window.materialsCollection = window.db.collection('materials');
|
|
|
|
// Also set regular variables for backward compatibility
|
|
db = window.db;
|
|
materialsCollection = window.materialsCollection;
|
|
|
|
// Test connection to catch 400 errors early
|
|
db.collection('test').limit(1).get()
|
|
.catch(error => {
|
|
console.error('Firebase connection test failed:', error);
|
|
if (error.code === 'permission-denied') {
|
|
console.error('Firebase permission denied. Check your Firestore rules.');
|
|
} else if (error.message && error.message.includes('400')) {
|
|
console.error('400 Bad Request: Your Firebase configuration is likely invalid.');
|
|
console.error('Make sure you have replaced the placeholder values with your own Firebase config.');
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Firebase initialization error:', error);
|
|
alert('Error initializing Firebase. Check the console for details (F12) or click the Debug Firebase button.');
|
|
}
|
|
|
|
// Materials Database Variables
|
|
let currentEditId = null;
|
|
let materials = [];
|
|
|
|
let map;
|
|
let currentPolyline;
|
|
let drawingManager;
|
|
let isSettingStartPoint = false;
|
|
let isPlacingGate = false;
|
|
let gateMarkers = [];
|
|
|
|
const METERS_TO_FEET = 3.28084;
|
|
const STEP_METERS = 3.048; // Approx 10 feet
|
|
const CORNER_DEGREE_THRESHOLD = 15;
|
|
|
|
function initMap() {
|
|
const chathamON = { lat: 42.4048, lng: -82.1910 };
|
|
|
|
map = new google.maps.Map(document.getElementById("map"), {
|
|
zoom: 18,
|
|
center: chathamON,
|
|
mapTypeId: "satellite",
|
|
disableDefaultUI: true,
|
|
zoomControl: true,
|
|
mapTypeControl: true,
|
|
streetViewControl: true,
|
|
fullscreenControl: true,
|
|
});
|
|
|
|
currentPolyline = new google.maps.Polyline({
|
|
strokeColor: "#0000FF",
|
|
strokeOpacity: 1.0,
|
|
strokeWeight: 3,
|
|
editable: true,
|
|
map: map,
|
|
});
|
|
|
|
drawingManager = new google.maps.drawing.DrawingManager({
|
|
drawingMode: null,
|
|
drawingControl: false,
|
|
polylineOptions: {
|
|
editable: true,
|
|
strokeColor: "#0000FF",
|
|
strokeWeight: 3,
|
|
},
|
|
});
|
|
drawingManager.setMap(map);
|
|
|
|
resetPolyline(map.getCenter());
|
|
setupEventListeners();
|
|
}
|
|
|
|
function listenForPolylineChanges(poly) {
|
|
google.maps.event.addListener(poly.getPath(), 'insert_at', calculateAllMetrics);
|
|
google.maps.event.addListener(poly.getPath(), 'remove_at', calculateAllMetrics);
|
|
google.maps.event.addListener(poly.getPath(), 'set_at', calculateAllMetrics);
|
|
}
|
|
|
|
function resetPolyline(position) {
|
|
const path = currentPolyline.getPath();
|
|
path.clear();
|
|
if (position) {
|
|
path.push(position);
|
|
map.setCenter(position);
|
|
}
|
|
calculateAllMetrics();
|
|
document.getElementById('result').classList.add('hidden');
|
|
gateMarkers.forEach(marker => marker.setMap(null));
|
|
gateMarkers = [];
|
|
renderGateList();
|
|
}
|
|
|
|
function calculateAllMetrics() {
|
|
const path = currentPolyline.getPath();
|
|
const pointsArray = path.getArray();
|
|
let perimeter = 0;
|
|
let cornerPosts = 0;
|
|
let endPosts = 0;
|
|
|
|
if (pointsArray.length >= 2) {
|
|
perimeter = google.maps.geometry.spherical.computeLength(path);
|
|
endPosts = 2; // A line always has a start and an end.
|
|
}
|
|
|
|
if (pointsArray.length > 2) {
|
|
for (let i = 1; i < pointsArray.length - 1; i++) {
|
|
const p0 = pointsArray[i - 1];
|
|
const p1 = pointsArray[i];
|
|
const p2 = pointsArray[i + 1];
|
|
|
|
const heading1 = google.maps.geometry.spherical.computeHeading(p0, p1);
|
|
const heading2 = google.maps.geometry.spherical.computeHeading(p1, p2);
|
|
|
|
const angleChange = 180 - Math.abs(Math.abs(heading1 - heading2) - 180);
|
|
|
|
if (angleChange > CORNER_DEGREE_THRESHOLD) {
|
|
cornerPosts++;
|
|
}
|
|
}
|
|
}
|
|
|
|
const lengthInFeet = (perimeter * METERS_TO_FEET).toFixed(2);
|
|
document.getElementById('perimeterDisplay').textContent = lengthInFeet;
|
|
document.getElementById('endPosts').textContent = endPosts;
|
|
document.getElementById('cornerPosts').textContent = cornerPosts;
|
|
document.getElementById('linePosts').textContent = 0; // Reset line posts
|
|
document.getElementById('gatePosts').textContent = 0; // Reset gate posts
|
|
}
|
|
|
|
function renderGateList() {
|
|
const gateListEl = document.getElementById('gate-list');
|
|
gateListEl.innerHTML = '';
|
|
gateMarkers.forEach(marker => {
|
|
const gateEl = document.createElement('div');
|
|
gateEl.className = 'flex justify-between items-center bg-gray-100 p-1 rounded text-sm';
|
|
gateEl.innerHTML = `
|
|
<span>${marker.gateWidth} ft. Gate</span>
|
|
<button data-id="${marker.gateId}" class="remove-gate text-red-500 hover:text-red-700 font-bold text-lg leading-none p-1">×</button>
|
|
`;
|
|
gateListEl.appendChild(gateEl);
|
|
});
|
|
|
|
document.querySelectorAll('.remove-gate').forEach(button => {
|
|
button.addEventListener('click', (e) => {
|
|
const gateIdToRemove = parseInt(e.target.dataset.id);
|
|
const markerIndex = gateMarkers.findIndex(m => m.gateId === gateIdToRemove);
|
|
if (markerIndex > -1) {
|
|
gateMarkers[markerIndex].setMap(null); // Remove from map
|
|
gateMarkers.splice(markerIndex, 1); // Remove from array
|
|
}
|
|
renderGateList(); // Re-render the list
|
|
document.getElementById('result').classList.add('hidden'); // Hide old results
|
|
});
|
|
});
|
|
}
|
|
|
|
function placeGateOnLine(clickEvent) {
|
|
if (!google.maps.geometry.poly.isLocationOnEdge(clickEvent.latLng, currentPolyline, 1e-4)) {
|
|
alert("Please click directly on the fence line to place a gate.");
|
|
return;
|
|
}
|
|
|
|
const gateWidth = parseFloat(document.getElementById('gate-width-input').value);
|
|
if (isNaN(gateWidth) || gateWidth <= 0) {
|
|
alert("Please enter a valid number for the gate width.");
|
|
return;
|
|
}
|
|
|
|
const gateMarker = new google.maps.Marker({
|
|
position: clickEvent.latLng,
|
|
map: map,
|
|
icon: {
|
|
path: google.maps.SymbolPath.CIRCLE,
|
|
scale: 7,
|
|
fillColor: '#10B981',
|
|
fillOpacity: 1,
|
|
strokeColor: 'white',
|
|
strokeWeight: 2,
|
|
},
|
|
title: `${gateWidth} ft. Gate`
|
|
});
|
|
|
|
gateMarker.gateWidth = gateWidth;
|
|
gateMarker.gateId = Date.now(); // Unique ID for removal
|
|
gateMarkers.push(gateMarker);
|
|
renderGateList();
|
|
document.getElementById('result').classList.add('hidden');
|
|
}
|
|
|
|
function move(direction) {
|
|
const path = currentPolyline.getPath();
|
|
if (path.getLength() === 0) {
|
|
path.push(map.getCenter());
|
|
}
|
|
const lastPoint = path.getAt(path.getLength() - 1);
|
|
let heading;
|
|
|
|
switch (direction) {
|
|
case 'up': heading = 0; break;
|
|
case 'down': heading = 180; break;
|
|
case 'left': heading = 270; break;
|
|
case 'right': heading = 90; break;
|
|
}
|
|
|
|
const newPoint = google.maps.geometry.spherical.computeOffset(lastPoint, STEP_METERS, heading);
|
|
path.push(newPoint);
|
|
map.panTo(newPoint);
|
|
calculateAllMetrics();
|
|
}
|
|
|
|
function setupEventListeners() {
|
|
const resultDiv = document.getElementById('result');
|
|
const resultList = document.getElementById('result-list');
|
|
const setStartPointBtn = document.getElementById('set-start-point');
|
|
const pencilBtn = document.getElementById('pencil');
|
|
const placeGateBtn = document.getElementById('place-gate');
|
|
const addressInput = document.getElementById('address-input');
|
|
|
|
const autocomplete = new google.maps.places.Autocomplete(addressInput, { fields: ["geometry", "name"], types: ["address"] });
|
|
autocomplete.bindTo("bounds", map);
|
|
autocomplete.addListener("place_changed", () => {
|
|
const place = autocomplete.getPlace();
|
|
if (place.geometry && place.geometry.location) {
|
|
resetPolyline(place.geometry.location);
|
|
map.setZoom(19);
|
|
} else {
|
|
alert("Could not find address. Please try again.");
|
|
}
|
|
});
|
|
|
|
google.maps.event.addListener(drawingManager, 'overlaycomplete', function(event) {
|
|
drawingManager.setDrawingMode(null);
|
|
pencilBtn.textContent = 'Pencil';
|
|
pencilBtn.classList.remove('bg-green-600');
|
|
pencilBtn.classList.add('bg-indigo-500');
|
|
|
|
if (event.type === google.maps.drawing.OverlayType.POLYLINE) {
|
|
currentPolyline.setPath([]);
|
|
currentPolyline = event.overlay;
|
|
listenForPolylineChanges(currentPolyline);
|
|
calculateAllMetrics();
|
|
}
|
|
});
|
|
|
|
pencilBtn.addEventListener('click', () => {
|
|
const isDrawing = drawingManager.getDrawingMode() != null;
|
|
if (isDrawing) {
|
|
drawingManager.setDrawingMode(null);
|
|
pencilBtn.textContent = 'Pencil';
|
|
pencilBtn.classList.remove('bg-green-600');
|
|
pencilBtn.classList.add('bg-indigo-500');
|
|
} else {
|
|
drawingManager.setDrawingMode(google.maps.drawing.OverlayType.POLYLINE);
|
|
pencilBtn.textContent = 'Drawing...';
|
|
pencilBtn.classList.add('bg-green-600');
|
|
pencilBtn.classList.remove('bg-indigo-500');
|
|
}
|
|
});
|
|
|
|
placeGateBtn.addEventListener('click', () => {
|
|
isPlacingGate = true;
|
|
map.setOptions({ draggableCursor: 'crosshair' });
|
|
placeGateBtn.textContent = 'Click on line...';
|
|
placeGateBtn.classList.add('bg-green-600');
|
|
placeGateBtn.classList.remove('bg-teal-500');
|
|
});
|
|
|
|
document.getElementById('up').addEventListener('click', () => move('up'));
|
|
document.getElementById('down').addEventListener('click', () => move('down'));
|
|
document.getElementById('left').addEventListener('click', () => move('left'));
|
|
document.getElementById('right').addEventListener('click', () => move('right'));
|
|
|
|
setStartPointBtn.addEventListener('click', () => {
|
|
isSettingStartPoint = true;
|
|
map.setOptions({ draggableCursor: 'crosshair' });
|
|
setStartPointBtn.textContent = 'Click on map...';
|
|
setStartPointBtn.classList.add('bg-green-600');
|
|
setStartPointBtn.classList.remove('bg-purple-500');
|
|
});
|
|
|
|
map.addListener('click', (event) => {
|
|
if (isSettingStartPoint) {
|
|
resetPolyline(event.latLng);
|
|
isSettingStartPoint = false;
|
|
map.setOptions({ draggableCursor: null });
|
|
setStartPointBtn.textContent = 'Set Start Point';
|
|
setStartPointBtn.classList.remove('bg-green-600');
|
|
setStartPointBtn.classList.add('bg-purple-500');
|
|
} else if (isPlacingGate) {
|
|
placeGateOnLine(event);
|
|
isPlacingGate = false;
|
|
map.setOptions({ draggableCursor: null });
|
|
placeGateBtn.textContent = 'Place Gate';
|
|
placeGateBtn.classList.remove('bg-green-600');
|
|
placeGateBtn.classList.add('bg-teal-500');
|
|
}
|
|
});
|
|
|
|
document.getElementById('undo').addEventListener('click', () => {
|
|
const path = currentPolyline.getPath();
|
|
if (path.getLength() > 1) {
|
|
path.pop();
|
|
calculateAllMetrics();
|
|
}
|
|
});
|
|
|
|
document.getElementById('reset').addEventListener('click', () => {
|
|
resetPolyline(map.getCenter());
|
|
});
|
|
|
|
document.getElementById('calculateMaterial').addEventListener('click', () => {
|
|
const perimeter = parseFloat(document.getElementById('perimeterDisplay').textContent);
|
|
const sectionLength = parseFloat(document.getElementById('coverageInput').value);
|
|
const endPosts = parseInt(document.getElementById('endPosts').textContent);
|
|
const cornerPosts = parseInt(document.getElementById('cornerPosts').textContent);
|
|
const productSelect = document.getElementById('productSelect');
|
|
const selectedOption = productSelect.options[productSelect.selectedIndex];
|
|
const productId = productSelect.value;
|
|
|
|
if (isNaN(perimeter) || isNaN(sectionLength) || perimeter <= 0 || sectionLength <= 0) {
|
|
resultDiv.classList.remove('hidden');
|
|
resultDiv.querySelector('p').textContent = "Please map a fence line and set a valid section length.";
|
|
resultDiv.classList.remove('bg-green-50', 'text-green-800');
|
|
resultDiv.classList.add('bg-red-50', 'text-red-800');
|
|
return;
|
|
}
|
|
|
|
const totalGateWidth = gateMarkers.reduce((sum, marker) => sum + marker.gateWidth, 0);
|
|
const fencingPerimeter = Math.max(0, perimeter - totalGateWidth);
|
|
const sectionsNeeded = Math.ceil(fencingPerimeter / sectionLength);
|
|
|
|
const gatePosts = gateMarkers.length * 2;
|
|
const linePosts = Math.max(0, sectionsNeeded - 1 - cornerPosts);
|
|
const totalPosts = endPosts + cornerPosts + linePosts + gatePosts;
|
|
|
|
let gateSummary = gateMarkers.length > 0 ? `<div class="flex justify-between"><span>Gates (${gateMarkers.length}):</span> <span class="font-bold">${totalGateWidth.toFixed(2)} ft. total</span></div>` : '';
|
|
|
|
resultList.innerHTML = `
|
|
<div class="flex justify-between"><span>Fence Panels/Sections:</span> <span class="font-bold">${sectionsNeeded}</span></div>
|
|
${gateSummary}
|
|
<hr class="my-1">
|
|
<div class="flex justify-between"><span>End Posts:</span> <span class="font-bold">${endPosts}</span></div>
|
|
<div class="flex justify-between"><span>Corner Posts:</span> <span class="font-bold">${cornerPosts}</span></div>
|
|
<div class="flex justify-between"><span>Gate Posts:</span> <span class="font-bold">${gatePosts}</span></div>
|
|
<div class="flex justify-between"><span>Line Posts:</span> <span class="font-bold">${linePosts}</span></div>
|
|
<hr class="my-1 border-t-2 border-green-200">
|
|
<div class="flex justify-between text-green-900"><strong>Total Posts:</strong> <strong class="font-bold">${totalPosts}</strong></div>
|
|
`;
|
|
resultDiv.classList.remove('hidden');
|
|
});
|
|
|
|
listenForPolylineChanges(currentPolyline);
|
|
}
|
|
|
|
// Tab Switching
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const calculatorTab = document.getElementById('calculatorTab');
|
|
const materialsTab = document.getElementById('materialsTab');
|
|
const calculatorSection = document.getElementById('calculatorSection');
|
|
const materialsSection = document.getElementById('materialsSection');
|
|
|
|
calculatorTab.addEventListener('click', function() {
|
|
calculatorSection.classList.remove('hidden');
|
|
materialsSection.classList.add('hidden');
|
|
calculatorTab.classList.remove('bg-white', 'text-gray-900');
|
|
calculatorTab.classList.add('bg-blue-600', 'text-white');
|
|
materialsTab.classList.remove('bg-blue-600', 'text-white');
|
|
materialsTab.classList.add('bg-white', 'text-gray-900');
|
|
});
|
|
|
|
materialsTab.addEventListener('click', function() {
|
|
calculatorSection.classList.add('hidden');
|
|
materialsSection.classList.remove('hidden');
|
|
materialsTab.classList.remove('bg-white', 'text-gray-900');
|
|
materialsTab.classList.add('bg-blue-600', 'text-white');
|
|
calculatorTab.classList.remove('bg-blue-600', 'text-white');
|
|
calculatorTab.classList.add('bg-white', 'text-gray-900');
|
|
loadMaterials();
|
|
});
|
|
});
|
|
</script>
|
|
|
|
<!-- Materials Database Script -->
|
|
<script src="materials-db.js"></script>
|
|
|
|
<!-- Firebase Debug Helper -->
|
|
<script src="firebase-debug.js"></script>
|
|
|
|
<!-- Google Maps API -->
|
|
<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyAhN9FDQeQe3CUGR5ZXb7TYCvciu1sfcNw&callback=initMap&libraries=geometry,drawing,places&v=weekly" async defer></script>
|
|
</body>
|
|
</html>
|