Learn how to find places around a location with a keyword search, then get detailed place information.
A nearby search finds places within a given radius of a location using the places service. The location typically represents a point on a map or the geolocation of a device.
To perform a nearby search, you use the places package from ArcGIS REST JS. With the results of the search, you can make another request to the service and return place attributes including the name, categories, ratings, and store hours.
In this tutorial, you use ArcGIS REST JS to perform a nearby search to find points of interest and return all available attributes associated with a place. The tutorial includes starter code that uses Calcite components to create a basic search interface.
Prerequisites
Steps
Get the starter code
- To get started, . It contains the Calcite Components necessary to create this application, as well as an initial MapLibre map.
Get an access token
You need an access token with the correct privileges to access the resources used in this tutorial.
-
Go to the Create an API key tutorial and create an API key with the following privilege(s):
- Privileges
- Location services > Basemaps
- Location services > Places
- Privileges
-
Copy the API key access token to your clipboard when prompted.
-
In CodePen, update the
access
variable to use your access token.Token Use dark colors for code blocks const accessToken = "YOUR_ACCESS_TOKEN"; const basemapEnum = "arcgis/outdoor"; const map = new maplibregl.Map({ container: "map", // the id of the div element style: `https://basemapstyles-api.arcgis.com/arcgis/rest/services/styles/v2/styles/${basemapEnum}?token=${accessToken}`, zoom: 12, // starting zoom center: [-118.805, 34.027] // starting location [longitude, latitude] });
-
Run the code to ensure the basemap is displayed in the map.
To learn about the other types of authentication available, go to Types of authentication.
Reference ArcGIS REST JS
-
Reference the
routing
andrequest
packages from ArcGIS REST JS.Use dark colors for code blocks <!-- Load MapLibre from CDN --> <script src=https://unpkg.com/maplibre-gl@4/dist/maplibre-gl.js></script> <link href=https://unpkg.com/maplibre-gl@4/dist/maplibre-gl.css rel="stylesheet" /> <!-- Calcite components --> <script type="module" src=https://js.arcgis.com/calcite-components/2.12.1/calcite.esm.js></script> <link rel="stylesheet" type="text/css" href=https://js.arcgis.com/calcite-components/2.12.1/calcite.css /> <!-- ArcGIS REST JS: request and places --> <script src="https://unpkg.com/@esri/arcgis-rest-request@4/dist/bundled/request.umd.js"></script> <script src="https://unpkg.com/@esri/arcgis-rest-places@1/dist/bundled/places.umd.js"></script>
-
Create a REST JS
Api
using your API key.Key Manager Use dark colors for code blocks const accessToken = "YOUR_ACCESS_TOKEN"; const basemapEnum = "arcgis/navigation"; const map = new maplibregl.Map({ container: "map", // the id of the div element style: `https://basemapstyles-api.arcgis.com/arcgis/rest/services/styles/v2/styles/${basemapEnum}?token=${accessToken}`, zoom: 14, // starting zoom center: [-118.46651, 33.98621] // starting location [longitude, latitude] }); const authentication = arcgisRest.ApiKeyManager.fromKey(accessToken);
Set the search category
To perform a place search, you typically need a category ID. Each category ID corresponds to a unique category of place, such as "Chinese Restaurants" (13099
) or "Sports and Recreation" (18000
). A Calcite category selector is included in the . It includes the default top-level categories as selectable options.
-
Declare global variables to track the active place category and clicked location. Set the active category to
16000
, which corresponds to theLandmarks and Outdoors
category of places.Use dark colors for code blocks const authentication = arcgisRest.ApiKeyManager.fromKey(accessToken); let activeCategory = "16000"; let userLocation, clickedPoint;
-
Create an event handler that updates the
active
when a new category is selected from the dropdown.Category Use dark colors for code blocks let activeCategory = "16000"; let userLocation, clickedPoint; categorySelect.addEventListener("calciteComboboxChange", e => { activeCategory = categorySelect.value; });
Set the search area
A nearby search is defined by a user location and a radius. The places service will search for points of interest near the provided location within the search radius. Add a click handler to set the user location, then draw a circle around that point to represent the search radius.
-
Add a reference to Turf.js, which will be used to visualize the search extent.
Use dark colors for code blocks <!-- Load MapLibre from CDN --> <script src=https://unpkg.com/maplibre-gl@4/dist/maplibre-gl.js></script> <link href=https://unpkg.com/maplibre-gl@4/dist/maplibre-gl.css rel="stylesheet" /> <!-- Calcite components --> <script type="module" src=https://js.arcgis.com/calcite-components/2.12.1/calcite.esm.js></script> <link rel="stylesheet" type="text/css" href=https://js.arcgis.com/calcite-components/2.12.1/calcite.css /> <!-- ArcGIS REST JS: request and places --> <script src="https://unpkg.com/@esri/arcgis-rest-request@4/dist/bundled/request.umd.js"></script> <script src="https://unpkg.com/@esri/arcgis-rest-places@1/dist/bundled/places.umd.js"></script> <script src="https://cdn.jsdelivr.net/npm/@turf/turf@7.1.0/turf.min.js"></script>
-
Add a
fill
layer to your map to represent the area what will be searched. Add a globalsearch
variable equal toRadius 750
meters.Use dark colors for code blocks let activeCategory = "16000"; let userLocation, clickedPoint; const searchRadius = 750; map.once("load", () => { map.addSource("clicked-point", { type: "geojson", data: { "type": "Feature", "geometry": { "type":"Point", "coordinates": [] }} }); map.addLayer({ id:"search-location", source:'clicked-point', type:"fill", paint: { 'fill-color': '#aaaaaa', 'fill-opacity':0.25, 'fill-outline-color':'#000000', } }) })
-
Create a click event handler that saves the location of clicks on the map.
Use dark colors for code blocks map.once("load", () => { map.addSource("clicked-point", { type: "geojson", data: { "type": "Feature", "geometry": { "type":"Point", "coordinates": [] }} }); map.addLayer({ id:"search-location", source:'clicked-point', type:"fill", paint: { 'fill-color': '#aaaaaa', 'fill-opacity':0.25, 'fill-outline-color':'#000000', } }) }) map.on('click', function (e) { userLocation = e.lngLat; })
-
When a click is registered, use
turf.circle
to create a circle around the point with a radius in meters. Set the source of yourfill
layer to this circle.Use dark colors for code blocks map.on('click', function (e) { userLocation = e.lngLat; const searchArea = turf.circle([e.lngLat.lng, e.lngLat.lat], searchRadius, { steps:36, units:"meters" }); map.getSource('clicked-point').setData(searchArea); })
Find places near a point
Once a location and category have been set, make a request to the places service to find places near a point.
-
Define a new function
show
. Inside, use the ArcGIS REST JSPlaces find
function to submit a request to the Places service. Pass thePlaces Near Point search
,Radius user
,Location active
, and yourCategory authentication
object.Use dark colors for code blocks map.once("load", () => { map.addSource("clicked-point", { type: "geojson", data: { "type": "Feature", "geometry": { "type":"Point", "coordinates": [] }} }); map.addLayer({ id:"search-location", source:'clicked-point', type:"fill", paint: { 'fill-color': '#aaaaaa', 'fill-opacity':0.25, 'fill-outline-color':'#000000', } }) }) map.on('click', function (e) { userLocation = e.lngLat; const searchArea = turf.circle([e.lngLat.lng, e.lngLat.lat], searchRadius, { steps:36, units:"meters" }); map.getSource('clicked-point').setData(searchArea); }) categorySelect.addEventListener("calciteComboboxChange", e => { activeCategory = categorySelect.value; }); function showPlaces() { arcgisRest.findPlacesNearPoint({ x: userLocation.lng, y: userLocation.lat, categoryIds:activeCategory, radius:searchRadius, authentication }) };
-
Call the
show
function inside both of the event handlers.Places Use dark colors for code blocks map.once("load", () => { map.addSource("clicked-point", { type: "geojson", data: { "type": "Feature", "geometry": { "type":"Point", "coordinates": [] }} }); map.addLayer({ id:"search-location", source:'clicked-point', type:"fill", paint: { 'fill-color': '#aaaaaa', 'fill-opacity':0.25, 'fill-outline-color':'#000000', } }) }) map.on('click', function (e) { userLocation = e.lngLat; const searchArea = turf.circle([e.lngLat.lng, e.lngLat.lat], searchRadius, { steps:36, units:"meters" }); map.getSource('clicked-point').setData(searchArea); showPlaces(); }) categorySelect.addEventListener("calciteComboboxChange", e => { activeCategory = categorySelect.value; if (userLocation) showPlaces(); }); function showPlaces() { arcgisRest.findPlacesNearPoint({ x: userLocation.lng, y: userLocation.lat, categoryIds:activeCategory, radius:searchRadius, authentication }) };
Display results on the map
The places service will return a list of places near your location that match the specified category. Display these results as points on your map.
-
Create a new list to store service results. When a new places request is made, remove all results from the map.
Use dark colors for code blocks const currentPlaces = []; function showPlaces() { for (let place of currentPlaces) { place.remove(); } resultPanel.innerHTML = ""; arcgisRest.findPlacesNearPoint({ x: userLocation.lng, y: userLocation.lat, categoryIds:activeCategory, radius:searchRadius, authentication }) };
-
Access the service response. For each result, add a MapLibre
Marker
to your map.Use dark colors for code blocks arcgisRest.findPlacesNearPoint({ x: userLocation.lng, y: userLocation.lat, categoryIds:activeCategory, radius:searchRadius, authentication }) .then((response)=>{ response.results.forEach((result)=>{ addResult(result); }); }); }; function addResult(place) { const marker = new maplibregl.Marker() .setLngLat([place.location.x, place.location.y]) .addTo(map); currentPlaces.push(marker); }
Display results in a list
To show more information about each place, you will also display place results as an element in a Calcite list. The template for this list is included in the starter code.
-
Create a new
calcite-list-item
element to display place information. Set the item properties to display the place name, category, and distance from the user's click. Add the element to the list of results.Use dark colors for code blocks function addResult(place) { const marker = new maplibregl.Marker() .setLngLat([place.location.x, place.location.y]) .addTo(map); currentPlaces.push(marker); const infoDiv = document.createElement("calcite-list-item"); resultPanel.appendChild(infoDiv); const description = ` ${place.categories[0].label} - ${Number((place.distance / 1000).toFixed(1))} km `; infoDiv.label = place.name; infoDiv.description = description; }
-
When the HTML element is clicked, display a popup at the associated marker.
Use dark colors for code blocks function addResult(place) { const marker = new maplibregl.Marker() .setLngLat([place.location.x, place.location.y]) .addTo(map); currentPlaces.push(marker); const infoDiv = document.createElement("calcite-list-item"); resultPanel.appendChild(infoDiv); const description = ` ${place.categories[0].label} - ${Number((place.distance / 1000).toFixed(1))} km `; infoDiv.label = place.name; infoDiv.description = description; marker.id = place.placeId; marker.setPopup(new maplibregl.Popup().setText(place.name)); infoDiv.addEventListener("click",e => { marker.togglePopup(); }) }
-
Run the app. Clicking on the map should perform a place search and display results as markers on the map. Result names and categories should display in the results panel to the left.
Get place details
The user of your application should be able to click on a result in the panel in order to see more information about it. Perform an arcgis
request to obtain detailed attributes about a specific point of interest.
-
Create a new function
get
. When a result is clicked, callDetails get
and pass the marker associated with the result.Details Use dark colors for code blocks function addResult(place) { const marker = new maplibregl.Marker() .setLngLat([place.location.x, place.location.y]) .addTo(map); currentPlaces.push(marker); const infoDiv = document.createElement("calcite-list-item"); resultPanel.appendChild(infoDiv); const description = ` ${place.categories[0].label} - ${Number((place.distance / 1000).toFixed(1))} km `; infoDiv.label = place.name; infoDiv.description = description; marker.id = place.placeId; marker.setPopup(new maplibregl.Popup().setText(place.name)); infoDiv.addEventListener("click",e => { marker.togglePopup(); getDetails(marker); }) } function getDetails(marker) { }
-
Call
arcgis
to get more information about a specific POI. Pass the place id to theRest.get Place Details place
parameter and provide authentication.Id Use dark colors for code blocks function getDetails(marker) { arcgisRest.getPlaceDetails({ placeId: marker.id, requestedFields: ["all"], authentication }) }
Display place details
Display the resulting place details in a new panel. The helper code for this panel is provided in the template file.
-
Access the response from the places service and create a new
calcite-flow-item
element to display results. Add the flow item to the<flow
element included in the template.> Use dark colors for code blocks function getDetails(marker) { arcgisRest.getPlaceDetails({ placeId: marker.id, requestedFields: ["all"], authentication }) .then((result)=>{ map.flyTo({center:marker.getLngLat()}); infoPanel = document.createElement("calcite-flow-item"); flow.appendChild(infoPanel); }); }
-
Set the name and description of the panel using the service response. Call the
set
helper function included with the starter code to display the rest of the attributes if they are valid. Select a Calcite UI icon for each attribute.Attribute Use dark colors for code blocks .then((result)=>{ map.flyTo({center:marker.getLngLat()}); infoPanel = document.createElement("calcite-flow-item"); flow.appendChild(infoPanel); const placeDetails = result.placeDetails; infoPanel.heading = placeDetails.name; infoPanel.description = placeDetails.categories[0].label; setAttribute("Description", "information", placeDetails?.description); setAttribute("Address", "map-pin", placeDetails?.address?.streetAddress); setAttribute("Phone", "mobile", placeDetails?.contactInfo?.telephone); setAttribute("Hours", "clock", placeDetails?.hours?.openingText); setAttribute("Rating", "star", placeDetails?.rating?.user); setAttribute("Email", "email-address", placeDetails?.contactInfo?.email); setAttribute("Website", "information", placeDetails?.contactInfo?.website?.split("://")[1].split("/")[0]); setAttribute("Facebook", "speech-bubble-social", (placeDetails?.socialMedia?.facebookId) ? `www.facebook.com/${placeDetails.socialMedia.facebookId}` : null); setAttribute("Twitter", "speech-bubbles", (placeDetails?.socialMedia?.twitter) ? `www.twitter.com/${placeDetails.socialMedia.twitter}` : null); setAttribute("Instagram", "camera", (placeDetails?.socialMedia?.instagram) ? `www.instagram.com/${placeDetails.socialMedia.instagram}` : null); });
-
When the info panel is closed, close any open map popups.
Use dark colors for code blocks function getDetails(marker) { arcgisRest.getPlaceDetails({ placeId: marker.id, requestedFields: ["all"], authentication }) .then((result)=>{ map.flyTo({center:marker.getLngLat()}); infoPanel = document.createElement("calcite-flow-item"); flow.appendChild(infoPanel); const placeDetails = result.placeDetails; infoPanel.heading = placeDetails.name; infoPanel.description = placeDetails.categories[0].label; setAttribute("Description", "information", placeDetails?.description); setAttribute("Address", "map-pin", placeDetails?.address?.streetAddress); setAttribute("Phone", "mobile", placeDetails?.contactInfo?.telephone); setAttribute("Hours", "clock", placeDetails?.hours?.openingText); setAttribute("Rating", "star", placeDetails?.rating?.user); setAttribute("Email", "email-address", placeDetails?.contactInfo?.email); setAttribute("Website", "information", placeDetails?.contactInfo?.website?.split("://")[1].split("/")[0]); setAttribute("Facebook", "speech-bubble-social", (placeDetails?.socialMedia?.facebookId) ? `www.facebook.com/${placeDetails.socialMedia.facebookId}` : null); setAttribute("Twitter", "speech-bubbles", (placeDetails?.socialMedia?.twitter) ? `www.twitter.com/${placeDetails.socialMedia.twitter}` : null); setAttribute("Instagram", "camera", (placeDetails?.socialMedia?.instagram) ? `www.instagram.com/${placeDetails.socialMedia.instagram}` : null); infoPanel.addEventListener("calciteFlowItemBack", e => { marker.togglePopup(); }) }); }
-
Close the info panel when a new search is performed.
Use dark colors for code blocks function showPlaces() { for (let place of currentPlaces) { place.remove(); } resultPanel.innerHTML = ""; if (infoPanel) infoPanel.remove(); arcgisRest.findPlacesNearPoint({ x: userLocation.lng, y: userLocation.lat, categoryIds:activeCategory, radius:searchRadius, authentication }) .then((response)=>{ response.results.forEach((result)=>{ addResult(result); }); }); };
Run the app
In CodePen, run your code to display the application. The app should display a map with a result panel on the left, which will populate with values when the map is clicked. Upon clicking on a place result, more information about the place should appear in the panel.
What's next?
Learn how to use additional ArcGIS location services in these tutorials: