Skip to main content
Glama
index.html53.6 kB
<!DOCTYPE html> <html> <head> <title>GrabMaps Official Integration Demo</title> <link href="https://unpkg.com/maplibre-gl@3.x/dist/maplibre-gl.css" rel="stylesheet" /> <style> body { margin: 0; padding: 0; } #map { position: absolute; top: 0; bottom: 0; width: 70%; } .coordinates { position: absolute; bottom: 10px; left: 10px; background: rgba(255,255,255,0.8); padding: 5px 10px; border-radius: 4px; font-size: 12px; z-index: 1; } .control-panel { position: absolute; top: 0; right: 0; width: 30%; height: 100%; background-color: white; padding: 10px; overflow-y: auto; box-shadow: -2px 0 5px rgba(0,0,0,0.1); } .search-results { margin-top: 10px; max-height: 200px; overflow-y: auto; } /* Tab styles */ .tab-container { width: 100%; margin-top: 15px; } .tab-buttons { overflow: hidden; border-bottom: 1px solid #ccc; display: flex; } .tab-button { background-color: #f1f1f1; border: none; outline: none; cursor: pointer; padding: 10px; flex: 1; transition: 0.3s; font-size: 14px; } .tab-button:hover { background-color: #ddd; } .tab-button.active { background-color: #3887be; color: white; } .tab-content { display: none; padding: 15px 0; border-top: none; } .tab-content.active { display: block; } /* Form styles */ label { display: block; margin-top: 10px; margin-bottom: 5px; font-weight: bold; } input, select, button { width: 100%; padding: 8px; margin-bottom: 10px; border: 1px solid #ddd; border-radius: 4px; } button { background-color: #3887be; color: white; border: none; cursor: pointer; font-weight: bold; } button:hover { background-color: #2a6496; } body { margin: 0; padding: 0; font-family: Arial, sans-serif; } .error { color: red; font-weight: bold; padding: 5px; border-left: 3px solid red; background-color: rgba(255, 0, 0, 0.05); } .map-components-container { margin-top: 10px; } h4, h5 { margin-top: 15px; margin-bottom: 10px; } hr { margin: 15px 0; border: 0; border-top: 1px solid #eee; } </style> </head> <body> <div id="map"></div> <div class="coordinates" id="coordinates"></div> <div class="control-panel"> <h3>GrabMaps Official Demo</h3> <div class="tab-container"> <div class="tab-buttons"> <button class="tab-button active" onclick="openTab(event, 'setup-tab')">Setup</button> <button class="tab-button" onclick="openTab(event, 'places-tab')">Places</button> <button class="tab-button" onclick="openTab(event, 'maps-tab')">Maps</button> <button class="tab-button" onclick="openTab(event, 'routes-tab')">Routes</button> </div> <!-- Setup Tab --> <div id="setup-tab" class="tab-content active"> <label for="api-key">API Key:</label> <input type="text" id="api-key" placeholder="Enter your GrabMaps API key"> <label for="region">Region:</label> <select id="region"> <option value="ap-southeast-5">Malaysia (ap-southeast-5)</option> <option value="ap-southeast-1">Singapore (ap-southeast-1)</option> </select> <label for="map-name">Map Name:</label> <input type="text" id="map-name" value="explore.map.Grab" placeholder="Map resource name"> <label for="place-index-name">Place Index Name:</label> <input type="text" id="place-index-name" value="explore.place.Grab" placeholder="Place index resource name"> <label for="route-calculator-name">Route Calculator Name:</label> <input type="text" id="route-calculator-name" value="explore.route-calculator.Grab" placeholder="Route calculator resource name"> <button id="initialize-map">Initialize Map</button> <p><small>Note: Map will use official GrabMaps tiles via AWS Location Service</small></p> </div> <!-- Places Tab --> <div id="places-tab" class="tab-content"> <h4>Search Places</h4> <input type="text" id="search-input" placeholder="Enter a place name"> <button id="search-button">Search</button> <div id="search-results" class="search-results"></div> <hr> <h4>Reverse Geocoding</h4> <p><small>Click anywhere on the map to find places at that location</small></p> <div id="reverse-geocode-results" class="search-results"></div> <hr> <h4>Place Details</h4> <div id="place-details"></div> </div> <!-- Maps Tab --> <div id="maps-tab" class="tab-content"> <h4>Map Components</h4> <div class="map-components-container"> <h5>Style Descriptor</h5> <button id="test-style-descriptor">Test Style Descriptor</button> <div id="style-descriptor-result"></div> <h5>Map Tile</h5> <p><small>Test fetching a specific map tile</small></p> <div> <label for="tile-z">Zoom level (z):</label> <input type="number" id="tile-z" value="15" min="0" max="20"> </div> <div> <label for="tile-x">Tile X:</label> <input type="number" id="tile-x" value="25905" min="0"> </div> <div> <label for="tile-y">Tile Y:</label> <input type="number" id="tile-y" value="14333" min="0"> </div> <button id="test-map-tile">Test Map Tile</button> <div id="map-tile-result"></div> <h5>Map Sprites</h5> <button id="test-map-sprites">Test Map Sprites</button> <div id="map-sprites-result"></div> <h5>Map Glyphs</h5> <div> <label for="font-stack">Font Stack:</label> <select id="font-stack"> <option value="Amazon Ember Regular,Noto Sans Regular">Amazon Ember Regular</option> <option value="Amazon Ember Bold,Noto Sans Bold">Amazon Ember Bold</option> <option value="Amazon Ember Medium,Noto Sans Medium">Amazon Ember Medium</option> <option value="Amazon Ember Regular Italic,Noto Sans Italic">Amazon Ember Regular Italic</option> <option value="Amazon Ember Condensed RC Regular,Noto Sans Regular">Amazon Ember Condensed RC Regular</option> </select> </div> <div> <label for="font-range">Unicode Range:</label> <input type="text" id="font-range" value="0-255" placeholder="e.g. 0-255"> <small>(Will be formatted as required)</small> </div> <button id="test-map-glyphs">Test Map Glyphs</button> <div id="map-glyphs-result"></div> </div> </div> <!-- Routes Tab --> <div id="routes-tab" class="tab-content"> <h4>Calculate Route</h4> <p><small>Search for places and click on two locations to calculate a route</small></p> <div class="route-info" id="route-info" style="display: none;"> <h5>Route Information</h5> <div id="route-details"></div> <button id="clear-route">Clear Route</button> </div> <hr> <h4>Route Matrix</h4> <p><small>Calculate travel times between multiple origins and destinations</small></p> <button id="test-route-matrix">Test Route Matrix</button> <div id="route-matrix-result"></div> </div> </div> </div> <script src="https://unpkg.com/maplibre-gl@3.x/dist/maplibre-gl.js"></script> <script> // Tab functionality function openTab(evt, tabName) { // Hide all tab content const tabContents = document.getElementsByClassName("tab-content"); for (let i = 0; i < tabContents.length; i++) { tabContents[i].classList.remove("active"); } // Remove active class from all tab buttons const tabButtons = document.getElementsByClassName("tab-button"); for (let i = 0; i < tabButtons.length; i++) { tabButtons[i].classList.remove("active"); } // Show the selected tab content and mark button as active document.getElementById(tabName).classList.add("active"); evt.currentTarget.classList.add("active"); } // Default values let defaultApiKey = "v1.public.eyJqdGkiOiJmYzkxMjE2NS1kY2ZlLTRlNDgtODhhNC00MTllYzdjZmY3ZTkifT-UshJK5jG1JYpvdS7TPh4yHfohpRnPF0yo8ULkCKNy1CvnsJbFjEGzKT6FAQzIOTE_JBidTXhEbIsctFrrP3wGhHLJwvQgR7XG9li4RElOvWs9ZKPzGn0FRBr3cBwLXoVHvqef2wMv6nXepVICsLrYRs_MkAZQb0-Bv46nA__nq4y5y2VdCUFXXXLXK9YCrGnEvZqnnj_4uvggzxxwv3n-JDFaQUG-HXS-iAq1HP7hRMiZeVSpp_J7vQqPr9rpVl1kCTk0PZKi0cX5hG5RB7z5UCVwabVbXFCLrBLjeswQGLc9RDa5oJDuNF3a4sUlk7T-bky3FUyBYKw5z6qEz8A.MDgwYWE2NDctYWYyMy00NzU1LTk5ODMtOTZiZDUyY2FiNjdl"; let defaultMapName = "explore.map.Grab"; let defaultRegion = "ap-southeast-5"; let defaultPlaceIndexName = "explore.place.Grab"; let defaultRouteCalculatorName = "explore.route-calculator.Grab"; // Initial center coordinates (Kuala Lumpur) const INITIAL_CENTER = [101.6942371, 3.1516964]; const INITIAL_ZOOM = 11; // MCP server URL const MCP_SERVER_URL = 'http://localhost:3000'; // Global variables let map = null; let markers = []; let routeSource = null; let routeLayer = null; // Fill form fields with defaults document.getElementById('api-key').value = defaultApiKey; document.getElementById('map-name').value = defaultMapName; document.getElementById('region').value = defaultRegion; document.getElementById('place-index-name').value = defaultPlaceIndexName; document.getElementById('route-calculator-name').value = defaultRouteCalculatorName; // Initialize map button click handler document.getElementById('initialize-map').addEventListener('click', initializeMap); // Initialize map function function initializeMap() { // Get values from form const apiKey = document.getElementById('api-key').value || defaultApiKey; const mapName = document.getElementById('map-name').value || defaultMapName; const region = document.getElementById('region').value || defaultRegion; // Remove existing map if it exists if (map) { map.remove(); } // Clear any existing markers clearMarkers(); // Initialize the map map = new maplibregl.Map({ container: "map", style: `https://maps.geo.${region}.amazonaws.com/maps/v0/maps/${mapName}/style-descriptor?key=${apiKey}`, center: INITIAL_CENTER, zoom: INITIAL_ZOOM, }); // Add navigation controls map.addControl(new maplibregl.NavigationControl(), "top-left"); // Display coordinates map.on('mousemove', function(e) { const coords = document.getElementById('coordinates'); coords.innerHTML = `Longitude: ${e.lngLat.lng.toFixed(6)} | Latitude: ${e.lngLat.lat.toFixed(6)}`; }); // Handle click for reverse geocoding map.on('click', function(e) { // Add a marker at click location const clickMarker = new maplibregl.Marker({color: '#ff0000'}) .setLngLat([e.lngLat.lng, e.lngLat.lat]) .addTo(map); markers.push(clickMarker); // Perform reverse geocoding reverseGeocode(e.lngLat.lng, e.lngLat.lat); // Switch to Places tab openTab({currentTarget: document.querySelector('button[onclick="openTab(event, \'places-tab\')"]')}, 'places-tab'); }); // Wait for map to load map.on('load', function() { // Add a source for route lines map.addSource('route', { type: 'geojson', data: { type: 'Feature', properties: {}, geometry: { type: 'LineString', coordinates: [] } } }); routeSource = map.getSource('route'); // Add a layer for route lines map.addLayer({ id: 'route', type: 'line', source: 'route', layout: { 'line-join': 'round', 'line-cap': 'round' }, paint: { 'line-color': '#3887be', 'line-width': 5, 'line-opacity': 0.75 } }); routeLayer = 'route'; // Enable buttons document.getElementById('search-button').disabled = false; document.getElementById('test-style-descriptor').disabled = false; document.getElementById('test-map-tile').disabled = false; document.getElementById('test-map-sprites').disabled = false; document.getElementById('test-map-glyphs').disabled = false; document.getElementById('test-route-matrix').disabled = false; // Success message alert('Map initialized successfully! You can now use all features.'); }); // Set up event handlers for all buttons setupEventHandlers(); } // Setup all event handlers function setupEventHandlers() { // Places tab document.getElementById('search-button').addEventListener('click', searchPlaces); // Maps tab document.getElementById('test-style-descriptor').addEventListener('click', testStyleDescriptor); document.getElementById('test-map-tile').addEventListener('click', testMapTile); document.getElementById('test-map-sprites').addEventListener('click', testMapSprites); document.getElementById('test-map-glyphs').addEventListener('click', testMapGlyphs); // Routes tab document.getElementById('clear-route').addEventListener('click', clearRoute); document.getElementById('test-route-matrix').addEventListener('click', testRouteMatrix); } // Search places function async function searchPlaces() { const query = document.getElementById('search-input').value.trim(); if (!query) return; try { const placeIndexName = document.getElementById('place-index-name').value; console.log(`Searching places with query: ${query}, endpoint: ${MCP_SERVER_URL}/searchPlaceIndexForText`); const response = await fetch(`${MCP_SERVER_URL}/searchPlaceIndexForText`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query, maxResults: 10 }) }); if (!response.ok) { const errorText = await response.text(); throw new Error(`HTTP error ${response.status}: ${errorText}`); } const data = await response.json(); displaySearchResults(data.results || []); } catch (error) { console.error('Error searching places:', error); document.getElementById('search-results').innerHTML = `<p class="error">Error: ${error.message}</p>`; } } // Reverse geocode function async function reverseGeocode(longitude, latitude) { try { const placeIndexName = document.getElementById('place-index-name').value; console.log(`Reverse geocoding at: ${longitude}, ${latitude}, endpoint: ${MCP_SERVER_URL}/searchPlaceIndexForPosition`); const response = await fetch(`${MCP_SERVER_URL}/searchPlaceIndexForPosition`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ longitude: longitude, latitude: latitude, maxResults: 3, placeIndexName: placeIndexName }) }); if (!response.ok) { const errorText = await response.text(); throw new Error(`HTTP error ${response.status}: ${errorText}`); } const data = await response.json(); displayReverseGeocodeResults(data.results); } catch (error) { console.error('Error reverse geocoding:', error); document.getElementById('reverse-geocode-results').innerHTML = `<p class="error">Error: ${error.message}</p>`; } } // Display reverse geocode results function displayReverseGeocodeResults(results) { const resultsContainer = document.getElementById('reverse-geocode-results'); resultsContainer.innerHTML = ''; if (!results || results.length === 0) { resultsContainer.innerHTML = '<p>No results found</p>'; return; } const ul = document.createElement('ul'); ul.style.listStyle = 'none'; ul.style.padding = '0'; results.forEach((result) => { const li = document.createElement('li'); li.style.padding = '5px 0'; li.style.borderBottom = '1px solid #eee'; li.style.cursor = 'pointer'; li.innerHTML = `<strong>${result.name}</strong><br><small>${result.distance ? result.distance.toFixed(2) + ' m away' : ''}</small>`; li.addEventListener('click', () => { getPlaceDetails(result.placeId); }); ul.appendChild(li); }); resultsContainer.appendChild(ul); } // Get place details async function getPlaceDetails(placeId) { try { const placeIndexName = document.getElementById('place-index-name').value; const response = await fetch(`${MCP_SERVER_URL}/getPlace`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ placeId: placeId, placeIndexName: placeIndexName }) }); if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); } const data = await response.json(); displayPlaceDetails(data); } catch (error) { console.error('Error getting place details:', error); document.getElementById('place-details').innerHTML = `<p style="color: red;">Error: ${error.message}</p>`; } } // Display place details function displayPlaceDetails(place) { const detailsContainer = document.getElementById('place-details'); let addressStr = ''; if (place.address) { addressStr = `<p><strong>Address:</strong> ${place.address}</p>`; } detailsContainer.innerHTML = ` <div style="padding: 10px; background: #f0f0f0; border-radius: 4px;"> <h5>${place.name}</h5> ${addressStr} <p><strong>Coordinates:</strong> ${place.coordinates.latitude}, ${place.coordinates.longitude}</p> <p><strong>Place ID:</strong> ${place.placeId}</p> </div> `; } // Display search results function displaySearchResults(results) { // Clear existing markers clearMarkers(); const resultsContainer = document.getElementById('search-results'); resultsContainer.innerHTML = ''; if (!results || results.length === 0) { resultsContainer.innerHTML = '<p>No results found</p>'; return; } const ul = document.createElement('ul'); ul.style.listStyle = 'none'; ul.style.padding = '0'; results.forEach((result, index) => { // Create marker for each result const marker = new maplibregl.Marker() .setLngLat([result.coordinates.longitude, result.coordinates.latitude]) .addTo(map); markers.push(marker); // Create list item for each result const listItem = document.createElement('li'); listItem.style.padding = '5px 0'; listItem.style.borderBottom = '1px solid #eee'; listItem.style.cursor = 'pointer'; // Format address properly to avoid [object Object] display let addressText = ''; if (result.address) { if (typeof result.address === 'string') { addressText = result.address; } else if (typeof result.address === 'object') { // Try to extract meaningful address information addressText = JSON.stringify(result.address) .replace(/[{}",]/g, '') .replace(/:/g, ': ') .replace(/\\n/g, '<br>'); } } listItem.innerHTML = `<strong>${result.name}</strong><br><small>${addressText}</small>`; // Add click handler to select this place listItem.addEventListener('click', () => { selectPlace(result); }); ul.appendChild(listItem); }); resultsContainer.appendChild(ul); // Fit map to markers if (markers.length > 0) { const bounds = new maplibregl.LngLatBounds(); markers.forEach(marker => bounds.extend(marker.getLngLat())); map.fitBounds(bounds, { padding: 100 }); } } // Selected places for route calculation let selectedPlaces = []; // Select a place for route calculation function selectPlace(place) { console.log('Selecting place:', place); // Add to selected places (max 2) if (selectedPlaces.length < 2) { selectedPlaces.push(place); } else { // Clear existing route before replacing places clearRoute(); // Start fresh with this place selectedPlaces = [place]; } // Add a marker for this place if not already added const placeCoords = [place.coordinates.longitude, place.coordinates.latitude]; const markerExists = markers.some(marker => { const lngLat = marker.getLngLat(); return lngLat.lng === placeCoords[0] && lngLat.lat === placeCoords[1]; }); if (!markerExists) { // Add a marker for this place const marker = new maplibregl.Marker({ color: selectedPlaces.length === 1 ? '#33cc33' : '#3366ff' // Green for origin, blue for destination }) .setLngLat(placeCoords) .addTo(map); markers.push(marker); } // If we have 2 places, calculate route if (selectedPlaces.length === 2) { calculateRoute(selectedPlaces[0], selectedPlaces[1]); // Switch to Routes tab openTab({currentTarget: document.querySelector('button[onclick="openTab(event, \'routes-tab\')"]')}, 'routes-tab'); } } // Calculate route between two places async function calculateRoute(origin, destination) { try { // Show loading indicator document.getElementById('route-details').innerHTML = '<p>Calculating route...</p>'; document.getElementById('route-info').style.display = 'block'; const routeCalculatorName = document.getElementById('route-calculator-name').value; console.log(`Calculating route from ${origin.coordinates.longitude},${origin.coordinates.latitude} to ${destination.coordinates.longitude},${destination.coordinates.latitude}, endpoint: ${MCP_SERVER_URL}/calculateRoute, routeCalculatorName: ${routeCalculatorName}`); // Clear any existing route on the map before calculating a new one if (routeSource) { routeSource.setData({ type: 'Feature', properties: {}, geometry: { type: 'LineString', coordinates: [] } }); } const response = await fetch(`${MCP_SERVER_URL}/calculateRoute`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ departurePosition: [origin.coordinates.longitude, origin.coordinates.latitude], destinationPosition: [destination.coordinates.longitude, destination.coordinates.latitude], travelMode: 'Car', routeCalculatorName: routeCalculatorName }) }); if (!response.ok) { const errorText = await response.text(); throw new Error(`HTTP error ${response.status}: ${errorText}`); } const data = await response.json(); // Validate route data if (!data || !data.legs || data.legs.length === 0) { throw new Error('Invalid route data: No route legs found'); } // Check if any legs have geometry let hasValidGeometry = false; for (const leg of data.legs) { if (leg.geometry && leg.geometry.lineString && leg.geometry.lineString.length > 0) { hasValidGeometry = true; break; } } if (!hasValidGeometry) { throw new Error('No valid route geometry found between these locations'); } displayRoute(data); } catch (error) { console.error('Error calculating route:', error); document.getElementById('route-details').innerHTML = `<p class="error">Error: ${error.message}</p>`; document.getElementById('route-info').style.display = 'block'; } } // Display route on map function displayRoute(routeData) { // Show route info panel document.getElementById('route-info').style.display = 'block'; // Update route source with new coordinates if (routeSource && routeData.legs && routeData.legs.length > 0) { const coordinates = []; routeData.legs.forEach(leg => { if (leg.geometry && leg.geometry.lineString) { coordinates.push(...leg.geometry.lineString); } }); console.log(`Route has ${coordinates.length} coordinate points`); if (coordinates.length === 0) { document.getElementById('route-details').innerHTML = `<p class="error">Error: No valid route found between these locations</p>`; return; } routeSource.setData({ type: 'Feature', properties: {}, geometry: { type: 'LineString', coordinates: coordinates } }); // Display route details const distanceKm = routeData.summary.distance / 1000; const durationMin = routeData.summary.durationSeconds / 60; document.getElementById('route-details').innerHTML = ` <p><strong>Distance:</strong> ${distanceKm.toFixed(2)} km</p> <p><strong>Duration:</strong> ${durationMin.toFixed(0)} minutes</p> <p><strong>Origin:</strong> ${selectedPlaces[0].name || 'Selected location'}</p> <p><strong>Destination:</strong> ${selectedPlaces[1].name || 'Selected location'}</p> `; // Fit map to route const bounds = new maplibregl.LngLatBounds(); coordinates.forEach(coord => bounds.extend(coord)); map.fitBounds(bounds, { padding: 100 }); } else { document.getElementById('route-details').innerHTML = `<p class="error">Error: Invalid route data received</p>`; } } // Test route matrix calculation async function testRouteMatrix() { try { let origins = []; let destinations = []; // Check if we have markers on the map to use as origins/destinations if (markers.length >= 2) { // Use the first marker as origin 1 const origin1 = markers[0].getLngLat(); origins.push([origin1.lng, origin1.lat]); // Use the second marker as origin 2 or destination 1 const point2 = markers[1].getLngLat(); if (markers.length >= 3) { // If we have 3+ markers, use second as origin 2 and third as destination 1 origins.push([point2.lng, point2.lat]); const dest1 = markers[2].getLngLat(); destinations.push([dest1.lng, dest1.lat]); // If we have a fourth marker, use it as destination 2 if (markers.length >= 4) { const dest2 = markers[3].getLngLat(); destinations.push([dest2.lng, dest2.lat]); } else { // Otherwise create a nearby point as destination 2 destinations.push([dest1.lng + 0.01, dest1.lat - 0.01]); } } else { // If we only have 2 markers, use second as destination 1 destinations.push([point2.lng, point2.lat]); // Create a nearby point as destination 2 destinations.push([point2.lng + 0.01, point2.lat - 0.01]); // Create a nearby point as origin 2 origins.push([origin1.lng - 0.01, origin1.lat + 0.01]); } } else { // Fallback to default test locations if no markers origins = [ [101.6942, 3.1516], // KLCC [101.6867, 3.1577] // KL Tower ]; destinations = [ [101.7068, 3.1587], // National Museum [101.6983, 3.1349] // KL Sentral ]; } // Add debug information to the UI before making the request const debugContainer = document.getElementById('route-matrix-result'); debugContainer.innerHTML = '<h5>Route Matrix Results</h5>'; debugContainer.innerHTML += '<p>Calculating route matrix...</p>'; const debugInfo = document.createElement('div'); debugInfo.style.marginBottom = '10px'; debugInfo.style.fontSize = '12px'; debugInfo.style.color = '#666'; debugInfo.innerHTML = '<strong>Debug Info (Request):</strong><br>'; debugInfo.innerHTML += '<strong>Origins:</strong><br>'; origins.forEach((origin, i) => { debugInfo.innerHTML += `Origin ${i+1}: [${origin[0]}, ${origin[1]}]<br>`; }); debugInfo.innerHTML += '<strong>Destinations:</strong><br>'; destinations.forEach((dest, i) => { debugInfo.innerHTML += `Destination ${i+1}: [${dest[0]}, ${dest[1]}]<br>`; }); debugContainer.appendChild(debugInfo); const routeCalculatorName = document.getElementById('route-calculator-name').value; console.log(`Calculating route matrix with ${origins.length} origins and ${destinations.length} destinations, endpoint: ${MCP_SERVER_URL}/calculateRouteMatrix, routeCalculatorName: ${routeCalculatorName}`); const response = await fetch(`${MCP_SERVER_URL}/calculateRouteMatrix`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ departurePositions: origins, destinationPositions: destinations, travelMode: 'Car', routeCalculatorName: routeCalculatorName }) }); if (!response.ok) { const errorText = await response.text(); throw new Error(`HTTP error ${response.status}: ${errorText}`); } const data = await response.json(); displayRouteMatrix(data, origins, destinations); } catch (error) { console.error('Error calculating route matrix:', error); document.getElementById('route-matrix-result').innerHTML = `<p class="error">Error: ${error.message}</p>`; } } // Display route matrix results function displayRouteMatrix(matrixData, origins, destinations) { const container = document.getElementById('route-matrix-result'); container.innerHTML = '<h5>Route Matrix Results</h5>'; console.log('Route matrix data:', matrixData); console.log('Origins:', origins); console.log('Destinations:', destinations); // Add debug information const debugInfo = document.createElement('div'); debugInfo.style.marginBottom = '10px'; debugInfo.style.fontSize = '12px'; debugInfo.style.color = '#666'; // Show coordinates used for calculation debugInfo.innerHTML = '<strong>Debug Info (Response):</strong><br>'; debugInfo.innerHTML += '<strong>Origins:</strong><br>'; origins.forEach((origin, i) => { debugInfo.innerHTML += `Origin ${i+1}: [${origin[0]}, ${origin[1]}]<br>`; }); debugInfo.innerHTML += '<strong>Destinations:</strong><br>'; destinations.forEach((dest, i) => { debugInfo.innerHTML += `Destination ${i+1}: [${dest[0]}, ${dest[1]}]<br>`; }); // Add response status if (matrixData && matrixData.routeMatrix && Array.isArray(matrixData.routeMatrix)) { debugInfo.innerHTML += `<strong>Response Status:</strong> Success (${matrixData.routeMatrix.length} origin rows)<br>`; } else { debugInfo.innerHTML += '<strong>Response Status:</strong> <span style="color: red;">Error - Invalid or empty response</span><br>'; } container.appendChild(debugInfo); if (!matrixData || !matrixData.routeMatrix || !Array.isArray(matrixData.routeMatrix)) { container.innerHTML += '<p style="color: red;">No valid matrix data available. Check console for details.</p>'; return; } // Create table const table = document.createElement('table'); table.style.width = '100%'; table.style.borderCollapse = 'collapse'; table.style.marginTop = '10px'; // Create header row const headerRow = document.createElement('tr'); headerRow.innerHTML = '<th>Origin → Destination</th>'; for (let i = 0; i < destinations.length; i++) { const th = document.createElement('th'); th.textContent = `Dest ${i+1}`; th.style.padding = '5px'; th.style.border = '1px solid #ddd'; headerRow.appendChild(th); } table.appendChild(headerRow); // Create data rows for (let i = 0; i < origins.length; i++) { const row = document.createElement('tr'); const originCell = document.createElement('td'); originCell.textContent = `Origin ${i+1}`; originCell.style.padding = '5px'; originCell.style.border = '1px solid #ddd'; originCell.style.fontWeight = 'bold'; row.appendChild(originCell); // Check if this origin has data if (i >= matrixData.routeMatrix.length || !Array.isArray(matrixData.routeMatrix[i])) { for (let j = 0; j < destinations.length; j++) { const cell = document.createElement('td'); cell.textContent = 'No data'; cell.style.padding = '5px'; cell.style.border = '1px solid #ddd'; cell.style.textAlign = 'center'; row.appendChild(cell); } } else { // Add cells for each destination for (let j = 0; j < destinations.length; j++) { const cell = document.createElement('td'); // Check if we have data for this destination if (j < matrixData.routeMatrix[i].length) { const routeInfo = matrixData.routeMatrix[i][j]; if (routeInfo && routeInfo.distance !== undefined && routeInfo.distance !== null) { const distance = (routeInfo.distance / 1000).toFixed(2); const duration = ((routeInfo.durationSeconds || routeInfo.duration) / 60).toFixed(0); cell.innerHTML = `${distance} km<br>${duration} min`; } else if (routeInfo && routeInfo.error) { cell.innerHTML = `<span style="color: red;">Error: ${routeInfo.error.code || routeInfo.error.message || 'Unknown'}</span>`; } else { cell.innerHTML = `<span style="color: red;">Error: Unknown</span>`; } } else { cell.textContent = 'N/A'; } cell.style.padding = '5px'; cell.style.border = '1px solid #ddd'; cell.style.textAlign = 'center'; row.appendChild(cell); } } table.appendChild(row); } container.appendChild(table); // Add origin/destination coordinates info const coordInfo = document.createElement('div'); coordInfo.style.marginTop = '10px'; coordInfo.style.fontSize = '12px'; let originsText = '<p><strong>Origins:</strong><br>'; origins.forEach((coord, i) => { originsText += `Origin ${i+1}: [${coord[0].toFixed(4)}, ${coord[1].toFixed(4)}]<br>`; }); originsText += '</p>'; let destsText = '<p><strong>Destinations:</strong><br>'; destinations.forEach((coord, i) => { destsText += `Destination ${i+1}: [${coord[0].toFixed(4)}, ${coord[1].toFixed(4)}]<br>`; }); destsText += '</p>'; coordInfo.innerHTML = originsText + destsText; container.appendChild(coordInfo); } // Test map style descriptor async function testStyleDescriptor() { try { const mapName = document.getElementById('map-name').value; console.log(`Testing style descriptor for map: ${mapName}, endpoint: ${MCP_SERVER_URL}/getMapStyleDescriptor`); const response = await fetch(`${MCP_SERVER_URL}/getMapStyleDescriptor`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ mapName: mapName }) }); if (!response.ok) { const errorText = await response.text(); throw new Error(`HTTP error ${response.status}: ${errorText}`); } const data = await response.json(); displayStyleDescriptor(data); } catch (error) { console.error('Error getting style descriptor:', error); document.getElementById('style-descriptor-result').innerHTML = `<p style="color: red;">Error: ${error.message}</p>`; } } // Display style descriptor function displayStyleDescriptor(styleData) { const container = document.getElementById('style-descriptor-result'); // Create a formatted JSON display const pre = document.createElement('pre'); pre.style.maxHeight = '200px'; pre.style.overflow = 'auto'; pre.style.background = '#f5f5f5'; pre.style.padding = '10px'; pre.style.borderRadius = '4px'; pre.style.fontSize = '12px'; // Format the JSON with indentation pre.textContent = JSON.stringify(styleData, null, 2); container.innerHTML = ''; container.appendChild(pre); } // Test map tile async function testMapTile() { try { const mapName = document.getElementById('map-name').value; let z = parseInt(document.getElementById('tile-z').value); let x = parseInt(document.getElementById('tile-x').value); let y = parseInt(document.getElementById('tile-y').value); // Validate inputs if (isNaN(z) || isNaN(x) || isNaN(y)) { throw new Error('Tile coordinates must be valid numbers'); } // Ensure zoom level doesn't exceed maximum (14 for VectorGrabStandardLight) const MAX_ZOOM = 14; if (z > MAX_ZOOM) { console.warn(`Zoom level ${z} exceeds maximum of ${MAX_ZOOM}. Limiting to ${MAX_ZOOM}.`); z = MAX_ZOOM; document.getElementById('tile-z').value = MAX_ZOOM; } // Validate X and Y coordinates based on zoom level // For zoom level z, valid X and Y values are 0 to 2^z-1 const maxCoord = Math.pow(2, z) - 1; // Auto-correct X and Y if they're out of range if (x < 0) { x = 0; document.getElementById('tile-x').value = 0; } else if (x > maxCoord) { x = maxCoord; document.getElementById('tile-x').value = maxCoord; console.warn(`X coordinate adjusted to maximum value: ${maxCoord}`); } if (y < 0) { y = 0; document.getElementById('tile-y').value = 0; } else if (y > maxCoord) { y = maxCoord; document.getElementById('tile-y').value = maxCoord; console.warn(`Y coordinate adjusted to maximum value: ${maxCoord}`); } console.log(`Getting map tile with z: ${z}, x: ${x}, y: ${y}, mapName: ${mapName}`); const response = await fetch(`${MCP_SERVER_URL}/getMapTile`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ mapName: mapName, z: z, x: x, y: y }) }); if (!response.ok) { const errorText = await response.text(); throw new Error(`HTTP error ${response.status}: ${errorText}`); } const data = await response.json(); displayMapTile(data); } catch (error) { console.error('Error getting map tile:', error); document.getElementById('map-tile-result').innerHTML = `<p style="color: red;">Error: ${error.message}</p>`; } } // Display map tile function displayMapTile(tileData) { const container = document.getElementById('map-tile-result'); container.innerHTML = ''; if (tileData && tileData.blob) { // Create an image from the base64 blob const img = document.createElement('img'); img.src = `data:image/png;base64,${tileData.blob}`; img.style.maxWidth = '100%'; img.style.border = '1px solid #ddd'; img.style.borderRadius = '4px'; container.appendChild(img); } else { container.innerHTML = '<p>No tile data available</p>'; } } // Test map sprites async function testMapSprites() { try { const mapName = document.getElementById('map-name').value; console.log(`Testing map sprites for map: ${mapName}, endpoint: ${MCP_SERVER_URL}/getMapSprites`); const response = await fetch(`${MCP_SERVER_URL}/getMapSprites`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ mapName: mapName, fileName: 'sprites.png' }) }); if (!response.ok) { const errorText = await response.text(); throw new Error(`HTTP error ${response.status}: ${errorText}`); } const data = await response.json(); displayMapSprites(data); } catch (error) { console.error('Error getting map sprites:', error); document.getElementById('map-sprites-result').innerHTML = `<p class="error">Error: ${error.message}</p>`; } } // Display map sprites function displayMapSprites(spriteData) { const container = document.getElementById('map-sprites-result'); container.innerHTML = ''; if (spriteData && spriteData.blob) { // Create an image from the base64 blob const img = document.createElement('img'); img.src = `data:image/png;base64,${spriteData.blob}`; img.style.maxWidth = '100%'; img.style.border = '1px solid #ddd'; img.style.borderRadius = '4px'; container.appendChild(img); // If we have sprite JSON data, display it if (spriteData.json) { const pre = document.createElement('pre'); pre.style.maxHeight = '150px'; pre.style.overflow = 'auto'; pre.style.background = '#f5f5f5'; pre.style.padding = '10px'; pre.style.borderRadius = '4px'; pre.style.fontSize = '12px'; pre.style.marginTop = '10px'; // Format the JSON with indentation pre.textContent = JSON.stringify(spriteData.json, null, 2); container.appendChild(pre); } } else { container.innerHTML = '<p>No sprite data available</p>'; } } // Test map glyphs async function testMapGlyphs() { try { const mapName = document.getElementById('map-name').value; const fontStack = document.getElementById('font-stack').value || 'Arial Unicode MS'; const unicodeRange = document.getElementById('font-range').value || '0-255'; if (!fontStack || !unicodeRange) { throw new Error('Font stack and Unicode range are required'); } console.log(`Testing map glyphs with fontStack: ${fontStack}, unicodeRange: ${unicodeRange}, endpoint: ${MCP_SERVER_URL}/getMapGlyphs`); const response = await fetch(`${MCP_SERVER_URL}/getMapGlyphs`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ mapName: mapName, fontStack: fontStack, unicodeRange: unicodeRange }) }); if (!response.ok) { const errorText = await response.text(); throw new Error(`HTTP error ${response.status}: ${errorText}`); } const data = await response.json(); if (!data || !data.glyphs) { throw new Error('Invalid glyph data received from server'); } displayMapGlyphs(data); } catch (error) { console.error('Error getting map glyphs:', error); document.getElementById('map-glyphs-result').innerHTML = `<p style="color: red;">Error: ${error.message}</p>`; } } // Display map glyphs function displayMapGlyphs(glyphData) { const container = document.getElementById('map-glyphs-result'); container.innerHTML = ''; if (glyphData && glyphData.glyphs) { // Create a div with info about the glyph data const info = document.createElement('div'); info.innerHTML = `<p>Glyph data received successfully (${glyphData.glyphs.length} bytes)</p>`; container.appendChild(info); // Create a pre element to show some of the raw data const pre = document.createElement('pre'); pre.style.maxHeight = '100px'; pre.style.overflow = 'auto'; pre.style.background = '#f5f5f5'; pre.style.padding = '10px'; pre.style.borderRadius = '4px'; pre.style.fontSize = '12px'; // Show just the first part of the base64 data const previewLength = Math.min(100, glyphData.glyphs.length); pre.textContent = `${glyphData.glyphs.substring(0, previewLength)}...`; container.appendChild(pre); // Add content type info if (glyphData.contentType) { const contentTypeInfo = document.createElement('p'); contentTypeInfo.innerHTML = `<small>Content Type: ${glyphData.contentType}</small>`; container.appendChild(contentTypeInfo); } // Add note about binary data const note = document.createElement('p'); note.innerHTML = '<small>Note: Glyph data is binary and cannot be directly displayed</small>'; container.appendChild(note); } else { container.innerHTML = '<p>No glyph data available</p>'; } } // Clear route and selected places function clearRoute() { console.log('Clearing route and selected places'); selectedPlaces = []; // Clear route from map if (routeSource) { routeSource.setData({ type: 'Feature', properties: {}, geometry: { type: 'LineString', coordinates: [] } }); } // Clear all markers clearMarkers(); // Hide route info panel document.getElementById('route-info').style.display = 'none'; document.getElementById('route-details').innerHTML = ''; } // Clear all markers from the map function clearMarkers() { markers.forEach(marker => marker.remove()); markers = []; } // Initialize map on page load // Function to update tile coordinate limits based on zoom level function updateTileCoordinateLimits() { const z = parseInt(document.getElementById('tile-z').value) || 0; const maxCoord = Math.pow(2, z) - 1; // Update X and Y inputs const xInput = document.getElementById('tile-x'); const yInput = document.getElementById('tile-y'); // Set max attribute xInput.setAttribute('max', maxCoord); yInput.setAttribute('max', maxCoord); // Adjust values if they exceed the new maximum if (parseInt(xInput.value) > maxCoord) { xInput.value = maxCoord; } if (parseInt(yInput.value) > maxCoord) { yInput.value = maxCoord; } // Update placeholder text xInput.placeholder = `0-${maxCoord}`; yInput.placeholder = `0-${maxCoord}`; } document.addEventListener('DOMContentLoaded', function() { // Disable search button until map is initialized document.getElementById('search-button').disabled = true; // Set up event handlers for map components document.getElementById('test-style-descriptor').addEventListener('click', testStyleDescriptor); document.getElementById('test-map-tile').addEventListener('click', testMapTile); document.getElementById('test-map-sprites').addEventListener('click', testMapSprites); document.getElementById('test-map-glyphs').addEventListener('click', testMapGlyphs); document.getElementById('test-route-matrix').addEventListener('click', testRouteMatrix); document.getElementById('clear-route').addEventListener('click', clearRoute); // Add event listener to zoom level input to update X and Y max values document.getElementById('tile-z').addEventListener('change', updateTileCoordinateLimits); // Initialize tile coordinate limits updateTileCoordinateLimits(); }); </script> </body> </html>

Latest Blog Posts

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/hithereiamaliff/mcp-grabmaps'

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