Merge main into feature/open-mapping, resolve conflicts
This commit is contained in:
parent
dd4861458d
commit
f726bac67a
|
|
@ -1,5 +1,12 @@
|
|||
/**
|
||||
* LayerPanel - UI for managing map layers
|
||||
*
|
||||
* Features:
|
||||
* - Toggle layer visibility
|
||||
* - Adjust layer opacity
|
||||
* - Reorder layers (z-index)
|
||||
* - Add custom layers (GeoJSON, tiles)
|
||||
* - Import/export layer configurations
|
||||
*/
|
||||
|
||||
import type { MapLayer } from '../types';
|
||||
|
|
@ -11,9 +18,21 @@ interface LayerPanelProps {
|
|||
onLayerReorder?: (layerIds: string[]) => void;
|
||||
onLayerAdd?: (layer: Omit<MapLayer, 'id'>) => void;
|
||||
onLayerRemove?: (layerId: string) => void;
|
||||
onLayerEdit?: (layerId: string, updates: Partial<MapLayer>) => void;
|
||||
}
|
||||
|
||||
export function LayerPanel({ layers, onLayerToggle }: LayerPanelProps) {
|
||||
export function LayerPanel({
|
||||
layers,
|
||||
onLayerToggle,
|
||||
onLayerOpacity,
|
||||
onLayerReorder,
|
||||
onLayerAdd,
|
||||
onLayerRemove,
|
||||
onLayerEdit,
|
||||
}: LayerPanelProps) {
|
||||
// TODO: Implement layer panel UI
|
||||
// This will be implemented in Phase 2
|
||||
|
||||
return (
|
||||
<div className="open-mapping-layer-panel">
|
||||
<h3>Layers</h3>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
/**
|
||||
* MapCanvas - Main map component integrating with tldraw canvas
|
||||
* MapCanvas - Main map component that integrates with tldraw canvas
|
||||
*
|
||||
* Renders a MapLibre GL JS map as a layer within the tldraw canvas,
|
||||
* enabling collaborative route planning with full canvas editing capabilities.
|
||||
*/
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
|
@ -11,7 +14,7 @@ interface MapCanvasProps {
|
|||
onViewportChange?: (viewport: MapViewport) => void;
|
||||
onMapClick?: (coordinate: Coordinate) => void;
|
||||
onMapLoad?: () => void;
|
||||
style?: string;
|
||||
style?: string; // MapLibre style URL
|
||||
interactive?: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -28,18 +31,42 @@ export function MapCanvas({
|
|||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: Initialize MapLibre GL JS instance (Phase 1)
|
||||
// TODO: Initialize MapLibre GL JS instance
|
||||
// This will be implemented in Phase 1
|
||||
console.log('MapCanvas: Initializing with viewport', viewport);
|
||||
return () => { /* Cleanup */ };
|
||||
|
||||
return () => {
|
||||
// Cleanup map instance
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: Update layers when they change
|
||||
console.log('MapCanvas: Updating layers', layers);
|
||||
}, [layers]);
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: Sync viewport changes
|
||||
if (isLoaded) {
|
||||
console.log('MapCanvas: Viewport changed', viewport);
|
||||
}
|
||||
}, [viewport, isLoaded]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="open-mapping-canvas"
|
||||
style={{ width: '100%', height: '100%', position: 'relative' }}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{!isLoaded && <div className="open-mapping-loading">Loading map...</div>}
|
||||
{!isLoaded && (
|
||||
<div className="open-mapping-loading">
|
||||
Loading map...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
/**
|
||||
* RouteLayer - Renders route polylines on the map
|
||||
*
|
||||
* Displays computed routes with support for:
|
||||
* - Multiple alternative routes
|
||||
* - Turn-by-turn visualization
|
||||
* - Elevation profile overlay
|
||||
* - Interactive route editing
|
||||
*/
|
||||
|
||||
import type { Route, RoutingProfile } from '../types';
|
||||
|
|
@ -15,14 +21,33 @@ interface RouteLayerProps {
|
|||
}
|
||||
|
||||
const DEFAULT_PROFILE_COLORS: Record<RoutingProfile, string> = {
|
||||
car: '#3B82F6', truck: '#6366F1', motorcycle: '#8B5CF6',
|
||||
bicycle: '#10B981', mountain_bike: '#059669', road_bike: '#14B8A6',
|
||||
foot: '#F59E0B', hiking: '#D97706', wheelchair: '#EC4899', transit: '#6B7280',
|
||||
car: '#3B82F6', // blue
|
||||
truck: '#6366F1', // indigo
|
||||
motorcycle: '#8B5CF6', // violet
|
||||
bicycle: '#10B981', // emerald
|
||||
mountain_bike: '#059669', // green
|
||||
road_bike: '#14B8A6', // teal
|
||||
foot: '#F59E0B', // amber
|
||||
hiking: '#D97706', // orange
|
||||
wheelchair: '#EC4899', // pink
|
||||
transit: '#6B7280', // gray
|
||||
};
|
||||
|
||||
export function RouteLayer({ routes, selectedRouteId, profileColors = {} }: RouteLayerProps) {
|
||||
// TODO: Implement route rendering (Phase 2)
|
||||
return null;
|
||||
export function RouteLayer({
|
||||
routes,
|
||||
selectedRouteId,
|
||||
showAlternatives = true,
|
||||
showElevation = false,
|
||||
onRouteSelect,
|
||||
onRouteEdit,
|
||||
profileColors = {},
|
||||
}: RouteLayerProps) {
|
||||
const colors = { ...DEFAULT_PROFILE_COLORS, ...profileColors };
|
||||
|
||||
// TODO: Implement route rendering with MapLibre GL JS
|
||||
// This will be implemented in Phase 2
|
||||
|
||||
return null; // Routes are rendered directly on the map canvas
|
||||
}
|
||||
|
||||
export default RouteLayer;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
/**
|
||||
* WaypointMarker - Interactive waypoint markers
|
||||
* WaypointMarker - Interactive waypoint markers on the map
|
||||
*
|
||||
* Features:
|
||||
* - Drag-and-drop repositioning
|
||||
* - Custom icons and colors
|
||||
* - Info popups with waypoint details
|
||||
* - Time/budget annotations
|
||||
*/
|
||||
|
||||
import type { Waypoint } from '../types';
|
||||
|
|
@ -9,14 +15,30 @@ interface WaypointMarkerProps {
|
|||
index?: number;
|
||||
isSelected?: boolean;
|
||||
isDraggable?: boolean;
|
||||
showLabel?: boolean;
|
||||
showTime?: boolean;
|
||||
showBudget?: boolean;
|
||||
onSelect?: (waypointId: string) => void;
|
||||
onDragEnd?: (waypointId: string, newCoordinate: { lat: number; lng: number }) => void;
|
||||
onDelete?: (waypointId: string) => void;
|
||||
}
|
||||
|
||||
export function WaypointMarker({ waypoint, isSelected = false }: WaypointMarkerProps) {
|
||||
// TODO: Implement marker rendering (Phase 1)
|
||||
return null;
|
||||
export function WaypointMarker({
|
||||
waypoint,
|
||||
index,
|
||||
isSelected = false,
|
||||
isDraggable = true,
|
||||
showLabel = true,
|
||||
showTime = false,
|
||||
showBudget = false,
|
||||
onSelect,
|
||||
onDragEnd,
|
||||
onDelete,
|
||||
}: WaypointMarkerProps) {
|
||||
// TODO: Implement marker rendering with MapLibre GL JS
|
||||
// This will be implemented in Phase 1
|
||||
|
||||
return null; // Markers are rendered directly on the map
|
||||
}
|
||||
|
||||
export default WaypointMarker;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,22 @@
|
|||
/**
|
||||
* useCollaboration - Hook for real-time collaborative map editing via Y.js
|
||||
* useCollaboration - Hook for real-time collaborative map editing
|
||||
*
|
||||
* Uses Y.js for CRDT-based synchronization, enabling:
|
||||
* - Real-time waypoint/route sharing
|
||||
* - Cursor presence awareness
|
||||
* - Conflict-free concurrent edits
|
||||
* - Offline-first with sync on reconnect
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import type { CollaborationSession, Participant, Route, Waypoint, MapLayer, Coordinate } from '../types';
|
||||
import type {
|
||||
CollaborationSession,
|
||||
Participant,
|
||||
Route,
|
||||
Waypoint,
|
||||
MapLayer,
|
||||
Coordinate,
|
||||
} from '../types';
|
||||
|
||||
interface UseCollaborationOptions {
|
||||
sessionId?: string;
|
||||
|
|
@ -13,43 +26,106 @@ interface UseCollaborationOptions {
|
|||
serverUrl?: string;
|
||||
onParticipantJoin?: (participant: Participant) => void;
|
||||
onParticipantLeave?: (participantId: string) => void;
|
||||
onRouteUpdate?: (routes: Route[]) => void;
|
||||
onWaypointUpdate?: (waypoints: Waypoint[]) => void;
|
||||
}
|
||||
|
||||
interface UseCollaborationReturn {
|
||||
session: CollaborationSession | null;
|
||||
participants: Participant[];
|
||||
isConnected: boolean;
|
||||
createSession: (name: string) => Promise<string>;
|
||||
joinSession: (sessionId: string) => Promise<void>;
|
||||
leaveSession: () => void;
|
||||
updateCursor: (coordinate: Coordinate) => void;
|
||||
broadcastRouteChange: (route: Route) => void;
|
||||
broadcastWaypointChange: (waypoint: Waypoint) => void;
|
||||
broadcastLayerChange: (layer: MapLayer) => void;
|
||||
}
|
||||
|
||||
export function useCollaboration({
|
||||
sessionId, userId, userName, userColor = '#3B82F6', serverUrl,
|
||||
}: UseCollaborationOptions) {
|
||||
sessionId,
|
||||
userId,
|
||||
userName,
|
||||
userColor = '#3B82F6',
|
||||
serverUrl,
|
||||
onParticipantJoin,
|
||||
onParticipantLeave,
|
||||
onRouteUpdate,
|
||||
onWaypointUpdate,
|
||||
}: UseCollaborationOptions): UseCollaborationReturn {
|
||||
const [session, setSession] = useState<CollaborationSession | null>(null);
|
||||
const [participants, setParticipants] = useState<Participant[]>([]);
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
|
||||
// TODO: Initialize Y.js document and WebSocket provider
|
||||
useEffect(() => {
|
||||
if (!sessionId) return;
|
||||
// TODO: Initialize Y.js (Phase 3)
|
||||
|
||||
console.log('useCollaboration: Would connect to session', sessionId);
|
||||
// const ydoc = new Y.Doc();
|
||||
// const provider = new WebsocketProvider(serverUrl, sessionId, ydoc);
|
||||
|
||||
setIsConnected(true);
|
||||
return () => { setIsConnected(false); };
|
||||
|
||||
return () => {
|
||||
// provider.destroy();
|
||||
// ydoc.destroy();
|
||||
setIsConnected(false);
|
||||
};
|
||||
}, [sessionId, serverUrl]);
|
||||
|
||||
const createSession = useCallback(async (name: string) => {
|
||||
const createSession = useCallback(async (name: string): Promise<string> => {
|
||||
// TODO: Create new Y.js document and return session ID
|
||||
const newSessionId = `session-${Date.now()}`;
|
||||
console.log('useCollaboration: Creating session', name, newSessionId);
|
||||
return newSessionId;
|
||||
}, []);
|
||||
|
||||
const joinSession = useCallback(async (sessionIdToJoin: string) => {
|
||||
console.log('Joining session', sessionIdToJoin);
|
||||
const joinSession = useCallback(async (sessionIdToJoin: string): Promise<void> => {
|
||||
// TODO: Join existing Y.js session
|
||||
console.log('useCollaboration: Joining session', sessionIdToJoin);
|
||||
}, []);
|
||||
|
||||
const leaveSession = useCallback(() => {
|
||||
setSession(null); setParticipants([]); setIsConnected(false);
|
||||
// TODO: Disconnect from session
|
||||
console.log('useCollaboration: Leaving session');
|
||||
setSession(null);
|
||||
setParticipants([]);
|
||||
setIsConnected(false);
|
||||
}, []);
|
||||
|
||||
const updateCursor = useCallback((coordinate: Coordinate) => {}, []);
|
||||
const broadcastRouteChange = useCallback((route: Route) => {}, []);
|
||||
const broadcastWaypointChange = useCallback((waypoint: Waypoint) => {}, []);
|
||||
const broadcastLayerChange = useCallback((layer: MapLayer) => {}, []);
|
||||
const updateCursor = useCallback((coordinate: Coordinate) => {
|
||||
// TODO: Broadcast cursor position via Y.js awareness
|
||||
// awareness.setLocalStateField('cursor', coordinate);
|
||||
}, []);
|
||||
|
||||
const broadcastRouteChange = useCallback((route: Route) => {
|
||||
// TODO: Update Y.js shared route array
|
||||
console.log('useCollaboration: Broadcasting route change', route.id);
|
||||
}, []);
|
||||
|
||||
const broadcastWaypointChange = useCallback((waypoint: Waypoint) => {
|
||||
// TODO: Update Y.js shared waypoint array
|
||||
console.log('useCollaboration: Broadcasting waypoint change', waypoint.id);
|
||||
}, []);
|
||||
|
||||
const broadcastLayerChange = useCallback((layer: MapLayer) => {
|
||||
// TODO: Update Y.js shared layer array
|
||||
console.log('useCollaboration: Broadcasting layer change', layer.id);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
session, participants, isConnected, createSession, joinSession, leaveSession,
|
||||
updateCursor, broadcastRouteChange, broadcastWaypointChange, broadcastLayerChange,
|
||||
session,
|
||||
participants,
|
||||
isConnected,
|
||||
createSession,
|
||||
joinSession,
|
||||
leaveSession,
|
||||
updateCursor,
|
||||
broadcastRouteChange,
|
||||
broadcastWaypointChange,
|
||||
broadcastLayerChange,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,29 +1,124 @@
|
|||
/**
|
||||
* useLayers - Hook for managing map layers
|
||||
*
|
||||
* Provides:
|
||||
* - Layer CRUD operations
|
||||
* - Visibility and opacity controls
|
||||
* - Layer ordering (z-index)
|
||||
* - Preset layer templates
|
||||
*/
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import type { MapLayer } from '../types';
|
||||
import type { MapLayer, LayerType, LayerSource, LayerStyle } from '../types';
|
||||
|
||||
interface UseLayersOptions {
|
||||
initialLayers?: MapLayer[];
|
||||
onLayerChange?: (layers: MapLayer[]) => void;
|
||||
}
|
||||
|
||||
export type LayerPreset = 'osm-standard' | 'osm-humanitarian' | 'satellite' | 'terrain' | 'cycling' | 'hiking';
|
||||
interface UseLayersReturn {
|
||||
layers: MapLayer[];
|
||||
addLayer: (layer: Omit<MapLayer, 'id'>) => string;
|
||||
removeLayer: (layerId: string) => void;
|
||||
updateLayer: (layerId: string, updates: Partial<MapLayer>) => void;
|
||||
toggleVisibility: (layerId: string) => void;
|
||||
setOpacity: (layerId: string, opacity: number) => void;
|
||||
reorderLayers: (layerIds: string[]) => void;
|
||||
getLayer: (layerId: string) => MapLayer | undefined;
|
||||
addPresetLayer: (preset: LayerPreset) => string;
|
||||
}
|
||||
|
||||
export type LayerPreset =
|
||||
| 'osm-standard'
|
||||
| 'osm-humanitarian'
|
||||
| 'satellite'
|
||||
| 'terrain'
|
||||
| 'cycling'
|
||||
| 'hiking';
|
||||
|
||||
const PRESET_LAYERS: Record<LayerPreset, Omit<MapLayer, 'id'>> = {
|
||||
'osm-standard': { name: 'OpenStreetMap', type: 'basemap', visible: true, opacity: 1, zIndex: 0, source: { type: 'raster', tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'], attribution: '© OpenStreetMap' } },
|
||||
'osm-humanitarian': { name: 'Humanitarian', type: 'basemap', visible: false, opacity: 1, zIndex: 0, source: { type: 'raster', tiles: ['https://a.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png'], attribution: '© OSM, HOT' } },
|
||||
'satellite': { name: 'Satellite', type: 'satellite', visible: false, opacity: 1, zIndex: 0, source: { type: 'raster', tiles: [] } },
|
||||
'terrain': { name: 'Terrain', type: 'terrain', visible: false, opacity: 0.5, zIndex: 1, source: { type: 'raster', tiles: ['https://stamen-tiles.a.ssl.fastly.net/terrain/{z}/{x}/{y}.png'], attribution: 'Stamen' } },
|
||||
'cycling': { name: 'Cycling Routes', type: 'route', visible: false, opacity: 0.8, zIndex: 2, source: { type: 'raster', tiles: ['https://tile.waymarkedtrails.org/cycling/{z}/{x}/{y}.png'], attribution: 'Waymarked Trails' } },
|
||||
'hiking': { name: 'Hiking Trails', type: 'route', visible: false, opacity: 0.8, zIndex: 2, source: { type: 'raster', tiles: ['https://tile.waymarkedtrails.org/hiking/{z}/{x}/{y}.png'], attribution: 'Waymarked Trails' } },
|
||||
'osm-standard': {
|
||||
name: 'OpenStreetMap',
|
||||
type: 'basemap',
|
||||
visible: true,
|
||||
opacity: 1,
|
||||
zIndex: 0,
|
||||
source: {
|
||||
type: 'raster',
|
||||
tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'],
|
||||
attribution: '© OpenStreetMap contributors',
|
||||
},
|
||||
},
|
||||
'osm-humanitarian': {
|
||||
name: 'Humanitarian',
|
||||
type: 'basemap',
|
||||
visible: false,
|
||||
opacity: 1,
|
||||
zIndex: 0,
|
||||
source: {
|
||||
type: 'raster',
|
||||
tiles: ['https://a.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png'],
|
||||
attribution: '© OpenStreetMap contributors, Tiles: HOT',
|
||||
},
|
||||
},
|
||||
'satellite': {
|
||||
name: 'Satellite',
|
||||
type: 'satellite',
|
||||
visible: false,
|
||||
opacity: 1,
|
||||
zIndex: 0,
|
||||
source: {
|
||||
type: 'raster',
|
||||
// Note: Would need proper satellite tile source (e.g., Mapbox, ESRI)
|
||||
tiles: [],
|
||||
attribution: '',
|
||||
},
|
||||
},
|
||||
'terrain': {
|
||||
name: 'Terrain',
|
||||
type: 'terrain',
|
||||
visible: false,
|
||||
opacity: 0.5,
|
||||
zIndex: 1,
|
||||
source: {
|
||||
type: 'raster',
|
||||
tiles: ['https://stamen-tiles.a.ssl.fastly.net/terrain/{z}/{x}/{y}.png'],
|
||||
attribution: 'Map tiles by Stamen Design',
|
||||
},
|
||||
},
|
||||
'cycling': {
|
||||
name: 'Cycling Routes',
|
||||
type: 'route',
|
||||
visible: false,
|
||||
opacity: 0.8,
|
||||
zIndex: 2,
|
||||
source: {
|
||||
type: 'raster',
|
||||
tiles: ['https://tile.waymarkedtrails.org/cycling/{z}/{x}/{y}.png'],
|
||||
attribution: 'Waymarked Trails',
|
||||
},
|
||||
},
|
||||
'hiking': {
|
||||
name: 'Hiking Trails',
|
||||
type: 'route',
|
||||
visible: false,
|
||||
opacity: 0.8,
|
||||
zIndex: 2,
|
||||
source: {
|
||||
type: 'raster',
|
||||
tiles: ['https://tile.waymarkedtrails.org/hiking/{z}/{x}/{y}.png'],
|
||||
attribution: 'Waymarked Trails',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let layerIdCounter = 0;
|
||||
const generateLayerId = () => `layer-${++layerIdCounter}-${Date.now()}`;
|
||||
|
||||
export function useLayers({ initialLayers = [], onLayerChange }: UseLayersOptions = {}) {
|
||||
export function useLayers({
|
||||
initialLayers = [],
|
||||
onLayerChange,
|
||||
}: UseLayersOptions = {}): UseLayersReturn {
|
||||
const [layers, setLayers] = useState<MapLayer[]>(initialLayers);
|
||||
|
||||
const updateAndNotify = useCallback((newLayers: MapLayer[]) => {
|
||||
|
|
@ -31,24 +126,68 @@ export function useLayers({ initialLayers = [], onLayerChange }: UseLayersOption
|
|||
onLayerChange?.(newLayers);
|
||||
}, [onLayerChange]);
|
||||
|
||||
const addLayer = useCallback((layer: Omit<MapLayer, 'id'>) => {
|
||||
const id = `layer-${++layerIdCounter}-${Date.now()}`;
|
||||
updateAndNotify([...layers, { ...layer, id }]);
|
||||
const addLayer = useCallback((layer: Omit<MapLayer, 'id'>): string => {
|
||||
const id = generateLayerId();
|
||||
const newLayer: MapLayer = { ...layer, id };
|
||||
updateAndNotify([...layers, newLayer]);
|
||||
return id;
|
||||
}, [layers, updateAndNotify]);
|
||||
|
||||
const removeLayer = useCallback((layerId: string) => updateAndNotify(layers.filter((l) => l.id !== layerId)), [layers, updateAndNotify]);
|
||||
const updateLayer = useCallback((layerId: string, updates: Partial<MapLayer>) => updateAndNotify(layers.map((l) => l.id === layerId ? { ...l, ...updates } : l)), [layers, updateAndNotify]);
|
||||
const toggleVisibility = useCallback((layerId: string) => updateAndNotify(layers.map((l) => l.id === layerId ? { ...l, visible: !l.visible } : l)), [layers, updateAndNotify]);
|
||||
const setOpacity = useCallback((layerId: string, opacity: number) => updateAndNotify(layers.map((l) => l.id === layerId ? { ...l, opacity: Math.max(0, Math.min(1, opacity)) } : l)), [layers, updateAndNotify]);
|
||||
const removeLayer = useCallback((layerId: string) => {
|
||||
updateAndNotify(layers.filter((l) => l.id !== layerId));
|
||||
}, [layers, updateAndNotify]);
|
||||
|
||||
const updateLayer = useCallback((layerId: string, updates: Partial<MapLayer>) => {
|
||||
updateAndNotify(
|
||||
layers.map((l) => (l.id === layerId ? { ...l, ...updates } : l))
|
||||
);
|
||||
}, [layers, updateAndNotify]);
|
||||
|
||||
const toggleVisibility = useCallback((layerId: string) => {
|
||||
updateAndNotify(
|
||||
layers.map((l) => (l.id === layerId ? { ...l, visible: !l.visible } : l))
|
||||
);
|
||||
}, [layers, updateAndNotify]);
|
||||
|
||||
const setOpacity = useCallback((layerId: string, opacity: number) => {
|
||||
updateAndNotify(
|
||||
layers.map((l) => (l.id === layerId ? { ...l, opacity: Math.max(0, Math.min(1, opacity)) } : l))
|
||||
);
|
||||
}, [layers, updateAndNotify]);
|
||||
|
||||
const reorderLayers = useCallback((layerIds: string[]) => {
|
||||
const reordered = layerIds.map((id, i) => { const l = layers.find((x) => x.id === id); return l ? { ...l, zIndex: i } : null; }).filter((l): l is MapLayer => !!l);
|
||||
const reordered = layerIds
|
||||
.map((id, index) => {
|
||||
const layer = layers.find((l) => l.id === id);
|
||||
return layer ? { ...layer, zIndex: index } : null;
|
||||
})
|
||||
.filter((l): l is MapLayer => l !== null);
|
||||
updateAndNotify(reordered);
|
||||
}, [layers, updateAndNotify]);
|
||||
const getLayer = useCallback((layerId: string) => layers.find((l) => l.id === layerId), [layers]);
|
||||
const addPresetLayer = useCallback((preset: LayerPreset) => addLayer(PRESET_LAYERS[preset]), [addLayer]);
|
||||
|
||||
return { layers, addLayer, removeLayer, updateLayer, toggleVisibility, setOpacity, reorderLayers, getLayer, addPresetLayer };
|
||||
const getLayer = useCallback((layerId: string): MapLayer | undefined => {
|
||||
return layers.find((l) => l.id === layerId);
|
||||
}, [layers]);
|
||||
|
||||
const addPresetLayer = useCallback((preset: LayerPreset): string => {
|
||||
const presetConfig = PRESET_LAYERS[preset];
|
||||
if (!presetConfig) {
|
||||
throw new Error(`Unknown layer preset: ${preset}`);
|
||||
}
|
||||
return addLayer(presetConfig);
|
||||
}, [addLayer]);
|
||||
|
||||
return {
|
||||
layers,
|
||||
addLayer,
|
||||
removeLayer,
|
||||
updateLayer,
|
||||
toggleVisibility,
|
||||
setOpacity,
|
||||
reorderLayers,
|
||||
getLayer,
|
||||
addPresetLayer,
|
||||
};
|
||||
}
|
||||
|
||||
export default useLayers;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
/**
|
||||
* useMapInstance - Hook for managing MapLibre GL JS instance
|
||||
*
|
||||
* Provides:
|
||||
* - Map initialization and cleanup
|
||||
* - Viewport state management
|
||||
* - Event handlers (click, move, zoom)
|
||||
* - Ref to underlying map instance for advanced usage
|
||||
*/
|
||||
|
||||
import { useEffect, useRef, useState, useCallback } from 'react';
|
||||
|
|
@ -13,38 +19,83 @@ interface UseMapInstanceOptions {
|
|||
onClick?: (coordinate: Coordinate) => void;
|
||||
}
|
||||
|
||||
interface UseMapInstanceReturn {
|
||||
isLoaded: boolean;
|
||||
viewport: MapViewport;
|
||||
setViewport: (viewport: MapViewport) => void;
|
||||
flyTo: (coordinate: Coordinate, zoom?: number) => void;
|
||||
fitBounds: (bounds: [[number, number], [number, number]]) => void;
|
||||
getMap: () => unknown; // MapLibre map instance
|
||||
}
|
||||
|
||||
const DEFAULT_VIEWPORT: MapViewport = {
|
||||
center: { lat: 0, lng: 0 }, zoom: 2, bearing: 0, pitch: 0,
|
||||
center: { lat: 0, lng: 0 },
|
||||
zoom: 2,
|
||||
bearing: 0,
|
||||
pitch: 0,
|
||||
};
|
||||
|
||||
export function useMapInstance({
|
||||
container, config, initialViewport = DEFAULT_VIEWPORT, onViewportChange,
|
||||
}: UseMapInstanceOptions) {
|
||||
container,
|
||||
config,
|
||||
initialViewport = DEFAULT_VIEWPORT,
|
||||
onViewportChange,
|
||||
onClick,
|
||||
}: UseMapInstanceOptions): UseMapInstanceReturn {
|
||||
const mapRef = useRef<unknown>(null);
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
const [viewport, setViewportState] = useState<MapViewport>(initialViewport);
|
||||
|
||||
// Initialize map
|
||||
useEffect(() => {
|
||||
if (!container) return;
|
||||
// TODO: Initialize MapLibre GL JS (Phase 1)
|
||||
|
||||
// TODO: Initialize MapLibre GL JS
|
||||
// const map = new maplibregl.Map({
|
||||
// container,
|
||||
// style: config.styleUrl,
|
||||
// center: [initialViewport.center.lng, initialViewport.center.lat],
|
||||
// zoom: initialViewport.zoom,
|
||||
// bearing: initialViewport.bearing,
|
||||
// pitch: initialViewport.pitch,
|
||||
// });
|
||||
|
||||
console.log('useMapInstance: Would initialize map with config', config);
|
||||
setIsLoaded(true);
|
||||
return () => { mapRef.current = null; setIsLoaded(false); };
|
||||
|
||||
return () => {
|
||||
// map.remove();
|
||||
mapRef.current = null;
|
||||
setIsLoaded(false);
|
||||
};
|
||||
}, [container]);
|
||||
|
||||
const setViewport = useCallback((newViewport: MapViewport) => {
|
||||
setViewportState(newViewport);
|
||||
onViewportChange?.(newViewport);
|
||||
// TODO: Update map instance
|
||||
}, [onViewportChange]);
|
||||
|
||||
const flyTo = useCallback((coordinate: Coordinate, zoom?: number) => {
|
||||
console.log('flyTo', coordinate, zoom);
|
||||
// TODO: Implement flyTo animation
|
||||
console.log('useMapInstance: flyTo', coordinate, zoom);
|
||||
}, []);
|
||||
|
||||
const fitBounds = useCallback((bounds: [[number, number], [number, number]]) => {
|
||||
console.log('fitBounds', bounds);
|
||||
// TODO: Implement fitBounds
|
||||
console.log('useMapInstance: fitBounds', bounds);
|
||||
}, []);
|
||||
|
||||
return { isLoaded, viewport, setViewport, flyTo, fitBounds, getMap: () => mapRef.current };
|
||||
const getMap = useCallback(() => mapRef.current, []);
|
||||
|
||||
return {
|
||||
isLoaded,
|
||||
viewport,
|
||||
setViewport,
|
||||
flyTo,
|
||||
fitBounds,
|
||||
getMap,
|
||||
};
|
||||
}
|
||||
|
||||
export default useMapInstance;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,21 @@
|
|||
/**
|
||||
* useRouting - Hook for route calculation and management
|
||||
*
|
||||
* Provides:
|
||||
* - Route calculation between waypoints
|
||||
* - Multi-route comparison
|
||||
* - Route optimization (reorder waypoints)
|
||||
* - Isochrone calculation
|
||||
*/
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import type { Waypoint, Route, RoutingOptions, RoutingServiceConfig, Coordinate } from '../types';
|
||||
import type {
|
||||
Waypoint,
|
||||
Route,
|
||||
RoutingOptions,
|
||||
RoutingServiceConfig,
|
||||
Coordinate,
|
||||
} from '../types';
|
||||
import { RoutingService } from '../services/RoutingService';
|
||||
|
||||
interface UseRoutingOptions {
|
||||
|
|
@ -12,15 +24,40 @@ interface UseRoutingOptions {
|
|||
onError?: (error: Error) => void;
|
||||
}
|
||||
|
||||
export function useRouting({ config, onRouteCalculated, onError }: UseRoutingOptions) {
|
||||
interface UseRoutingReturn {
|
||||
routes: Route[];
|
||||
isCalculating: boolean;
|
||||
error: Error | null;
|
||||
calculateRoute: (waypoints: Waypoint[], options?: Partial<RoutingOptions>) => Promise<Route | null>;
|
||||
calculateAlternatives: (waypoints: Waypoint[], count?: number) => Promise<Route[]>;
|
||||
optimizeOrder: (waypoints: Waypoint[]) => Promise<Waypoint[]>;
|
||||
calculateIsochrone: (center: Coordinate, minutes: number[]) => Promise<GeoJSON.FeatureCollection>;
|
||||
clearRoutes: () => void;
|
||||
}
|
||||
|
||||
export function useRouting({
|
||||
config,
|
||||
onRouteCalculated,
|
||||
onError,
|
||||
}: UseRoutingOptions): UseRoutingReturn {
|
||||
const [routes, setRoutes] = useState<Route[]>([]);
|
||||
const [isCalculating, setIsCalculating] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const service = new RoutingService(config);
|
||||
|
||||
const calculateRoute = useCallback(async (waypoints: Waypoint[], options?: Partial<RoutingOptions>) => {
|
||||
if (waypoints.length < 2) { setError(new Error('At least 2 waypoints required')); return null; }
|
||||
setIsCalculating(true); setError(null);
|
||||
const calculateRoute = useCallback(async (
|
||||
waypoints: Waypoint[],
|
||||
options?: Partial<RoutingOptions>
|
||||
): Promise<Route | null> => {
|
||||
if (waypoints.length < 2) {
|
||||
setError(new Error('At least 2 waypoints required'));
|
||||
return null;
|
||||
}
|
||||
|
||||
setIsCalculating(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const route = await service.calculateRoute(waypoints, options);
|
||||
setRoutes((prev) => [...prev, route]);
|
||||
|
|
@ -28,37 +65,85 @@ export function useRouting({ config, onRouteCalculated, onError }: UseRoutingOpt
|
|||
return route;
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err : new Error('Route calculation failed');
|
||||
setError(error); onError?.(error); return null;
|
||||
} finally { setIsCalculating(false); }
|
||||
setError(error);
|
||||
onError?.(error);
|
||||
return null;
|
||||
} finally {
|
||||
setIsCalculating(false);
|
||||
}
|
||||
}, [service, onRouteCalculated, onError]);
|
||||
|
||||
const calculateAlternatives = useCallback(async (waypoints: Waypoint[], count = 3) => {
|
||||
setIsCalculating(true); setError(null);
|
||||
const calculateAlternatives = useCallback(async (
|
||||
waypoints: Waypoint[],
|
||||
count = 3
|
||||
): Promise<Route[]> => {
|
||||
setIsCalculating(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const alternatives = await service.calculateAlternatives(waypoints, count);
|
||||
setRoutes(alternatives);
|
||||
return alternatives;
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err : new Error('Failed');
|
||||
setError(error); onError?.(error); return [];
|
||||
} finally { setIsCalculating(false); }
|
||||
const error = err instanceof Error ? err : new Error('Alternative routes calculation failed');
|
||||
setError(error);
|
||||
onError?.(error);
|
||||
return [];
|
||||
} finally {
|
||||
setIsCalculating(false);
|
||||
}
|
||||
}, [service, onError]);
|
||||
|
||||
const optimizeOrder = useCallback(async (waypoints: Waypoint[]) => {
|
||||
setIsCalculating(true); setError(null);
|
||||
try { return await service.optimizeWaypointOrder(waypoints); }
|
||||
catch (err) { const error = err instanceof Error ? err : new Error('Failed'); setError(error); return waypoints; }
|
||||
finally { setIsCalculating(false); }
|
||||
}, [service]);
|
||||
|
||||
const calculateIsochrone = useCallback(async (center: Coordinate, minutes: number[]) => {
|
||||
const optimizeOrder = useCallback(async (waypoints: Waypoint[]): Promise<Waypoint[]> => {
|
||||
setIsCalculating(true);
|
||||
try { return await service.calculateIsochrone(center, minutes); }
|
||||
catch { return { type: 'FeatureCollection' as const, features: [] }; }
|
||||
finally { setIsCalculating(false); }
|
||||
}, [service]);
|
||||
setError(null);
|
||||
|
||||
return { routes, isCalculating, error, calculateRoute, calculateAlternatives, optimizeOrder, calculateIsochrone, clearRoutes: () => setRoutes([]) };
|
||||
try {
|
||||
return await service.optimizeWaypointOrder(waypoints);
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err : new Error('Waypoint optimization failed');
|
||||
setError(error);
|
||||
onError?.(error);
|
||||
return waypoints;
|
||||
} finally {
|
||||
setIsCalculating(false);
|
||||
}
|
||||
}, [service, onError]);
|
||||
|
||||
const calculateIsochrone = useCallback(async (
|
||||
center: Coordinate,
|
||||
minutes: number[]
|
||||
): Promise<GeoJSON.FeatureCollection> => {
|
||||
setIsCalculating(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
return await service.calculateIsochrone(center, minutes);
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err : new Error('Isochrone calculation failed');
|
||||
setError(error);
|
||||
onError?.(error);
|
||||
return { type: 'FeatureCollection', features: [] };
|
||||
} finally {
|
||||
setIsCalculating(false);
|
||||
}
|
||||
}, [service, onError]);
|
||||
|
||||
const clearRoutes = useCallback(() => {
|
||||
setRoutes([]);
|
||||
setError(null);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
routes,
|
||||
isCalculating,
|
||||
error,
|
||||
calculateRoute,
|
||||
calculateAlternatives,
|
||||
optimizeOrder,
|
||||
calculateIsochrone,
|
||||
clearRoutes,
|
||||
};
|
||||
}
|
||||
|
||||
export default useRouting;
|
||||
|
|
|
|||
|
|
@ -2,11 +2,14 @@
|
|||
* Open Mapping Type Definitions
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// Core Geographic Types
|
||||
// ============================================================================
|
||||
|
||||
export interface Coordinate {
|
||||
lat: number;
|
||||
lng: number;
|
||||
alt?: number;
|
||||
alt?: number; // elevation in meters
|
||||
}
|
||||
|
||||
export interface BoundingBox {
|
||||
|
|
@ -16,7 +19,10 @@ export interface BoundingBox {
|
|||
west: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Waypoint & Route Types
|
||||
// ============================================================================
|
||||
|
||||
export interface Waypoint {
|
||||
id: string;
|
||||
coordinate: Coordinate;
|
||||
|
|
@ -26,7 +32,7 @@ export interface Waypoint {
|
|||
color?: string;
|
||||
arrivalTime?: Date;
|
||||
departureTime?: Date;
|
||||
stayDuration?: number;
|
||||
stayDuration?: number; // minutes
|
||||
budget?: WaypointBudget;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
|
@ -51,10 +57,10 @@ export interface Route {
|
|||
}
|
||||
|
||||
export interface RouteSummary {
|
||||
distance: number;
|
||||
duration: number;
|
||||
ascent?: number;
|
||||
descent?: number;
|
||||
distance: number; // meters
|
||||
duration: number; // seconds
|
||||
ascent?: number; // meters
|
||||
descent?: number; // meters
|
||||
cost?: RouteCost;
|
||||
}
|
||||
|
||||
|
|
@ -66,7 +72,7 @@ export interface RouteCost {
|
|||
}
|
||||
|
||||
export interface RouteLeg {
|
||||
startWaypoint: string;
|
||||
startWaypoint: string; // waypoint id
|
||||
endWaypoint: string;
|
||||
distance: number;
|
||||
duration: number;
|
||||
|
|
@ -107,7 +113,10 @@ export interface RouteMetadata {
|
|||
shareLink?: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Routing Profiles & Options
|
||||
// ============================================================================
|
||||
|
||||
export type RoutingProfile =
|
||||
| 'car' | 'truck' | 'motorcycle'
|
||||
| 'bicycle' | 'mountain_bike' | 'road_bike'
|
||||
|
|
@ -121,7 +130,7 @@ export interface RoutingOptions {
|
|||
avoidHighways?: boolean;
|
||||
avoidFerries?: boolean;
|
||||
preferScenic?: boolean;
|
||||
alternatives?: number;
|
||||
alternatives?: number; // number of alternative routes to compute
|
||||
departureTime?: Date;
|
||||
arrivalTime?: Date;
|
||||
optimize?: OptimizationType;
|
||||
|
|
@ -139,7 +148,10 @@ export interface RoutingConstraints {
|
|||
vehicleWidth?: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Layer Management
|
||||
// ============================================================================
|
||||
|
||||
export interface MapLayer {
|
||||
id: string;
|
||||
name: string;
|
||||
|
|
@ -175,7 +187,10 @@ export interface LayerStyle {
|
|||
iconSize?: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Collaboration Types
|
||||
// ============================================================================
|
||||
|
||||
export interface CollaborationSession {
|
||||
id: string;
|
||||
name: string;
|
||||
|
|
@ -212,7 +227,10 @@ export interface MapViewport {
|
|||
pitch: number;
|
||||
}
|
||||
|
||||
// Calendar & Scheduling
|
||||
// ============================================================================
|
||||
// Calendar & Scheduling Integration
|
||||
// ============================================================================
|
||||
|
||||
export interface TripItinerary {
|
||||
id: string;
|
||||
name: string;
|
||||
|
|
@ -262,10 +280,13 @@ export interface BudgetItem {
|
|||
date?: Date;
|
||||
waypointId?: string;
|
||||
eventId?: string;
|
||||
receipt?: string;
|
||||
receipt?: string; // URL or file path
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Service Configurations
|
||||
// ============================================================================
|
||||
|
||||
export interface RoutingServiceConfig {
|
||||
provider: 'osrm' | 'valhalla' | 'graphhopper' | 'openrouteservice';
|
||||
baseUrl: string;
|
||||
|
|
|
|||
Loading…
Reference in New Issue