Merge main into feature/open-mapping, resolve conflicts
This commit is contained in:
parent
8dac699acf
commit
8cda0d4e28
|
|
@ -1,5 +1,12 @@
|
||||||
/**
|
/**
|
||||||
* LayerPanel - UI for managing map layers
|
* 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';
|
import type { MapLayer } from '../types';
|
||||||
|
|
@ -11,9 +18,21 @@ interface LayerPanelProps {
|
||||||
onLayerReorder?: (layerIds: string[]) => void;
|
onLayerReorder?: (layerIds: string[]) => void;
|
||||||
onLayerAdd?: (layer: Omit<MapLayer, 'id'>) => void;
|
onLayerAdd?: (layer: Omit<MapLayer, 'id'>) => void;
|
||||||
onLayerRemove?: (layerId: string) => 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 (
|
return (
|
||||||
<div className="open-mapping-layer-panel">
|
<div className="open-mapping-layer-panel">
|
||||||
<h3>Layers</h3>
|
<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';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
@ -11,7 +14,7 @@ interface MapCanvasProps {
|
||||||
onViewportChange?: (viewport: MapViewport) => void;
|
onViewportChange?: (viewport: MapViewport) => void;
|
||||||
onMapClick?: (coordinate: Coordinate) => void;
|
onMapClick?: (coordinate: Coordinate) => void;
|
||||||
onMapLoad?: () => void;
|
onMapLoad?: () => void;
|
||||||
style?: string;
|
style?: string; // MapLibre style URL
|
||||||
interactive?: boolean;
|
interactive?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -28,18 +31,42 @@ export function MapCanvas({
|
||||||
const [isLoaded, setIsLoaded] = useState(false);
|
const [isLoaded, setIsLoaded] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
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);
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className="open-mapping-canvas"
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
/**
|
/**
|
||||||
* RouteLayer - Renders route polylines on the map
|
* 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';
|
import type { Route, RoutingProfile } from '../types';
|
||||||
|
|
@ -15,14 +21,33 @@ interface RouteLayerProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_PROFILE_COLORS: Record<RoutingProfile, string> = {
|
const DEFAULT_PROFILE_COLORS: Record<RoutingProfile, string> = {
|
||||||
car: '#3B82F6', truck: '#6366F1', motorcycle: '#8B5CF6',
|
car: '#3B82F6', // blue
|
||||||
bicycle: '#10B981', mountain_bike: '#059669', road_bike: '#14B8A6',
|
truck: '#6366F1', // indigo
|
||||||
foot: '#F59E0B', hiking: '#D97706', wheelchair: '#EC4899', transit: '#6B7280',
|
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) {
|
export function RouteLayer({
|
||||||
// TODO: Implement route rendering (Phase 2)
|
routes,
|
||||||
return null;
|
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;
|
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';
|
import type { Waypoint } from '../types';
|
||||||
|
|
@ -9,14 +15,30 @@ interface WaypointMarkerProps {
|
||||||
index?: number;
|
index?: number;
|
||||||
isSelected?: boolean;
|
isSelected?: boolean;
|
||||||
isDraggable?: boolean;
|
isDraggable?: boolean;
|
||||||
|
showLabel?: boolean;
|
||||||
|
showTime?: boolean;
|
||||||
|
showBudget?: boolean;
|
||||||
onSelect?: (waypointId: string) => void;
|
onSelect?: (waypointId: string) => void;
|
||||||
onDragEnd?: (waypointId: string, newCoordinate: { lat: number; lng: number }) => void;
|
onDragEnd?: (waypointId: string, newCoordinate: { lat: number; lng: number }) => void;
|
||||||
onDelete?: (waypointId: string) => void;
|
onDelete?: (waypointId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WaypointMarker({ waypoint, isSelected = false }: WaypointMarkerProps) {
|
export function WaypointMarker({
|
||||||
// TODO: Implement marker rendering (Phase 1)
|
waypoint,
|
||||||
return null;
|
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;
|
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 { 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 {
|
interface UseCollaborationOptions {
|
||||||
sessionId?: string;
|
sessionId?: string;
|
||||||
|
|
@ -13,43 +26,106 @@ interface UseCollaborationOptions {
|
||||||
serverUrl?: string;
|
serverUrl?: string;
|
||||||
onParticipantJoin?: (participant: Participant) => void;
|
onParticipantJoin?: (participant: Participant) => void;
|
||||||
onParticipantLeave?: (participantId: string) => 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({
|
export function useCollaboration({
|
||||||
sessionId, userId, userName, userColor = '#3B82F6', serverUrl,
|
sessionId,
|
||||||
}: UseCollaborationOptions) {
|
userId,
|
||||||
|
userName,
|
||||||
|
userColor = '#3B82F6',
|
||||||
|
serverUrl,
|
||||||
|
onParticipantJoin,
|
||||||
|
onParticipantLeave,
|
||||||
|
onRouteUpdate,
|
||||||
|
onWaypointUpdate,
|
||||||
|
}: UseCollaborationOptions): UseCollaborationReturn {
|
||||||
const [session, setSession] = useState<CollaborationSession | null>(null);
|
const [session, setSession] = useState<CollaborationSession | null>(null);
|
||||||
const [participants, setParticipants] = useState<Participant[]>([]);
|
const [participants, setParticipants] = useState<Participant[]>([]);
|
||||||
const [isConnected, setIsConnected] = useState(false);
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
|
|
||||||
|
// TODO: Initialize Y.js document and WebSocket provider
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!sessionId) return;
|
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);
|
setIsConnected(true);
|
||||||
return () => { setIsConnected(false); };
|
|
||||||
|
return () => {
|
||||||
|
// provider.destroy();
|
||||||
|
// ydoc.destroy();
|
||||||
|
setIsConnected(false);
|
||||||
|
};
|
||||||
}, [sessionId, serverUrl]);
|
}, [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()}`;
|
const newSessionId = `session-${Date.now()}`;
|
||||||
|
console.log('useCollaboration: Creating session', name, newSessionId);
|
||||||
return newSessionId;
|
return newSessionId;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const joinSession = useCallback(async (sessionIdToJoin: string) => {
|
const joinSession = useCallback(async (sessionIdToJoin: string): Promise<void> => {
|
||||||
console.log('Joining session', sessionIdToJoin);
|
// TODO: Join existing Y.js session
|
||||||
|
console.log('useCollaboration: Joining session', sessionIdToJoin);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const leaveSession = useCallback(() => {
|
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 updateCursor = useCallback((coordinate: Coordinate) => {
|
||||||
const broadcastRouteChange = useCallback((route: Route) => {}, []);
|
// TODO: Broadcast cursor position via Y.js awareness
|
||||||
const broadcastWaypointChange = useCallback((waypoint: Waypoint) => {}, []);
|
// awareness.setLocalStateField('cursor', coordinate);
|
||||||
const broadcastLayerChange = useCallback((layer: MapLayer) => {}, []);
|
}, []);
|
||||||
|
|
||||||
|
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 {
|
return {
|
||||||
session, participants, isConnected, createSession, joinSession, leaveSession,
|
session,
|
||||||
updateCursor, broadcastRouteChange, broadcastWaypointChange, broadcastLayerChange,
|
participants,
|
||||||
|
isConnected,
|
||||||
|
createSession,
|
||||||
|
joinSession,
|
||||||
|
leaveSession,
|
||||||
|
updateCursor,
|
||||||
|
broadcastRouteChange,
|
||||||
|
broadcastWaypointChange,
|
||||||
|
broadcastLayerChange,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,124 @@
|
||||||
/**
|
/**
|
||||||
* useLayers - Hook for managing map layers
|
* 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 { useState, useCallback } from 'react';
|
||||||
import type { MapLayer } from '../types';
|
import type { MapLayer, LayerType, LayerSource, LayerStyle } from '../types';
|
||||||
|
|
||||||
interface UseLayersOptions {
|
interface UseLayersOptions {
|
||||||
initialLayers?: MapLayer[];
|
initialLayers?: MapLayer[];
|
||||||
onLayerChange?: (layers: MapLayer[]) => void;
|
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'>> = {
|
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-standard': {
|
||||||
'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' } },
|
name: 'OpenStreetMap',
|
||||||
'satellite': { name: 'Satellite', type: 'satellite', visible: false, opacity: 1, zIndex: 0, source: { type: 'raster', tiles: [] } },
|
type: 'basemap',
|
||||||
'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' } },
|
visible: true,
|
||||||
'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' } },
|
opacity: 1,
|
||||||
'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' } },
|
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;
|
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 [layers, setLayers] = useState<MapLayer[]>(initialLayers);
|
||||||
|
|
||||||
const updateAndNotify = useCallback((newLayers: MapLayer[]) => {
|
const updateAndNotify = useCallback((newLayers: MapLayer[]) => {
|
||||||
|
|
@ -31,24 +126,68 @@ export function useLayers({ initialLayers = [], onLayerChange }: UseLayersOption
|
||||||
onLayerChange?.(newLayers);
|
onLayerChange?.(newLayers);
|
||||||
}, [onLayerChange]);
|
}, [onLayerChange]);
|
||||||
|
|
||||||
const addLayer = useCallback((layer: Omit<MapLayer, 'id'>) => {
|
const addLayer = useCallback((layer: Omit<MapLayer, 'id'>): string => {
|
||||||
const id = `layer-${++layerIdCounter}-${Date.now()}`;
|
const id = generateLayerId();
|
||||||
updateAndNotify([...layers, { ...layer, id }]);
|
const newLayer: MapLayer = { ...layer, id };
|
||||||
|
updateAndNotify([...layers, newLayer]);
|
||||||
return id;
|
return id;
|
||||||
}, [layers, updateAndNotify]);
|
}, [layers, updateAndNotify]);
|
||||||
|
|
||||||
const removeLayer = useCallback((layerId: string) => updateAndNotify(layers.filter((l) => l.id !== layerId)), [layers, updateAndNotify]);
|
const removeLayer = useCallback((layerId: string) => {
|
||||||
const updateLayer = useCallback((layerId: string, updates: Partial<MapLayer>) => updateAndNotify(layers.map((l) => l.id === layerId ? { ...l, ...updates } : l)), [layers, updateAndNotify]);
|
updateAndNotify(layers.filter((l) => l.id !== layerId));
|
||||||
const toggleVisibility = useCallback((layerId: string) => updateAndNotify(layers.map((l) => l.id === layerId ? { ...l, visible: !l.visible } : l)), [layers, updateAndNotify]);
|
}, [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 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 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);
|
updateAndNotify(reordered);
|
||||||
}, [layers, updateAndNotify]);
|
}, [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;
|
export default useLayers;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
/**
|
/**
|
||||||
* useMapInstance - Hook for managing MapLibre GL JS instance
|
* 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';
|
import { useEffect, useRef, useState, useCallback } from 'react';
|
||||||
|
|
@ -13,38 +19,83 @@ interface UseMapInstanceOptions {
|
||||||
onClick?: (coordinate: Coordinate) => void;
|
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 = {
|
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({
|
export function useMapInstance({
|
||||||
container, config, initialViewport = DEFAULT_VIEWPORT, onViewportChange,
|
container,
|
||||||
}: UseMapInstanceOptions) {
|
config,
|
||||||
|
initialViewport = DEFAULT_VIEWPORT,
|
||||||
|
onViewportChange,
|
||||||
|
onClick,
|
||||||
|
}: UseMapInstanceOptions): UseMapInstanceReturn {
|
||||||
const mapRef = useRef<unknown>(null);
|
const mapRef = useRef<unknown>(null);
|
||||||
const [isLoaded, setIsLoaded] = useState(false);
|
const [isLoaded, setIsLoaded] = useState(false);
|
||||||
const [viewport, setViewportState] = useState<MapViewport>(initialViewport);
|
const [viewport, setViewportState] = useState<MapViewport>(initialViewport);
|
||||||
|
|
||||||
|
// Initialize map
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!container) return;
|
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);
|
setIsLoaded(true);
|
||||||
return () => { mapRef.current = null; setIsLoaded(false); };
|
|
||||||
|
return () => {
|
||||||
|
// map.remove();
|
||||||
|
mapRef.current = null;
|
||||||
|
setIsLoaded(false);
|
||||||
|
};
|
||||||
}, [container]);
|
}, [container]);
|
||||||
|
|
||||||
const setViewport = useCallback((newViewport: MapViewport) => {
|
const setViewport = useCallback((newViewport: MapViewport) => {
|
||||||
setViewportState(newViewport);
|
setViewportState(newViewport);
|
||||||
onViewportChange?.(newViewport);
|
onViewportChange?.(newViewport);
|
||||||
|
// TODO: Update map instance
|
||||||
}, [onViewportChange]);
|
}, [onViewportChange]);
|
||||||
|
|
||||||
const flyTo = useCallback((coordinate: Coordinate, zoom?: number) => {
|
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]]) => {
|
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;
|
export default useMapInstance;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,21 @@
|
||||||
/**
|
/**
|
||||||
* useRouting - Hook for route calculation and management
|
* 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 { 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';
|
import { RoutingService } from '../services/RoutingService';
|
||||||
|
|
||||||
interface UseRoutingOptions {
|
interface UseRoutingOptions {
|
||||||
|
|
@ -12,15 +24,40 @@ interface UseRoutingOptions {
|
||||||
onError?: (error: Error) => void;
|
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 [routes, setRoutes] = useState<Route[]>([]);
|
||||||
const [isCalculating, setIsCalculating] = useState(false);
|
const [isCalculating, setIsCalculating] = useState(false);
|
||||||
const [error, setError] = useState<Error | null>(null);
|
const [error, setError] = useState<Error | null>(null);
|
||||||
|
|
||||||
const service = new RoutingService(config);
|
const service = new RoutingService(config);
|
||||||
|
|
||||||
const calculateRoute = useCallback(async (waypoints: Waypoint[], options?: Partial<RoutingOptions>) => {
|
const calculateRoute = useCallback(async (
|
||||||
if (waypoints.length < 2) { setError(new Error('At least 2 waypoints required')); return null; }
|
waypoints: Waypoint[],
|
||||||
setIsCalculating(true); setError(null);
|
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 {
|
try {
|
||||||
const route = await service.calculateRoute(waypoints, options);
|
const route = await service.calculateRoute(waypoints, options);
|
||||||
setRoutes((prev) => [...prev, route]);
|
setRoutes((prev) => [...prev, route]);
|
||||||
|
|
@ -28,37 +65,85 @@ export function useRouting({ config, onRouteCalculated, onError }: UseRoutingOpt
|
||||||
return route;
|
return route;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = err instanceof Error ? err : new Error('Route calculation failed');
|
const error = err instanceof Error ? err : new Error('Route calculation failed');
|
||||||
setError(error); onError?.(error); return null;
|
setError(error);
|
||||||
} finally { setIsCalculating(false); }
|
onError?.(error);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
setIsCalculating(false);
|
||||||
|
}
|
||||||
}, [service, onRouteCalculated, onError]);
|
}, [service, onRouteCalculated, onError]);
|
||||||
|
|
||||||
const calculateAlternatives = useCallback(async (waypoints: Waypoint[], count = 3) => {
|
const calculateAlternatives = useCallback(async (
|
||||||
setIsCalculating(true); setError(null);
|
waypoints: Waypoint[],
|
||||||
|
count = 3
|
||||||
|
): Promise<Route[]> => {
|
||||||
|
setIsCalculating(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const alternatives = await service.calculateAlternatives(waypoints, count);
|
const alternatives = await service.calculateAlternatives(waypoints, count);
|
||||||
setRoutes(alternatives);
|
setRoutes(alternatives);
|
||||||
return alternatives;
|
return alternatives;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = err instanceof Error ? err : new Error('Failed');
|
const error = err instanceof Error ? err : new Error('Alternative routes calculation failed');
|
||||||
setError(error); onError?.(error); return [];
|
setError(error);
|
||||||
} finally { setIsCalculating(false); }
|
onError?.(error);
|
||||||
|
return [];
|
||||||
|
} finally {
|
||||||
|
setIsCalculating(false);
|
||||||
|
}
|
||||||
}, [service, onError]);
|
}, [service, onError]);
|
||||||
|
|
||||||
const optimizeOrder = useCallback(async (waypoints: Waypoint[]) => {
|
const optimizeOrder = useCallback(async (waypoints: Waypoint[]): Promise<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[]) => {
|
|
||||||
setIsCalculating(true);
|
setIsCalculating(true);
|
||||||
try { return await service.calculateIsochrone(center, minutes); }
|
setError(null);
|
||||||
catch { return { type: 'FeatureCollection' as const, features: [] }; }
|
|
||||||
finally { setIsCalculating(false); }
|
|
||||||
}, [service]);
|
|
||||||
|
|
||||||
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;
|
export default useRouting;
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,14 @@
|
||||||
* Open Mapping Type Definitions
|
* Open Mapping Type Definitions
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
// Core Geographic Types
|
// Core Geographic Types
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
export interface Coordinate {
|
export interface Coordinate {
|
||||||
lat: number;
|
lat: number;
|
||||||
lng: number;
|
lng: number;
|
||||||
alt?: number;
|
alt?: number; // elevation in meters
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BoundingBox {
|
export interface BoundingBox {
|
||||||
|
|
@ -16,7 +19,10 @@ export interface BoundingBox {
|
||||||
west: number;
|
west: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
// Waypoint & Route Types
|
// Waypoint & Route Types
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
export interface Waypoint {
|
export interface Waypoint {
|
||||||
id: string;
|
id: string;
|
||||||
coordinate: Coordinate;
|
coordinate: Coordinate;
|
||||||
|
|
@ -26,7 +32,7 @@ export interface Waypoint {
|
||||||
color?: string;
|
color?: string;
|
||||||
arrivalTime?: Date;
|
arrivalTime?: Date;
|
||||||
departureTime?: Date;
|
departureTime?: Date;
|
||||||
stayDuration?: number;
|
stayDuration?: number; // minutes
|
||||||
budget?: WaypointBudget;
|
budget?: WaypointBudget;
|
||||||
metadata?: Record<string, unknown>;
|
metadata?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
@ -51,10 +57,10 @@ export interface Route {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RouteSummary {
|
export interface RouteSummary {
|
||||||
distance: number;
|
distance: number; // meters
|
||||||
duration: number;
|
duration: number; // seconds
|
||||||
ascent?: number;
|
ascent?: number; // meters
|
||||||
descent?: number;
|
descent?: number; // meters
|
||||||
cost?: RouteCost;
|
cost?: RouteCost;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -66,7 +72,7 @@ export interface RouteCost {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RouteLeg {
|
export interface RouteLeg {
|
||||||
startWaypoint: string;
|
startWaypoint: string; // waypoint id
|
||||||
endWaypoint: string;
|
endWaypoint: string;
|
||||||
distance: number;
|
distance: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
|
|
@ -107,7 +113,10 @@ export interface RouteMetadata {
|
||||||
shareLink?: string;
|
shareLink?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
// Routing Profiles & Options
|
// Routing Profiles & Options
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
export type RoutingProfile =
|
export type RoutingProfile =
|
||||||
| 'car' | 'truck' | 'motorcycle'
|
| 'car' | 'truck' | 'motorcycle'
|
||||||
| 'bicycle' | 'mountain_bike' | 'road_bike'
|
| 'bicycle' | 'mountain_bike' | 'road_bike'
|
||||||
|
|
@ -121,7 +130,7 @@ export interface RoutingOptions {
|
||||||
avoidHighways?: boolean;
|
avoidHighways?: boolean;
|
||||||
avoidFerries?: boolean;
|
avoidFerries?: boolean;
|
||||||
preferScenic?: boolean;
|
preferScenic?: boolean;
|
||||||
alternatives?: number;
|
alternatives?: number; // number of alternative routes to compute
|
||||||
departureTime?: Date;
|
departureTime?: Date;
|
||||||
arrivalTime?: Date;
|
arrivalTime?: Date;
|
||||||
optimize?: OptimizationType;
|
optimize?: OptimizationType;
|
||||||
|
|
@ -139,7 +148,10 @@ export interface RoutingConstraints {
|
||||||
vehicleWidth?: number;
|
vehicleWidth?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
// Layer Management
|
// Layer Management
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
export interface MapLayer {
|
export interface MapLayer {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -175,7 +187,10 @@ export interface LayerStyle {
|
||||||
iconSize?: number;
|
iconSize?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
// Collaboration Types
|
// Collaboration Types
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
export interface CollaborationSession {
|
export interface CollaborationSession {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -212,7 +227,10 @@ export interface MapViewport {
|
||||||
pitch: number;
|
pitch: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calendar & Scheduling
|
// ============================================================================
|
||||||
|
// Calendar & Scheduling Integration
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
export interface TripItinerary {
|
export interface TripItinerary {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -262,10 +280,13 @@ export interface BudgetItem {
|
||||||
date?: Date;
|
date?: Date;
|
||||||
waypointId?: string;
|
waypointId?: string;
|
||||||
eventId?: string;
|
eventId?: string;
|
||||||
receipt?: string;
|
receipt?: string; // URL or file path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
// Service Configurations
|
// Service Configurations
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
export interface RoutingServiceConfig {
|
export interface RoutingServiceConfig {
|
||||||
provider: 'osrm' | 'valhalla' | 'graphhopper' | 'openrouteservice';
|
provider: 'osrm' | 'valhalla' | 'graphhopper' | 'openrouteservice';
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue