Skip to content

Commit 6aeefab

Browse files
committed
fix(map-server): use CARTO @2x retina tiles for sharp rendering
Root cause: Standard OSM tiles are 256x256 pixels. On Retina displays (devicePixelRatio=2), these get scaled up 2x, making them look pixelated. Solution: Switch to CARTO Voyager tiles which provide @2x retina support: - Detect high-DPI displays using window.devicePixelRatio - Use 512x512 tiles with @2x suffix on Retina displays - Use 256x256 tiles on standard displays - Set tileWidth/tileHeight to match actual tile size - Use CARTO subdomains (a,b,c,d) for load balancing - Update CSP to allow CARTO tile domains CARTO Voyager provides clean OSM-based tiles with free @2x retina support, no API key required.
1 parent cb6f9ab commit 6aeefab

2 files changed

Lines changed: 36 additions & 20 deletions

File tree

examples/map-server/server.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,15 +97,16 @@ export function createServer(): McpServer {
9797
const cspMeta = {
9898
ui: {
9999
csp: {
100-
// Allow fetching tiles from OSM and Cesium assets
100+
// Allow fetching tiles from CARTO, OSM (for geocoding), and Cesium assets
101101
connectDomains: [
102-
"https://*.openstreetmap.org",
102+
"https://*.basemaps.cartocdn.com", // CARTO @2x tiles
103+
"https://*.openstreetmap.org", // Nominatim geocoding
103104
"https://cesium.com",
104105
"https://*.cesium.com",
105106
],
106107
// Allow loading tile images, scripts, and Cesium CDN resources
107108
resourceDomains: [
108-
"https://*.openstreetmap.org",
109+
"https://*.basemaps.cartocdn.com", // CARTO @2x tiles
109110
"https://cesium.com",
110111
"https://*.cesium.com",
111112
],

examples/map-server/src/mcp-app.ts

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,7 @@ function getScaleDimensions(extent: BoundingBox): {
130130
* Search for places within a bounding box using Nominatim
131131
* Returns array of place names visible in the area
132132
*/
133-
async function searchPlacesInBox(
134-
extent: BoundingBox,
135-
): Promise<string[]> {
133+
async function searchPlacesInBox(extent: BoundingBox): Promise<string[]> {
136134
try {
137135
// Nominatim viewbox format: west,south,east,north (x1,y1,x2,y2)
138136
const viewbox = `${extent.west},${extent.south},${extent.east},${extent.north}`;
@@ -289,31 +287,48 @@ async function initCesium(): Promise<any> {
289287

290288
log.info("Globe configured");
291289

292-
// Create and add OpenStreetMap imagery layer
293-
log.info("Creating OSM imagery provider...");
290+
// Create and add map imagery layer
291+
// Use CARTO Voyager tiles with @2x for high-DPI displays (512x512 tiles)
292+
// Standard OSM tiles are only 256x256 which looks pixelated on Retina displays
293+
log.info("Creating CARTO Voyager @2x imagery provider...");
294294
try {
295-
const osmProvider = new Cesium.UrlTemplateImageryProvider({
296-
url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
295+
// Detect if we're on a high-DPI display
296+
const isHighDPI = window.devicePixelRatio > 1;
297+
const tileSize = isHighDPI ? 512 : 256;
298+
const scale = isHighDPI ? "@2x" : "";
299+
300+
// CARTO Voyager provides clean OSM-based tiles with @2x retina support
301+
// URL format: https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}@2x.png
302+
const cartoProvider = new Cesium.UrlTemplateImageryProvider({
303+
url: `https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}${scale}.png`,
304+
subdomains: ["a", "b", "c", "d"],
297305
minimumLevel: 0,
298-
maximumLevel: 19,
299-
credit: new Cesium.Credit("© OpenStreetMap contributors"),
306+
maximumLevel: 20,
307+
tileWidth: tileSize,
308+
tileHeight: tileSize,
309+
credit: new Cesium.Credit(
310+
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, © <a href="https://carto.com/attributions">CARTO</a>',
311+
true,
312+
),
300313
});
301-
log.info("OSM provider created");
314+
log.info(
315+
`CARTO provider created (${tileSize}x${tileSize} tiles, scale=${scale || "1x"})`,
316+
);
302317

303318
// Log any imagery provider errors
304-
osmProvider.errorEvent.addEventListener((error: any) => {
305-
log.error("OSM imagery provider error:", error);
319+
cartoProvider.errorEvent.addEventListener((error: any) => {
320+
log.error("CARTO imagery provider error:", error);
306321
});
307322

308323
// Wait for provider to be ready
309-
if (osmProvider.ready !== undefined && !osmProvider.ready) {
310-
log.info("Waiting for OSM provider to be ready...");
311-
await osmProvider.readyPromise;
312-
log.info("OSM provider ready");
324+
if (cartoProvider.ready !== undefined && !cartoProvider.ready) {
325+
log.info("Waiting for CARTO provider to be ready...");
326+
await cartoProvider.readyPromise;
327+
log.info("CARTO provider ready");
313328
}
314329

315330
// Add the imagery layer to the viewer
316-
cesiumViewer.imageryLayers.addImageryProvider(osmProvider);
331+
cesiumViewer.imageryLayers.addImageryProvider(cartoProvider);
317332
log.info(
318333
"OSM imagery layer added, layer count:",
319334
cesiumViewer.imageryLayers.length,

0 commit comments

Comments
 (0)