Localities Nearby POI

Use the LocalitiesService to find out nearby POI.

  1. Example
  2. Running the Sample Locally

Example

Localities Nearby POI
        const availableCategories = [
  "transit.station",
  "transit.station.airport",
  "transit.station.rail",
  "business",
  "business.cinema",
  "business.theatre",
  "business.nightclub",
  "business.finance",
  "business.finance.bank",
  "business.fuel",
  "business.parking",
  "business.mall",
  "business.food_and_drinks",
  "business.food_and_drinks.bar",
  "business.food_and_drinks.biergarten",
  "business.food_and_drinks.cafe",
  "business.food_and_drinks.fast_food",
  "business.food_and_drinks.pub",
  "business.food_and_drinks.restaurant",
  "business.food_and_drinks.food_court",
  "business.shop",
  "business.shop.mall",
  "business.shop.bakery",
  "business.shop.butcher",
  "business.shop.library",
  "business.shop.grocery",
  "business.shop.sports",
  "business.shop.toys",
  "business.shop.clothes",
  "business.shop.furniture",
  "business.shop.electronics",
  "education",
  "education.school",
  "education.kindergarten",
  "education.university",
  "education.college",
  "education.library",
  "hospitality",
  "hospitality.hotel",
  "hospitality.hostel",
  "hospitality.guest_house",
  "hospitality.bed_and_breakfast",
  "hospitality.motel",
  "medical",
  "medical.hospital",
  "medical.pharmacy",
  "medical.clinic",
  "tourism",
  "tourism.attraction",
  "tourism.attraction.amusement_park",
  "tourism.attraction.zoo",
  "tourism.attraction.aquarium",
  "tourism.monument",
  "tourism.monument.castle",
  "tourism.museum",
  "government",
  "park",
  "place_of_worship",
  "police",
  "post_office",
  "sports",
];
const categories: Set<string> = new Set();
let map: woosmap.map.Map;
let results: HTMLOListElement;
let nearbyCircle: woosmap.map.Circle;
let marker: woosmap.map.Marker;
let localitiesService: woosmap.map.LocalitiesService;
let autocompleteRequest: woosmap.map.localities.LocalitiesAutocompleteRequest;
let nearbyRequest: woosmap.map.localities.LocalitiesNearbyRequest;

const buildTree = (availableCategories: string[]) => {
  const tree = {};
  availableCategories.forEach((category) => {
    const parts = category.split(".");
    let node = tree;
    parts.forEach((part) => {
      node[part] = node[part] || {};
      node = node[part];
    });
  });
  return tree;
};

const createList = (node: any, prefix = "") => {
  const ul = document.createElement("ul");
  for (const key in node) {
    const fullKey = prefix ? `${prefix}.${key}` : key;
    const li = document.createElement("li");

    const checkbox = document.createElement("input");
    checkbox.type = "checkbox";
    checkbox.id = `category-${fullKey}`;
    checkbox.name = "categories";
    checkbox.classList.add("category");
    checkbox.value = fullKey;

    const label = document.createElement("label");
    label.htmlFor = `category-${fullKey}`;
    label.textContent = key;

    li.appendChild(checkbox);
    li.appendChild(label);

    const children = createList(node[key], fullKey);
    if (children) {
      li.appendChild(children);
    }
    ul.appendChild(li);
  }
  return ul.childElementCount ? ul : null;
};

function initMap() {
  map = new window.woosmap.map.Map(
    document.getElementById("map") as HTMLElement,
    {
      center: { lat: 40.71399, lng: -74.00499 },
      zoom: 14,
      styles: [
        {
          featureType: "point_of_interest",
          elementType: "all",
          stylers: [
            {
              visibility: "on",
            },
          ],
        },
      ],
    },
  );
  localitiesService = new woosmap.map.LocalitiesService();

  map.addListener("click", (e) => {
    handleRadius(nearbyRequest.radius || 1000, e.latlng);
  });

  autocompleteRequest = {
    input: "",
    types: ["locality", "postal_code"],
  };
  nearbyRequest = {
    types: "point_of_interest",
    location: map.getCenter(),
    radius: 1000,
    categories: "",
    page: 1,
    limit: 10,
  };
  marker = new woosmap.map.Marker({
    position: { lat: 0, lng: 0 },
    icon: {
      url: "https://images.woosmap.com/marker.png",
      scaledSize: {
        height: 50,
        width: 32,
      },
    },
  });

  initUI();
  performNearbyRequest();
}

function buildCategoriesList() {
  const tree = buildTree(availableCategories);
  const list = createList(tree);
  (
    document.querySelector(".categoriesOptions__list") as HTMLDivElement
  ).appendChild(list as HTMLElement);
  document.querySelectorAll(".category").forEach((el) =>
    el.addEventListener("click", (ev) => {
      const inputElement = ev.target as HTMLInputElement;
      const parentLi = inputElement.closest("li");
      const childrenCheckboxes = parentLi
        ? Array.from(parentLi.children)
            .filter((child) => child !== inputElement)
            .flatMap((child) => Array.from(child.querySelectorAll(".category")))
        : [];

      if (inputElement.checked) {
        categories.add(inputElement.value);
        if (childrenCheckboxes.length > 0) {
          childrenCheckboxes.forEach((checkbox) => {
            (checkbox as HTMLInputElement).disabled = true;
          });
        }
      } else {
        categories.delete(inputElement.value);
        if (childrenCheckboxes.length > 0) {
          childrenCheckboxes.forEach((checkbox) => {
            (checkbox as HTMLInputElement).disabled = false;
          });
        }
      }
      performNearbyRequest();
    }),
  );
}

function handleRadius(
  radiusValue: number,
  center?: woosmap.map.LatLng | woosmap.map.LatLngLiteral | null,
) {
  const label = document.getElementById("radius-label");
  if (radiusValue < 1000 && label) {
    label.innerHTML = `${radiusValue}&thinsp;m`;
  } else if (label) {
    label.innerHTML = `${radiusValue / 1000}&thinsp;km`;
  }
  // circle.getBounds() returns wrong LatLngBounds -> const bounds = nearbyCircle.getBounds();
  // TODO fixed circle getBounds
  // used this log scale to compute the zoom level between z18 (radius 10m) and z7 (radius 50km)
  const zoomLevel = Math.round(
    18 - (Math.log(radiusValue / 10) / Math.log(50000 / 10)) * (18 - 7),
  );
  map.flyTo({ center: center || nearbyCircle.getCenter(), zoom: zoomLevel });
  nearbyRequest.radius = radiusValue;
  performNearbyRequest(new woosmap.map.LatLng(center || nearbyCircle.getCenter()));
}

function initUI() {
  results = document.querySelector("#results") as HTMLOListElement;
  buildCategoriesList();
  const debouncedHandleRadius = debounce(handleRadius, 300);

  document.getElementById("radius")?.addEventListener("input", (e) => {
    const radiusValue = parseInt((e.target as HTMLInputElement).value);
    debouncedHandleRadius(radiusValue);
  });
  document.getElementById("page-previous")?.addEventListener("click", previousPage);
  document.getElementById("page-next")?.addEventListener("click", nextPage);
}
function previousPage(){
  let newQuery = true
  if(nearbyRequest.page && nearbyRequest.page > 1) {
    nearbyRequest.page--;
    newQuery=false;
  }
  performNearbyRequest(null, newQuery);
}

function nextPage(){
  let newQuery = true
  if(nearbyRequest.page) {
    nearbyRequest.page++;
    newQuery=false;
  }
  performNearbyRequest(null, newQuery);
}

function performNearbyRequest(
  overrideCenter: woosmap.map.LatLng | null = null,
  newQuery = true,
) {
  const requestCenter = overrideCenter || map.getCenter();
  nearbyRequest.location = requestCenter;
  nearbyRequest.categories = "";
  if (categories.size > 0) {
    nearbyRequest.categories = Array.from(categories).join("|");
  }
  if (newQuery) {
    nearbyRequest.page = 1;
  }

  results.innerHTML = "";

  if (nearbyRequest.radius && nearbyRequest.radius > 50000) {
    results.innerHTML = "<li style='color: red;'><b>Radius should be less than or equal to 50km.</b></li>";
    return;
  }
  else if (nearbyRequest.radius && nearbyRequest.radius < 10) {
    results.innerHTML = "<li style='color: red;'><b>Radius should be greater than or equal to 10m.</b></li>";
    return;
  }

  //@ts-ignore
  localitiesService.nearby(nearbyRequest).then((responseJson) => {
    drawNearbyZone(requestCenter, nearbyRequest.radius);
    updateResults(responseJson, requestCenter);
  });
}

function drawNearbyZone(center, radius) {
  if (nearbyCircle) {
    nearbyCircle.setMap(null);
  }
  nearbyCircle = new woosmap.map.Circle({
    map,
    center: center,
    radius: radius,
    strokeColor: "#1165c2",
    strokeOpacity: 0.8,
    strokeWeight: 2,
    fillColor: "#3283c5",
    fillOpacity: 0.2,
  });
}

function updatePagination(pagination: woosmap.map.localities.LocalitiesNearbyPagination) {
  if (pagination.next_page) {
    document.getElementById("page-next")?.removeAttribute("disabled");
  } else {
    document.getElementById("page-next")?.setAttribute("disabled", "true");
  }
  if(pagination.previous_page) {
    document.getElementById("page-previous")?.removeAttribute("disabled");
  } else {
    document.getElementById("page-previous")?.setAttribute("disabled", "true");
  }
}

function updateResults(response: woosmap.map.localities.LocalitiesNearbyResponse, center) {
  updatePagination(response.pagination);
  response.results.forEach((result:woosmap.map.localities.LocalitiesNearbyResult) => {
    const distance = measure(
      center.lat(),
      center.lng(),
      result.geometry.location.lat,
      result.geometry.location.lng,
    );
    const resultListItem = document.createElement("li");
    resultListItem.innerHTML = `
        <b>${result.name}</b>
        <i>${result.categories}</i>
        
        <span class="distance">${distance.toFixed(0)}m</span>
    `;
    resultListItem.addEventListener("click", () => {
      map.setCenter({
        lat: result.geometry.location.lat,
        lng: result.geometry.location.lng,
      });

      marker.setPosition({
        lat: result.geometry.location.lat,
        lng: result.geometry.location.lng,
      });
      marker.setMap(map);
    });
    results.appendChild(resultListItem);
  });
}

function measure(lat1, lon1, lat2, lon2) {
  // generally used geo measurement function
  const R = 6378.137; // Radius of earth in KM
  const dLat = (lat2 * Math.PI) / 180 - (lat1 * Math.PI) / 180;
  const dLon = (lon2 * Math.PI) / 180 - (lon1 * Math.PI) / 180;
  const a =
    Math.sin(dLat / 2) * Math.sin(dLat / 2) +
    Math.cos((lat1 * Math.PI) / 180) *
      Math.cos((lat2 * Math.PI) / 180) *
      Math.sin(dLon / 2) *
      Math.sin(dLon / 2);
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  const d = R * c;
  return d * 1000; // meters
}

const inputElement = document.getElementById(
  "autocomplete-input",
) as HTMLInputElement;
const suggestionsList = document.getElementById(
  "suggestions-list",
) as HTMLUListElement;
const clearSearchBtn = document.getElementsByClassName(
  "clear-searchButton",
)[0] as HTMLButtonElement;
if (inputElement && suggestionsList) {
  inputElement.addEventListener("input", handleAutocomplete);
  inputElement.addEventListener("keydown", (event) => {
    if (event.key === "Enter") {
      const firstLi = suggestionsList.querySelector("li");
      if (firstLi) {
        firstLi.click();
      }
    }
  });
}
clearSearchBtn.addEventListener("click", () => {
  inputElement.value = "";
  suggestionsList.style.display = "none";
  clearSearchBtn.style.display = "none";
  if (marker) {
    marker.setMap(null);
  }
  inputElement.focus();
});

function handleAutocomplete(): void {
  if (inputElement && suggestionsList) {
    autocompleteRequest.input = inputElement.value;
    if (autocompleteRequest.input) {
      localitiesService
        .autocomplete(autocompleteRequest)
        .then((localities) => displaySuggestions(localities))
        .catch((error) =>
          console.error("Error autocomplete localities:", error),
        );
    } else {
      suggestionsList.style.display = "none";
      clearSearchBtn.style.display = "none";
    }
  }
}

function handleDetails(publicId: string) {
  localitiesService
    .getDetails({ publicId })
    .then((locality) => displayLocality(locality.result))
    .catch((error) => console.error("Error getting locality details:", error));
}

function displayLocality(
  locality: woosmap.map.localities.LocalitiesDetailsResult,
) {
  if (locality?.geometry && nearbyRequest.radius) {
    map.setCenter(locality.geometry.location);
    handleRadius(nearbyRequest.radius, locality.geometry.location);
  }
}

function displaySuggestions(
  localitiesPredictions: woosmap.map.localities.LocalitiesAutocompleteResponse,
) {
  if (inputElement && suggestionsList) {
    suggestionsList.innerHTML = "";
    if (localitiesPredictions.localities.length > 0 && autocompleteRequest["input"]) {
      localitiesPredictions.localities.forEach((locality) => {
        const li = document.createElement("li");
        li.textContent = locality.description ?? "";
        li.addEventListener("click", () => {
          inputElement.value = locality.description ?? "";
          suggestionsList.style.display = "none";
          handleDetails(locality.public_id);
        });
        suggestionsList.appendChild(li);
      });
      suggestionsList.style.display = "block";
      clearSearchBtn.style.display = "block";
    } else {
      suggestionsList.style.display = "none";
    }
  }
}

document.addEventListener("click", (event) => {
  const targetElement = event.target as Element;
  const isClickInsideAutocomplete = targetElement.closest(
    "#autocomplete-container",
  );

  if (!isClickInsideAutocomplete && suggestionsList) {
    suggestionsList.style.display = "none";
  }
});

function debounce(func: (...args: any[]) => void, wait: number) {
  let timeout: NodeJS.Timeout;
  return function executedFunction(...args: any[]) {
    const later = () => {
      clearTimeout(timeout);
      func(...args);
    };
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  };
}

document.addEventListener('DOMContentLoaded', () => {
  const radiusInput = document.getElementById('radius') as HTMLInputElement;
  const radiusLabel = document.getElementById('radius-label') as HTMLLabelElement;

  if (!radiusInput || !radiusLabel) {
      console.error('Elements not found in the DOM.');
      return;
  }

  // Update the range input when the label content is modified
  radiusLabel.addEventListener('blur', () => {
      const parsedValue = parseLabel(radiusLabel.textContent || '');
      if (parsedValue !== null) {
          radiusInput.value = parsedValue.toString();
          handleRadius(parsedValue)
      } else {
          // Revert to the current range value if parsing fails
          radiusLabel.textContent = formatValue(parseInt(radiusInput.value, 10));
      }
  });

  radiusLabel.addEventListener('keypress', (e: KeyboardEvent) => {
      if (e.key === 'Enter') {
          e.preventDefault(); // Prevent line breaks
          radiusLabel.blur(); // Trigger the blur event to validate and update
      }
  });

  // Format the value in meters to "km" or "m" for display
  const formatValue = (value: number): string => {
      return value >= 1000 ? `${value / 1000} km` : `${value} m`;
  };

  // Parse the label content back to meters
  const parseLabel = (label: string): number | null => {
      const kmMatch = label.match(/^(\d+(?:\.\d+)?)\s*km$/i);
      const mMatch = label.match(/^(\d+)\s*m$/i);

      if (kmMatch) {
          return Math.round(parseFloat(kmMatch[1]) * 1000); // Convert km to meters
      } else if (mMatch) {
          return parseInt(mMatch[1], 10); // Keep value in meters
      }
      return null; // Invalid input
  };
});


declare global {
  interface Window {
    initMap: () => void;
  }
}
window.initMap = initMap;

    
        const availableCategories = [
  "transit.station",
  "transit.station.airport",
  "transit.station.rail",
  "business",
  "business.cinema",
  "business.theatre",
  "business.nightclub",
  "business.finance",
  "business.finance.bank",
  "business.fuel",
  "business.parking",
  "business.mall",
  "business.food_and_drinks",
  "business.food_and_drinks.bar",
  "business.food_and_drinks.biergarten",
  "business.food_and_drinks.cafe",
  "business.food_and_drinks.fast_food",
  "business.food_and_drinks.pub",
  "business.food_and_drinks.restaurant",
  "business.food_and_drinks.food_court",
  "business.shop",
  "business.shop.mall",
  "business.shop.bakery",
  "business.shop.butcher",
  "business.shop.library",
  "business.shop.grocery",
  "business.shop.sports",
  "business.shop.toys",
  "business.shop.clothes",
  "business.shop.furniture",
  "business.shop.electronics",
  "education",
  "education.school",
  "education.kindergarten",
  "education.university",
  "education.college",
  "education.library",
  "hospitality",
  "hospitality.hotel",
  "hospitality.hostel",
  "hospitality.guest_house",
  "hospitality.bed_and_breakfast",
  "hospitality.motel",
  "medical",
  "medical.hospital",
  "medical.pharmacy",
  "medical.clinic",
  "tourism",
  "tourism.attraction",
  "tourism.attraction.amusement_park",
  "tourism.attraction.zoo",
  "tourism.attraction.aquarium",
  "tourism.monument",
  "tourism.monument.castle",
  "tourism.museum",
  "government",
  "park",
  "place_of_worship",
  "police",
  "post_office",
  "sports",
];
const categories = new Set();
let map;
let results;
let nearbyCircle;
let marker;
let localitiesService;
let autocompleteRequest;
let nearbyRequest;

const buildTree = (availableCategories) => {
  const tree = {};

  availableCategories.forEach((category) => {
    const parts = category.split(".");
    let node = tree;

    parts.forEach((part) => {
      node[part] = node[part] || {};
      node = node[part];
    });
  });
  return tree;
};

const createList = (node, prefix = "") => {
  const ul = document.createElement("ul");

  for (const key in node) {
    const fullKey = prefix ? `${prefix}.${key}` : key;
    const li = document.createElement("li");
    const checkbox = document.createElement("input");

    checkbox.type = "checkbox";
    checkbox.id = `category-${fullKey}`;
    checkbox.name = "categories";
    checkbox.classList.add("category");
    checkbox.value = fullKey;

    const label = document.createElement("label");

    label.htmlFor = `category-${fullKey}`;
    label.textContent = key;
    li.appendChild(checkbox);
    li.appendChild(label);

    const children = createList(node[key], fullKey);

    if (children) {
      li.appendChild(children);
    }

    ul.appendChild(li);
  }
  return ul.childElementCount ? ul : null;
};

function initMap() {
  map = new window.woosmap.map.Map(document.getElementById("map"), {
    center: { lat: 40.71399, lng: -74.00499 },
    zoom: 14,
    styles: [
      {
        featureType: "point_of_interest",
        elementType: "all",
        stylers: [
          {
            visibility: "on",
          },
        ],
      },
    ],
  });
  localitiesService = new woosmap.map.LocalitiesService();
  map.addListener("click", (e) => {
    handleRadius(nearbyRequest.radius || 1000, e.latlng);
  });
  autocompleteRequest = {
    input: "",
    types: ["locality", "postal_code"],
  };
  nearbyRequest = {
    types: "point_of_interest",
    location: map.getCenter(),
    radius: 1000,
    categories: "",
    page: 1,
    limit: 10,
  };
  marker = new woosmap.map.Marker({
    position: { lat: 0, lng: 0 },
    icon: {
      url: "https://images.woosmap.com/marker.png",
      scaledSize: {
        height: 50,
        width: 32,
      },
    },
  });
  initUI();
  performNearbyRequest();
}

function buildCategoriesList() {
  const tree = buildTree(availableCategories);
  const list = createList(tree);

  document.querySelector(".categoriesOptions__list").appendChild(list);
  document.querySelectorAll(".category").forEach((el) =>
    el.addEventListener("click", (ev) => {
      const inputElement = ev.target;
      const parentLi = inputElement.closest("li");
      const childrenCheckboxes = parentLi
        ? Array.from(parentLi.children)
            .filter((child) => child !== inputElement)
            .flatMap((child) => Array.from(child.querySelectorAll(".category")))
        : [];

      if (inputElement.checked) {
        categories.add(inputElement.value);
        if (childrenCheckboxes.length > 0) {
          childrenCheckboxes.forEach((checkbox) => {
            checkbox.disabled = true;
          });
        }
      } else {
        categories.delete(inputElement.value);
        if (childrenCheckboxes.length > 0) {
          childrenCheckboxes.forEach((checkbox) => {
            checkbox.disabled = false;
          });
        }
      }

      performNearbyRequest();
    }),
  );
}

function handleRadius(radiusValue, center) {
  const label = document.getElementById("radius-label");

  if (radiusValue < 1000 && label) {
    label.innerHTML = `${radiusValue}&thinsp;m`;
  } else if (label) {
    label.innerHTML = `${radiusValue / 1000}&thinsp;km`;
  }

  // circle.getBounds() returns wrong LatLngBounds -> const bounds = nearbyCircle.getBounds();
  // TODO fixed circle getBounds
  // used this log scale to compute the zoom level between z18 (radius 10m) and z7 (radius 50km)
  const zoomLevel = Math.round(
    18 - (Math.log(radiusValue / 10) / Math.log(50000 / 10)) * (18 - 7),
  );

  map.flyTo({ center: center || nearbyCircle.getCenter(), zoom: zoomLevel });
  nearbyRequest.radius = radiusValue;
  performNearbyRequest(
    new woosmap.map.LatLng(center || nearbyCircle.getCenter()),
  );
}

function initUI() {
  results = document.querySelector("#results");
  buildCategoriesList();

  const debouncedHandleRadius = debounce(handleRadius, 300);

  document.getElementById("radius")?.addEventListener("input", (e) => {
    const radiusValue = parseInt(e.target.value);

    debouncedHandleRadius(radiusValue);
  });
  document
    .getElementById("page-previous")
    ?.addEventListener("click", previousPage);
  document.getElementById("page-next")?.addEventListener("click", nextPage);
}

function previousPage() {
  let newQuery = true;

  if (nearbyRequest.page && nearbyRequest.page > 1) {
    nearbyRequest.page--;
    newQuery = false;
  }

  performNearbyRequest(null, newQuery);
}

function nextPage() {
  let newQuery = true;

  if (nearbyRequest.page) {
    nearbyRequest.page++;
    newQuery = false;
  }

  performNearbyRequest(null, newQuery);
}

function performNearbyRequest(overrideCenter = null, newQuery = true) {
  const requestCenter = overrideCenter || map.getCenter();

  nearbyRequest.location = requestCenter;
  nearbyRequest.categories = "";
  if (categories.size > 0) {
    nearbyRequest.categories = Array.from(categories).join("|");
  }

  if (newQuery) {
    nearbyRequest.page = 1;
  }

  results.innerHTML = "";
  if (nearbyRequest.radius && nearbyRequest.radius > 50000) {
    results.innerHTML =
      "<li style='color: red;'><b>Radius should be less than or equal to 50km.</b></li>";
    return;
  } else if (nearbyRequest.radius && nearbyRequest.radius < 10) {
    results.innerHTML =
      "<li style='color: red;'><b>Radius should be greater than or equal to 10m.</b></li>";
    return;
  }

  //@ts-ignore
  localitiesService.nearby(nearbyRequest).then((responseJson) => {
    drawNearbyZone(requestCenter, nearbyRequest.radius);
    updateResults(responseJson, requestCenter);
  });
}

function drawNearbyZone(center, radius) {
  if (nearbyCircle) {
    nearbyCircle.setMap(null);
  }

  nearbyCircle = new woosmap.map.Circle({
    map,
    center: center,
    radius: radius,
    strokeColor: "#1165c2",
    strokeOpacity: 0.8,
    strokeWeight: 2,
    fillColor: "#3283c5",
    fillOpacity: 0.2,
  });
}

function updatePagination(pagination) {
  if (pagination.next_page) {
    document.getElementById("page-next")?.removeAttribute("disabled");
  } else {
    document.getElementById("page-next")?.setAttribute("disabled", "true");
  }

  if (pagination.previous_page) {
    document.getElementById("page-previous")?.removeAttribute("disabled");
  } else {
    document.getElementById("page-previous")?.setAttribute("disabled", "true");
  }
}

function updateResults(response, center) {
  updatePagination(response.pagination);
  response.results.forEach((result) => {
    const distance = measure(
      center.lat(),
      center.lng(),
      result.geometry.location.lat,
      result.geometry.location.lng,
    );
    const resultListItem = document.createElement("li");

    resultListItem.innerHTML = `
        <b>${result.name}</b>
        <i>${result.categories}</i>
        
        <span class="distance">${distance.toFixed(0)}m</span>
    `;
    resultListItem.addEventListener("click", () => {
      map.setCenter({
        lat: result.geometry.location.lat,
        lng: result.geometry.location.lng,
      });
      marker.setPosition({
        lat: result.geometry.location.lat,
        lng: result.geometry.location.lng,
      });
      marker.setMap(map);
    });
    results.appendChild(resultListItem);
  });
}

function measure(lat1, lon1, lat2, lon2) {
  // generally used geo measurement function
  const R = 6378.137; // Radius of earth in KM
  const dLat = (lat2 * Math.PI) / 180 - (lat1 * Math.PI) / 180;
  const dLon = (lon2 * Math.PI) / 180 - (lon1 * Math.PI) / 180;
  const a =
    Math.sin(dLat / 2) * Math.sin(dLat / 2) +
    Math.cos((lat1 * Math.PI) / 180) *
      Math.cos((lat2 * Math.PI) / 180) *
      Math.sin(dLon / 2) *
      Math.sin(dLon / 2);
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  const d = R * c;
  return d * 1000; // meters
}

const inputElement = document.getElementById("autocomplete-input");
const suggestionsList = document.getElementById("suggestions-list");
const clearSearchBtn = document.getElementsByClassName("clear-searchButton")[0];

if (inputElement && suggestionsList) {
  inputElement.addEventListener("input", handleAutocomplete);
  inputElement.addEventListener("keydown", (event) => {
    if (event.key === "Enter") {
      const firstLi = suggestionsList.querySelector("li");

      if (firstLi) {
        firstLi.click();
      }
    }
  });
}

clearSearchBtn.addEventListener("click", () => {
  inputElement.value = "";
  suggestionsList.style.display = "none";
  clearSearchBtn.style.display = "none";
  if (marker) {
    marker.setMap(null);
  }

  inputElement.focus();
});

function handleAutocomplete() {
  if (inputElement && suggestionsList) {
    autocompleteRequest.input = inputElement.value;
    if (autocompleteRequest.input) {
      localitiesService
        .autocomplete(autocompleteRequest)
        .then((localities) => displaySuggestions(localities))
        .catch((error) =>
          console.error("Error autocomplete localities:", error),
        );
    } else {
      suggestionsList.style.display = "none";
      clearSearchBtn.style.display = "none";
    }
  }
}

function handleDetails(publicId) {
  localitiesService
    .getDetails({ publicId })
    .then((locality) => displayLocality(locality.result))
    .catch((error) => console.error("Error getting locality details:", error));
}

function displayLocality(locality) {
  if (locality?.geometry && nearbyRequest.radius) {
    map.setCenter(locality.geometry.location);
    handleRadius(nearbyRequest.radius, locality.geometry.location);
  }
}

function displaySuggestions(localitiesPredictions) {
  if (inputElement && suggestionsList) {
    suggestionsList.innerHTML = "";
    if (
      localitiesPredictions.localities.length > 0 &&
      autocompleteRequest["input"]
    ) {
      localitiesPredictions.localities.forEach((locality) => {
        const li = document.createElement("li");

        li.textContent = locality.description ?? "";
        li.addEventListener("click", () => {
          inputElement.value = locality.description ?? "";
          suggestionsList.style.display = "none";
          handleDetails(locality.public_id);
        });
        suggestionsList.appendChild(li);
      });
      suggestionsList.style.display = "block";
      clearSearchBtn.style.display = "block";
    } else {
      suggestionsList.style.display = "none";
    }
  }
}

document.addEventListener("click", (event) => {
  const targetElement = event.target;
  const isClickInsideAutocomplete = targetElement.closest(
    "#autocomplete-container",
  );

  if (!isClickInsideAutocomplete && suggestionsList) {
    suggestionsList.style.display = "none";
  }
});

function debounce(func, wait) {
  let timeout;

  return function executedFunction(...args) {
    const later = () => {
      clearTimeout(timeout);
      func(...args);
    };

    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  };
}

document.addEventListener("DOMContentLoaded", () => {
  const radiusInput = document.getElementById("radius");
  const radiusLabel = document.getElementById("radius-label");

  if (!radiusInput || !radiusLabel) {
    console.error("Elements not found in the DOM.");
    return;
  }

  // Update the range input when the label content is modified
  radiusLabel.addEventListener("blur", () => {
    const parsedValue = parseLabel(radiusLabel.textContent || "");

    if (parsedValue !== null) {
      radiusInput.value = parsedValue.toString();
      handleRadius(parsedValue);
    } else {
      // Revert to the current range value if parsing fails
      radiusLabel.textContent = formatValue(parseInt(radiusInput.value, 10));
    }
  });
  radiusLabel.addEventListener("keypress", (e) => {
    if (e.key === "Enter") {
      e.preventDefault(); // Prevent line breaks
      radiusLabel.blur(); // Trigger the blur event to validate and update
    }
  });

  // Format the value in meters to "km" or "m" for display
  const formatValue = (value) => {
    return value >= 1000 ? `${value / 1000} km` : `${value} m`;
  };

  // Parse the label content back to meters
  const parseLabel = (label) => {
    const kmMatch = label.match(/^(\d+(?:\.\d+)?)\s*km$/i);
    const mMatch = label.match(/^(\d+)\s*m$/i);

    if (kmMatch) {
      return Math.round(parseFloat(kmMatch[1]) * 1000); // Convert km to meters
    } else if (mMatch) {
      return parseInt(mMatch[1], 10); // Keep value in meters
    }
    return null; // Invalid input
  };
});
window.initMap = initMap;

    
        html,
body {
  height: 100%;
  margin: 0;
  padding: 0;
  font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
}

#container {
  height: 100%;
  display: flex;
}

#sidebar {
  flex-basis: 12rem;
  flex-grow: 1;
  max-width: 30rem;
  height: 100%;
  box-sizing: border-box;
  overflow: auto;
}

#map {
  flex-basis: 70vw;
  flex-grow: 5;
  height: 100%;
}

/*
 * Always set the map height explicitly to define the size of the div element
 * that contains the map.
 */
#map {
  height: 100%;
}

/*
 * Optional: Makes the sample page fill the window.
 */
html,
body {
  height: 100%;
  margin: 0;
  padding: 0;
  font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
}

#autocomplete-container {
  display: flex;
  position: absolute;
  top: 10px;
  left: 10px;
  z-index: 1;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2), 0 -1px 0px rgba(0, 0, 0, 0.02);
  background: #fff;
  border-radius: 12px;
  padding: 0 12px;
  max-width: 320px;
  width: 100%;
  height: 42px;
  border: none;
  box-sizing: border-box;
  align-items: center;
  cursor: text;
  font-size: 15px;
}

#autocomplete-container .search-icon, #autocomplete-container .clear-icon {
  color: inherit;
  flex-shrink: 0;
  height: 16px;
  width: 16px;
}

#autocomplete-container .clear-icon {
  transform: scale(1.3);
}

#autocomplete-input {
  box-sizing: border-box;
  padding: 0;
  height: 40px;
  line-height: 24px;
  vertical-align: top;
  transition-property: color;
  transition-duration: 0.3s;
  width: 100%;
  text-overflow: ellipsis;
  background: transparent;
  border-radius: 0;
  border: 0;
  margin: 0 8px;
  outline: 0;
  overflow: visible;
  appearance: textfield;
  font-size: 100%;
}

.clear-searchButton {
  display: none;
  height: 18px;
  width: 22px;
  background: none;
  border: none;
  vertical-align: middle;
  pointer-events: all;
  cursor: pointer;
}

#suggestions-list {
  border-radius: 12px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2), 0 -1px 0px rgba(0, 0, 0, 0.02);
  box-sizing: border-box;
  position: absolute;
  max-width: 320px;
  width: 100%;
  top: 100%;
  left: 0;
  z-index: 1;
  list-style: none;
  max-height: 80vh;
  margin: 5px 0 0;
  padding: 0;
  display: none;
  overflow-y: auto;
  background-color: #fff;
}

#suggestions-list.visible {
  display: block;
}

#suggestions-list li {
  padding: 12px;
  cursor: pointer;
  transition: background-color 0.3s ease;
}

#suggestions-list li:hover {
  background-color: #f2f2f2;
}

#sidebar {
  flex-basis: 18rem;
  box-shadow: 0 -2px 4px 0 rgba(0, 0, 0, 0.12);
  z-index: 1;
}

#innerWrapper {
  display: flex;
  flex-direction: column;
  flex-grow: 1;
  overflow: hidden;
  overflow-y: auto;
  padding: 0 10px 40px;
}

#mapContainer {
  display: flex;
  flex-direction: column;
  flex-basis: 70vw;
  flex-grow: 5;
  position: relative;
}

#map.cursor-crosshair .mapboxgl-canvas-container {
  cursor: crosshair !important;
}

.sectionHeader {
  background: #f1f1f1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  border-bottom: 1px solid #eeeeee;
  margin: 20px -10px 5px;
  padding: 5px 10px;
  color: #222;
}
.sectionHeader span {
  font-size: 0.85em;
  font-weight: 600;
}
.sectionHeader:first-child {
  margin-top: 0;
}

.categoriesOptions {
  padding: 0;
  margin: 0;
  list-style: none;
  height: 100%;
  background: #fff;
  display: flex;
  font-size: 13px;
}
.categoriesOptions__list {
  width: 100%;
  height: 100%;
  max-height: 30vh;
  overflow: scroll;
}
.categoriesOptions__list ul {
  list-style: none;
  padding: 0;
  margin: 0;
}
.categoriesOptions__list ul li {
  padding: 3px 0;
}
.categoriesOptions__list ul li input[type=checkbox] {
  margin-right: 3px;
}
.categoriesOptions__list ul li input[type=checkbox]:disabled {
  box-shadow: inset 0 0 20px #999;
}
.categoriesOptions__list ul li label {
  margin-left: 3px;
  font-size: 0.85em;
}
.categoriesOptions__list ul li ul {
  margin-left: 10px;
  padding-left: 5px;
  border-left: 1px solid #ddd;
}
.categoriesOptions__input {
  height: 24px;
  display: flex;
  align-items: baseline;
}

.radius__container {
  display: flex;
  flex-direction: row;
  align-items: center;
}
.radius__container > label {
  padding-left: 10px;
}

#page-previous {
  margin-right: 5px;
}

ol#results {
  list-style-type: none;
  margin: 0;
  padding-left: 0;
}
ol#results > li {
  margin-top: 10px;
  background-color: white;
  font-size: 10pt;
  line-height: 1.2rem;
  padding: 5px;
}
ol#results > li > * {
  display: block;
}
ol#results > li > .distance {
  padding-top: 0.2rem;
  font-weight: lighter;
}
ol#results > li:hover {
  cursor: pointer;
  background-color: #f2f2f2;
}

#radius {
  flex: 1; /* Take all remaining space */
}

#radius-label {
  border: 1px dashed #ccc;
  outline: none;
}


    
        <html>
  <head>
    <title>Localities Nearby POI</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta charset="utf-8" />

    <link rel="stylesheet" type="text/css" href="./style.css" />
    <script type="module" src="./index.js"></script>
  </head>
  <body>
    <div id="container">
      <div id="sidebar">
        <div id="innerWrapper">
          <div class="sectionHeader"><span>Categories</span></div>
          <div class="categoriesOptions__list"></div>
          <div class="sectionHeader"><span>Radius</span></div>
          <div class="radius__container">
            <input
              type="range"
              id="radius"
              min="100"
              max="10000"
              value="1000"
              step="100"
            /><label id="radius-label" contenteditable="true"
              >1&thinsp;km</label
            >
          </div>

          <div class="sectionHeader">
            <span>Results</span>
            <span is="pagination"
              ><button id="page-previous" disabled>&#8249;</button
              ><button id="page-next" disabled>&#8250;</button></span
            >
          </div>
          <ol id="results"></ol>
        </div>
      </div>

      <div id="mapContainer">
        <div id="autocomplete-container">
          <svg class="search-icon" viewBox="0 0 16 16">
            <path
              d="M3.617 7.083a4.338 4.338 0 1 1 8.676 0 4.338 4.338 0 0 1-8.676 0m4.338-5.838a5.838 5.838 0 1 0 2.162 11.262l2.278 2.279a1 1 0 0 0 1.415-1.414l-1.95-1.95A5.838 5.838 0 0 0 7.955 1.245"
              fill-rule="evenodd"
              clip-rule="evenodd"
            ></path>
          </svg>

          <input
            type="text"
            id="autocomplete-input"
            placeholder="Search Localities..."
            autocomplete="off"
          />
          <button aria-label="Clear" class="clear-searchButton" type="button">
            <svg class="clear-icon" viewBox="0 0 24 24">
              <path
                d="M7.074 5.754a.933.933 0 1 0-1.32 1.317L10.693 12l-4.937 4.929a.931.931 0 1 0 1.319 1.317l4.938-4.93 4.937 4.93a.933.933 0 0 0 1.581-.662.93.93 0 0 0-.262-.655L13.331 12l4.937-4.929a.93.93 0 0 0-.663-1.578.93.93 0 0 0-.656.261l-4.938 4.93z"
              ></path>
            </svg>
          </button>
          <ul id="suggestions-list"></ul>
        </div>

        <div id="map"></div>
      </div>
    </div>


    <script
      src="https://sdk.woosmap.com/map/map.js?key=woos-48c80350-88aa-333e-835a-07f4b658a9a4&callback=initMap"
      defer
    ></script>
  </body>
</html>

    

Running the Sample Locally

Before you can run this sample on your local machine, you need to have Git and Node.js installed. If they’re not already installed, follow these instructions to get them set up.

Once you have Git and Node.js installed, you can run the sample by following these steps:

  1. Clone the repository and navigate to the directory of the sample.

  2. Install the necessary dependencies.

  3. Start running the sample.

Here are the commands you can use in your terminal to do this:

Shell
        git clone -b sample/localities-nearby-poi https://github.com/woosmap/js-samples.git
cd js-samples
npm i
npm start

    

You can experiment with other samples by switching to any branch that starts with the pattern sample/SAMPLE_NAME.

Shell
        git checkout sample/SAMPLE_NAME
npm i
npm start

    
Was this article helpful?
Have more questions? Submit a request