Skip to content
Find the Nearest Location

Find the Nearest Location by Travel Time in Maptoolkit Maps JS

Which branch is closest depends on the roads, not the map distance. This example ranks seven branches in Cologne by travel time from a draggable pin, with one Matrix API request, and draws the route to the winner. Drag the blue pin or switch between car, bike and walking to re-rank.

const API_KEY = "YOUR_API_KEY";
    let routeType = "car";

    // The Matrix API covers North Rhine-Westphalia, so the example is set in Cologne.
    const BRANCHES = [
      { name: "Deutz", coordinates: [6.9784, 50.9364] },
      { name: "Ehrenfeld", coordinates: [6.9164, 50.9483] },
      { name: "Kalk", coordinates: [7.0086, 50.9392] },
      { name: "Mülheim", coordinates: [7.0052, 50.9627] },
      { name: "Nippes", coordinates: [6.9530, 50.9660] },
      { name: "Rodenkirchen", coordinates: [6.9920, 50.8906] },
      { name: "Sülz", coordinates: [6.9269, 50.9231] },
    ];

    const map = new maptoolkit.Map({
      container: "map",
      apiKey: API_KEY,
      style: `https://styles.maptoolkit.net/maptoolkit/maptoolkit.summer.json?api_key=${API_KEY}`,
      center: [6.93, 50.935],
      zoom: 12,
      attributionControl: { compact: false },
    });
    map.addControl(new maptoolkit.NavigationControl(), "bottom-right");

    function pin(className, text) {
      const element = document.createElement("div");
      element.className = `pin ${className}`;
      element.textContent = text;
      return element;
    }

    const you = new maptoolkit.Marker({ element: pin("you", "You"), draggable: true })
      .setLngLat([6.9603, 50.9375]).addTo(map);
    const branchMarkers = BRANCHES.map((branch) =>
      new maptoolkit.Marker({ element: pin("", "") }).setLngLat(branch.coordinates).addTo(map));

    function formatDistance(meters) {
      return meters < 1000 ? `${Math.round(meters)} m` : `${(meters / 1000).toFixed(1)} km`;
    }

    let latestRequest = 0;

    // Fit the pin and every branch into the part of the map the panel does not cover.
    function fitAll() {
      const panel = document.getElementById("panel").getBoundingClientRect();
      const padding = panel.width < window.innerWidth / 2
        ? { top: 50, bottom: 50, left: panel.right + 40, right: 50 }
        : { top: panel.bottom + 30, bottom: 40, left: 30, right: 30 };
      const bounds = new maptoolkit.LngLatBounds(you.getLngLat(), you.getLngLat());
      for (const branch of BRANCHES) bounds.extend(branch.coordinates);
      map.fitBounds(bounds, { padding, duration: 0 });
    }

    async function rankBranches() {
      const request = ++latestRequest;
      document.getElementById("panel").classList.add("loading");
      const origin = you.getLngLat();

      // One origin, every branch as a destination: a single request returns all travel times.
      const url = new URL("https://routing.maptoolkit.net/matrix");
      url.searchParams.set("routeType", routeType);
      url.searchParams.append("from", `${origin.lat},${origin.lng}`);
      for (const branch of BRANCHES) url.searchParams.append("to", `${branch.coordinates[1]},${branch.coordinates[0]}`);
      url.searchParams.set("api_key", API_KEY);
      const matrix = await fetch(url).then((response) => response.json());
      if (request !== latestRequest) return;

      // Row 0 is our only origin. Times are in seconds, distances in meters, null if unreachable.
      const ranked = BRANCHES.map((branch, i) => ({
        ...branch,
        marker: branchMarkers[i],
        seconds: matrix.times[0][i],
        meters: matrix.distances[0][i],
        air: origin.distanceTo(maptoolkit.LngLat.convert(branch.coordinates)),
      }))
        .filter((branch) => branch.seconds !== null)
        .sort((a, b) => a.seconds - b.seconds);

      ranked.forEach((branch, i) => {
        const element = branch.marker.getElement();
        element.textContent = i + 1;
        element.classList.toggle("best", i === 0);
      });

      document.getElementById("branches").replaceChildren(...ranked.map((branch, i) => {
        const item = document.createElement("li");
        item.classList.toggle("best", i === 0);
        item.innerHTML = `<span class="rank">${i + 1}</span>` +
          `<span class="name">${branch.name}<span class="air">${formatDistance(branch.air)} straight line</span></span>` +
          `<span class="time">${Math.round(branch.seconds / 60)} min</span>`;
        item.addEventListener("click", () => map.flyTo({ center: branch.coordinates, zoom: 14 }));
        return item;
      }));

      const best = ranked[0];
      document.getElementById("best").textContent = `Nearest: ${best.name}`;
      document.getElementById("best-details").textContent =
        `${Math.round(best.seconds / 60)} min, ${formatDistance(best.meters)} by road`;

      // Draw the way to the nearest branch with the Routing API.
      const route = new URL("https://routing.maptoolkit.net/route");
      route.searchParams.append("point", `${origin.lat},${origin.lng}`);
      route.searchParams.append("point", `${best.coordinates[1]},${best.coordinates[0]}`);
      route.searchParams.set("routeType", routeType);
      route.searchParams.set("points_encoded", "false");
      route.searchParams.set("api_key", API_KEY);
      const data = await fetch(route).then((response) => response.json());
      if (request !== latestRequest) return;
      document.getElementById("panel").classList.remove("loading");
      if (data.paths) map.getSource("route").setData({ type: "Feature", properties: {}, geometry: data.paths[0].points });
    }

    // "style.load" fires as soon as the style is parsed; "load" would also wait for every tile.
    map.once("style.load", () => {
      map.addSource("route", { type: "geojson", data: { type: "FeatureCollection", features: [] } });
      const round = { "line-join": "round", "line-cap": "round" };
      map.addLayer({ id: "route-casing", type: "line", source: "route", layout: round, paint: { "line-color": "#ffffff", "line-width": 8 } });
      map.addLayer({ id: "route", type: "line", source: "route", layout: round, paint: { "line-color": "#2f9e44", "line-width": 4 } });
      fitAll();
      rankBranches();
    });

    you.on("dragend", rankBranches);

    document.querySelectorAll(".modes button").forEach((button) => {
      button.addEventListener("click", () => {
        routeType = button.dataset.mode;
        document.querySelectorAll(".modes button").forEach((b) => b.classList.toggle("active", b === button));
        rankBranches();
      });
    });
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <script src="https://unpkg.com/@maptoolkit/maps@11.0.0-beta.3/dist/maptoolkit.js"></script>
  <link rel="stylesheet" href="https://unpkg.com/@maptoolkit/maps@11.0.0-beta.3/dist/maptoolkit.css" />
  <style>
    html, body { width: 100%; height: 100%; margin: 0; padding: 0; }
    #map { width: 100%; height: 100%; }
    #panel {
      /* max-height leaves the Maptoolkit logo in the bottom-left corner visible. */
      position: absolute; top: 12px; left: 12px; z-index: 1; width: 300px; max-height: calc(100% - 56px);
      display: flex; flex-direction: column; background: #fff; border-radius: 12px;
      box-shadow: 0 4px 20px rgba(20, 30, 60, 0.18); font: 14px/1.4 system-ui, sans-serif; color: #1f2430;
      overflow: hidden;
    }
    .modes { display: flex; gap: 4px; margin: 14px 14px 10px; padding: 4px; background: #eef0f6; border-radius: 10px; }
    .modes button {
      flex: 1; padding: 7px 0; border: none; border-radius: 7px; background: none; cursor: pointer;
      font: 600 13px system-ui, sans-serif; color: #4a5068;
    }
    .modes button.active { background: #fff; color: #303f7e; box-shadow: 0 1px 3px rgba(20, 30, 60, 0.15); }
    .summary { margin: 0 14px 10px; padding: 12px 14px; border-radius: 10px; background: #303f7e; color: #fff; transition: opacity 0.2s; }
    .summary .name { font: 700 18px/1.3 system-ui, sans-serif; }
    .summary .details { margin-top: 2px; color: #c9cfe8; font-size: 13px; }
    #panel.loading .summary, #panel.loading #branches { opacity: 0.55; }
    #branches { flex: 1; margin: 0; padding: 0 6px 10px; overflow-y: auto; list-style: none; }
    #branches li { display: flex; align-items: center; gap: 10px; padding: 8px; border-radius: 8px; cursor: pointer; }
    #branches li:hover { background: #f3f4f9; }
    #branches .rank {
      flex: none; width: 24px; height: 24px; border-radius: 50%; background: #eef0f6; color: #303f7e;
      font: 700 12px/24px system-ui, sans-serif; text-align: center;
    }
    #branches li.best .rank { background: #2f9e44; color: #fff; }
    #branches .name { flex: 1; }
    #branches .time { font-weight: 600; white-space: nowrap; }
    #branches .air { display: block; color: #7a8099; font-size: 12px; }
    .hint { padding: 8px 14px 12px; color: #7a8099; font-size: 12px; border-top: 1px solid #eef0f6; }
    .pin {
      width: 26px; height: 26px; border-radius: 50%; border: 3px solid #fff; box-sizing: border-box;
      box-shadow: 0 2px 6px rgba(0, 0, 0, 0.35); background: #7a8099; color: #fff;
      font: 700 12px/20px system-ui, sans-serif; text-align: center;
    }
    .pin.best { background: #2f9e44; }
    .pin.you { width: 30px; height: 30px; background: #303f7e; font-size: 10px; line-height: 24px; cursor: grab; }
    @media (max-width: 600px) {
      #panel { width: auto; right: 12px; max-height: 45%; }
    }
  </style>
</head>
<body>
  <div id="map"></div>
  <div id="panel">
    <div class="modes">
      <button data-mode="car" class="active">Car</button>
      <button data-mode="bike">Bike</button>
      <button data-mode="foot">Walk</button>
    </div>
    <div class="summary"><div class="name" id="best">&nbsp;</div><div class="details" id="best-details">&nbsp;</div></div>
    <ol id="branches"></ol>
    <div class="hint">Drag the blue pin to rank the branches from somewhere else.</div>
  </div>
  <script>
    const API_KEY = "YOUR_API_KEY";
    let routeType = "car";

    // The Matrix API covers North Rhine-Westphalia, so the example is set in Cologne.
    const BRANCHES = [
      { name: "Deutz", coordinates: [6.9784, 50.9364] },
      { name: "Ehrenfeld", coordinates: [6.9164, 50.9483] },
      { name: "Kalk", coordinates: [7.0086, 50.9392] },
      { name: "Mülheim", coordinates: [7.0052, 50.9627] },
      { name: "Nippes", coordinates: [6.9530, 50.9660] },
      { name: "Rodenkirchen", coordinates: [6.9920, 50.8906] },
      { name: "Sülz", coordinates: [6.9269, 50.9231] },
    ];

    const map = new maptoolkit.Map({
      container: "map",
      apiKey: API_KEY,
      style: `https://styles.maptoolkit.net/maptoolkit/maptoolkit.summer.json?api_key=${API_KEY}`,
      center: [6.93, 50.935],
      zoom: 12,
      attributionControl: { compact: false },
    });
    map.addControl(new maptoolkit.NavigationControl(), "bottom-right");

    function pin(className, text) {
      const element = document.createElement("div");
      element.className = `pin ${className}`;
      element.textContent = text;
      return element;
    }

    const you = new maptoolkit.Marker({ element: pin("you", "You"), draggable: true })
      .setLngLat([6.9603, 50.9375]).addTo(map);
    const branchMarkers = BRANCHES.map((branch) =>
      new maptoolkit.Marker({ element: pin("", "") }).setLngLat(branch.coordinates).addTo(map));

    function formatDistance(meters) {
      return meters < 1000 ? `${Math.round(meters)} m` : `${(meters / 1000).toFixed(1)} km`;
    }

    let latestRequest = 0;

    // Fit the pin and every branch into the part of the map the panel does not cover.
    function fitAll() {
      const panel = document.getElementById("panel").getBoundingClientRect();
      const padding = panel.width < window.innerWidth / 2
        ? { top: 50, bottom: 50, left: panel.right + 40, right: 50 }
        : { top: panel.bottom + 30, bottom: 40, left: 30, right: 30 };
      const bounds = new maptoolkit.LngLatBounds(you.getLngLat(), you.getLngLat());
      for (const branch of BRANCHES) bounds.extend(branch.coordinates);
      map.fitBounds(bounds, { padding, duration: 0 });
    }

    async function rankBranches() {
      const request = ++latestRequest;
      document.getElementById("panel").classList.add("loading");
      const origin = you.getLngLat();

      // One origin, every branch as a destination: a single request returns all travel times.
      const url = new URL("https://routing.maptoolkit.net/matrix");
      url.searchParams.set("routeType", routeType);
      url.searchParams.append("from", `${origin.lat},${origin.lng}`);
      for (const branch of BRANCHES) url.searchParams.append("to", `${branch.coordinates[1]},${branch.coordinates[0]}`);
      url.searchParams.set("api_key", API_KEY);
      const matrix = await fetch(url).then((response) => response.json());
      if (request !== latestRequest) return;

      // Row 0 is our only origin. Times are in seconds, distances in meters, null if unreachable.
      const ranked = BRANCHES.map((branch, i) => ({
        ...branch,
        marker: branchMarkers[i],
        seconds: matrix.times[0][i],
        meters: matrix.distances[0][i],
        air: origin.distanceTo(maptoolkit.LngLat.convert(branch.coordinates)),
      }))
        .filter((branch) => branch.seconds !== null)
        .sort((a, b) => a.seconds - b.seconds);

      ranked.forEach((branch, i) => {
        const element = branch.marker.getElement();
        element.textContent = i + 1;
        element.classList.toggle("best", i === 0);
      });

      document.getElementById("branches").replaceChildren(...ranked.map((branch, i) => {
        const item = document.createElement("li");
        item.classList.toggle("best", i === 0);
        item.innerHTML = `<span class="rank">${i + 1}</span>` +
          `<span class="name">${branch.name}<span class="air">${formatDistance(branch.air)} straight line</span></span>` +
          `<span class="time">${Math.round(branch.seconds / 60)} min</span>`;
        item.addEventListener("click", () => map.flyTo({ center: branch.coordinates, zoom: 14 }));
        return item;
      }));

      const best = ranked[0];
      document.getElementById("best").textContent = `Nearest: ${best.name}`;
      document.getElementById("best-details").textContent =
        `${Math.round(best.seconds / 60)} min, ${formatDistance(best.meters)} by road`;

      // Draw the way to the nearest branch with the Routing API.
      const route = new URL("https://routing.maptoolkit.net/route");
      route.searchParams.append("point", `${origin.lat},${origin.lng}`);
      route.searchParams.append("point", `${best.coordinates[1]},${best.coordinates[0]}`);
      route.searchParams.set("routeType", routeType);
      route.searchParams.set("points_encoded", "false");
      route.searchParams.set("api_key", API_KEY);
      const data = await fetch(route).then((response) => response.json());
      if (request !== latestRequest) return;
      document.getElementById("panel").classList.remove("loading");
      if (data.paths) map.getSource("route").setData({ type: "Feature", properties: {}, geometry: data.paths[0].points });
    }

    // "style.load" fires as soon as the style is parsed; "load" would also wait for every tile.
    map.once("style.load", () => {
      map.addSource("route", { type: "geojson", data: { type: "FeatureCollection", features: [] } });
      const round = { "line-join": "round", "line-cap": "round" };
      map.addLayer({ id: "route-casing", type: "line", source: "route", layout: round, paint: { "line-color": "#ffffff", "line-width": 8 } });
      map.addLayer({ id: "route", type: "line", source: "route", layout: round, paint: { "line-color": "#2f9e44", "line-width": 4 } });
      fitAll();
      rankBranches();
    });

    you.on("dragend", rankBranches);

    document.querySelectorAll(".modes button").forEach((button) => {
      button.addEventListener("click", () => {
        routeType = button.dataset.mode;
        document.querySelectorAll(".modes button").forEach((b) => b.classList.toggle("active", b === button));
        rankBranches();
      });
    });
  </script>
</body>
</html>

Use the prompt below with any LLM to get the same result. Make sure the Maptoolkit MCP server is connected first — check out AI Integration & MCP to get started.

Use the Maptoolkit Connector. Create a Maptoolkit Maps JS map of Cologne with seven branch locations and a draggable pin. Rank the branches by travel time from the pin with one Matrix API request, show the ranking with straight-line distances in a side panel, number the markers by rank, draw the route to the nearest branch with the Routing API, and add a car, bike and walk switch.

How it works

One request ranks every branch. The Matrix API takes one from and any number of to coordinates and answers with a table: times[i][j] and distances[i][j] from origin i to destination j, in seconds and meters. With a single origin, row 0 holds one value per branch, in the order the branches were sent, which is why the code maps over BRANCHES with the same index. A branch that cannot be reached comes back as null and is filtered out before sorting.

Seconds here, milliseconds in routing. The Matrix API reports time in seconds. The Routing API, which draws the green line to the nearest branch, reports time in milliseconds. Mixing the two is off by a factor of 1,000.

Straight-line distance is the wrong ranking. Each row shows the distance as the crow flies next to the travel time, and the two orders disagree: from the start position, Sülz is closer in a straight line than Kalk or Nippes but further away by car. Bridges, one-way streets and road types decide, which is the reason to rank by travel time at all.

The from and to parameters take latitude,longitude, while the branch list and the markers use [longitude, latitude], so the request swaps the order.

The example is set in Cologne, inside North Rhine-Westphalia, the region the Matrix API page lists as covered.

The map is fitted once, around the blue pin and every branch, into the part of the map the panel does not cover. Dragging the pin or switching the travel mode sends a new matrix request and a new route request; latestRequest drops answers that arrive after a newer request.

Next steps

The same request scales in the other direction: several from coordinates answer “which customer is closest to each branch”, and a square matrix with every location as both origin and destination is the input a delivery-route optimizer needs.

For the area reachable within a time limit, instead of a ranked list, Travel Time Bands draws it with the Isochrone API.