diff --git a/public/src/components/map/MapView.tsx b/public/src/components/map/MapView.tsx
index acd58b0..5b16df0 100644
--- a/public/src/components/map/MapView.tsx
+++ b/public/src/components/map/MapView.tsx
@@ -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,49 @@ function MapController() {
return null;
}
+function AutoZoom({ mapId, refreshTrigger }: { mapId: string; refreshTrigger: number }) {
+ 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 [lng, lat] = device.geometry.coordinates;
+ 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, refreshTrigger, map]);
+
+ return null;
+}
+
export function MapView({ mapId, activeLayer, mapLayers, showShareDialog = false, shareMapId, onCloseShareDialog }: MapViewProps) {
const [refreshTrigger, setRefreshTrigger] = useState(0);
@@ -89,6 +133,7 @@ export function MapView({ mapId, activeLayer, mapLayers, showShareDialog = false
style={{ background: '#f0f0f0' }}
>
+
OpenStreetMap 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: '© OpenStreetMap 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(null);
const [showShareDialog, setShowShareDialog] = useState(false);
const [shareMapId, setShareMapId] = useState(null);
- const [activeLayer, setActiveLayer] = useState('osm');
+ const [activeLayer, setActiveLayer] = useState('google');
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const handleShareMap = (mapId: string) => {
diff --git a/public/src/pages/SharedMap.tsx b/public/src/pages/SharedMap.tsx
index 32d5e69..eb25da2 100644
--- a/public/src/pages/SharedMap.tsx
+++ b/public/src/pages/SharedMap.tsx
@@ -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: '© OpenStreetMap 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: '© OpenStreetMap 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,53 @@ function MapController() {
return null;
}
+function AutoZoom({ mapId, refreshTrigger }: { mapId: string; refreshTrigger: number }) {
+ 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 [lng, lat] = device.geometry.coordinates;
+ 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, refreshTrigger, map]);
+
+ return null;
+}
+
export function SharedMap() {
const { token } = useParams<{ token: string }>();
const { darkMode, toggleDarkMode } = useUIStore();
- const [activeLayer, setActiveLayer] = useState('osm');
+ const [activeLayer, setActiveLayer] = useState('google');
const [refreshTrigger, setRefreshTrigger] = useState(0);
const [mapData, setMapData] = useState(null);
const [loading, setLoading] = useState(true);
@@ -232,6 +276,7 @@ export function SharedMap() {
style={{ background: '#f0f0f0' }}
>
+