commit a39e6f6f920ac712ea704a20aa9fc699de210a51 Author: WovenCoast Date: Mon Dec 8 21:48:03 2025 +0500 it works i think diff --git a/app.js b/app.js new file mode 100644 index 0000000..cde1fed --- /dev/null +++ b/app.js @@ -0,0 +1,1958 @@ +// Initialize map - will be set to saved position or F. Dharanboodhoo, Maldives +const saved = localStorage.getItem('mapMakerData'); +let initialView = [3.0667, 73.1833]; +let initialZoom = 15; +let initialIsSatellite = false; + +if (saved) { + const data = JSON.parse(saved); + if (data.mapView) { + initialView = [data.mapView.center.lat, data.mapView.center.lng]; + initialZoom = data.mapView.zoom; + initialIsSatellite = data.mapView.isSatellite || false; + } +} + +const map = L.map('map', { + maxZoom: 25, + dragging: false // Disable default left-click dragging +}).setView(initialView, initialZoom); + +// Create custom panes for proper z-index ordering +map.createPane('linesPane'); +map.createPane('segmentsPane'); +map.createPane('switchesPane'); +map.createPane('nodesPane'); + +// Set z-index for each pane (higher = on top) +map.getPane('linesPane').style.zIndex = 400; +map.getPane('segmentsPane').style.zIndex = 410; // Above lines, below switches +map.getPane('switchesPane').style.zIndex = 450; +map.getPane('nodesPane').style.zIndex = 500; + +// Enable middle mouse button and right-click dragging +let isPanning = false; +let panStart = null; + +map.getContainer().addEventListener('mousedown', (e) => { + // Middle mouse button OR right-click in select mode + if (e.button === 1 || (e.button === 2 && currentTool === 'select')) { + e.preventDefault(); + isPanning = true; + panStart = { x: e.clientX, y: e.clientY }; + map.getContainer().style.cursor = 'grabbing'; + } +}); + +map.getContainer().addEventListener('mousemove', (e) => { + if (isPanning && panStart) { + const dx = e.clientX - panStart.x; + const dy = e.clientY - panStart.y; + + const center = map.getCenter(); + const zoom = map.getZoom(); + const scale = 256 * Math.pow(2, zoom); + + const newCenter = map.unproject( + map.project(center, zoom).subtract([dx, dy]), + zoom + ); + + map.panTo(newCenter, { animate: false }); + panStart = { x: e.clientX, y: e.clientY }; + } +}); + +map.getContainer().addEventListener('mouseup', (e) => { + if (e.button === 1 || e.button === 2) { + isPanning = false; + panStart = null; + map.getContainer().style.cursor = ''; + } +}); + +map.getContainer().addEventListener('mouseleave', () => { + if (isPanning) { + isPanning = false; + panStart = null; + map.getContainer().style.cursor = ''; + } +}); + +// Base layers +const streetLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap contributors', + maxZoom: 25, + maxNativeZoom: 19 +}); + +const satelliteLayer = L.tileLayer('https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}', { + attribution: '© Google', + maxZoom: 25, + maxNativeZoom: 22 +}); + +// Start with saved view or street view +let isSatelliteView = initialIsSatellite; +if (isSatelliteView) { + satelliteLayer.addTo(map); +} else { + streetLayer.addTo(map); +} + +// Update button text when DOM is ready +window.addEventListener('DOMContentLoaded', () => { + if (isSatelliteView) { + document.getElementById('satelliteToggle').textContent = 'Street View'; + } +}); + +// State management +let currentTool = 'select'; +let currentColor = '#FF0000'; +let drawingLine = false; +let linePoints = []; +let rulerPoints = []; +let selectedObject = null; +let tempMarkers = []; +let selectedSegments = new Set(); // For sum tool +let sumDisplay = null; + +// Storage for all drawn objects +let drawnObjects = { + lines: [], + nodes: [], + switches: [] +}; + +// Layer groups +const layerGroup = L.layerGroup().addTo(map); + +// Tool button handlers +document.querySelectorAll('.tool-btn').forEach(btn => { + btn.addEventListener('click', (e) => { + document.querySelectorAll('.tool-btn').forEach(b => b.classList.remove('active')); + e.target.classList.add('active'); + + const toolId = e.target.id; + if (toolId === 'lineTool') currentTool = 'line'; + else if (toolId === 'nodeTool') currentTool = 'node'; + else if (toolId === 'switchTool') currentTool = 'switch'; + else if (toolId === 'rulerTool') currentTool = 'ruler'; + else if (toolId === 'sumTool') currentTool = 'sum'; + else if (toolId === 'selectTool') currentTool = 'select'; + else if (toolId === 'moveTool') currentTool = 'move'; + else if (toolId === 'deleteTool') currentTool = 'delete'; + + // Reset temporary states + finishLineDrawing(); + rulerPoints = []; + document.getElementById('rulerDisplay').style.display = 'none'; + + // Clear sum selections when switching tools + if (toolId !== 'sumTool') { + clearSumSelections(); + } else { + showSumDisplay(); + } + }); +}); + +// Satellite toggle handler +document.getElementById('satelliteToggle').addEventListener('click', () => { + if (isSatelliteView) { + map.removeLayer(satelliteLayer); + map.addLayer(streetLayer); + document.getElementById('satelliteToggle').textContent = 'Satellite View'; + isSatelliteView = false; + } else { + map.removeLayer(streetLayer); + map.addLayer(satelliteLayer); + document.getElementById('satelliteToggle').textContent = 'Street View'; + isSatelliteView = true; + } + saveToLocalStorage(); +}); + +// Color picker handlers +document.querySelectorAll('.color-btn').forEach(btn => { + btn.addEventListener('click', (e) => { + document.querySelectorAll('.color-btn').forEach(b => b.classList.remove('active')); + e.target.classList.add('active'); + currentColor = e.target.dataset.color; + + // If a node is selected, change its color + if (selectedObject && selectedObject.type === 'node') { + changeNodeColor(selectedObject.data, currentColor); + } + }); +}); + +// Prevent popup clicks from triggering map clicks +map.on('popupopen', (e) => { + const popup = e.popup; + const container = popup.getElement(); + + if (container) { + container.addEventListener('click', (e) => { + e.stopPropagation(); + }); + container.addEventListener('mousedown', (e) => { + e.stopPropagation(); + }); + container.addEventListener('mouseup', (e) => { + e.stopPropagation(); + }); + } +}); + +// Map click handler +map.on('click', (e) => { + const latlng = e.latlng; + + if (currentTool === 'node') { + addNode(latlng, currentColor); + } else if (currentTool === 'switch') { + addSwitch(latlng); + } else if (currentTool === 'line') { + handleLineDrawing(latlng); + } else if (currentTool === 'ruler') { + handleRulerMeasurement(latlng); + } +}); + +// Map right-click to perform current tool action +map.on('contextmenu', (e) => { + L.DomEvent.preventDefault(e); + const latlng = e.latlng; + + if (currentTool === 'select') { + // Allow map panning with right-click in select mode + return; + } else if (currentTool === 'line') { + if (drawingLine) { + finishLineDrawing(); + } else { + handleLineDrawing(latlng); + } + } else if (currentTool === 'node') { + addNode(latlng, currentColor); + } else if (currentTool === 'switch') { + addSwitch(latlng); + } else if (currentTool === 'ruler') { + handleRulerMeasurement(latlng); + } +}); + +// Add node (now a point marker) +function addNode(latlng, color) { + const marker = L.circleMarker(latlng, { + radius: 6, + fillColor: color, + color: '#fff', + weight: 2, + fillOpacity: 1, + pane: 'nodesPane', + interactive: true + }).addTo(layerGroup); + + const nodeData = { + id: Date.now(), + latlng: latlng, + color: color, + layer: marker + }; + + // Track attached lines + marker._attachedLines = []; + marker._nodeData = nodeData; + + updateNodePopup(nodeData); + + marker.on('click', (e) => { + L.DomEvent.stopPropagation(e); + if (currentTool === 'select') { + selectObject(nodeData, 'node'); + } else if (currentTool === 'move') { + // Start dragging the node + const startLatLng = marker.getLatLng(); + let isDragging = false; + + const onMouseMove = (e) => { + isDragging = true; + const newLatLng = e.latlng; + marker.setLatLng(newLatLng); + nodeData.latlng = newLatLng; + + // Update all attached lines + if (marker._attachedLines) { + marker._attachedLines.forEach(lineInfo => { + const { line, pointIndex } = lineInfo; + line.points[pointIndex] = newLatLng; + line.layer.setLatLngs(line.points); + line.markers[pointIndex].setLatLng(newLatLng); + updateLineLabels(line); + }); + } + }; + + const onMouseUp = () => { + map.off('mousemove', onMouseMove); + map.off('mouseup', onMouseUp); + if (isDragging) { + saveToLocalStorage(); + } + }; + + map.on('mousemove', onMouseMove); + map.on('mouseup', onMouseUp); + } else if (currentTool === 'line') { + // Use this node as a line point + handleLineDrawing(marker.getLatLng(), nodeData); + // Auto-finish the line when clicking on a node + if (drawingLine && linePoints.length >= 2) { + finishLineDrawing(); + } + } else if (currentTool === 'delete') { + // Delete the node + if (confirm('Are you sure you want to delete this node?')) { + // Remove all attached lines + if (marker._attachedLines && marker._attachedLines.length > 0) { + const linesToDelete = [...marker._attachedLines]; + linesToDelete.forEach(lineInfo => { + const lineData = lineInfo.line; + layerGroup.removeLayer(lineData.layer); + lineData.markers.forEach(m => layerGroup.removeLayer(m)); + lineData.labels.forEach(l => layerGroup.removeLayer(l)); + lineData.segments.forEach(s => layerGroup.removeLayer(s.layer)); + drawnObjects.lines = drawnObjects.lines.filter(l => l.id !== lineData.id); + }); + } + + // Remove the node + layerGroup.removeLayer(marker); + drawnObjects.nodes = drawnObjects.nodes.filter(n => n.id !== nodeData.id); + updateNodeCount(); + saveToLocalStorage(); + } + } + }); + + marker.on('contextmenu', (e) => { + L.DomEvent.stopPropagation(e); + L.DomEvent.preventDefault(e); + + if (currentTool === 'move') { + // Move the node with right-click in move mode + let isDragging = false; + + const onMouseMove = (e) => { + isDragging = true; + const newLatLng = e.latlng; + marker.setLatLng(newLatLng); + nodeData.latlng = newLatLng; + + if (marker._attachedLines) { + marker._attachedLines.forEach(lineInfo => { + const { line, pointIndex } = lineInfo; + line.points[pointIndex] = newLatLng; + line.layer.setLatLngs(line.points); + line.markers[pointIndex].setLatLng(newLatLng); + updateLineLabels(line); + }); + } + }; + + const onMouseUp = () => { + map.off('mousemove', onMouseMove); + map.off('mouseup', onMouseUp); + if (isDragging) { + saveToLocalStorage(); + } + }; + + map.on('mousemove', onMouseMove); + map.on('mouseup', onMouseUp); + } else { + // Delete the node in other modes + if (confirm('Are you sure you want to delete this node?')) { + layerGroup.removeLayer(marker); + drawnObjects.nodes = drawnObjects.nodes.filter(n => n.id !== nodeData.id); + if (selectedObject && selectedObject.data.id === nodeData.id) { + selectedObject = null; + } + updateNodeCount(); + saveToLocalStorage(); + } + } + }); + + // Make marker draggable in move mode + marker.options.draggable = false; + + drawnObjects.nodes.push(nodeData); + updateNodeCount(); + saveToLocalStorage(); +} + +function updateNodePopup(nodeData) { + const colorOptions = [ + '#FF0000', '#00FF00', '#0000FF', '#FFFF00', + '#FF00FF', '#00FFFF', '#FFA500', '#800080' + ]; + + const colorButtons = colorOptions.map(color => + `` + ).join(''); + + nodeData.layer.bindPopup(` +
+ Node
+
+ Change Color:
+
+ ${colorButtons} +
+
+ +
+ `); +} + +function changeNodeColor(nodeData, newColor) { + nodeData.color = newColor; + nodeData.layer.setStyle({ fillColor: newColor }); + updateNodePopup(nodeData); + updateNodeCount(); + saveToLocalStorage(); +} + +// Add switch +function addSwitch(latlng) { + const ports = prompt("How many ports for this switch?", "8"); + if (!ports || isNaN(ports) || parseInt(ports) < 1) return; + + const portCount = parseInt(ports); + + const marker = L.marker(latlng, { + icon: L.divIcon({ + className: 'switch-marker', + html: `
${portCount}
`, + iconSize: [30, 30], + iconAnchor: [15, 15] + }), + pane: 'switchesPane' + }).addTo(layerGroup); + + const switchData = { + id: Date.now(), + latlng: latlng, + ports: portCount, + layer: marker + }; + + // Track attached lines + marker._attachedLines = []; + marker._switchData = switchData; + marker._maxPorts = portCount; + + updateSwitchPopup(switchData); + + marker.on('click', (e) => { + L.DomEvent.stopPropagation(e); + if (currentTool === 'select') { + selectObject(switchData, 'switch'); + } else if (currentTool === 'move') { + // Close popup for move mode + marker.closePopup(); + + // Start dragging the switch + let isDragging = false; + + const onMouseMove = (e) => { + isDragging = true; + const newLatLng = e.latlng; + marker.setLatLng(newLatLng); + switchData.latlng = newLatLng; + + // Update all attached lines + if (marker._attachedLines) { + marker._attachedLines.forEach(lineInfo => { + const { line, pointIndex } = lineInfo; + line.points[pointIndex] = newLatLng; + line.layer.setLatLngs(line.points); + line.markers[pointIndex].setLatLng(newLatLng); + updateLineLabels(line); + }); + } + }; + + const onMouseUp = () => { + map.off('mousemove', onMouseMove); + map.off('mouseup', onMouseUp); + if (isDragging) { + saveToLocalStorage(); + } + }; + + map.on('mousemove', onMouseMove); + map.on('mouseup', onMouseUp); + } else if (currentTool === 'line') { + // Close popup for line mode + marker.closePopup(); + + // Check port limit + if (marker._attachedLines.length >= marker._maxPorts) { + alert(`This switch only has ${marker._maxPorts} ports and they are all in use!`); + return; + } + handleLineDrawing(marker.getLatLng(), switchData); + // Auto-finish the line when clicking on a switch + if (drawingLine && linePoints.length >= 2) { + finishLineDrawing(); + } + } else if (currentTool === 'delete') { + // Delete the switch + marker.closePopup(); + if (confirm('Are you sure you want to delete this switch?')) { + // Remove all attached lines + if (marker._attachedLines && marker._attachedLines.length > 0) { + const linesToDelete = [...marker._attachedLines]; + linesToDelete.forEach(lineInfo => { + const lineData = lineInfo.line; + layerGroup.removeLayer(lineData.layer); + lineData.markers.forEach(m => layerGroup.removeLayer(m)); + lineData.labels.forEach(l => layerGroup.removeLayer(l)); + lineData.segments.forEach(s => layerGroup.removeLayer(s.layer)); + drawnObjects.lines = drawnObjects.lines.filter(l => l.id !== lineData.id); + }); + } + + // Remove the switch + layerGroup.removeLayer(marker); + drawnObjects.switches = drawnObjects.switches.filter(s => s.id !== switchData.id); + saveToLocalStorage(); + } + } + }); + + marker.on('contextmenu', (e) => { + L.DomEvent.stopPropagation(e); + L.DomEvent.preventDefault(e); + + if (currentTool === 'move') { + // Move the switch with right-click in move mode + marker.closePopup(); + let isDragging = false; + + const onMouseMove = (e) => { + isDragging = true; + const newLatLng = e.latlng; + marker.setLatLng(newLatLng); + switchData.latlng = newLatLng; + + if (marker._attachedLines) { + marker._attachedLines.forEach(lineInfo => { + const { line, pointIndex } = lineInfo; + line.points[pointIndex] = newLatLng; + line.layer.setLatLngs(line.points); + line.markers[pointIndex].setLatLng(newLatLng); + updateLineLabels(line); + }); + } + }; + + const onMouseUp = () => { + map.off('mousemove', onMouseMove); + map.off('mouseup', onMouseUp); + if (isDragging) { + saveToLocalStorage(); + } + }; + + map.on('mousemove', onMouseMove); + map.on('mouseup', onMouseUp); + } else { + // Delete the switch in other modes + if (confirm('Are you sure you want to delete this switch?')) { + layerGroup.removeLayer(marker); + drawnObjects.switches = drawnObjects.switches.filter(s => s.id !== switchData.id); + if (selectedObject && selectedObject.data.id === switchData.id) { + selectedObject = null; + } + saveToLocalStorage(); + } + } + }); + + drawnObjects.switches.push(switchData); + saveToLocalStorage(); +} + +function updateSwitchPopup(switchData) { + const usedPorts = switchData.layer._attachedLines ? switchData.layer._attachedLines.length : 0; + + switchData.layer.bindPopup(` +
+ Network Switch
+
+ Ports: + +
+ Used: ${usedPorts} / ${switchData.ports}
+ Available: ${switchData.ports - usedPorts}
+ +
+ `); +} + +// Global function for popup buttons +window.changeNodeColorFromPopup = function(nodeId, color) { + const node = drawnObjects.nodes.find(n => n.id == nodeId); + if (node) { + changeNodeColor(node, color); + } +}; + +window.updateSwitchPorts = function(switchId) { + const switchObj = drawnObjects.switches.find(s => s.id == switchId); + if (!switchObj) return; + + const newPorts = parseInt(document.getElementById(`editPorts_${switchId}`).value); + const usedPorts = switchObj.layer._attachedLines ? switchObj.layer._attachedLines.length : 0; + + if (isNaN(newPorts) || newPorts < 1) { + alert("Switch must have at least 1 port."); + return; + } + + if (newPorts < usedPorts) { + alert(`Cannot set ports to ${newPorts}. ${usedPorts} ports are currently in use.`); + return; + } + + switchObj.ports = newPorts; + switchObj.layer._maxPorts = newPorts; + + // Update icon + switchObj.layer.setIcon(L.divIcon({ + className: 'switch-marker', + html: `
${newPorts}
`, + iconSize: [30, 30], + iconAnchor: [15, 15] + })); + + updateSwitchPopup(switchObj); + saveToLocalStorage(); +}; + +window.deleteObject = function(objId, type) { + if (!confirm(`Are you sure you want to delete this ${type}?`)) { + return; + } + + if (type === 'node') { + const node = drawnObjects.nodes.find(n => n.id == objId); + if (node) { + layerGroup.removeLayer(node.layer); + drawnObjects.nodes = drawnObjects.nodes.filter(n => n.id != objId); + updateNodeCount(); + saveToLocalStorage(); + } + } else if (type === 'switch') { + const switchObj = drawnObjects.switches.find(s => s.id == objId); + if (switchObj) { + layerGroup.removeLayer(switchObj.layer); + drawnObjects.switches = drawnObjects.switches.filter(s => s.id != objId); + saveToLocalStorage(); + } + } else if (type === 'line') { + const line = drawnObjects.lines.find(l => l.id == objId); + if (line) { + layerGroup.removeLayer(line.layer); + line.markers.forEach(m => { + // Remove from attached nodes/switches + if (m._attachedNode && m._attachedNode.layer) { + m._attachedNode.layer._attachedLines = + m._attachedNode.layer._attachedLines.filter(l => l.line.id !== line.id); + if (m._attachedNode.layer._switchData) { + updateSwitchPopup(m._attachedNode.layer._switchData); + } + } + layerGroup.removeLayer(m); + }); + line.labels.forEach(l => layerGroup.removeLayer(l)); + line.segments.forEach(s => layerGroup.removeLayer(s.layer)); + drawnObjects.lines = drawnObjects.lines.filter(l => l.id != objId); + saveToLocalStorage(); + } + } + + if (selectedObject && selectedObject.data.id == objId) { + selectedObject = null; + } +}; + +// Handle multi-segment line drawing +let attachedNodes = []; // Track which points are attached to nodes + +function handleLineDrawing(latlng, nodeData = null) { + if (!drawingLine) { + // Start new line + drawingLine = true; + linePoints = [latlng]; + attachedNodes = [nodeData]; // Store node reference (can be null) + tempMarkers = []; + + // Create start marker + const marker = L.circleMarker(latlng, { + radius: 5, + fillColor: '#3498db', + color: '#fff', + weight: 2, + fillOpacity: 1, + draggable: false + }).addTo(layerGroup); + + tempMarkers.push(marker); + } else { + // Add point to line + linePoints.push(latlng); + attachedNodes.push(nodeData); // Store node reference for this point + + // Create marker for this point + const marker = L.circleMarker(latlng, { + radius: 5, + fillColor: '#3498db', + color: '#fff', + weight: 2, + fillOpacity: 1, + draggable: false + }).addTo(layerGroup); + + tempMarkers.push(marker); + + // Update or create temporary polyline + if (window.tempPolyline) { + layerGroup.removeLayer(window.tempPolyline); + } + + window.tempPolyline = L.polyline(linePoints, { + color: '#3498db', + weight: 3, + opacity: 0.7 + }).addTo(layerGroup); + } +} + +// Helper function to find nearby node or switch and snap to it +function findNearbyObject(latlng, snapDistance = 20) { + const point = map.latLngToContainerPoint(latlng); + + // Check nodes + for (const node of drawnObjects.nodes) { + const nodePoint = map.latLngToContainerPoint(node.latlng); + const distance = Math.sqrt( + Math.pow(point.x - nodePoint.x, 2) + + Math.pow(point.y - nodePoint.y, 2) + ); + if (distance <= snapDistance) { + return { type: 'node', object: node, latlng: node.latlng }; + } + } + + // Check switches + for (const switchObj of drawnObjects.switches) { + const switchPoint = map.latLngToContainerPoint(switchObj.latlng); + const distance = Math.sqrt( + Math.pow(point.x - switchPoint.x, 2) + + Math.pow(point.y - switchPoint.y, 2) + ); + if (distance <= snapDistance) { + return { type: 'switch', object: switchObj, latlng: switchObj.latlng }; + } + } + + return null; +} + +function finishLineDrawing() { + if (!drawingLine || linePoints.length < 2) { + // Clean up if we didn't make a valid line + tempMarkers.forEach(m => layerGroup.removeLayer(m)); + if (window.tempPolyline) { + layerGroup.removeLayer(window.tempPolyline); + window.tempPolyline = null; + } + tempMarkers = []; + linePoints = []; + attachedNodes = []; + drawingLine = false; + window.extendingLine = null; + return; + } + + // Remove temporary markers and polyline + tempMarkers.forEach(m => layerGroup.removeLayer(m)); + if (window.tempPolyline) { + layerGroup.removeLayer(window.tempPolyline); + window.tempPolyline = null; + } + tempMarkers = []; + + // Snap endpoints to nearby nodes or switches + const firstPoint = linePoints[0]; + const lastPoint = linePoints[linePoints.length - 1]; + + const nearbyFirst = findNearbyObject(firstPoint); + if (nearbyFirst && !attachedNodes[0]) { + linePoints[0] = nearbyFirst.latlng; + if (nearbyFirst.type === 'node') { + attachedNodes[0] = nearbyFirst.object; + } else if (nearbyFirst.type === 'switch') { + // Attach to switch + const usedPorts = nearbyFirst.object.layer._attachedLines ? nearbyFirst.object.layer._attachedLines.length : 0; + if (usedPorts < nearbyFirst.object.ports) { + attachedNodes[0] = nearbyFirst.object; + } + } + } + + const nearbyLast = findNearbyObject(lastPoint); + const lastIndex = linePoints.length - 1; + if (nearbyLast && !attachedNodes[lastIndex]) { + linePoints[lastIndex] = nearbyLast.latlng; + if (nearbyLast.type === 'node') { + attachedNodes[lastIndex] = nearbyLast.object; + } else if (nearbyLast.type === 'switch') { + // Attach to switch + const usedPorts = nearbyLast.object.layer._attachedLines ? nearbyLast.object.layer._attachedLines.length : 0; + if (usedPorts < nearbyLast.ports) { + attachedNodes[lastIndex] = nearbyLast.object; + } + } + } + + // Check if we're extending an existing line + if (window.extendingLine) { + const { lineData, fromStart } = window.extendingLine; + + // Remove the old line + layerGroup.removeLayer(lineData.layer); + lineData.markers.forEach(m => { + if (m._attachedNode && m._attachedNode.layer) { + m._attachedNode.layer._attachedLines = + m._attachedNode.layer._attachedLines.filter(l => l.line.id !== lineData.id); + } + layerGroup.removeLayer(m); + }); + lineData.labels.forEach(l => layerGroup.removeLayer(l)); + lineData.segments.forEach(s => layerGroup.removeLayer(s.layer)); + drawnObjects.lines = drawnObjects.lines.filter(l => l.id !== lineData.id); + + // Create new line with extended points + if (fromStart) { + // Reverse the new points and prepend to existing + linePoints.reverse(); + attachedNodes.reverse(); + } + + createEditableLine(linePoints, attachedNodes); + window.extendingLine = null; + } else { + // Create the final line with draggable endpoints + createEditableLine(linePoints, attachedNodes); + } + + // Reset state + linePoints = []; + attachedNodes = []; + drawingLine = false; +} + +function createEditableLine(points, nodeAttachments = []) { + const polyline = L.polyline(points, { + color: '#3498db', + weight: 3, + pane: 'linesPane' + }).addTo(layerGroup); + + // Calculate total distance + let totalDistance = 0; + for (let i = 0; i < points.length - 1; i++) { + totalDistance += map.distance(points[i], points[i + 1]); + } + + const distanceKm = (totalDistance / 1000).toFixed(2); + const distanceM = totalDistance.toFixed(2); + + // Create draggable endpoint markers + const markers = points.map((point, index) => { + const attachedNode = nodeAttachments[index]; + + const marker = L.marker(point, { + draggable: false, + icon: L.divIcon({ + className: 'endpoint-marker', + html: '
', + iconSize: [12, 12], + iconAnchor: [6, 6] + }), + pane: 'linesPane' + }).addTo(layerGroup); + + // Store reference to line data and attached node + marker._lineData = null; // Will be set after lineData is created + marker._attachedNode = attachedNode; + + marker.on('click', (e) => { + L.DomEvent.stopPropagation(e); + if (currentTool === 'move') { + marker.dragging.enable(); + } else if (currentTool === 'line') { + // Continue the line from this endpoint + const lineData = marker._lineData; + if (!lineData) return; + + // Check if this is the first or last point + const isFirstPoint = index === 0; + const isLastPoint = index === lineData.points.length - 1; + + if (isFirstPoint || isLastPoint) { + // Start drawing from this endpoint + drawingLine = true; + linePoints = [...lineData.points]; + attachedNodes = lineData.markers.map(m => m._attachedNode); + tempMarkers = []; + + // Store the line we're extending + window.extendingLine = { + lineData: lineData, + fromStart: isFirstPoint + }; + } + } else if (currentTool === 'delete') { + // Delete the line endpoint (and entire line if only 2 points) + const lineData = marker._lineData; + if (!lineData) return; + + if (lineData.points.length === 2) { + if (confirm('Are you sure you want to delete this line?')) { + layerGroup.removeLayer(lineData.layer); + lineData.markers.forEach(m => { + if (m._attachedNode && m._attachedNode.layer) { + m._attachedNode.layer._attachedLines = + m._attachedNode.layer._attachedLines.filter(l => l.line.id !== lineData.id); + } + layerGroup.removeLayer(m); + }); + lineData.labels.forEach(l => layerGroup.removeLayer(l)); + lineData.segments.forEach(s => layerGroup.removeLayer(s.layer)); + drawnObjects.lines = drawnObjects.lines.filter(l => l.id !== lineData.id); + saveToLocalStorage(); + } + } else { + if (confirm('Are you sure you want to delete this line point?')) { + // Remove from attached node if exists + if (marker._attachedNode && marker._attachedNode.layer) { + marker._attachedNode.layer._attachedLines = + marker._attachedNode.layer._attachedLines.filter( + info => info.line.id !== lineData.id || info.pointIndex !== index + ); + } + + lineData.points.splice(index, 1); + layerGroup.removeLayer(marker); + lineData.markers.splice(index, 1); + lineData.layer.setLatLngs(lineData.points); + + // Update all marker references + lineData.markers.forEach((m, newIndex) => { + m._lineData = lineData; + if (m._attachedNode && m._attachedNode.layer) { + m._attachedNode.layer._attachedLines = + m._attachedNode.layer._attachedLines.map(info => { + if (info.line.id === lineData.id) { + return { ...info, pointIndex: newIndex }; + } + return info; + }); + } + }); + + updateLineLabels(lineData); + saveToLocalStorage(); + } + } + } + }); + + marker.on('contextmenu', (e) => { + L.DomEvent.stopPropagation(e); + L.DomEvent.preventDefault(e); + + const lineData = marker._lineData; + if (!lineData) return; + + // Remove from attached node if exists + if (marker._attachedNode && marker._attachedNode.layer) { + marker._attachedNode.layer._attachedLines = + marker._attachedNode.layer._attachedLines.filter( + info => info.line.id !== lineData.id || info.pointIndex !== index + ); + } + + // If line has only 2 points, delete the entire line + if (lineData.points.length === 2) { + if (confirm('Are you sure you want to delete this line?')) { + layerGroup.removeLayer(lineData.layer); + lineData.markers.forEach(m => { + if (m._attachedNode && m._attachedNode.layer) { + m._attachedNode.layer._attachedLines = + m._attachedNode.layer._attachedLines.filter(l => l.line.id !== lineData.id); + } + layerGroup.removeLayer(m); + }); + lineData.labels.forEach(l => layerGroup.removeLayer(l)); + lineData.segments.forEach(s => layerGroup.removeLayer(s.layer)); + drawnObjects.lines = drawnObjects.lines.filter(l => l.id !== lineData.id); + saveToLocalStorage(); + } + } else { + // Remove this point from the line + if (confirm('Are you sure you want to delete this line point?')) { + lineData.points.splice(index, 1); + + // Remove this marker + layerGroup.removeLayer(marker); + lineData.markers.splice(index, 1); + + // Update the line + lineData.layer.setLatLngs(lineData.points); + + // Update all marker references + lineData.markers.forEach((m, newIndex) => { + m._lineData = lineData; + // Update attached node references with new index + if (m._attachedNode && m._attachedNode.layer) { + m._attachedNode.layer._attachedLines = + m._attachedNode.layer._attachedLines.map(info => { + if (info.line.id === lineData.id) { + return { ...info, pointIndex: newIndex }; + } + return info; + }); + } + }); + + updateLineLabels(lineData); + saveToLocalStorage(); + } + } + }); + + // Update line when marker is dragged + marker.on('drag', () => { + const lineData = marker._lineData; + if (lineData) { + lineData.points[index] = marker.getLatLng(); + polyline.setLatLngs(lineData.points); + updateLineLabels(lineData); + } + }); + + marker.on('dragend', () => { + marker.dragging.disable(); + saveToLocalStorage(); + }); + + return marker; + }); + + // Add distance labels for each segment + const labels = []; + const segments = []; + for (let i = 0; i < points.length - 1; i++) { + const segmentDistance = map.distance(points[i], points[i + 1]); + const segmentM = segmentDistance.toFixed(2); + + const midpoint = L.latLng( + (points[i].lat + points[i + 1].lat) / 2, + (points[i].lng + points[i + 1].lng) / 2 + ); + + const label = L.marker(midpoint, { + icon: L.divIcon({ + className: 'distance-label', + html: `${segmentM}m`, + iconSize: null + }), + interactive: true, + opacity: 0 + }).addTo(layerGroup); + + // Create segment polyline for sum tool (make it wider for easier hovering) + const segmentLine = L.polyline([points[i], points[i + 1]], { + color: '#3498db', + weight: 10, + opacity: 0, + interactive: true, + pane: 'segmentsPane' + }).addTo(layerGroup); + + const segmentData = { + index: i, + distance: segmentDistance, + layer: segmentLine, + points: [points[i], points[i + 1]], + label: label + }; + + // Add hover handlers to show/hide label + segmentLine.on('mouseover', (e) => { + label.setOpacity(1); + }); + + segmentLine.on('mouseout', (e) => { + label.setOpacity(0); + }); + + // Add click handler to the segment line + segmentLine.on('click', (e) => { + L.DomEvent.stopPropagation(e); + if (currentTool === 'sum') { + toggleSegmentSelection(lineData, segmentData); + } else if (currentTool === 'select') { + selectObject(lineData, 'line'); + } + }); + + // Add click handler to the label as well + label.on('click', (e) => { + L.DomEvent.stopPropagation(e); + if (currentTool === 'sum') { + toggleSegmentSelection(lineData, segmentData); + } else if (currentTool === 'select') { + selectObject(lineData, 'line'); + } + }); + + // Keep label visible when hovering over it + label.on('mouseover', (e) => { + label.setOpacity(1); + }); + + label.on('mouseout', (e) => { + label.setOpacity(0); + }); + + segments.push(segmentData); + labels.push(label); + } + + const lineData = { + id: Date.now(), + points: points, + distance: totalDistance, + layer: polyline, + markers: markers, + labels: labels, + segments: segments + }; + + // Set line data reference in markers and register with attached nodes + markers.forEach((m, i) => { + m._lineData = lineData; + + // If this marker is attached to a node, register the line with that node + if (m._attachedNode && m._attachedNode.layer) { + if (!m._attachedNode.layer._attachedLines) { + m._attachedNode.layer._attachedLines = []; + } + m._attachedNode.layer._attachedLines.push({ + line: lineData, + pointIndex: i + }); + // Update switch popup if attached to a switch + if (m._attachedNode.layer._switchData) { + updateSwitchPopup(m._attachedNode.layer._switchData); + } + } + }); + + polyline.bindPopup(` +
+ Line
+ Total Distance: ${distanceM}m
+ ${distanceKm}km
+ Segments: ${points.length - 1}
+ +
+ `); + + polyline.on('click', (e) => { + L.DomEvent.stopPropagation(e); + if (currentTool === 'select') { + selectObject(lineData, 'line'); + } + }); + + polyline.on('contextmenu', (e) => { + L.DomEvent.stopPropagation(e); + L.DomEvent.preventDefault(e); + // Delete the line + if (confirm('Are you sure you want to delete this line?')) { + layerGroup.removeLayer(polyline); + lineData.markers.forEach(m => { + // Remove from attached nodes + const attachedNode = m._attachedNode; + if (attachedNode) { + attachedNode._attachedLines = attachedNode._attachedLines.filter(l => l !== lineData); + } + layerGroup.removeLayer(m); + }); + lineData.labels.forEach(l => layerGroup.removeLayer(l)); + lineData.segments.forEach(s => layerGroup.removeLayer(s.layer)); + drawnObjects.lines = drawnObjects.lines.filter(l => l.id !== lineData.id); + if (selectedObject && selectedObject.data.id === lineData.id) { + selectedObject = null; + } + saveToLocalStorage(); + } + }); + + // Make polyline draggable in move mode + polyline.on('mousedown', (e) => { + if (currentTool === 'move') { + L.DomEvent.stopPropagation(e); + const startLatLng = e.latlng; + const originalPoints = lineData.points.map(p => ({lat: p.lat, lng: p.lng})); + + const onMouseMove = (e) => { + const latDiff = e.latlng.lat - startLatLng.lat; + const lngDiff = e.latlng.lng - startLatLng.lng; + + lineData.points = originalPoints.map(p => + L.latLng(p.lat + latDiff, p.lng + lngDiff) + ); + + polyline.setLatLngs(lineData.points); + lineData.markers.forEach((marker, i) => { + marker.setLatLng(lineData.points[i]); + }); + updateLineLabels(lineData); + }; + + const onMouseUp = () => { + map.off('mousemove', onMouseMove); + map.off('mouseup', onMouseUp); + saveToLocalStorage(); + }; + + map.on('mousemove', onMouseMove); + map.on('mouseup', onMouseUp); + } + }); + + drawnObjects.lines.push(lineData); + saveToLocalStorage(); +} + +function updateLineLabels(lineData) { + const points = lineData.points; + + // Remove old labels and segments + lineData.labels.forEach(label => layerGroup.removeLayer(label)); + lineData.labels = []; + + if (lineData.segments) { + lineData.segments.forEach(seg => layerGroup.removeLayer(seg.layer)); + } + lineData.segments = []; + + // Calculate new total distance + let totalDistance = 0; + for (let i = 0; i < points.length - 1; i++) { + totalDistance += map.distance(points[i], points[i + 1]); + } + lineData.distance = totalDistance; + + // Create new labels and segments + for (let i = 0; i < points.length - 1; i++) { + const segmentDistance = map.distance(points[i], points[i + 1]); + const segmentM = segmentDistance.toFixed(2); + + const midpoint = L.latLng( + (points[i].lat + points[i + 1].lat) / 2, + (points[i].lng + points[i + 1].lng) / 2 + ); + + const label = L.marker(midpoint, { + icon: L.divIcon({ + className: 'distance-label', + html: `${segmentM}m`, + iconSize: null + }), + interactive: true, + opacity: 0 + }).addTo(layerGroup); + + // Recreate segment polyline for sum tool (make it wider for easier hovering) + const segmentLine = L.polyline([points[i], points[i + 1]], { + color: '#3498db', + weight: 10, + opacity: 0, + interactive: true, + pane: 'segmentsPane' + }).addTo(layerGroup); + + const segmentData = { + index: i, + distance: segmentDistance, + layer: segmentLine, + points: [points[i], points[i + 1]], + label: label + }; + + // Add hover handlers to show/hide label + segmentLine.on('mouseover', (e) => { + label.setOpacity(1); + }); + + segmentLine.on('mouseout', (e) => { + label.setOpacity(0); + }); + + // Add click handler to the segment line + segmentLine.on('click', (e) => { + L.DomEvent.stopPropagation(e); + if (currentTool === 'sum') { + toggleSegmentSelection(lineData, segmentData); + } else if (currentTool === 'select') { + selectObject(lineData, 'line'); + } + }); + + // Add click handler to the label as well + label.on('click', (e) => { + L.DomEvent.stopPropagation(e); + if (currentTool === 'sum') { + toggleSegmentSelection(lineData, segmentData); + } else if (currentTool === 'select') { + selectObject(lineData, 'line'); + } + }); + + // Keep label visible when hovering over it + label.on('mouseover', (e) => { + label.setOpacity(1); + }); + + label.on('mouseout', (e) => { + label.setOpacity(0); + }); + + lineData.segments.push(segmentData); + lineData.labels.push(label); + } + + // Update popup + const distanceKm = (totalDistance / 1000).toFixed(2); + const distanceM = totalDistance.toFixed(2); + lineData.layer.setPopupContent(` +
+ Line
+ Total Distance: ${distanceM}m
+ ${distanceKm}km
+ Segments: ${points.length - 1}
+ +
+ `); +} + +// Handle ruler measurement +function handleRulerMeasurement(latlng) { + rulerPoints.push(latlng); + + if (rulerPoints.length === 1) { + document.getElementById('rulerDisplay').style.display = 'block'; + document.getElementById('rulerDisplay').textContent = 'Click second point...'; + } else if (rulerPoints.length === 2) { + const distance = map.distance(rulerPoints[0], rulerPoints[1]); + const distanceKm = (distance / 1000).toFixed(2); + const distanceM = distance.toFixed(2); + + document.getElementById('rulerDisplay').innerHTML = ` + Distance: ${distanceM}m
+ ${distanceKm}km + `; + + const tempLine = L.polyline(rulerPoints, { + color: '#f39c12', + weight: 2, + dashArray: '5, 10' + }).addTo(layerGroup); + + setTimeout(() => { + layerGroup.removeLayer(tempLine); + rulerPoints = []; + document.getElementById('rulerDisplay').style.display = 'none'; + }, 3000); + } +} + +// Sum tool functions +function toggleSegmentSelection(lineData, segmentData) { + const segmentId = `${lineData.id}-${segmentData.index}`; + + if (selectedSegments.has(segmentId)) { + // Deselect + selectedSegments.delete(segmentId); + segmentData.layer.setStyle({ opacity: 0, weight: 3 }); + } else { + // Select + selectedSegments.add(segmentId); + segmentData.layer.setStyle({ opacity: 0.5, weight: 5, color: '#27ae60' }); + } + + updateSumDisplay(); +} + +function updateSumDisplay() { + if (!sumDisplay) return; + + let totalDistance = 0; + drawnObjects.lines.forEach(line => { + line.segments.forEach(segment => { + const segmentId = `${line.id}-${segment.index}`; + if (selectedSegments.has(segmentId)) { + totalDistance += segment.distance; + } + }); + }); + + const distanceKm = (totalDistance / 1000).toFixed(2); + const distanceM = totalDistance.toFixed(2); + const count = selectedSegments.size; + + sumDisplay.innerHTML = ` + Sum Tool
+ Segments: ${count}
+ Total: ${distanceM}m
+ ${distanceKm}km + `; +} + +function showSumDisplay() { + if (!sumDisplay) { + sumDisplay = document.getElementById('rulerDisplay'); + } + sumDisplay.style.display = 'block'; + updateSumDisplay(); +} + +function clearSumSelections() { + drawnObjects.lines.forEach(line => { + if (line.segments) { + line.segments.forEach(segment => { + segment.layer.setStyle({ opacity: 0, weight: 3, color: '#3498db' }); + }); + } + }); + selectedSegments.clear(); + if (sumDisplay) { + sumDisplay.style.display = 'none'; + } +} + +// Select object +function selectObject(objData, type) { + if (selectedObject) { + // Deselect previous + if (selectedObject.type === 'node') { + selectedObject.data.layer.setStyle({ weight: 2 }); + } else if (selectedObject.type === 'line') { + selectedObject.data.layer.setStyle({ weight: 3 }); + } + } + + selectedObject = { data: objData, type: type }; + + // Highlight selected + if (type === 'node') { + objData.layer.setStyle({ weight: 4 }); + } else if (type === 'line') { + objData.layer.setStyle({ weight: 5 }); + } +} + +// Clear all +// Export button - copy base64 encoded localStorage to clipboard +document.getElementById('exportBtn').addEventListener('click', () => { + const data = localStorage.getItem('mapMakerData'); + console.log('Exporting data:', data); + if (!data) { + alert('No data to export!'); + return; + } + const base64 = btoa(data); + console.log('Base64 encoded:', base64); + + // Try to copy to clipboard + navigator.clipboard.writeText(base64).then(() => { + // Show in prompt as well so user can manually copy if needed + prompt('Data copied to clipboard! You can also copy it manually from here:', base64); + }).catch((err) => { + console.error('Clipboard error:', err); + // Fallback if clipboard API fails + prompt('Copy this export code:', base64); + }); +}); + +// Import button - prompt for base64 string and load it +document.getElementById('importBtn').addEventListener('click', () => { + const base64 = prompt('Paste the export code here:'); + console.log('Import base64:', base64); + if (!base64 || base64.trim() === '') { + console.log('Import cancelled - no data'); + return; + } + + try { + const data = atob(base64.trim()); + console.log('Decoded data:', data); + + // Validate it's valid JSON + const parsed = JSON.parse(data); + console.log('Parsed data:', parsed); + + // Set a flag to prevent beforeunload from saving + window.importingData = true; + + // Clear existing data and set new data + console.log('Clearing localStorage...'); + localStorage.clear(); + console.log('Setting new data...'); + localStorage.setItem('mapMakerData', data); + console.log('Data saved, current localStorage:', localStorage.getItem('mapMakerData')); + + alert('Data imported successfully! The page will now reload.'); + location.reload(); + } catch (e) { + console.error('Import error:', e); + alert('Invalid import code! Please make sure you copied it correctly. Error: ' + e.message); + } +}); + +document.getElementById('clearBtn').addEventListener('click', () => { + if (confirm('Are you sure you want to clear all objects?')) { + layerGroup.clearLayers(); + drawnObjects = { lines: [], nodes: [], switches: [] }; + selectedObject = null; + updateNodeCount(); + saveToLocalStorage(); + } +}); + +// Update node count display +function updateNodeCount() { + const counts = {}; + drawnObjects.nodes.forEach(node => { + counts[node.color] = (counts[node.color] || 0) + 1; + }); + + const countHtml = Object.entries(counts).map(([color, count]) => ` +
+
+
${count}
+
+ `).join(''); + + document.getElementById('nodeCount').innerHTML = countHtml || '
No nodes
'; +} + +// Update stats display +function updateStatsDisplay() { + const statsDisplay = document.getElementById('statsDisplay'); + if (!statsDisplay) return; + + // Calculate total distance of all lines + let totalDistance = 0; + drawnObjects.lines.forEach(line => { + totalDistance += line.distance || 0; + }); + + const distanceKm = (totalDistance / 1000).toFixed(2); + const distanceM = totalDistance.toFixed(2); + const switchCount = drawnObjects.switches.length; + const lineCount = drawnObjects.lines.length; + + statsDisplay.innerHTML = ` + Total Stats
+ Lines: ${lineCount}
+ Switches: ${switchCount}
+ Total Length: ${distanceM}m
+ (${distanceKm}km) + `; +} + +// LocalStorage functions +function saveToLocalStorage() { + const data = { + lines: drawnObjects.lines.map(l => ({ + id: l.id, + points: l.points.map(p => [p.lat, p.lng]), + distance: l.distance + })), + nodes: drawnObjects.nodes.map(n => ({ + id: n.id, + latlng: [n.latlng.lat, n.latlng.lng], + color: n.color + })), + switches: drawnObjects.switches.map(s => ({ + id: s.id, + latlng: [s.latlng.lat, s.latlng.lng], + ports: s.ports + })), + mapView: { + center: map.getCenter(), + zoom: map.getZoom(), + isSatellite: isSatelliteView + } + }; + + localStorage.setItem('mapMakerData', JSON.stringify(data)); + updateStatsDisplay(); +} + +// Helper function to restore a node +function restoreNode(nodeData, latlng) { + const marker = L.circleMarker(latlng, { + radius: 6, + fillColor: nodeData.color, + color: '#fff', + weight: 2, + fillOpacity: 1, + pane: 'nodesPane', + interactive: true + }).addTo(layerGroup); + + const restoredNode = { + id: nodeData.id, + latlng: latlng, + color: nodeData.color, + layer: marker + }; + + marker._attachedLines = []; + marker._nodeData = restoredNode; + + updateNodePopup(restoredNode); + + marker.on('click', (e) => { + L.DomEvent.stopPropagation(e); + if (currentTool === 'select') { + selectObject(restoredNode, 'node'); + } else if (currentTool === 'move') { + let isDragging = false; + + const onMouseMove = (e) => { + isDragging = true; + const newLatLng = e.latlng; + marker.setLatLng(newLatLng); + restoredNode.latlng = newLatLng; + + if (marker._attachedLines) { + marker._attachedLines.forEach(lineInfo => { + const { line, pointIndex } = lineInfo; + line.points[pointIndex] = newLatLng; + line.layer.setLatLngs(line.points); + line.markers[pointIndex].setLatLng(newLatLng); + updateLineLabels(line); + }); + } + }; + + const onMouseUp = () => { + map.off('mousemove', onMouseMove); + map.off('mouseup', onMouseUp); + if (isDragging) { + saveToLocalStorage(); + } + }; + + map.on('mousemove', onMouseMove); + map.on('mouseup', onMouseUp); + } else if (currentTool === 'line') { + handleLineDrawing(marker.getLatLng(), restoredNode); + // Auto-finish the line when clicking on a node + if (drawingLine && linePoints.length >= 2) { + finishLineDrawing(); + } + } + }); + + marker.on('contextmenu', (e) => { + L.DomEvent.stopPropagation(e); + L.DomEvent.preventDefault(e); + + if (currentTool === 'move') { + // Move the node with right-click in move mode + let isDragging = false; + + const onMouseMove = (e) => { + isDragging = true; + const newLatLng = e.latlng; + marker.setLatLng(newLatLng); + restoredNode.latlng = newLatLng; + + if (marker._attachedLines) { + marker._attachedLines.forEach(lineInfo => { + const { line, pointIndex } = lineInfo; + line.points[pointIndex] = newLatLng; + line.layer.setLatLngs(line.points); + line.markers[pointIndex].setLatLng(newLatLng); + updateLineLabels(line); + }); + } + }; + + const onMouseUp = () => { + map.off('mousemove', onMouseMove); + map.off('mouseup', onMouseUp); + if (isDragging) { + saveToLocalStorage(); + } + }; + + map.on('mousemove', onMouseMove); + map.on('mouseup', onMouseUp); + } else { + // Delete the node in other modes + if (confirm('Are you sure you want to delete this node?')) { + layerGroup.removeLayer(marker); + drawnObjects.nodes = drawnObjects.nodes.filter(n => n.id !== restoredNode.id); + if (selectedObject && selectedObject.data.id === restoredNode.id) { + selectedObject = null; + } + updateNodeCount(); + saveToLocalStorage(); + } + } + }); + + drawnObjects.nodes.push(restoredNode); +} + +// Helper function to restore a switch +function restoreSwitch(switchData, latlng) { + const marker = L.marker(latlng, { + icon: L.divIcon({ + className: 'switch-marker', + html: `
${switchData.ports}
`, + iconSize: [30, 30], + iconAnchor: [15, 15] + }), + pane: 'switchesPane' + }).addTo(layerGroup); + + const restoredSwitch = { + id: switchData.id, + latlng: latlng, + ports: switchData.ports, + layer: marker + }; + + marker._attachedLines = []; + marker._switchData = restoredSwitch; + marker._maxPorts = switchData.ports; + + updateSwitchPopup(restoredSwitch); + + marker.on('click', (e) => { + L.DomEvent.stopPropagation(e); + if (currentTool === 'select') { + selectObject(restoredSwitch, 'switch'); + } else if (currentTool === 'move') { + // Close popup for move mode + marker.closePopup(); + + let isDragging = false; + + const onMouseMove = (e) => { + isDragging = true; + const newLatLng = e.latlng; + marker.setLatLng(newLatLng); + restoredSwitch.latlng = newLatLng; + + if (marker._attachedLines) { + marker._attachedLines.forEach(lineInfo => { + const { line, pointIndex } = lineInfo; + line.points[pointIndex] = newLatLng; + line.layer.setLatLngs(line.points); + line.markers[pointIndex].setLatLng(newLatLng); + updateLineLabels(line); + }); + } + }; + + const onMouseUp = () => { + map.off('mousemove', onMouseMove); + map.off('mouseup', onMouseUp); + if (isDragging) { + saveToLocalStorage(); + } + }; + + map.on('mousemove', onMouseMove); + map.on('mouseup', onMouseUp); + } else if (currentTool === 'line') { + // Close popup for line mode + marker.closePopup(); + + if (marker._attachedLines.length >= marker._maxPorts) { + alert(`This switch only has ${marker._maxPorts} ports and they are all in use!`); + return; + } + handleLineDrawing(marker.getLatLng(), restoredSwitch); + // Auto-finish the line when clicking on a switch + if (drawingLine && linePoints.length >= 2) { + finishLineDrawing(); + } + } + }); + + marker.on('contextmenu', (e) => { + L.DomEvent.stopPropagation(e); + L.DomEvent.preventDefault(e); + + if (currentTool === 'move') { + // Move the switch with right-click in move mode + marker.closePopup(); + let isDragging = false; + + const onMouseMove = (e) => { + isDragging = true; + const newLatLng = e.latlng; + marker.setLatLng(newLatLng); + restoredSwitch.latlng = newLatLng; + + if (marker._attachedLines) { + marker._attachedLines.forEach(lineInfo => { + const { line, pointIndex } = lineInfo; + line.points[pointIndex] = newLatLng; + line.layer.setLatLngs(line.points); + line.markers[pointIndex].setLatLng(newLatLng); + updateLineLabels(line); + }); + } + }; + + const onMouseUp = () => { + map.off('mousemove', onMouseMove); + map.off('mouseup', onMouseUp); + if (isDragging) { + saveToLocalStorage(); + } + }; + + map.on('mousemove', onMouseMove); + map.on('mouseup', onMouseUp); + } else { + // Delete the switch in other modes + if (confirm('Are you sure you want to delete this switch?')) { + layerGroup.removeLayer(marker); + drawnObjects.switches = drawnObjects.switches.filter(s => s.id !== restoredSwitch.id); + if (selectedObject && selectedObject.data.id === restoredSwitch.id) { + selectedObject = null; + } + saveToLocalStorage(); + } + } + }); + + drawnObjects.switches.push(restoredSwitch); +} + +function loadFromLocalStorage() { + const saved = localStorage.getItem('mapMakerData'); + if (!saved) return; + + const data = JSON.parse(saved); + + // Map view already restored during initialization + + // Restore nodes + if (data.nodes) { + data.nodes.forEach(nodeData => { + const latlng = L.latLng(nodeData.latlng[0], nodeData.latlng[1]); + restoreNode(nodeData, latlng); + }); + updateNodeCount(); + } + + // Restore switches + if (data.switches) { + data.switches.forEach(switchData => { + const latlng = L.latLng(switchData.latlng[0], switchData.latlng[1]); + restoreSwitch(switchData, latlng); + }); + } + + // Restore lines + if (data.lines) { + data.lines.forEach(lineData => { + const points = lineData.points.map(p => L.latLng(p[0], p[1])); + createEditableLine(points); + }); + } +} + +// Auto-save on map move +map.on('moveend', saveToLocalStorage); +map.on('zoomend', saveToLocalStorage); + +// Load saved data on startup +loadFromLocalStorage(); + +// Initialize stats display +updateStatsDisplay(); + +// Ensure data is saved before page unload (refresh, close, navigate away) +window.addEventListener('beforeunload', (e) => { + // Don't save if we're importing data (would overwrite the imported data) + if (window.importingData) { + return; + } + saveToLocalStorage(); +}); + +// Refresh on Escape key +document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { + e.preventDefault(); + e.stopPropagation(); + location.reload(); + } +}, true); diff --git a/index.html b/index.html new file mode 100644 index 0000000..734127f --- /dev/null +++ b/index.html @@ -0,0 +1,75 @@ + + + + + + Scale Map Maker + + + + +
+
+
+

Tools

+ + + + + + + + +
+ +
+

Line Instructions

+

+ Click to add points.
+ Right-click to finish. +

+
+ +
+

Node Color

+
+ + + + + + + + +
+
+ +
+

Node Count

+
+
+ +
+

Map View

+ +
+ +
+

Actions

+ + + +
+
+ +
+
+
+
+
+
+ + + + + diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..bfa8938 --- /dev/null +++ b/styles.css @@ -0,0 +1,161 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: Arial, sans-serif; + overflow: hidden; +} + +.container { + display: flex; + height: 100vh; +} + +.toolbar { + width: 250px; + background: #2c3e50; + color: white; + padding: 20px; + overflow-y: auto; + z-index: 1000; +} + +.tool-section { + margin-bottom: 30px; +} + +.tool-section h3 { + margin-bottom: 10px; + font-size: 14px; + color: #ecf0f1; +} + +.tool-btn, .action-btn { + display: block; + width: 100%; + padding: 10px; + margin-bottom: 5px; + background: #34495e; + color: white; + border: 2px solid transparent; + cursor: pointer; + border-radius: 4px; + font-size: 14px; +} + +.tool-btn:hover, .action-btn:hover { + background: #3d566e; +} + +.tool-btn.active { + background: #3498db; + border-color: #2980b9; +} + +.action-btn { + background: #e74c3c; +} + +.action-btn:hover { + background: #c0392b; +} + +.color-palette { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 8px; +} + +.color-btn { + width: 100%; + height: 40px; + border: 3px solid transparent; + border-radius: 4px; + cursor: pointer; +} + +.color-btn.active { + border-color: white; + box-shadow: 0 0 10px rgba(255, 255, 255, 0.5); +} + +.map-container { + flex: 1; + position: relative; +} + +#map { + width: 100%; + height: 100%; +} + +.ruler-display { + position: absolute; + top: 10px; + right: 10px; + background: rgba(0, 0, 0, 0.8); + color: white; + padding: 10px 15px; + border-radius: 4px; + font-size: 14px; + display: none; + z-index: 1000; + pointer-events: none; +} + +.stats-display { + position: absolute; + top: 10px; + right: 10px; + background: rgba(0, 0, 0, 0.8); + color: white; + padding: 10px 15px; + border-radius: 4px; + font-size: 14px; + z-index: 1000; + pointer-events: none; +} + +#nodeCount { + font-size: 12px; +} + +.node-count-item { + display: flex; + align-items: center; + margin-bottom: 5px; +} + +.node-count-color { + width: 20px; + height: 20px; + border-radius: 50%; + margin-right: 8px; + border: 2px solid white; +} + +.node-count-text { + color: #ecf0f1; +} + +.leaflet-popup-content { + margin: 8px; + font-size: 13px; +} + +.distance-label { + background: white; + padding: 4px 8px; + border: 2px solid #3498db; + border-radius: 3px; + font-weight: bold; + font-size: 12px; + white-space: nowrap; +} + +.node-marker { + z-index: 1000 !important; +}