<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<title>Create a FeatureLayer with client-side graphics | Sample | ArcGIS Maps SDK for JavaScript</title>
<!-- Load the ArcGIS Maps SDK for JavaScript from CDN -->
<script type="module" src="https://js.arcgis.com/5.1/"></script>
<!-- Library for reading image metadata, including AVIF EXIF GPS data -->
<script src="https://cdn.jsdelivr.net/npm/exifreader/dist/exif-reader.min.js"></script>
<arcgis-map basemap="gray-vector">
<arcgis-zoom slot="top-left"></arcgis-zoom>
<arcgis-popup slot="popup"></arcgis-popup>
const [FeatureLayer, MediaContent, promiseUtils, Graphic, Point, locator] = await $arcgis.import([
"@arcgis/core/layers/FeatureLayer.js",
"@arcgis/core/popup/content/MediaContent.js",
"@arcgis/core/core/promiseUtils.js",
"@arcgis/core/Graphic.js",
"@arcgis/core/geometry/Point.js",
"@arcgis/core/rest/locator.js",
const viewElement = document.querySelector("arcgis-map");
await viewElement.viewOnReady();
// set the view extent to show the area where photos were taken
// Configure the popup to dock so it doesn't cover points
const popupComponent = document.querySelector("arcgis-popup");
popupComponent.dockEnabled = true;
popupComponent.dockOptions = {
// Fetch a list of images and return their graphic promises.
const fetchImages = () => {
const graphicPromises = [];
const baseUrl = "%prefix%/assets/sample-code/layers-featurelayer-collection/";
for (let i = 1; i <= numPhotos; i++) {
const url = `${baseUrl}photo-${i}.avif`;
const graphicPromise = exifToGraphic(url, i);
graphicPromises.push(graphicPromise);
return promiseUtils.eachAlways(graphicPromises);
// Filters only promises that resolve with valid values (a graphic
// in this case) and resolves them as an array of graphics.
// In other words, each attempt at fetching an image returns a promise.
// Images that fail to fetch will be filtered out of the response array
// so the images that successfully load can be added to the layer.
const getFeaturesFromPromises = (eachAlwaysResponses) =>
.filter((graphicPromise) => graphicPromise.value)
.map((graphicPromise) => graphicPromise.value);
// Creates a client-side FeatureLayer from an array of graphics
const createLayer = (graphics) =>
objectIdField: "OBJECTID",
{ name: "OBJECTID", type: "oid" },
{ name: "url", type: "string" },
outFields: ["OBJECTID", "url"],
title: async ({ graphic: { geometry } }) => {
return `Lat: ${geometry.latitude.toFixed(3)}, Long: ${geometry.longitude.toFixed(3)}`;
content: ({ graphic: { attributes } }) => {
mediaInfos: [{ value: { sourceURL: attributes.url } }],
family: "CalciteWebCoreIcons",
// Load image metadata from a URL and return a point graphic for the photo location.
const exifToGraphic = async (url, id) => {
const tags = await ExifReader.load(url, { expanded: true });
const coordinates = getCoordinates(tags);
throw new Error(`Photo doesn't contain GPS information: ${url}`);
geometry: new Point(coordinates),
attributes: { url, OBJECTID: id },
const getDirection = (value) => {
if (Array.isArray(value)) return value.join("");
const dmsPartToNumber = (part) => {
if (typeof part === "number") return part;
if (Array.isArray(part) && part.length === 2) return part[0] / part[1];
const degreesMinutesSecondsToDecimal = ([degrees, minutes, seconds], direction) => {
let decimalDegrees = dmsPartToNumber(degrees) + dmsPartToNumber(minutes) / 60 + dmsPartToNumber(seconds) / 3600;
if (direction === "S" || direction === "W") decimalDegrees *= -1;
const getCoordinates = (tags) => {
if (typeof tags?.gps?.Latitude === "number" && typeof tags?.gps?.Longitude === "number") {
return { latitude: tags.gps.Latitude, longitude: tags.gps.Longitude };
const latitude = tags?.exif?.GPSLatitude?.value;
const latitudeDirection = getDirection(tags?.exif?.GPSLatitudeRef?.value);
const longitude = tags?.exif?.GPSLongitude?.value;
const longitudeDirection = getDirection(tags?.exif?.GPSLongitudeRef?.value);
if (!latitude || !longitude || !latitudeDirection || !longitudeDirection) {
latitude: degreesMinutesSecondsToDecimal(latitude, latitudeDirection),
longitude: degreesMinutesSecondsToDecimal(longitude, longitudeDirection),
// Main async function to fetch images, create graphics,
// create the FeatureLayer and add it to the map.
const images = await fetchImages();
const features = getFeaturesFromPromises(images);
const layer = createLayer(features);
viewElement.map.add(layer);
console.error("Creating FeatureLayer from photos failed", e);