<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.0/"></script>
<!-- Library for for reading EXIF data from image files
See https://github.com/exif-js/exif-js for more information -->
<script src="https://cdn.jsdelivr.net/npm/exif-js"></script>
<arcgis-map basemap="gray-vector">
<arcgis-zoom slot="top-left"></arcgis-zoom>
<arcgis-popup slot="popup"></arcgis-popup>
const [FeatureLayer, promiseUtils, Graphic, Point, locator] = await $arcgis.import([
"@arcgis/core/layers/FeatureLayer.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 = {
// 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);
* Fetches a list of images and returns a list of promises
const graphicPromises = [];
"https://arcgis.github.io/arcgis-samples-javascript/sample-data/featurelayer-collection/photo-";
for (let i = 1; i <= numPhotos; i++) {
const url = baseUrl + i.toString() + ".jpg";
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.
function getFeaturesFromPromises(eachAlwaysResponses) {
return eachAlwaysResponses
.filter((graphicPromise) => {
return graphicPromise.value;
.map((graphicPromise) => {
return graphicPromise.value;
// Creates a client-side FeatureLayer from an array of graphics
function createLayer(graphics) {
return new FeatureLayer({
objectIdField: "OBJECTID",
location: event.graphic.geometry,
return "The middle of nowhere";
content: "<img src='{url}'>",
family: "CalciteWebCoreIcons",
* Fetches and loads an image from a url and gets the latitude/longitude
* GPS data from the EXIF data of the image. Returns a promise that
* resolves to a Graphic with a point geometry representing the location
* where the photo was taken.
function exifToGraphic(url, id) {
return new Promise((resolve, reject) => {
const image = document.createElement("img");
image.load = image.onerror = null;
EXIF.getData(image, function () {
const latitude = EXIF.getTag(this, "GPSLatitude");
const latitudeDirection = EXIF.getTag(this, "GPSLatitudeRef");
const longitude = EXIF.getTag(this, "GPSLongitude");
const longitudeDirection = EXIF.getTag(this, "GPSLongitudeRef");
if (!latitude || !longitude) {
reject(new Error("Photo doesn't contain GPS information: ", this.src));
const location = new Point({
latitude: dmsDD(latitude, latitudeDirection),
longitude: dmsDD(longitude, longitudeDirection),
image.load = image.onerror = null;
reject(new Error("Error while loading the image"));
// Converts a DMS coordinate to decimal degrees
function dmsDD([degrees, minutes, seconds], direction) {
let dd = degrees + minutes / 60 + seconds / 3600;
if (direction === "S" || direction === "W") {