code icon Code

Capture Static Map

Capture a map tile from an address or coordinates using OpenStreetMap (no API key required)

Source Code

import fs from "fs";
import path from "path";

const [location, zoom = "13", outputDir] = process.argv.slice(2);

if (!location) {
  console.error("Error: location is required");
  process.exit(1);
}

if (!outputDir) {
  console.error("Error: outputDir is required");
  process.exit(1);
}

// Check if location is already coordinates (lat,lng)
function isCoordinates(loc) {
  const coordPattern = /^-?\d+\.?\d*,\s*-?\d+\.?\d*$/;
  return coordPattern.test(loc.trim());
}

// Convert lat/lon to OSM tile coordinates
function latLonToTile(lat, lon, z) {
  const x = Math.floor(((lon + 180) / 360) * Math.pow(2, z));
  const y = Math.floor(
    ((1 -
      Math.log(
        Math.tan((lat * Math.PI) / 180) + 1 / Math.cos((lat * Math.PI) / 180)
      ) /
        Math.PI) /
      2) *
      Math.pow(2, z)
  );
  return { x, y };
}

// Geocode address to coordinates using Nominatim (free, no API key)
async function geocode(address) {
  const params = new URLSearchParams({
    q: address,
    format: "json",
    limit: "1",
  });

  const response = await fetch(
    `https://nominatim.openstreetmap.org/search?${params}`,
    {
      headers: {
        "User-Agent": "TreasureMapCreator/1.0",
      },
    }
  );

  if (!response.ok) {
    throw new Error(`Geocoding failed: ${response.status}`);
  }

  const results = await response.json();
  if (!results.length) {
    throw new Error(`Location not found: "${address}"`);
  }

  return {
    lat: parseFloat(results[0].lat),
    lon: parseFloat(results[0].lon),
    displayName: results[0].display_name,
  };
}

async function main() {
  console.log(`Capturing map for: "${location}"`);

  let lat, lon;
  const zoomLevel = parseInt(zoom);

  // Parse coordinates or geocode address
  if (isCoordinates(location)) {
    const [latStr, lonStr] = location.split(",").map((s) => s.trim());
    lat = parseFloat(latStr);
    lon = parseFloat(lonStr);
    console.log(`Using coordinates: ${lat}, ${lon}`);
  } else {
    console.log("Geocoding address...");
    const geo = await geocode(location);
    lat = geo.lat;
    lon = geo.lon;
    console.log(`Found: ${geo.displayName}`);
    console.log(`Coordinates: ${lat}, ${lon}`);
  }

  // Calculate tile coordinates
  const tile = latLonToTile(lat, lon, zoomLevel);
  console.log(`Zoom: ${zoomLevel}, Tile: x=${tile.x}, y=${tile.y}`);

  // Fetch tile from official OSM tile server (free, reliable, no API key)
  const tileUrl = `https://tile.openstreetmap.org/${zoomLevel}/${tile.x}/${tile.y}.png`;

  console.log("Fetching map tile from OpenStreetMap...");

  const response = await fetch(tileUrl, {
    headers: {
      "User-Agent": "TreasureMapCreator/1.0",
    },
  });

  if (!response.ok) {
    const text = await response.text();
    console.error(`API error (${response.status}): ${text}`);
    process.exit(1);
  }

  const contentType = response.headers.get("content-type");
  if (!contentType?.includes("image")) {
    const text = await response.text();
    console.error(`Unexpected response type: ${contentType}`);
    console.error(text);
    process.exit(1);
  }

  const buffer = Buffer.from(await response.arrayBuffer());

  // Generate filename from location
  const slug = location
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-|-$/g, "")
    .slice(0, 40);

  fs.mkdirSync(outputDir, { recursive: true });
  const outputPath = path.join(outputDir, `map-${slug}.png`);
  fs.writeFileSync(outputPath, buffer);

  console.log(`✓ Saved: ${outputPath}`);
  console.log(
    JSON.stringify({
      success: true,
      path: outputPath,
      location: location,
      coordinates: { lat, lon },
      zoom: zoomLevel,
      tile: { x: tile.x, y: tile.y },
      size: "256x256",
    })
  );
}

main().catch((err) => {
  console.error("Failed:", err.message);
  process.exit(1);
});