Compare commits

..

6 Commits

Author SHA1 Message Date
a4820aab18 bruh....blame claude ._. wtf man
All checks were successful
Build and deploy / Build and Push Docker Images (push) Successful in 3m19s
2025-12-15 01:04:50 +05:00
ebc7f9cb84 fix auto zoom to do it only on new map selections
All checks were successful
Build and deploy / Build and Push Docker Images (push) Successful in 3m9s
2025-12-15 00:25:19 +05:00
4f0ce8744e fix frontend build
All checks were successful
Build and deploy / Build and Push Docker Images (push) Successful in 2m28s
2025-12-14 23:22:32 +05:00
e36e5e8fc5 google map as default and auto zoom
Some checks failed
Build and deploy / Build and Push Docker Images (push) Failing after 39s
2025-12-14 23:19:20 +05:00
57adb221f9 fix issue reconnecting cable after removing old cable: error ports full fix
All checks were successful
Build and deploy / Build and Push Docker Images (push) Successful in 2m17s
2025-12-14 21:30:40 +05:00
7e4bd35f3a snap radius to 2.5 meters and added indicator for snap 2025-12-14 21:25:59 +05:00
5 changed files with 237 additions and 60 deletions

View File

@@ -10,7 +10,8 @@ from app.models.user import User
router = APIRouter(prefix="/api/uploads", tags=["uploads"])
# Storage directory for uploaded images
STORAGE_DIR = Path("/home/shihaam/git/sarlink/mapmaker/storage/images")
# Use relative path from project root to work in both dev and production
STORAGE_DIR = Path(__file__).resolve().parent.parent.parent / "storage" / "images"
STORAGE_DIR.mkdir(parents=True, exist_ok=True)
# Allowed image types

View File

@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
import { useMapEvents, Polyline, Marker } from 'react-leaflet';
import { useMapEvents, Polyline, Marker, Circle } from 'react-leaflet';
import { useDrawingStore } from '../../stores/drawingStore';
import { useUIStore } from '../../stores/uiStore';
import { mapItemService } from '../../services/mapItemService';
@@ -8,9 +8,10 @@ import { CABLE_COLORS, type CableType } from '../../types/mapItem';
interface DrawingHandlerProps {
mapId: string;
onItemCreated: () => void;
refreshTrigger?: number;
}
export function DrawingHandler({ mapId, onItemCreated }: DrawingHandlerProps) {
export function DrawingHandler({ mapId, onItemCreated, refreshTrigger }: DrawingHandlerProps) {
const { activeTool, isDrawing, drawingPoints, setIsDrawing, addDrawingPoint, resetDrawing } =
useDrawingStore();
const { showToast } = useUIStore();
@@ -18,6 +19,8 @@ export function DrawingHandler({ mapId, onItemCreated }: DrawingHandlerProps) {
const [allItems, setAllItems] = useState<any[]>([]);
const [startDeviceId, setStartDeviceId] = useState<string | null>(null);
const [endDeviceId, setEndDeviceId] = useState<string | null>(null);
const [nearbyDevice, setNearbyDevice] = useState<any | null>(null);
const [nearbyFullDevice, setNearbyFullDevice] = useState<any | null>(null);
const isCableTool = ['fiber', 'cat6', 'cat6_poe'].includes(activeTool);
const isDeviceTool = ['switch', 'indoor_ap', 'outdoor_ap', 'other_device', 'info'].includes(activeTool);
@@ -34,10 +37,10 @@ export function DrawingHandler({ mapId, onItemCreated }: DrawingHandlerProps) {
}
};
loadItems();
}, [mapId]);
}, [mapId, refreshTrigger]);
// Find nearby device for snapping (exclude info markers)
const findNearbyDevice = (lat: number, lng: number, radiusMeters = 1): any | null => {
const findNearbyDevice = (lat: number, lng: number, radiusMeters = 2.5): any | null => {
const devices = allItems.filter(item =>
['switch', 'indoor_ap', 'outdoor_ap', 'other_device'].includes(item.type) &&
item.geometry.type === 'Point'
@@ -78,6 +81,10 @@ export function DrawingHandler({ mapId, onItemCreated }: DrawingHandlerProps) {
let { lat, lng } = e.latlng;
let clickedDevice = null;
// Clear snap indicator on click
setNearbyDevice(null);
setNearbyFullDevice(null);
// Check for nearby device when drawing cables
if (isCableTool && map) {
clickedDevice = findNearbyDevice(lat, lng);
@@ -151,7 +158,7 @@ export function DrawingHandler({ mapId, onItemCreated }: DrawingHandlerProps) {
// Wireless mesh - connect AP to AP only
if (isWirelessTool) {
// Must click on an AP
const ap = findNearbyDevice(lat, lng, 1);
const ap = findNearbyDevice(lat, lng, 2.5);
if (!ap || !['indoor_ap', 'outdoor_ap'].includes(ap.type)) {
showToast('Wireless mesh can only connect between Access Points. Please click on an AP.', 'error');
return;
@@ -191,6 +198,40 @@ export function DrawingHandler({ mapId, onItemCreated }: DrawingHandlerProps) {
if (isDrawing && (isCableTool || isWirelessTool)) {
setCursorPosition([e.latlng.lat, e.latlng.lng]);
}
// Check for nearby device to show snap indicator
if ((isCableTool || isWirelessTool) && map) {
const nearby = findNearbyDevice(e.latlng.lat, e.latlng.lng);
// For wireless mesh, only show indicator for APs
if (isWirelessTool) {
if (nearby && ['indoor_ap', 'outdoor_ap'].includes(nearby.type)) {
setNearbyDevice(nearby);
setNearbyFullDevice(null);
} else {
setNearbyDevice(null);
setNearbyFullDevice(null);
}
} else if (isCableTool) {
// For cables, check port availability
if (nearby) {
if (hasAvailablePorts(nearby)) {
setNearbyDevice(nearby);
setNearbyFullDevice(null);
} else {
// Device has full ports - show red indicator
setNearbyDevice(null);
setNearbyFullDevice(nearby);
}
} else {
setNearbyDevice(null);
setNearbyFullDevice(null);
}
}
} else {
setNearbyDevice(null);
setNearbyFullDevice(null);
}
},
contextmenu: async (e) => {
@@ -214,6 +255,8 @@ export function DrawingHandler({ mapId, onItemCreated }: DrawingHandlerProps) {
if (e.key === 'Escape' && isDrawing) {
resetDrawing();
setCursorPosition(null);
setNearbyDevice(null);
setNearbyFullDevice(null);
}
};
@@ -221,6 +264,12 @@ export function DrawingHandler({ mapId, onItemCreated }: DrawingHandlerProps) {
return () => document.removeEventListener('keydown', handleKeyDown);
}, [isDrawing, resetDrawing]);
// Clear nearby device indicator when tool changes
useEffect(() => {
setNearbyDevice(null);
setNearbyFullDevice(null);
}, [activeTool]);
const finishCable = async () => {
if (drawingPoints.length < 2) return;
@@ -244,6 +293,8 @@ export function DrawingHandler({ mapId, onItemCreated }: DrawingHandlerProps) {
setCursorPosition(null);
setStartDeviceId(null);
setEndDeviceId(null);
setNearbyDevice(null);
setNearbyFullDevice(null);
// Reload items
const items = await mapItemService.getMapItems(mapId);
setAllItems(items);
@@ -283,6 +334,8 @@ export function DrawingHandler({ mapId, onItemCreated }: DrawingHandlerProps) {
setCursorPosition(null);
setStartDeviceId(null);
setEndDeviceId(null);
setNearbyDevice(null);
setNearbyFullDevice(null);
// Reload items
const items = await mapItemService.getMapItems(mapId);
setAllItems(items);
@@ -293,50 +346,81 @@ export function DrawingHandler({ mapId, onItemCreated }: DrawingHandlerProps) {
setCursorPosition(null);
setStartDeviceId(null);
setEndDeviceId(null);
setNearbyDevice(null);
setNearbyFullDevice(null);
}
};
// Render drawing preview
if (isDrawing && drawingPoints.length > 0) {
const color = isCableTool
? CABLE_COLORS[activeTool as CableType]
: isWirelessTool
? '#10B981'
: '#6B7280';
// Render drawing preview and snap indicator
const color = isCableTool
? CABLE_COLORS[activeTool as CableType]
: isWirelessTool
? '#10B981'
: '#6B7280';
const dashArray = isWirelessTool ? '10, 10' : undefined;
const dashArray = isWirelessTool ? '10, 10' : undefined;
return (
<>
{/* Main line connecting all points */}
{drawingPoints.length > 1 && (
<Polyline
positions={drawingPoints}
color={color}
weight={3}
dashArray={dashArray}
opacity={0.8}
/>
)}
return (
<>
{/* Green snap indicator - show when hovering near a device with available ports */}
{nearbyDevice && nearbyDevice.geometry.type === 'Point' && (
<Circle
center={[nearbyDevice.geometry.coordinates[1], nearbyDevice.geometry.coordinates[0]]}
radius={3}
pathOptions={{
color: '#10B981',
fillColor: '#10B981',
fillOpacity: 0.2,
weight: 2,
}}
/>
)}
{/* Preview line from last point to cursor */}
{cursorPosition && drawingPoints.length > 0 && (
<Polyline
positions={[drawingPoints[drawingPoints.length - 1], cursorPosition]}
color={color}
weight={3}
dashArray="5, 5"
opacity={0.5}
/>
)}
{/* Red snap indicator - show when hovering near a device with full ports */}
{nearbyFullDevice && nearbyFullDevice.geometry.type === 'Point' && (
<Circle
center={[nearbyFullDevice.geometry.coordinates[1], nearbyFullDevice.geometry.coordinates[0]]}
radius={3}
pathOptions={{
color: '#EF4444',
fillColor: '#EF4444',
fillOpacity: 0.2,
weight: 2,
}}
/>
)}
{/* Markers at each point */}
{drawingPoints.map((point, idx) => (
<Marker key={idx} position={point} />
))}
</>
);
}
{/* Drawing preview - only show when actively drawing */}
{isDrawing && drawingPoints.length > 0 && (
<>
{/* Main line connecting all points */}
{drawingPoints.length > 1 && (
<Polyline
positions={drawingPoints}
color={color}
weight={3}
dashArray={dashArray}
opacity={0.8}
/>
)}
return null;
{/* Preview line from last point to cursor */}
{cursorPosition && drawingPoints.length > 0 && (
<Polyline
positions={[drawingPoints[drawingPoints.length - 1], cursorPosition]}
color={color}
weight={3}
dashArray="5, 5"
opacity={0.5}
/>
)}
{/* Markers at each point */}
{drawingPoints.map((point, idx) => (
<Marker key={idx} position={point} />
))}
</>
)}
</>
);
}

View File

@@ -6,6 +6,7 @@ import { DrawingHandler } from './DrawingHandler';
import { MapItemsLayer } from './MapItemsLayer';
import { ShareDialog } from './ShareDialog';
import { useMapWebSocket } from '../../hooks/useMapWebSocket';
import { mapItemService } from '../../services/mapItemService';
// Fix Leaflet's default icon paths for production builds
// Since we use custom DivIcons, we just need to prevent 404s
@@ -38,6 +39,50 @@ function MapController() {
return null;
}
function AutoZoom({ mapId }: { mapId: string }) {
const map = useMap();
useEffect(() => {
const zoomToDevices = async () => {
try {
const items = await mapItemService.getMapItems(mapId);
// Filter only devices (exclude cables, wireless mesh, and info markers)
const devices = items.filter(item =>
['switch', 'indoor_ap', 'outdoor_ap', 'other_device'].includes(item.type) &&
item.geometry.type === 'Point'
);
if (devices.length === 0) {
// No devices, keep default view
return;
}
// Create bounds from all device coordinates
const bounds = L.latLngBounds(
devices.map(device => {
const coords = (device.geometry as GeoJSON.Point).coordinates;
const [lng, lat] = coords;
return [lat, lng] as [number, number];
})
);
// Fit map to bounds with padding
map.fitBounds(bounds, {
padding: [50, 50],
maxZoom: 18,
});
} catch (error) {
console.error('Failed to auto-zoom to devices:', error);
}
};
zoomToDevices();
}, [mapId, map]);
return null;
}
export function MapView({ mapId, activeLayer, mapLayers, showShareDialog = false, shareMapId, onCloseShareDialog }: MapViewProps) {
const [refreshTrigger, setRefreshTrigger] = useState(0);
@@ -89,6 +134,7 @@ export function MapView({ mapId, activeLayer, mapLayers, showShareDialog = false
style={{ background: '#f0f0f0' }}
>
<MapController />
<AutoZoom mapId={mapId} />
<TileLayer
key={activeLayer}
url={layer.url}
@@ -99,7 +145,7 @@ export function MapView({ mapId, activeLayer, mapLayers, showShareDialog = false
{/* Drawing handler for creating new items - disabled for read-only */}
{permission !== 'read' && (
<DrawingHandler mapId={mapId} onItemCreated={handleItemCreated} />
<DrawingHandler mapId={mapId} onItemCreated={handleItemCreated} refreshTrigger={refreshTrigger} />
)}
{/* Render existing map items */}

View File

@@ -8,12 +8,6 @@ import { LayerSwitcher } from '../components/map/LayerSwitcher';
type MapLayer = 'osm' | 'google' | 'esri';
const MAP_LAYERS = {
osm: {
name: 'OpenStreetMap',
url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
maxZoom: 25,
},
google: {
name: 'Google Satellite',
url: 'https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}',
@@ -21,6 +15,12 @@ const MAP_LAYERS = {
maxZoom: 25,
maxNativeZoom: 22,
},
osm: {
name: 'OpenStreetMap',
url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
maxZoom: 25,
},
esri: {
name: 'ESRI Satellite',
url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
@@ -33,7 +33,7 @@ export function Dashboard() {
const [selectedMapId, setSelectedMapId] = useState<string | null>(null);
const [showShareDialog, setShowShareDialog] = useState(false);
const [shareMapId, setShareMapId] = useState<string | null>(null);
const [activeLayer, setActiveLayer] = useState<MapLayer>('osm');
const [activeLayer, setActiveLayer] = useState<MapLayer>('google');
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const handleShareMap = (mapId: string) => {

View File

@@ -10,6 +10,7 @@ import { Toolbar } from '../components/map/Toolbar';
import { useMapWebSocket } from '../hooks/useMapWebSocket';
import { apiClient } from '../services/api';
import { useUIStore } from '../stores/uiStore';
import { mapItemService } from '../services/mapItemService';
// Fix Leaflet's default icon paths for production builds
// Since we use custom DivIcons, we just need to prevent 404s
@@ -23,12 +24,6 @@ L.Icon.Default.mergeOptions({
type MapLayer = 'osm' | 'google' | 'esri';
const MAP_LAYERS = {
osm: {
name: 'OpenStreetMap',
url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
maxZoom: 25,
},
google: {
name: 'Google Satellite',
url: 'https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}',
@@ -36,6 +31,12 @@ const MAP_LAYERS = {
maxZoom: 25,
maxNativeZoom: 22,
},
osm: {
name: 'OpenStreetMap',
url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
maxZoom: 25,
},
esri: {
name: 'ESRI Satellite',
url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
@@ -56,10 +57,54 @@ function MapController() {
return null;
}
function AutoZoom({ mapId }: { mapId: string }) {
const map = useMap();
useEffect(() => {
const zoomToDevices = async () => {
try {
const items = await mapItemService.getMapItems(mapId);
// Filter only devices (exclude cables, wireless mesh, and info markers)
const devices = items.filter(item =>
['switch', 'indoor_ap', 'outdoor_ap', 'other_device'].includes(item.type) &&
item.geometry.type === 'Point'
);
if (devices.length === 0) {
// No devices, keep default view
return;
}
// Create bounds from all device coordinates
const bounds = L.latLngBounds(
devices.map(device => {
const coords = (device.geometry as GeoJSON.Point).coordinates;
const [lng, lat] = coords;
return [lat, lng] as [number, number];
})
);
// Fit map to bounds with padding
map.fitBounds(bounds, {
padding: [50, 50],
maxZoom: 18,
});
} catch (error) {
console.error('Failed to auto-zoom to devices:', error);
}
};
zoomToDevices();
}, [mapId, map]);
return null;
}
export function SharedMap() {
const { token } = useParams<{ token: string }>();
const { darkMode, toggleDarkMode } = useUIStore();
const [activeLayer, setActiveLayer] = useState<MapLayer>('osm');
const [activeLayer, setActiveLayer] = useState<MapLayer>('google');
const [refreshTrigger, setRefreshTrigger] = useState(0);
const [mapData, setMapData] = useState<any>(null);
const [loading, setLoading] = useState(true);
@@ -232,6 +277,7 @@ export function SharedMap() {
style={{ background: '#f0f0f0' }}
>
<MapController />
<AutoZoom mapId={mapData.id} />
<TileLayer
key={activeLayer}
url={MAP_LAYERS[activeLayer].url}
@@ -242,7 +288,7 @@ export function SharedMap() {
{/* Drawing handler for edit access */}
{!isReadOnly && (
<DrawingHandler mapId={mapData.id} onItemCreated={handleItemCreated} />
<DrawingHandler mapId={mapData.id} onItemCreated={handleItemCreated} refreshTrigger={refreshTrigger} />
)}
{/* Render existing map items */}