mirror of
https://github.com/ajvpot/meshexplorer.git
synced 2026-03-28 17:42:58 +01:00
save map position, stroke width slider
This commit is contained in:
@@ -207,4 +207,62 @@ html {
|
||||
font-weight: 500;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.07);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Range slider styles */
|
||||
.slider {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.slider::-webkit-slider-track {
|
||||
background: #e5e7eb;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.dark .slider::-webkit-slider-track {
|
||||
background: #4b5563;
|
||||
}
|
||||
|
||||
.slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background: #2563eb;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #ffffff;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.slider::-webkit-slider-thumb:hover {
|
||||
background: #1d4ed8;
|
||||
}
|
||||
|
||||
.slider::-moz-range-track {
|
||||
background: #e5e7eb;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.dark .slider::-moz-range-track {
|
||||
background: #4b5563;
|
||||
}
|
||||
|
||||
.slider::-moz-range-thumb {
|
||||
background: #2563eb;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #ffffff;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.slider::-moz-range-thumb:hover {
|
||||
background: #1d4ed8;
|
||||
}
|
||||
@@ -147,6 +147,30 @@ export default function MapLayerSettingsComponent({ onSettingsChange }: MapLayer
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">Show all neighbors</span>
|
||||
</label>
|
||||
|
||||
{/* Path stroke width */}
|
||||
<div className="mb-3">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Path stroke width
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min="1"
|
||||
max="8"
|
||||
step="1"
|
||||
value={settings.strokeWidth}
|
||||
onChange={(e) => updateSetting('strokeWidth', parseInt(e.target.value))}
|
||||
className="w-full h-2 bg-gray-200 dark:bg-neutral-600 rounded-lg appearance-none cursor-pointer slider"
|
||||
/>
|
||||
<div className="flex justify-between text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
<span>1px</span>
|
||||
<span className="font-medium">{settings.strokeWidth}px</span>
|
||||
<span>8px</span>
|
||||
</div>
|
||||
<p className="text-xs mt-1 text-gray-500 dark:text-gray-400">
|
||||
Controls the thickness of neighbor connection lines
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Use colors - indented sub-option */}
|
||||
<label className="flex items-center gap-2 ml-6 cursor-pointer">
|
||||
<input
|
||||
|
||||
@@ -18,12 +18,7 @@ import { NodePosition } from "@/types/map";
|
||||
import { useNeighbors, type Neighbor } from "@/hooks/useNeighbors";
|
||||
import { type AllNeighborsConnection } from "@/hooks/useAllNeighbors";
|
||||
import { useQueryParams } from "@/hooks/useQueryParams";
|
||||
|
||||
const DEFAULT = {
|
||||
lat: 46.56, // Center between Seattle and Portland
|
||||
lng: -122.51,
|
||||
zoom: 7, // Zoom level to show both cities
|
||||
};
|
||||
import { useMapPosition } from "@/hooks/useMapPosition";
|
||||
|
||||
interface MapQuery {
|
||||
lat?: number;
|
||||
@@ -32,6 +27,7 @@ interface MapQuery {
|
||||
}
|
||||
|
||||
|
||||
|
||||
type ClusteredMarkersProps = {
|
||||
nodes: NodePosition[];
|
||||
selectedNodeId: string | null;
|
||||
@@ -304,11 +300,13 @@ const ClusteredMarkers = React.memo(function ClusteredMarkers({
|
||||
function NeighborLines({
|
||||
selectedNodeId,
|
||||
neighbors,
|
||||
nodes
|
||||
nodes,
|
||||
strokeWidth = 2
|
||||
}: {
|
||||
selectedNodeId: string | null;
|
||||
neighbors: Neighbor[];
|
||||
nodes: NodePosition[];
|
||||
strokeWidth?: number;
|
||||
}) {
|
||||
if (!selectedNodeId || neighbors.length === 0) return null;
|
||||
|
||||
@@ -352,7 +350,7 @@ function NeighborLines({
|
||||
positions={positions}
|
||||
pathOptions={{
|
||||
color: lineColor,
|
||||
weight: isBidirectional ? 3 : 2,
|
||||
weight: isBidirectional ? strokeWidth + 1 : strokeWidth,
|
||||
opacity: 0.7,
|
||||
dashArray: isNeighborVisible ? undefined : '5, 5'
|
||||
}}
|
||||
@@ -368,12 +366,14 @@ function AllNeighborLines({
|
||||
connections,
|
||||
nodes,
|
||||
useColors = true,
|
||||
minPacketCount = 1
|
||||
minPacketCount = 1,
|
||||
strokeWidth = 2
|
||||
}: {
|
||||
connections: AllNeighborsConnection[];
|
||||
nodes: NodePosition[];
|
||||
useColors?: boolean;
|
||||
minPacketCount?: number;
|
||||
strokeWidth?: number;
|
||||
}) {
|
||||
if (connections.length === 0) return null;
|
||||
|
||||
@@ -454,8 +454,8 @@ function AllNeighborLines({
|
||||
|
||||
const lineColor = getConnectionColor(connection.connection_type, connection.packet_count);
|
||||
|
||||
// Consistent line weight for all connections
|
||||
const lineWeight = connection.connection_type === 'direct' ? 2 : 1;
|
||||
// Use strokeWidth setting for line weight
|
||||
const lineWeight = connection.connection_type === 'direct' ? strokeWidth : Math.max(1, strokeWidth - 1);
|
||||
|
||||
return (
|
||||
<Polyline
|
||||
@@ -498,17 +498,21 @@ export default function MapView({ target = '_self' }: MapViewProps = {}) {
|
||||
nodeTypes: ["meshcore"],
|
||||
showMeshcoreCoverageOverlay: false,
|
||||
minPacketCount: 1,
|
||||
strokeWidth: 2,
|
||||
});
|
||||
|
||||
// Use query params to persist map position
|
||||
const { query: mapQuery, updateQuery: updateMapQuery } = useQueryParams<MapQuery>({
|
||||
lat: DEFAULT.lat,
|
||||
lng: DEFAULT.lng,
|
||||
zoom: DEFAULT.zoom,
|
||||
});
|
||||
// Use localStorage to persist map position
|
||||
const [mapPosition, setMapPosition] = useMapPosition();
|
||||
|
||||
const mapCenter: [number, number] = [mapQuery.lat ?? DEFAULT.lat, mapQuery.lng ?? DEFAULT.lng];
|
||||
const mapZoom = mapQuery.zoom ?? DEFAULT.zoom;
|
||||
// Use query params for map position (for sharing URLs)
|
||||
const { query: mapQuery, updateQuery: updateMapQuery } = useQueryParams<MapQuery>({});
|
||||
|
||||
// Determine map center and zoom: query params take priority over localStorage
|
||||
const mapCenter: [number, number] = [
|
||||
mapQuery.lat ?? mapPosition.lat,
|
||||
mapQuery.lng ?? mapPosition.lng
|
||||
];
|
||||
const mapZoom = mapQuery.zoom ?? mapPosition.zoom;
|
||||
|
||||
// Neighbor-related state
|
||||
const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
|
||||
@@ -655,7 +659,14 @@ export default function MapView({ target = '_self' }: MapViewProps = {}) {
|
||||
const center = map.getCenter();
|
||||
const zoom = map.getZoom();
|
||||
|
||||
// Update URL with new map position
|
||||
// Update localStorage with new map position
|
||||
setMapPosition({
|
||||
lat: Math.round(center.lat * 100000) / 100000, // Round to 5 decimal places
|
||||
lng: Math.round(center.lng * 100000) / 100000,
|
||||
zoom: zoom
|
||||
});
|
||||
|
||||
// Update URL with new map position for sharing
|
||||
updateMapQuery({
|
||||
lat: Math.round(center.lat * 100000) / 100000, // Round to 5 decimal places
|
||||
lng: Math.round(center.lng * 100000) / 100000,
|
||||
@@ -690,7 +701,14 @@ export default function MapView({ target = '_self' }: MapViewProps = {}) {
|
||||
const center = map.getCenter();
|
||||
const zoom = map.getZoom();
|
||||
|
||||
// Update URL with new map position
|
||||
// Update localStorage with new map position
|
||||
setMapPosition({
|
||||
lat: Math.round(center.lat * 100000) / 100000, // Round to 5 decimal places
|
||||
lng: Math.round(center.lng * 100000) / 100000,
|
||||
zoom: zoom
|
||||
});
|
||||
|
||||
// Update URL with new map position for sharing
|
||||
updateMapQuery({
|
||||
lat: Math.round(center.lat * 100000) / 100000, // Round to 5 decimal places
|
||||
lng: Math.round(center.lng * 100000) / 100000,
|
||||
@@ -811,6 +829,7 @@ export default function MapView({ target = '_self' }: MapViewProps = {}) {
|
||||
selectedNodeId={selectedNodeId}
|
||||
neighbors={neighbors}
|
||||
nodes={nodePositions}
|
||||
strokeWidth={mapLayerSettings.strokeWidth}
|
||||
/>
|
||||
{showAllNeighbors && (
|
||||
<AllNeighborLines
|
||||
@@ -818,6 +837,7 @@ export default function MapView({ target = '_self' }: MapViewProps = {}) {
|
||||
nodes={nodePositions}
|
||||
useColors={mapLayerSettings.useColors}
|
||||
minPacketCount={mapLayerSettings.minPacketCount}
|
||||
strokeWidth={mapLayerSettings.strokeWidth}
|
||||
/>
|
||||
)}
|
||||
</MapContainer>
|
||||
|
||||
@@ -8,24 +8,29 @@ import { useState, useEffect, useRef } from 'react';
|
||||
* @returns A tuple of [value, setValue] similar to useState
|
||||
*/
|
||||
export function useLocalStorage<T>(key: string, defaultValue: T): [T, (value: T | ((prev: T) => T)) => void] {
|
||||
const [value, setValue] = useState<T>(defaultValue);
|
||||
const firstRender = useRef(true);
|
||||
|
||||
// Load from localStorage on mount
|
||||
useEffect(() => {
|
||||
// Load from localStorage synchronously on first render
|
||||
const getInitialValue = (): T => {
|
||||
if (typeof window === 'undefined') {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
try {
|
||||
const stored = localStorage.getItem(key);
|
||||
if (stored !== null) {
|
||||
const parsed = JSON.parse(stored);
|
||||
setValue(typeof defaultValue === 'object' && defaultValue !== null
|
||||
return typeof defaultValue === 'object' && defaultValue !== null
|
||||
? { ...defaultValue, ...parsed }
|
||||
: parsed
|
||||
);
|
||||
: parsed;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Failed to load from localStorage key "${key}":`, error);
|
||||
}
|
||||
}, [key, defaultValue]);
|
||||
|
||||
return defaultValue;
|
||||
};
|
||||
|
||||
const [value, setValue] = useState<T>(getInitialValue);
|
||||
const firstRender = useRef(true);
|
||||
|
||||
// Save to localStorage when value changes (except on first render)
|
||||
useEffect(() => {
|
||||
|
||||
@@ -13,6 +13,7 @@ export interface MapLayerSettings {
|
||||
nodeTypes: NodeType[];
|
||||
showMeshcoreCoverageOverlay: boolean;
|
||||
minPacketCount: number;
|
||||
strokeWidth: number;
|
||||
}
|
||||
|
||||
const DEFAULT_MAP_LAYER_SETTINGS: MapLayerSettings = {
|
||||
@@ -25,6 +26,7 @@ const DEFAULT_MAP_LAYER_SETTINGS: MapLayerSettings = {
|
||||
nodeTypes: ["meshcore"],
|
||||
showMeshcoreCoverageOverlay: false,
|
||||
minPacketCount: 1,
|
||||
strokeWidth: 2,
|
||||
};
|
||||
|
||||
export function useMapLayerSettings() {
|
||||
|
||||
19
src/hooks/useMapPosition.ts
Normal file
19
src/hooks/useMapPosition.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
"use client";
|
||||
|
||||
import { useLocalStorage } from './useLocalStorage';
|
||||
|
||||
export interface MapPosition {
|
||||
lat: number;
|
||||
lng: number;
|
||||
zoom: number;
|
||||
}
|
||||
|
||||
const DEFAULT_MAP_POSITION: MapPosition = {
|
||||
lat: 39.8283, // Center of the United States
|
||||
lng: -98.5795, // Center of the United States
|
||||
zoom: 4, // Zoom level to show the entire US
|
||||
};
|
||||
|
||||
export function useMapPosition() {
|
||||
return useLocalStorage<MapPosition>("mapPosition", DEFAULT_MAP_POSITION);
|
||||
}
|
||||
Reference in New Issue
Block a user