/* * Copyright (C) 2025 TomTom Navigation B.V. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { z } from "zod"; import { routingOptionsSchema } from "../routing/common"; const latitudeSchema = z .number() .describe( "Latitude coordinate (-90 to +90). Use precise coordinates from geocoding for best results. EXAMPLE: 52.3676 for Amsterdam Central Station." ); const longitudeSchema = z .number() .describe( "Longitude coordinate (-180 to +180). Use precise coordinates from geocoding for best results. EXAMPLE: 4.9041 for Amsterdam Central Station." ); function labelledPointSchema(labelDescription: string) { return z.object({ lat: latitudeSchema, lon: longitudeSchema, label: z.string().optional().describe(labelDescription), }); } const pointSchema = labelledPointSchema( "Optional custom label for this location. If not provided, defaults will be used (e.g., 'Start', 'End', 'Waypoint 1'). EXAMPLE: 'Amsterdam Central' or 'Coffee Stop'." ); const centerCoordinateSchema = z.object({ lat: latitudeSchema, lon: longitudeSchema }); const originCoordinateSchema = labelledPointSchema( "Optional custom label for the starting point. If not provided, defaults to 'Start'. EXAMPLE: 'Home' or 'Office'." ); const destinationCoordinateSchema = labelledPointSchema( "Optional custom label for the end point. If not provided, defaults to 'End'. EXAMPLE: 'Restaurant' or 'Museum'." ); const markerSchema = z.object({ lat: z .number() .describe("Marker latitude coordinate. EXAMPLE: 52.3676 for Amsterdam Central Station."), lon: z .number() .describe("Marker longitude coordinate. EXAMPLE: 4.9041 for Amsterdam Central Station."), label: z .string() .optional() .describe("Optional label text for the marker. EXAMPLE: 'Amsterdam Central' or 'My Location'."), color: z .string() .optional() .describe( "Optional color override for category dot markers (hex format). Only applies when 'category' is set — if omitted, color is auto-assigned per category. Ignored for location markers (no category), which always use the blue map pin. EXAMPLE: '#FF0000'." ), priority: z .enum(["low", "normal", "high", "critical"]) .optional() .describe( "Label display priority for crowded areas. Controls which labels are shown when space is limited:\n" + "• 'critical' - Always visible (landmarks, primary POIs)\n" + "• 'high' - High priority (important businesses, key locations)\n" + "• 'normal' - Standard priority (regular POIs) [DEFAULT]\n" + "• 'low' - Lower priority (supplementary info, may be hidden in dense areas)\n" + "Higher priority labels are displayed first when showLabels=true. Use 'critical' for must-see locations like 'Times Square' or main destinations. EXAMPLE: Use 'critical' for your main destination and 'normal' for secondary points." ), category: z .string() .optional() .describe( "Category of the marker (for POIs only). Markers with a category are rendered as colored dots; markers without a category are rendered as blue map pins. Do NOT set category for locations/addresses/landmarks/destinations — just omit it and they will automatically get the blue pin marker. All markers with the same category get the same dot color automatically. Also displayed in the popup. EXAMPLE: 'Restaurant', 'Hotel', 'Hospital', 'Gas Station', 'Park'." ), description: z .string() .optional() .describe( "Brief description shown in the popup below the title. EXAMPLE: 'Italian fine dining with rooftop terrace' or 'Open 24/7'." ), address: z .string() .optional() .describe( "Address text shown in the popup. EXAMPLE: '123 Main Street, Amsterdam, Netherlands'." ), tags: z .array(z.string()) .optional() .describe( "Tags displayed as badges in the popup for quick categorization. EXAMPLE: ['Italian', 'Fine Dining', '$$'] or ['EV Charging', 'Free Parking']." ), }); // Each entry is an independent origin→destination trip const routePlanSchema = z.object({ origin: originCoordinateSchema.describe( "Starting point for this route plan. EXAMPLE: {lat: 52.3676, lon: 4.9041, label: 'Amsterdam Central'}." ), destination: destinationCoordinateSchema.describe( "End point for this route plan. EXAMPLE: {lat: 52.36, lon: 4.8852, label: 'Rijksmuseum'}." ), waypoints: z .array(pointSchema) .optional() .describe( "Optional intermediate stops for this route. EXAMPLE: [{lat: 52.3745, lon: 4.8979, label: 'Anne Frank House'}]." ), label: z .string() .optional() .describe( "Display name for this route (shown in popups and labels). EXAMPLE: 'Morning Commute' or 'Scenic Route'." ), routeType: routingOptionsSchema.routeType.describe( "Route calculation strategy: 'fast' (time-optimized), 'short' (distance-optimized), 'efficient' (fuel-efficient), 'thrilling' (scenic). DEFAULT: 'fast'." ), travelMode: routingOptionsSchema.travelMode.describe( "Mode of transport. DEFAULT: 'car'. Only 'car' is supported." ), avoid: routingOptionsSchema.avoid.describe( "Road types to avoid. EXAMPLE: ['tollRoads', 'motorways']." ), traffic: z.boolean().optional().describe("Whether to include live traffic data. DEFAULT: false."), color: z .string() .optional() .describe( "Hex color override for this route line. If omitted, a distinct color is auto-assigned. EXAMPLE: '#FF0000' for red." ), }); // Drawn straight lines, not road-following const routeSchema = z.object({ points: z .array(pointSchema) .describe( "Array of route points in various coordinate formats. EXAMPLE: For a simple route from Amsterdam Central to Rijksmuseum: [{lat: 52.3676, lon: 4.9041, label: 'Start'}, {lat: 52.36, lon: 4.8852, label: 'End'}]" ), name: z .string() .optional() .describe( "Optional route name that appears when showLabels=true. EXAMPLE: 'Walking Tour' or 'Scenic Drive'." ), color: z .string() .optional() .describe( "Route color in hex format (e.g., '#0066cc'). DEFAULT: '#007cbf'. EXAMPLE: '#FF0000' for red route, '#00FF00' for green route." ), }); const polygonSchema = z.object({ type: z .enum(["polygon", "circle"]) .optional() .describe( "Shape type: 'polygon' for custom shapes, 'circle' for circular areas. DEFAULT: 'polygon'. EXAMPLE: For a triangle around Amsterdam, use type: 'polygon' with coordinates: [[4.9041, 52.3676], [4.8979, 52.3745], [4.8852, 52.36], [4.9041, 52.3676]]." ), coordinates: z .array(z.array(z.number()).length(2)) .min(3) .optional() .describe( "Array of coordinate pairs in [longitude, latitude] format (NOTE: longitude first, latitude second) forming the polygon boundary. Required for type='polygon'. Minimum 3 points required. To create a closed polygon, the first and last coordinates must be identical. EXAMPLE: [[4.9041, 52.3676], [4.8979, 52.3745], [4.8852, 52.36], [4.9041, 52.3676]] creates a triangle with the last point closing the shape." ), // Circle properties (for type: 'circle') center: z .object({ lat: z.number().describe("Circle center latitude. EXAMPLE: 52.3676 for Amsterdam Central."), lon: z.number().describe("Circle center longitude. EXAMPLE: 4.9041 for Amsterdam Central."), }) .optional() .describe( "Center point for circles. Required for type='circle'. EXAMPLE: {lat: 52.3676, lon: 4.9041} for Amsterdam Central." ), radius: z .number() .min(1) .optional() .describe( "Circle radius in meters. Required for type='circle'. Examples: 500 (small area), 2000 (neighborhood), 5000 (district). EXAMPLE: 1000 for a 1km radius around a point." ), // Styling (applies to both polygons and circles) label: z .string() .optional() .describe( "Optional text label to display in the shape center. EXAMPLE: 'Tourist Area' or '5min Walk Distance'." ), fillColor: z .string() .optional() .describe( "Fill color in CSS format. DEFAULT: 'rgba(0,123,255,0.3)' (transparent blue). EXAMPLE: 'rgba(255,0,0,0.3)' for transparent red, '#00FF00' for solid green. Lower alpha values (0.1-0.3) work best for large areas to avoid obscuring map details." ), strokeColor: z .string() .optional() .describe( "Border color in CSS format. DEFAULT: '#007bff'. EXAMPLE: '#FF0000' for red border, 'blue' for blue border." ), strokeWidth: z .number() .min(0) .max(10) .optional() .describe( "Border width in pixels (0-10). DEFAULT: 2. EXAMPLE: 0 for no border, 5 for thick border." ), name: z .string() .optional() .describe( "Optional polygon name for identification (not displayed on map). EXAMPLE: 'serviceArea' or 'district5'." ), }); const refinedPolygonSchema = polygonSchema.refine( (data) => { if (data.type === "polygon") return Array.isArray(data.coordinates) && data.coordinates.length >= 3; if (data.type === "circle") return data.center && typeof data.radius === "number"; return true; }, { message: "For type='polygon', 'coordinates' array is required. For type='circle', both 'center' and 'radius' are required.", } ); /** * Dynamic Map Schema for advanced map rendering with custom markers, routes, and styling * * COMMON PATTERNS: * 1. Simple marker map: Provide 'markers' array and let the view auto-calculate * 2. Route planning: Use 'routePlans' array for road-following route calculations * 3. Custom area visualization: Use 'polygons' with either polygon or circle types * 4. Fixed viewpoint: Specify exact 'bbox' or 'center'+'zoom' to control the map view * * AUTO-CALCULATION BEHAVIOR: * - If no 'bbox', 'center', or 'zoom' is provided, the map will automatically adjust to show all elements. * - For best control, always provide either 'bbox' or 'center'+'zoom' explicitly. */ export const tomtomDynamicMapSchema = { // Map positioning - either center+zoom, bbox, or auto-calculated from content center: centerCoordinateSchema .optional() .describe( "Map center coordinates. With 'zoom' it sets the view exactly; alone, the map centers here and zooms to fit the content. Ignored when 'bbox' is given. EXAMPLE: {lat: 52.3676, lon: 4.9041} for Amsterdam Central." ), bbox: z .array(z.number()) .length(4) .optional() .describe( "Bounding box in format [west, south, east, north] (min longitude, min latitude, max longitude, max latitude). Alternative to center+zoom. Use this parameter to ensure all map elements are fully visible. EXAMPLE: [4.87, 52.355, 4.915, 52.385] for central Amsterdam area." ), zoom: z .number() .min(0) .max(22) .optional() .describe( "Zoom level (0-22). EXAMPLES: 3 (continent), 6 (country), 10 (city), 15 (neighborhood), 18 (street), 20-22 (building detail). Without it the zoom fits the content (or the bbox). NOTE: Zoom levels 20+ are only useful for very small geographic areas." ), // Viewport dimensions the map is fitted to width: z .number() .min(100) .max(2048) .optional() .describe( "Map viewport width in pixels (100-2048), used to fit the initial framing. DEFAULT: 600. EXAMPLE: 800 for standard display, 1200 for a wider view." ), height: z .number() .min(100) .max(2048) .optional() .describe( "Map viewport height in pixels (100-2048), used to fit the initial framing. DEFAULT: 400. EXAMPLE: 600 for standard display, 900 for a taller view." ), // Content to render markers: z .array(markerSchema) .optional() .describe( "Array of markers to display on the map. Each marker can have custom color, label, and priority. EXAMPLE: [{lat: 52.3676, lon: 4.9041, color: '#FF4444', label: 'Amsterdam Central', priority: 'high'}]." ), routes: z .array(routeSchema) .optional() .describe( "Draw straight lines between coordinates — for visualizing custom paths, connections, or external data (e.g. flight paths, supply chains, hiking trails). These are NOT road-following routes and have no distance/time info. For actual driving or walking routes that follow roads, use 'routePlans' instead. EXAMPLE: [{points: [{lat: 52.3676, lon: 4.9041}, {lat: 52.36, lon: 4.8852}], color: '#0000FF', name: 'Flight Path'}]." ), polygons: z .array(refinedPolygonSchema) .optional() .describe( "Array of polygons and circles to display on the map. Supports both custom polygon shapes (with coordinate arrays) and circular areas (with center point and radius). Each shape can have custom styling and labels. EXAMPLE for polygon: [{type: 'polygon', coordinates: [[4.9041, 52.3676], [4.8979, 52.3745], [4.8852, 52.36], [4.9041, 52.3676]], fillColor: 'rgba(255,0,0,0.3)', label: 'Tourist Area'}]. EXAMPLE for circle: [{type: 'circle', center: {lat: 52.3676, lon: 4.9041}, radius: 1000, fillColor: 'rgba(0,0,255,0.2)', label: '1km Radius'}]." ), // Route planning mode — array of independent route calculations routePlans: z .array(routePlanSchema) .optional() .describe( "Array of route calculations to draw on the map. Each entry is an independent origin→destination trip calculated via TomTom Routing API. NOTE: For standalone route queries (directions, travel time, distance), prefer the tomtom-routing tool instead. Use routePlans here only when you need to visualize calculated routes alongside other map elements (markers, polygons) in a single map. Each plan can have its own routeType, travelMode, and color. EXAMPLE: [{origin: {lat: 52.37, lon: 4.89}, destination: {lat: 52.36, lon: 4.89}, label: 'Morning Commute'}, {origin: {lat: 48.86, lon: 2.35}, destination: {lat: 48.85, lon: 2.29}, label: 'Paris Tour'}]." ), // Display options showLabels: z .boolean() .optional() .describe( "Whether to show text labels on markers, routes, and polygons. DEFAULT: false. EXAMPLE: true to display all labels." ), // MCP App visualization control show_ui: z .boolean() .optional() .default(true) .describe( "Render the map in the interactive MCP app (zoom, pan, click). The map is drawn by the app, so leaving this on is what makes the tool produce anything visual. Set to false only when the caller just wants the request acknowledged without a map. DEFAULT: true." ), }; export type DynamicMapParams = z.input>;