diff --git a/components/map/geojson-layer.tsx b/components/map/geojson-layer.tsx index 63ac4e50..76db0a46 100644 --- a/components/map/geojson-layer.tsx +++ b/components/map/geojson-layer.tsx @@ -20,6 +20,7 @@ export function GeoJsonLayer({ id, data }: GeoJsonLayerProps) { const pointLayerId = `geojson-point-layer-${id}` const polygonLayerId = `geojson-polygon-layer-${id}` const polygonOutlineLayerId = `geojson-polygon-outline-layer-${id}` + const lineStringLayerId = `geojson-linestring-layer-${id}` const onMapLoad = () => { // Add source if it doesn't exist @@ -62,6 +63,25 @@ export function GeoJsonLayer({ id, data }: GeoJsonLayerProps) { }) } + // Add linestring layer for routes + if (!map.getLayer(lineStringLayerId)) { + map.addLayer({ + id: lineStringLayerId, + type: 'line', + source: sourceId, + filter: ['any', ['==', '$type', 'LineString'], ['==', '$type', 'MultiLineString']], + layout: { + 'line-join': 'round', + 'line-cap': 'round' + }, + paint: { + 'line-color': '#3b82f6', // blue-500 + 'line-width': 4, + 'line-opacity': 0.8 + } + }) + } + // Add point layer for circles if (!map.getLayer(pointLayerId)) { map.addLayer({ @@ -91,6 +111,7 @@ export function GeoJsonLayer({ id, data }: GeoJsonLayerProps) { if (map.getLayer(pointLayerId)) map.removeLayer(pointLayerId) if (map.getLayer(polygonLayerId)) map.removeLayer(polygonLayerId) if (map.getLayer(polygonOutlineLayerId)) map.removeLayer(polygonOutlineLayerId) + if (map.getLayer(lineStringLayerId)) map.removeLayer(lineStringLayerId) if (map.getSource(sourceId)) map.removeSource(sourceId) } } diff --git a/components/map/map-data-context.tsx b/components/map/map-data-context.tsx index 9b102547..d0eae4a6 100644 --- a/components/map/map-data-context.tsx +++ b/components/map/map-data-context.tsx @@ -16,8 +16,13 @@ export interface MapData { targetPosition?: { lat: number; lng: number } | null; // For flying to a location cameraState?: CameraState; // For saving camera state currentTimezone?: string; // Current timezone identifier - // TODO: Add other relevant map data types later (e.g., routeGeoJSON, poiList) - mapFeature?: any | null; // Generic feature from MCP hook's processLocationQuery + // TODO: Add other relevant map data types later (e.g., poiList) + mapFeature?: { + place_name?: string; + mapUrl?: string; + routeGeoJSON?: any; + [key: string]: any; + } | null; // Generic feature from MCP hook's processLocationQuery drawnFeatures?: Array<{ // Added to store drawn features and their measurements id: string; type: 'Polygon' | 'LineString'; diff --git a/components/map/map-query-handler.tsx b/components/map/map-query-handler.tsx index ea460170..33b62793 100644 --- a/components/map/map-query-handler.tsx +++ b/components/map/map-query-handler.tsx @@ -13,6 +13,7 @@ interface McpResponseData { address?: string; }; mapUrl?: string; + routeGeoJSON?: any; } interface GeospatialToolOutput { @@ -42,8 +43,8 @@ export const MapQueryHandler: React.FC = ({ toolOutput }) // Optionally store more info from mcp_response if needed by MapboxMap component later mapFeature: { place_name, - // Potentially add mapUrl or other details from toolOutput.mcp_response - mapUrl: toolOutput.mcp_response?.mapUrl + mapUrl: toolOutput.mcp_response?.mapUrl, + routeGeoJSON: toolOutput.mcp_response?.routeGeoJSON } })); } else { diff --git a/components/map/mapbox-map.tsx b/components/map/mapbox-map.tsx index 55552b5d..43d15e9e 100644 --- a/components/map/mapbox-map.tsx +++ b/components/map/mapbox-map.tsx @@ -11,7 +11,8 @@ import 'mapbox-gl/dist/mapbox-gl.css' import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css' import { useMapToggle, MapToggleEnum } from '../map-toggle-context' import { useMapData } from './map-data-context'; // Add this import -import { useMapLoading } from '../map-loading-context'; // Import useMapLoading +import { useMapLoading } from '../map-loading-context' +import { GeoJsonLayer } from './geojson-layer'; // Import useMapLoading import { useMap } from './map-context' mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN as string; @@ -550,11 +551,8 @@ export const Mapbox: React.FC<{ position?: { latitude: number; longitude: number updateMapPosition(lat, lng); } } - // TODO: Handle mapData.mapFeature for drawing routes, polygons, etc. in a future step. - // For example: - // if (mapData.mapFeature && mapData.mapFeature.route_geometry && typeof drawRoute === 'function') { - // drawRoute(mapData.mapFeature.route_geometry); // Implement drawRoute function if needed - // } + // mapFeature route rendering is now handled declaratively by GeoJsonLayer + // mounted conditionally in the component return }, [mapData.targetPosition, mapData.mapFeature, updateMapPosition]); // Long-press handlers @@ -593,6 +591,19 @@ export const Mapbox: React.FC<{ position?: { latitude: number; longitude: number onMouseUp={handleMouseUp} onMouseLeave={handleMouseUp} // Clear timer if mouse leaves container while pressed /> + {mapData.mapFeature?.routeGeoJSON && ( + + )} ) } diff --git a/lib/agents/tools/geospatial.tsx b/lib/agents/tools/geospatial.tsx index ca5f9f49..d0665d68 100644 --- a/lib/agents/tools/geospatial.tsx +++ b/lib/agents/tools/geospatial.tsx @@ -25,6 +25,7 @@ interface Location { interface McpResponse { location: Location; mapUrl?: string; + routeGeoJSON?: any; } interface MapboxConfig { @@ -393,13 +394,33 @@ Uses the Mapbox Search Box Text Search API endpoint to power searching for and g // Process results if (typeof content === 'object' && content !== null) { const parsedData = content as any; + + // Extract route geometry if available (Directions tool) + let routeGeoJSON = undefined; + if (parsedData.geometry) { + routeGeoJSON = parsedData.geometry; + } else if (parsedData.routes && parsedData.routes.length > 0 && parsedData.routes[0].geometry) { + routeGeoJSON = parsedData.routes[0].geometry; + } + if (parsedData.results?.length > 0) { const firstResult = parsedData.results[0]; - mcpData = { location: { latitude: firstResult.coordinates?.latitude, longitude: firstResult.coordinates?.longitude, place_name: firstResult.name || firstResult.place_name, address: firstResult.full_address || firstResult.address }, mapUrl: parsedData.mapUrl }; + mcpData = { location: { latitude: firstResult.coordinates?.latitude, longitude: firstResult.coordinates?.longitude, place_name: firstResult.name || firstResult.place_name, address: firstResult.full_address || firstResult.address }, mapUrl: parsedData.mapUrl, routeGeoJSON }; } else if (parsedData.location) { - mcpData = { location: { latitude: parsedData.location.latitude, longitude: parsedData.location.longitude, place_name: parsedData.location.place_name || parsedData.location.name, address: parsedData.location.address || parsedData.location.formatted_address }, mapUrl: parsedData.mapUrl || parsedData.map_url }; + mcpData = { location: { latitude: parsedData.location.latitude, longitude: parsedData.location.longitude, place_name: parsedData.location.place_name || parsedData.location.name, address: parsedData.location.address || parsedData.location.formatted_address }, mapUrl: parsedData.mapUrl || parsedData.map_url, routeGeoJSON }; + } else if (parsedData.routes && parsedData.routes.length > 0) { + // It's a routing response, pick first route coordinates for map center + const route = parsedData.routes[0]; + let lat, lng; + if (route.geometry && route.geometry.coordinates && route.geometry.coordinates.length > 0) { + // Pick a point roughly in the middle, or the start + const coords = route.geometry.coordinates[Math.floor(route.geometry.coordinates.length / 2)]; + lng = coords[0]; + lat = coords[1]; + } + mcpData = { location: { latitude: lat, longitude: lng, place_name: "Route", address: "Generated Route" }, routeGeoJSON }; } else { - throw new Error("Response missing required 'location' or 'results' field"); + throw new Error("Response missing required 'location', 'results' or 'routes' field"); } } else throw new Error('Unexpected response format from mapping service');