Files
estimator/index.html
2025-06-21 14:58:56 -04:00

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">&times;</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>