save map position, stroke width slider

This commit is contained in:
ajvpot
2025-09-29 18:06:11 +02:00
parent b173351011
commit e6e74589c1
6 changed files with 158 additions and 30 deletions

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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>

View File

@@ -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(() => {

View File

@@ -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() {

View 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);
}