Merge main into feature/open-mapping, resolve conflicts

This commit is contained in:
Jeff Emmett 2025-12-04 06:51:35 -08:00
parent dd4861458d
commit f726bac67a
9 changed files with 561 additions and 96 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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: '&copy; 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: '&copy; 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: '&copy; 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: '&copy; 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;

View File

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

View File

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

View File

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