Skip to main content
Glama
geometryUtils.ts11.2 kB
/* * Copyright (C) 2025 TomTom NV * * 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 { logger } from "../../utils/logger"; /** * Represents a geographic point with latitude and longitude */ export interface Point { /** Latitude in degrees (-90 to 90) */ lat: number; /** Longitude in degrees (-180 to 180) */ lon: number; } /** * Represents a marker on the map with optional styling */ export interface MapMarker extends Point { /** Optional label for the marker */ label?: string; /** Color in hex or rgba format */ color?: string; /** Display priority for label visibility */ priority?: 'critical' | 'high' | 'normal' | 'low'; } /** * Represents a polygon with styling options */ export interface MapPolygon { /** Type of shape */ type: 'polygon' | 'circle'; /** Optional label for the polygon */ label?: string; /** Fill color in rgba format */ fillColor?: string; /** Stroke color in hex format */ strokeColor?: string; /** Stroke width in pixels */ strokeWidth?: number; /** For type='polygon': Array of [longitude, latitude] coordinates */ coordinates?: [number, number][]; /** For type='circle': Center point */ center?: Point; /** For type='circle': Radius in meters */ radius?: number; } /** * Represents geographic bounds */ export interface Bounds { /** Northern latitude bound */ north: number; /** Southern latitude bound */ south: number; /** Eastern longitude bound */ east: number; /** Western longitude bound */ west: number; } /** * Result of bounds calculation including center and zoom */ export interface BoundsResult { /** Calculated bounds with padding */ bounds: Bounds; /** Center point [longitude, latitude] */ center: [number, number]; /** Calculated optimal zoom level */ zoom: number; } /** * Generate points to approximate a circle using great circle calculations */ export function generateCirclePoints( centerLat: number, centerLon: number, radiusMeters: number, numPoints: number = 64 ): Point[] { const points: Point[] = []; const earthRadiusMeters = 6371000; // Convert radius from meters to radians const radiusRadians = radiusMeters / earthRadiusMeters; // Convert center to radians const centerLatRad = (centerLat * Math.PI) / 180; const centerLonRad = (centerLon * Math.PI) / 180; for (let i = 0; i < numPoints; i++) { const angle = (2 * Math.PI * i) / numPoints; // Calculate point on circle using great circle formula const latRad = Math.asin( Math.sin(centerLatRad) * Math.cos(radiusRadians) + Math.cos(centerLatRad) * Math.sin(radiusRadians) * Math.cos(angle) ); const lonRad = centerLonRad + Math.atan2( Math.sin(angle) * Math.sin(radiusRadians) * Math.cos(centerLatRad), Math.cos(radiusRadians) - Math.sin(centerLatRad) * Math.sin(latRad) ); // Convert back to degrees points.push({ lat: (latRad * 180) / Math.PI, lon: (lonRad * 180) / Math.PI }); } return points; } /** * Calculate optimal zoom level for the given bounds and map dimensions */ export function calculateOptimalZoom( bounds: Bounds, mapWidth: number, mapHeight: number, paddingPixels: number = 80 ): number { const WORLD_PX_HEIGHT = 256; // Height of map in pixels at zoom level 0 const WORLD_PX_WIDTH = 256; // Width of map in pixels at zoom level 0 // Calculate effective dimensions const effectiveWidth = mapWidth - paddingPixels * 2; const effectiveHeight = mapHeight - paddingPixels * 2; // Calculate spans const latSpan = bounds.north - bounds.south; const lngSpan = bounds.east - bounds.west; // Calculate zoom based on latitude const latZoom = Math.log2( (effectiveHeight * 360) / (latSpan * WORLD_PX_HEIGHT) ); // Calculate zoom based on longitude const lngZoom = Math.log2( (effectiveWidth * 360) / (lngSpan * WORLD_PX_WIDTH) ); // Use the more restrictive zoom const zoom = Math.min(latZoom, lngZoom); // Add additional zoom out factor for better view const zoomOutFactor = 0.5; // Increased from 0.1 to 0.5 for better overview // Clamp to reasonable bounds after applying zoom out return Math.max(1, Math.min(17, zoom - zoomOutFactor)); } /** * Calculate enhanced bounds with buffer for a set of points */ export function calculateEnhancedBounds( markers: any[], routes: any[], mapWidth: number, mapHeight: number, polygons: any[] = [] ): BoundsResult { // Collect all points const points: Point[] = []; // Add marker points if (markers?.length > 0) { markers.forEach((marker, index) => { const coords = extractCoordinates(marker, index, "marker"); if (coords) points.push(coords); }); } // Add route points if (routes?.length > 0) { routes.forEach((route, routeIndex) => { if (Array.isArray(route)) { route.forEach((point, pointIndex) => { const coords = extractCoordinates(point, `${routeIndex}-${pointIndex}`, "route point"); if (coords) points.push(coords); }); } else if (route.points && Array.isArray(route.points)) { route.points.forEach((point: any, pointIndex: number) => { const coords = extractCoordinates(point, `${routeIndex}-${pointIndex}`, "route point"); if (coords) points.push(coords); }); } }); } // Add polygon points and handle circles if (polygons?.length > 0) { polygons.forEach((polygon, polygonIndex) => { // Handle polygon coordinates if (polygon.coordinates && Array.isArray(polygon.coordinates)) { polygon.coordinates.forEach((coord: [number, number]) => { if (Array.isArray(coord) && coord.length >= 2) { points.push({ lat: coord[1], lon: coord[0] }); } }); } // Handle circles by converting to polygon points if (polygon.type === 'circle' && polygon.center && polygon.radius) { const circlePoints = generateCirclePoints( polygon.center.lat, polygon.center.lon, polygon.radius, 64 // number of points to approximate circle ); points.push(...circlePoints); } }); } if (points.length === 0) { throw new Error("No valid coordinates found to calculate bounds"); } // Calculate raw bounds const bounds: Bounds = { north: Math.max(...points.map(p => p.lat)), south: Math.min(...points.map(p => p.lat)), east: Math.max(...points.map(p => p.lon)), west: Math.min(...points.map(p => p.lon)) }; // Calculate spans const latSpan = bounds.north - bounds.south; const lngSpan = bounds.east - bounds.west; const maxSpan = Math.max(latSpan, lngSpan); const markerCount = markers ? markers.length : 0; // Calculate buffer with enhanced padding let bufferDegrees: number; // Base buffer calculation if (markerCount === 1) { // Single marker needs more padding for better visibility // For a single point (where spans are 0), use a default buffer of 0.1 degrees bufferDegrees = maxSpan === 0 ? 0.1 : maxSpan * 0.5; } else if (maxSpan < 0.001) { // Very small area needs significant padding bufferDegrees = 0.05; } else if (maxSpan < 0.01) { // Small area needs more relative padding bufferDegrees = maxSpan * 0.8; } else if (maxSpan < 0.1) { // Medium area needs moderate padding bufferDegrees = maxSpan * 0.6; } else { // Larger areas need proportional padding bufferDegrees = maxSpan * 0.4; } // Apply route-specific padding const hasRoutes = routes && routes.length > 0; if (hasRoutes) { if (markerCount > 1) { // Routes with multiple points need extra room for route visualization bufferDegrees *= 1.8; } else { // Routes with single marker still need some extra padding bufferDegrees *= 1.5; } } // Apply polygon-specific padding const hasPolygons = polygons && polygons.length > 0; if (hasPolygons) { bufferDegrees *= 1.3; // Extra space for polygon visualization } // Scale based on marker density if (markerCount > 3) { bufferDegrees *= 1.4; // More space for dense marker clusters } // Ensure minimum buffer for better visual appeal const minBuffer = maxSpan * 0.15; bufferDegrees = Math.max(bufferDegrees, minBuffer); // Apply buffer to bounds const bufferedBounds: Bounds = { north: Math.min(90, bounds.north + bufferDegrees), south: Math.max(-90, bounds.south - bufferDegrees), east: Math.min(180, bounds.east + bufferDegrees), west: Math.max(-180, bounds.west - bufferDegrees) }; // Calculate center as [longitude, latitude] const center: [number, number] = [ (bufferedBounds.west + bufferedBounds.east) / 2, (bufferedBounds.south + bufferedBounds.north) / 2 ]; // Calculate zoom const zoom = calculateOptimalZoom(bufferedBounds, mapWidth, mapHeight); return { bounds: bufferedBounds, center, zoom }; } /** * Extract and validate coordinates from various formats */ export function extractCoordinates( item: any, index: number | string, type: string = "marker" ): Point | null { let lat: number | undefined, lon: number | undefined; if (Array.isArray(item)) { // Handle array format [lat, lon] if (item.length >= 2) { lat = item[0]; lon = item[1]; } } else if (item.coordinates && Array.isArray(item.coordinates)) { // Handle {coordinates: [lat, lon]} format if (item.coordinates.length >= 2) { lat = item.coordinates[0]; lon = item.coordinates[1]; } } else if (item.lat !== undefined && item.lon !== undefined) { // Handle {lat: x, lon: y} format (standard) lat = item.lat; lon = item.lon; } if (lat === undefined || lon === undefined) { logger.warn(`❌ Could not extract coordinates from ${type} ${index}`); return null; } try { const validLat = validateCoordinate(lat, "latitude"); const validLon = validateCoordinate(lon, "longitude"); return { lat: validLat, lon: validLon }; } catch (error: any) { logger.warn(`❌ Invalid coordinates for ${type} ${index}: ${error.message}`); return null; } } /** * Validate and sanitize coordinate values */ function validateCoordinate(value: any, type: string): number { const num = parseFloat(value); if (isNaN(num)) { throw new Error(`Invalid ${type} coordinate: ${value}`); } if (type === "latitude" && (num < -90 || num > 90)) { throw new Error(`Latitude out of range [-90, 90]: ${num}`); } if (type === "longitude" && (num < -180 || num > 180)) { throw new Error(`Longitude out of range [-180, 180]: ${num}`); } return num; }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/tomtom-international/tomtom-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server