esri-leaflet
Version:
Leaflet plugins for consuming ArcGIS Online and ArcGIS Server services.
1,662 lines (1,402 loc) • 155 kB
JavaScript
/* esri-leaflet - v3.0.18 - Thu Aug 14 2025 15:02:37 GMT-0500 (Central Daylight Time)
* Copyright (c) 2025 Environmental Systems Research Institute, Inc.
* Apache-2.0 */
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('leaflet')) :
typeof define === 'function' && define.amd ? define(['exports', 'leaflet'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory((global.L = global.L || {}, global.L.esri = {}), global.L));
})(this, (function (exports, leaflet) { 'use strict';
var name = "esri-leaflet";
var description = "Leaflet plugins for consuming ArcGIS Online and ArcGIS Server services.";
var version$1 = "3.0.18";
var author = "Patrick Arlt <parlt@esri.com> (http://patrickarlt.com)";
var bugs = {
url: "https://github.com/esri/esri-leaflet/issues"
};
var contributors = [
"Patrick Arlt <parlt@esri.com> (http://patrickarlt.com)",
"John Gravois (https://johngravois.com)",
"Gavin Rehkemper <grehkemper@esri.com> (https://gavinr.com)",
"Jacob Wasilkowski (https://jwasilgeo.github.io)"
];
var dependencies = {
"@terraformer/arcgis": "^2.1.0",
"tiny-binary-search": "^1.0.3"
};
var devDependencies = {
"@rollup/plugin-json": "^6.0.0",
"@rollup/plugin-node-resolve": "^15.0.1",
"@rollup/plugin-terser": "^0.3.0",
chai: "4.3.7",
"chokidar-cli": "^3.0.0",
eslint: "^9.24.0",
"eslint-config-mourner": "^4.0.2",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-import-x": "^4.10.2",
"gh-release": "^7.0.1",
"highlight.js": "^11.7.0",
"http-server": "^14.1.1",
husky: "^1.1.1",
karma: "^6.4.1",
"karma-chrome-launcher": "^3.1.1",
"karma-coverage": "^2.2.0",
"karma-edgium-launcher": "github:matracey/karma-edgium-launcher",
"karma-firefox-launcher": "^2.1.2",
"karma-mocha": "^2.0.1",
"karma-mocha-reporter": "^2.2.5",
"karma-safari-launcher": "~1.0.0",
"karma-sinon-chai": "^2.0.2",
"karma-sourcemap-loader": "^0.3.8",
leaflet: "^1.6.0",
mkdirp: "^1.0.4",
mocha: "^10.2.0",
"npm-run-all": "^4.1.5",
prettier: "3.5.3",
rollup: "^2.79.1",
sinon: "^15.0.1",
"sinon-chai": "3.7.0"
};
var files = [
"src/**/*.js",
"dist/esri-leaflet.js",
"dist/esri-leaflet.js.map",
"dist/esri-leaflet-debug.js.map",
"dist/siteData.json",
"profiles/*.js"
];
var homepage = "https://developers.arcgis.com/esri-leaflet/";
var module = "src/EsriLeaflet.js";
var jspm = {
registry: "npm",
format: "es6",
main: "src/EsriLeaflet.js"
};
var keywords = [
"arcgis",
"esri",
"esri leaflet",
"gis",
"leaflet plugin",
"mapping"
];
var license = "Apache-2.0";
var main = "dist/esri-leaflet-debug.js";
var peerDependencies = {
leaflet: "^1.0.0"
};
var readmeFilename = "README.md";
var repository = {
type: "git",
url: "git@github.com:Esri/esri-leaflet.git"
};
var scripts = {
build: "rollup -c profiles/debug.js & rollup -c profiles/production.js",
lint: "npm run eslint && npm run prettier",
eslint: "eslint .",
eslintfix: "npm run eslint -- --fix",
prettier: "npx prettier . --check",
prettierfix: "npx prettier . --write",
prebuild: "mkdirp dist",
pretest: "npm run build",
release: "./scripts/release.sh",
"start-watch": "chokidar src -c \"npm run build\"",
start: "run-p start-watch serve",
serve: "http-server -p 5000 -c-1 -o",
test: "npm run lint && karma start"
};
var unpkg = "dist/esri-leaflet-debug.js";
var husky = {
hooks: {
"pre-commit": "npm run lint"
}
};
var packageInfo = {
name: name,
description: description,
version: version$1,
author: author,
bugs: bugs,
contributors: contributors,
dependencies: dependencies,
devDependencies: devDependencies,
files: files,
homepage: homepage,
module: module,
"jsnext:main": "src/EsriLeaflet.js",
jspm: jspm,
keywords: keywords,
license: license,
main: main,
peerDependencies: peerDependencies,
readmeFilename: readmeFilename,
repository: repository,
scripts: scripts,
unpkg: unpkg,
husky: husky
};
const cors =
window.XMLHttpRequest && "withCredentials" in new window.XMLHttpRequest();
const pointerEvents =
document.documentElement.style.pointerEvents === "";
const Support = {
cors,
pointerEvents,
};
const options = {
attributionWidthOffset: 55,
};
let callbacks = 0;
function serialize(params) {
let data = "";
params.f = params.f || "json";
for (const key in params) {
if (Object.hasOwn(params, key)) {
const param = params[key];
const type = Object.prototype.toString.call(param);
let value;
if (data.length) {
data += "&";
}
if (type === "[object Array]") {
value =
Object.prototype.toString.call(param[0]) === "[object Object]"
? JSON.stringify(param)
: param.join(",");
} else if (type === "[object Object]") {
value = JSON.stringify(param);
} else if (type === "[object Date]") {
value = param.valueOf();
} else {
value = param;
}
data += `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
}
}
const APOSTROPHE_URL_ENCODE = "%27";
return data.replaceAll("'", APOSTROPHE_URL_ENCODE);
}
function createRequest(callback, context) {
const httpRequest = new window.XMLHttpRequest();
httpRequest.onerror = function () {
httpRequest.onreadystatechange = leaflet.Util.falseFn;
callback.call(
context,
{
error: {
code: 500,
message: "XMLHttpRequest error",
},
},
null,
);
};
httpRequest.onreadystatechange = function () {
let response;
let error;
if (httpRequest.readyState === 4) {
try {
response = JSON.parse(httpRequest.responseText);
} catch (e) {
response = null;
error = {
code: 500,
message:
"Could not parse response as JSON. This could also be caused by a CORS or XMLHttpRequest error.",
};
}
if (!error && response.error) {
error = response.error;
response = null;
}
httpRequest.onerror = leaflet.Util.falseFn;
callback.call(context, error, response);
}
};
httpRequest.ontimeout = function () {
this.onerror();
};
return httpRequest;
}
function xmlHttpPost(url, params, callback, context) {
const httpRequest = createRequest(callback, context);
httpRequest.open("POST", url);
if (typeof context !== "undefined" && context !== null) {
if (typeof context.options !== "undefined") {
httpRequest.timeout = context.options.timeout;
}
}
httpRequest.setRequestHeader(
"Content-Type",
"application/x-www-form-urlencoded; charset=UTF-8",
);
httpRequest.send(serialize(params));
return httpRequest;
}
function xmlHttpGet(url, params, callback, context) {
const httpRequest = createRequest(callback, context);
httpRequest.open("GET", `${url}?${serialize(params)}`, true);
if (typeof context !== "undefined" && context !== null) {
if (typeof context.options !== "undefined") {
httpRequest.timeout = context.options.timeout;
if (context.options.withCredentials) {
httpRequest.withCredentials = true;
}
}
}
httpRequest.send(null);
return httpRequest;
}
// AJAX handlers for CORS (modern browsers) or JSONP (older browsers)
function request(url, params, callback, context) {
const paramString = serialize(params);
const httpRequest = createRequest(callback, context);
const requestLength = `${url}?${paramString}`.length;
// ie10/11 require the request be opened before a timeout is applied
if (requestLength <= 2000 && Support.cors) {
httpRequest.open("GET", `${url}?${paramString}`);
} else if (requestLength > 2000 && Support.cors) {
httpRequest.open("POST", url);
httpRequest.setRequestHeader(
"Content-Type",
"application/x-www-form-urlencoded; charset=UTF-8",
);
}
if (typeof context !== "undefined" && context !== null) {
if (typeof context.options !== "undefined") {
httpRequest.timeout = context.options.timeout;
if (context.options.withCredentials) {
httpRequest.withCredentials = true;
}
}
}
// request is less than 2000 characters and the browser supports CORS, make GET request with XMLHttpRequest
if (requestLength <= 2000 && Support.cors) {
httpRequest.send(null);
// request is more than 2000 characters and the browser supports CORS, make POST request with XMLHttpRequest
} else if (requestLength > 2000 && Support.cors) {
httpRequest.send(paramString);
// request is less than 2000 characters and the browser does not support CORS, make a JSONP request
} else if (requestLength <= 2000 && !Support.cors) {
return jsonp(url, params, callback, context);
// request is longer then 2000 characters and the browser does not support CORS, log a warning
} else {
warn(
`a request to ${url} was longer then 2000 characters and this browser cannot make a cross-domain post request. Please use a proxy https://developers.arcgis.com/esri-leaflet/api-reference/request/`,
);
return;
}
return httpRequest;
}
function jsonp(url, params, callback, context) {
window._EsriLeafletCallbacks = window._EsriLeafletCallbacks || {};
const callbackId = `c${callbacks}`;
params.callback = `window._EsriLeafletCallbacks.${callbackId}`;
window._EsriLeafletCallbacks[callbackId] = function (response) {
if (window._EsriLeafletCallbacks[callbackId] !== true) {
let error;
const responseType = Object.prototype.toString.call(response);
if (
!(
responseType === "[object Object]" ||
responseType === "[object Array]"
)
) {
error = {
error: {
code: 500,
message: "Expected array or object as JSONP response",
},
};
response = null;
}
if (!error && response.error) {
error = response;
response = null;
}
callback.call(context, error, response);
window._EsriLeafletCallbacks[callbackId] = true;
}
};
const script = leaflet.DomUtil.create("script", null, document.body);
script.type = "text/javascript";
script.src = `${url}?${serialize(params)}`;
script.id = callbackId;
script.onerror = function (error) {
if (error && window._EsriLeafletCallbacks[callbackId] !== true) {
// Can't get true error code: it can be 404, or 401, or 500
const err = {
error: {
code: 500,
message: "An unknown error occurred",
},
};
callback.call(context, err);
window._EsriLeafletCallbacks[callbackId] = true;
}
};
leaflet.DomUtil.addClass(script, "esri-leaflet-jsonp");
callbacks++;
return {
id: callbackId,
url: script.src,
abort() {
window._EsriLeafletCallbacks._callback[callbackId]({
code: 0,
message: "Request aborted.",
});
},
};
}
const get = Support.cors ? xmlHttpGet : jsonp;
get.CORS = xmlHttpGet;
get.JSONP = jsonp;
function warn(...args) {
if (console && console.warn) {
console.warn.apply(console, args);
}
}
// export the Request object to call the different handlers for debugging
const Request = {
request,
get,
post: xmlHttpPost,
};
/* @preserve
* @terraformer/arcgis - v2.0.7 - MIT
* Copyright (c) 2012-2021 Environmental Systems Research Institute, Inc.
* Thu Jul 22 2021 13:58:30 GMT-0700 (Pacific Daylight Time)
*/
/* Copyright (c) 2012-2019 Environmental Systems Research Institute, Inc.
* Apache-2.0 */
var edgeIntersectsEdge = function edgeIntersectsEdge(a1, a2, b1, b2) {
var uaT = (b2[0] - b1[0]) * (a1[1] - b1[1]) - (b2[1] - b1[1]) * (a1[0] - b1[0]);
var ubT = (a2[0] - a1[0]) * (a1[1] - b1[1]) - (a2[1] - a1[1]) * (a1[0] - b1[0]);
var uB = (b2[1] - b1[1]) * (a2[0] - a1[0]) - (b2[0] - b1[0]) * (a2[1] - a1[1]);
if (uB !== 0) {
var ua = uaT / uB;
var ub = ubT / uB;
if (ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1) {
return true;
}
}
return false;
};
var coordinatesContainPoint = function coordinatesContainPoint(coordinates, point) {
var contains = false;
for (var i = -1, l = coordinates.length, j = l - 1; ++i < l; j = i) {
if ((coordinates[i][1] <= point[1] && point[1] < coordinates[j][1] || coordinates[j][1] <= point[1] && point[1] < coordinates[i][1]) && point[0] < (coordinates[j][0] - coordinates[i][0]) * (point[1] - coordinates[i][1]) / (coordinates[j][1] - coordinates[i][1]) + coordinates[i][0]) {
contains = !contains;
}
}
return contains;
};
var pointsEqual = function pointsEqual(a, b) {
for (var i = 0; i < a.length; i++) {
if (a[i] !== b[i]) {
return false;
}
}
return true;
};
var arrayIntersectsArray = function arrayIntersectsArray(a, b) {
for (var i = 0; i < a.length - 1; i++) {
for (var j = 0; j < b.length - 1; j++) {
if (edgeIntersectsEdge(a[i], a[i + 1], b[j], b[j + 1])) {
return true;
}
}
}
return false;
};
/* Copyright (c) 2012-2019 Environmental Systems Research Institute, Inc.
* Apache-2.0 */
var closeRing = function closeRing(coordinates) {
if (!pointsEqual(coordinates[0], coordinates[coordinates.length - 1])) {
coordinates.push(coordinates[0]);
}
return coordinates;
}; // determine if polygon ring coordinates are clockwise. clockwise signifies outer ring, counter-clockwise an inner ring
// or hole. this logic was found at http://stackoverflow.com/questions/1165647/how-to-determine-if-a-list-of-polygon-
// points-are-in-clockwise-order
var ringIsClockwise = function ringIsClockwise(ringToTest) {
var total = 0;
var i = 0;
var rLength = ringToTest.length;
var pt1 = ringToTest[i];
var pt2;
for (i; i < rLength - 1; i++) {
pt2 = ringToTest[i + 1];
total += (pt2[0] - pt1[0]) * (pt2[1] + pt1[1]);
pt1 = pt2;
}
return total >= 0;
}; // This function ensures that rings are oriented in the right directions
// from http://jsperf.com/cloning-an-object/2
var shallowClone = function shallowClone(obj) {
var target = {};
for (var i in obj) {
// both arcgis attributes and geojson props are just hardcoded keys
if (obj.hasOwnProperty(i)) {
// eslint-disable-line no-prototype-builtins
target[i] = obj[i];
}
}
return target;
};
/* Copyright (c) 2012-2019 Environmental Systems Research Institute, Inc.
* Apache-2.0 */
var coordinatesContainCoordinates = function coordinatesContainCoordinates(outer, inner) {
var intersects = arrayIntersectsArray(outer, inner);
var contains = coordinatesContainPoint(outer, inner[0]);
if (!intersects && contains) {
return true;
}
return false;
}; // do any polygons in this array contain any other polygons in this array?
// used for checking for holes in arcgis rings
var convertRingsToGeoJSON = function convertRingsToGeoJSON(rings) {
var outerRings = [];
var holes = [];
var x; // iterator
var outerRing; // current outer ring being evaluated
var hole; // current hole being evaluated
// for each ring
for (var r = 0; r < rings.length; r++) {
var ring = closeRing(rings[r].slice(0));
if (ring.length < 4) {
continue;
} // is this ring an outer ring? is it clockwise?
if (ringIsClockwise(ring)) {
var polygon = [ring.slice().reverse()]; // wind outer rings counterclockwise for RFC 7946 compliance
outerRings.push(polygon); // push to outer rings
} else {
holes.push(ring.slice().reverse()); // wind inner rings clockwise for RFC 7946 compliance
}
}
var uncontainedHoles = []; // while there are holes left...
while (holes.length) {
// pop a hole off out stack
hole = holes.pop(); // loop over all outer rings and see if they contain our hole.
var contained = false;
for (x = outerRings.length - 1; x >= 0; x--) {
outerRing = outerRings[x][0];
if (coordinatesContainCoordinates(outerRing, hole)) {
// the hole is contained push it into our polygon
outerRings[x].push(hole);
contained = true;
break;
}
} // ring is not contained in any outer ring
// sometimes this happens https://github.com/Esri/esri-leaflet/issues/320
if (!contained) {
uncontainedHoles.push(hole);
}
} // if we couldn't match any holes using contains we can try intersects...
while (uncontainedHoles.length) {
// pop a hole off out stack
hole = uncontainedHoles.pop(); // loop over all outer rings and see if any intersect our hole.
var intersects = false;
for (x = outerRings.length - 1; x >= 0; x--) {
outerRing = outerRings[x][0];
if (arrayIntersectsArray(outerRing, hole)) {
// the hole is contained push it into our polygon
outerRings[x].push(hole);
intersects = true;
break;
}
}
if (!intersects) {
outerRings.push([hole.reverse()]);
}
}
if (outerRings.length === 1) {
return {
type: 'Polygon',
coordinates: outerRings[0]
};
} else {
return {
type: 'MultiPolygon',
coordinates: outerRings
};
}
};
var getId = function getId(attributes, idAttribute) {
var keys = idAttribute ? [idAttribute, 'OBJECTID', 'FID'] : ['OBJECTID', 'FID'];
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
if (key in attributes && (typeof attributes[key] === 'string' || typeof attributes[key] === 'number')) {
return attributes[key];
}
}
throw Error('No valid id attribute found');
};
var arcgisToGeoJSON$1 = function arcgisToGeoJSON(arcgis, idAttribute) {
var geojson = {};
if (arcgis.features) {
geojson.type = 'FeatureCollection';
geojson.features = [];
for (var i = 0; i < arcgis.features.length; i++) {
geojson.features.push(arcgisToGeoJSON(arcgis.features[i], idAttribute));
}
}
if (typeof arcgis.x === 'number' && typeof arcgis.y === 'number') {
geojson.type = 'Point';
geojson.coordinates = [arcgis.x, arcgis.y];
if (typeof arcgis.z === 'number') {
geojson.coordinates.push(arcgis.z);
}
}
if (arcgis.points) {
geojson.type = 'MultiPoint';
geojson.coordinates = arcgis.points.slice(0);
}
if (arcgis.paths) {
if (arcgis.paths.length === 1) {
geojson.type = 'LineString';
geojson.coordinates = arcgis.paths[0].slice(0);
} else {
geojson.type = 'MultiLineString';
geojson.coordinates = arcgis.paths.slice(0);
}
}
if (arcgis.rings) {
geojson = convertRingsToGeoJSON(arcgis.rings.slice(0));
}
if (typeof arcgis.xmin === 'number' && typeof arcgis.ymin === 'number' && typeof arcgis.xmax === 'number' && typeof arcgis.ymax === 'number') {
geojson.type = 'Polygon';
geojson.coordinates = [[[arcgis.xmax, arcgis.ymax], [arcgis.xmin, arcgis.ymax], [arcgis.xmin, arcgis.ymin], [arcgis.xmax, arcgis.ymin], [arcgis.xmax, arcgis.ymax]]];
}
if (arcgis.geometry || arcgis.attributes) {
geojson.type = 'Feature';
geojson.geometry = arcgis.geometry ? arcgisToGeoJSON(arcgis.geometry) : null;
geojson.properties = arcgis.attributes ? shallowClone(arcgis.attributes) : null;
if (arcgis.attributes) {
try {
geojson.id = getId(arcgis.attributes, idAttribute);
} catch (err) {// don't set an id
}
}
} // if no valid geometry was encountered
if (JSON.stringify(geojson.geometry) === JSON.stringify({})) {
geojson.geometry = null;
}
if (arcgis.spatialReference && arcgis.spatialReference.wkid && arcgis.spatialReference.wkid !== 4326) {
console.warn('Object converted in non-standard crs - ' + JSON.stringify(arcgis.spatialReference));
}
return geojson;
};
/* Copyright (c) 2012-2019 Environmental Systems Research Institute, Inc.
* Apache-2.0 */
// outer rings are clockwise, holes are counterclockwise
// used for converting GeoJSON Polygons to ArcGIS Polygons
var orientRings = function orientRings(poly) {
var output = [];
var polygon = poly.slice(0);
var outerRing = closeRing(polygon.shift().slice(0));
if (outerRing.length >= 4) {
if (!ringIsClockwise(outerRing)) {
outerRing.reverse();
}
output.push(outerRing);
for (var i = 0; i < polygon.length; i++) {
var hole = closeRing(polygon[i].slice(0));
if (hole.length >= 4) {
if (ringIsClockwise(hole)) {
hole.reverse();
}
output.push(hole);
}
}
}
return output;
}; // This function flattens holes in multipolygons to one array of polygons
// used for converting GeoJSON Polygons to ArcGIS Polygons
var flattenMultiPolygonRings = function flattenMultiPolygonRings(rings) {
var output = [];
for (var i = 0; i < rings.length; i++) {
var polygon = orientRings(rings[i]);
for (var x = polygon.length - 1; x >= 0; x--) {
var ring = polygon[x].slice(0);
output.push(ring);
}
}
return output;
};
var geojsonToArcGIS$1 = function geojsonToArcGIS(geojson, idAttribute) {
idAttribute = idAttribute || 'OBJECTID';
var spatialReference = {
wkid: 4326
};
var result = {};
var i;
switch (geojson.type) {
case 'Point':
result.x = geojson.coordinates[0];
result.y = geojson.coordinates[1];
if (geojson.coordinates[2]) {
result.z = geojson.coordinates[2];
}
result.spatialReference = spatialReference;
break;
case 'MultiPoint':
result.points = geojson.coordinates.slice(0);
if (geojson.coordinates[0][2]) {
result.hasZ = true;
}
result.spatialReference = spatialReference;
break;
case 'LineString':
result.paths = [geojson.coordinates.slice(0)];
if (geojson.coordinates[0][2]) {
result.hasZ = true;
}
result.spatialReference = spatialReference;
break;
case 'MultiLineString':
result.paths = geojson.coordinates.slice(0);
if (geojson.coordinates[0][0][2]) {
result.hasZ = true;
}
result.spatialReference = spatialReference;
break;
case 'Polygon':
result.rings = orientRings(geojson.coordinates.slice(0));
if (geojson.coordinates[0][0][2]) {
result.hasZ = true;
}
result.spatialReference = spatialReference;
break;
case 'MultiPolygon':
result.rings = flattenMultiPolygonRings(geojson.coordinates.slice(0));
if (geojson.coordinates[0][0][0][2]) {
result.hasZ = true;
}
result.spatialReference = spatialReference;
break;
case 'Feature':
if (geojson.geometry) {
result.geometry = geojsonToArcGIS(geojson.geometry, idAttribute);
}
result.attributes = geojson.properties ? shallowClone(geojson.properties) : {};
if (geojson.id) {
result.attributes[idAttribute] = geojson.id;
}
break;
case 'FeatureCollection':
result = [];
for (i = 0; i < geojson.features.length; i++) {
result.push(geojsonToArcGIS(geojson.features[i], idAttribute));
}
break;
case 'GeometryCollection':
result = [];
for (i = 0; i < geojson.geometries.length; i++) {
result.push(geojsonToArcGIS(geojson.geometries[i], idAttribute));
}
break;
}
return result;
};
const POWERED_BY_ESRI_ATTRIBUTION_STRING =
'Powered by <a href="https://www.esri.com">Esri</a>';
function geojsonToArcGIS(geojson, idAttr) {
return geojsonToArcGIS$1(geojson, idAttr);
}
function arcgisToGeoJSON(arcgis, idAttr) {
return arcgisToGeoJSON$1(arcgis, idAttr);
}
// convert an extent (ArcGIS) to LatLngBounds (Leaflet)
function extentToBounds(extent) {
// "NaN" coordinates from ArcGIS Server indicate a null geometry
if (
extent.xmin !== "NaN" &&
extent.ymin !== "NaN" &&
extent.xmax !== "NaN" &&
extent.ymax !== "NaN"
) {
const sw = leaflet.latLng(extent.ymin, extent.xmin);
const ne = leaflet.latLng(extent.ymax, extent.xmax);
return leaflet.latLngBounds(sw, ne);
}
return null;
}
// convert an LatLngBounds (Leaflet) to extent (ArcGIS)
function boundsToExtent(bounds) {
bounds = leaflet.latLngBounds(bounds);
return {
xmin: bounds.getSouthWest().lng,
ymin: bounds.getSouthWest().lat,
xmax: bounds.getNorthEast().lng,
ymax: bounds.getNorthEast().lat,
spatialReference: {
wkid: 4326,
},
};
}
const knownFieldNames = /^(OBJECTID|FID|OID|ID)$/i;
// Attempts to find the ID Field from response
function _findIdAttributeFromResponse(response) {
let result;
if (response.objectIdFieldName) {
// Find Id Field directly
result = response.objectIdFieldName;
} else if (response.fields) {
// Find ID Field based on field type
for (let j = 0; j <= response.fields.length - 1; j++) {
if (response.fields[j].type === "esriFieldTypeOID") {
result = response.fields[j].name;
break;
}
}
if (!result) {
// If no field was marked as being the esriFieldTypeOID try well known field names
for (let j = 0; j <= response.fields.length - 1; j++) {
if (response.fields[j].name.match(knownFieldNames)) {
result = response.fields[j].name;
break;
}
}
}
}
return result;
}
// This is the 'last' resort, find the Id field from the specified feature
function _findIdAttributeFromFeature(feature) {
for (const key in feature.attributes) {
if (key.match(knownFieldNames)) {
return key;
}
}
}
function responseToFeatureCollection(response, idAttribute) {
let objectIdField;
const features = response.features || response.results;
const count = features && features.length;
if (idAttribute) {
objectIdField = idAttribute;
} else {
objectIdField = _findIdAttributeFromResponse(response);
}
const featureCollection = {
type: "FeatureCollection",
features: [],
};
if (count) {
for (let i = features.length - 1; i >= 0; i--) {
const feature = arcgisToGeoJSON(
features[i],
objectIdField || _findIdAttributeFromFeature(features[i]),
);
featureCollection.features.push(feature);
}
}
return featureCollection;
}
// trim url whitespace and add a trailing slash if needed
function cleanUrl(url) {
// trim leading and trailing spaces, but not spaces inside the url
url = leaflet.Util.trim(url);
// add a trailing slash to the url if the user omitted it
if (url[url.length - 1] !== "/") {
url += "/";
}
return url;
}
/* Extract url params if any and store them in requestParams attribute.
Return the options params updated */
function getUrlParams(options) {
if (options.url.indexOf("?") !== -1) {
options.requestParams = options.requestParams || {};
const queryString = options.url.substring(options.url.indexOf("?") + 1);
options.url = options.url.split("?")[0];
options.requestParams = JSON.parse(
`{"${decodeURI(queryString).replace(/"/g, '\\"').replace(/&/g, '","').replace(/=/g, '":"')}"}`,
);
}
options.url = cleanUrl(options.url.split("?")[0]);
return options;
}
function isArcgisOnline(url) {
/* hosted feature services support geojson as an output format
utility.arcgis.com services are proxied from a variety of ArcGIS Server vintages, and may not */
return /^(?!.*utility\.arcgis\.com).*\.arcgis\.com.*FeatureServer/i.test(url);
}
function geojsonTypeToArcGIS(geoJsonType) {
let arcgisGeometryType;
switch (geoJsonType) {
case "Point":
arcgisGeometryType = "esriGeometryPoint";
break;
case "MultiPoint":
arcgisGeometryType = "esriGeometryMultipoint";
break;
case "LineString":
arcgisGeometryType = "esriGeometryPolyline";
break;
case "MultiLineString":
arcgisGeometryType = "esriGeometryPolyline";
break;
case "Polygon":
arcgisGeometryType = "esriGeometryPolygon";
break;
case "MultiPolygon":
arcgisGeometryType = "esriGeometryPolygon";
break;
}
return arcgisGeometryType;
}
function calcAttributionWidth(map) {
// either crop at 55px or user defined buffer
return `${map.getSize().x - options.attributionWidthOffset}px`;
}
function setEsriAttribution(map) {
if (!map.attributionControl) {
return;
}
if (!map.attributionControl._esriAttributionLayerCount) {
map.attributionControl._esriAttributionLayerCount = 0;
}
if (map.attributionControl._esriAttributionLayerCount === 0) {
// Dynamically creating the CSS rules, only run this once per page load:
if (!map.attributionControl._esriAttributionAddedOnce) {
const hoverAttributionStyle = document.createElement("style");
hoverAttributionStyle.type = "text/css";
hoverAttributionStyle.innerHTML =
".esri-truncated-attribution:hover {white-space: normal;}";
document
.getElementsByTagName("head")[0]
.appendChild(hoverAttributionStyle);
// define a new css class in JS to trim attribution into a single line
const attributionStyle = document.createElement("style");
attributionStyle.type = "text/css";
attributionStyle.innerHTML =
".esri-truncated-attribution {" +
"vertical-align: -3px;" +
"white-space: nowrap;" +
"overflow: hidden;" +
"text-overflow: ellipsis;" +
"display: inline-block;" +
"transition: 0s white-space;" +
"transition-delay: 1s;" +
`max-width: ${calcAttributionWidth(map)};` +
"}";
document.getElementsByTagName("head")[0].appendChild(attributionStyle);
// update the width used to truncate when the map itself is resized
map.on("resize", (e) => {
if (map.attributionControl) {
map.attributionControl._container.style.maxWidth =
calcAttributionWidth(e.target);
}
});
map.attributionControl._esriAttributionAddedOnce = true;
}
leaflet.DomUtil.addClass(
map.attributionControl._container,
"esri-truncated-attribution:hover",
);
leaflet.DomUtil.addClass(
map.attributionControl._container,
"esri-truncated-attribution",
);
}
// Track the number of esri-leaflet layers that are on the map so we can know when we can remove the attribution (below in removeEsriAttribution)
map.attributionControl._esriAttributionLayerCount =
map.attributionControl._esriAttributionLayerCount + 1;
}
function getEsriAttributionHtmlString() {
return POWERED_BY_ESRI_ATTRIBUTION_STRING;
}
function removeEsriAttribution(map) {
if (!map.attributionControl) {
return;
}
// Only remove the attribution if we're about to remove the LAST esri-leaflet layer (_esriAttributionLayerCount)
if (
map.attributionControl._esriAttributionLayerCount &&
map.attributionControl._esriAttributionLayerCount === 1
) {
leaflet.DomUtil.removeClass(
map.attributionControl._container,
"esri-truncated-attribution:hover",
);
leaflet.DomUtil.removeClass(
map.attributionControl._container,
"esri-truncated-attribution",
);
}
map.attributionControl._esriAttributionLayerCount =
map.attributionControl._esriAttributionLayerCount - 1;
}
function _setGeometry(geometry) {
const params = {
geometry: null,
geometryType: null,
};
// convert bounds to extent and finish
if (geometry instanceof leaflet.LatLngBounds) {
// set geometry + geometryType
params.geometry = boundsToExtent(geometry);
params.geometryType = "esriGeometryEnvelope";
return params;
}
// convert L.Marker > L.LatLng
if (geometry.getLatLng) {
geometry = geometry.getLatLng();
}
// convert L.LatLng to a geojson point and continue;
if (geometry instanceof leaflet.LatLng) {
geometry = {
type: "Point",
coordinates: [geometry.lng, geometry.lat],
};
}
// handle L.GeoJSON, pull out the first geometry
if (geometry instanceof leaflet.GeoJSON) {
// reassign geometry to the GeoJSON value (we are assuming that only one feature is present)
geometry = geometry.getLayers()[0].feature.geometry;
params.geometry = geojsonToArcGIS(geometry);
params.geometryType = geojsonTypeToArcGIS(geometry.type);
}
// Handle L.Polyline and L.Polygon
if (geometry.toGeoJSON) {
geometry = geometry.toGeoJSON();
}
// handle GeoJSON feature by pulling out the geometry
if (geometry.type === "Feature") {
// get the geometry of the geojson feature
geometry = geometry.geometry;
}
// confirm that our GeoJSON is a point, line or polygon
if (
geometry.type === "Point" ||
geometry.type === "LineString" ||
geometry.type === "Polygon" ||
geometry.type === "MultiPolygon"
) {
params.geometry = geojsonToArcGIS(geometry);
params.geometryType = geojsonTypeToArcGIS(geometry.type);
return params;
}
// warn the user if we havn't found an appropriate object
warn(
"invalid geometry passed to spatial query. Should be L.LatLng, L.LatLngBounds, L.Marker or a GeoJSON Point, Line, Polygon or MultiPolygon object",
);
}
function _getAttributionData(url, map) {
if (Support.cors) {
request(
url,
{},
leaflet.Util.bind((error, attributions) => {
if (error) {
return;
}
map._esriAttributions = [];
for (let c = 0; c < attributions.contributors.length; c++) {
const contributor = attributions.contributors[c];
for (let i = 0; i < contributor.coverageAreas.length; i++) {
const coverageArea = contributor.coverageAreas[i];
const southWest = leaflet.latLng(
coverageArea.bbox[0],
coverageArea.bbox[1],
);
const northEast = leaflet.latLng(
coverageArea.bbox[2],
coverageArea.bbox[3],
);
map._esriAttributions.push({
attribution: contributor.attribution,
score: coverageArea.score,
bounds: leaflet.latLngBounds(southWest, northEast),
minZoom: coverageArea.zoomMin,
maxZoom: coverageArea.zoomMax,
});
}
}
map._esriAttributions.sort((a, b) => b.score - a.score);
// pass the same argument as the map's 'moveend' event
const obj = { target: map };
_updateMapAttribution(obj);
}, this),
);
}
}
function _updateMapAttribution(evt) {
const map = evt.target;
const oldAttributions = map._esriAttributions;
if (!map || !map.attributionControl) {
return;
}
const attributionElement = map.attributionControl._container.querySelector(
".esri-dynamic-attribution",
);
if (attributionElement && oldAttributions) {
let newAttributions = "";
const bounds = map.getBounds();
const wrappedBounds = leaflet.latLngBounds(
bounds.getSouthWest().wrap(),
bounds.getNorthEast().wrap(),
);
const zoom = map.getZoom();
for (let i = 0; i < oldAttributions.length; i++) {
const attribution = oldAttributions[i];
const text = attribution.attribution;
if (
!newAttributions.match(text) &&
attribution.bounds.intersects(wrappedBounds) &&
zoom >= attribution.minZoom &&
zoom <= attribution.maxZoom
) {
newAttributions += `, ${text}`;
}
}
newAttributions = `${getEsriAttributionHtmlString()} | ${newAttributions.substr(2)}`;
attributionElement.innerHTML = newAttributions;
attributionElement.style.maxWidth = calcAttributionWidth(map);
map.fire("attributionupdated", {
attribution: newAttributions,
});
}
}
const EsriUtil = {
warn,
cleanUrl,
getUrlParams,
isArcgisOnline,
geojsonTypeToArcGIS,
responseToFeatureCollection,
geojsonToArcGIS,
arcgisToGeoJSON,
boundsToExtent,
extentToBounds,
calcAttributionWidth,
setEsriAttribution,
getEsriAttributionHtmlString,
removeEsriAttribution,
_setGeometry,
_getAttributionData,
_updateMapAttribution,
_findIdAttributeFromFeature,
_findIdAttributeFromResponse,
};
const Task = leaflet.Class.extend({
options: {
proxy: false,
useCors: cors,
},
// Generate a method for each methodName:paramName in the setters for this task.
generateSetter(param, context) {
return leaflet.Util.bind(function (value) {
this.params[param] = value;
return this;
}, context);
},
initialize(endpoint) {
// endpoint can be either a url (and options) for an ArcGIS Rest Service or an instance of EsriLeaflet.Service
if (endpoint.request && endpoint.options) {
this._service = endpoint;
leaflet.Util.setOptions(this, endpoint.options);
} else {
leaflet.Util.setOptions(this, endpoint);
this.options.url = cleanUrl(endpoint.url);
}
// clone default params into this object
this.params = leaflet.Util.extend({}, this.params || {});
// generate setter methods based on the setters object implimented a child class
if (this.setters) {
for (const setter in this.setters) {
const param = this.setters[setter];
this[setter] = this.generateSetter(param, this);
}
}
},
token(token) {
if (this._service) {
this._service.authenticate(token);
} else {
this.params.token = token;
}
return this;
},
apikey(apikey) {
return this.token(apikey);
},
// ArcGIS Server Find/Identify 10.5+
format(boolean) {
// use double negative to expose a more intuitive positive method name
this.params.returnUnformattedValues = !boolean;
return this;
},
request(callback, context) {
if (this.options.requestParams) {
leaflet.Util.extend(this.params, this.options.requestParams);
}
if (this._service) {
return this._service.request(this.path, this.params, callback, context);
}
return this._request("request", this.path, this.params, callback, context);
},
_request(method, path, params, callback, context) {
const url = this.options.proxy
? `${this.options.proxy}?${this.options.url}${path}`
: this.options.url + path;
if ((method === "get" || method === "request") && !this.options.useCors) {
return Request.get.JSONP(url, params, callback, context);
}
return Request[method](url, params, callback, context);
},
});
function task(options) {
options = getUrlParams(options);
return new Task(options);
}
const Query = Task.extend({
setters: {
offset: "resultOffset",
limit: "resultRecordCount",
fields: "outFields",
precision: "geometryPrecision",
featureIds: "objectIds",
returnGeometry: "returnGeometry",
returnM: "returnM",
transform: "datumTransformation",
token: "token",
},
path: "query",
params: {
returnGeometry: true,
where: "1=1",
outSR: 4326,
outFields: "*",
},
// Returns a feature if its shape is wholly contained within the search geometry. Valid for all shape type combinations.
within(geometry) {
this._setGeometryParams(geometry);
this.params.spatialRel = "esriSpatialRelContains"; // to the REST api this reads geometry **contains** layer
return this;
},
// Returns a feature if any spatial relationship is found. Applies to all shape type combinations.
intersects(geometry) {
this._setGeometryParams(geometry);
this.params.spatialRel = "esriSpatialRelIntersects";
return this;
},
// Returns a feature if its shape wholly contains the search geometry. Valid for all shape type combinations.
contains(geometry) {
this._setGeometryParams(geometry);
this.params.spatialRel = "esriSpatialRelWithin"; // to the REST api this reads geometry **within** layer
return this;
},
// Returns a feature if the intersection of the interiors of the two shapes is not empty and has a lower dimension than the maximum dimension of the two shapes. Two lines that share an endpoint in common do not cross. Valid for Line/Line, Line/Area, Multi-point/Area, and Multi-point/Line shape type combinations.
crosses(geometry) {
this._setGeometryParams(geometry);
this.params.spatialRel = "esriSpatialRelCrosses";
return this;
},
// Returns a feature if the two shapes share a common boundary. However, the intersection of the interiors of the two shapes must be empty. In the Point/Line case, the point may touch an endpoint only of the line. Applies to all combinations except Point/Point.
touches(geometry) {
this._setGeometryParams(geometry);
this.params.spatialRel = "esriSpatialRelTouches";
return this;
},
// Returns a feature if the intersection of the two shapes results in an object of the same dimension, but different from both of the shapes. Applies to Area/Area, Line/Line, and Multi-point/Multi-point shape type combinations.
overlaps(geometry) {
this._setGeometryParams(geometry);
this.params.spatialRel = "esriSpatialRelOverlaps";
return this;
},
// Returns a feature if the envelope of the two shapes intersects.
bboxIntersects(geometry) {
this._setGeometryParams(geometry);
this.params.spatialRel = "esriSpatialRelEnvelopeIntersects";
return this;
},
// if someone can help decipher the ArcObjects explanation and translate to plain speak, we should mention this method in the doc
indexIntersects(geometry) {
this._setGeometryParams(geometry);
this.params.spatialRel = "esriSpatialRelIndexIntersects"; // Returns a feature if the envelope of the query geometry intersects the index entry for the target geometry
return this;
},
// only valid for Feature Services running on ArcGIS Server 10.3+ or ArcGIS Online
nearby(latlng, radius) {
latlng = leaflet.latLng(latlng);
this.params.geometry = [latlng.lng, latlng.lat];
this.params.geometryType = "esriGeometryPoint";
this.params.spatialRel = "esriSpatialRelIntersects";
this.params.units = "esriSRUnit_Meter";
this.params.distance = radius;
this.params.inSR = 4326;
return this;
},
where(string) {
// instead of converting double-quotes to single quotes, pass as is, and provide a more informative message if a 400 is encountered
this.params.where = string;
return this;
},
between(start, end) {
this.params.time = [start.valueOf(), end.valueOf()];
return this;
},
simplify(map, factor) {
const mapWidth = Math.abs(
map.getBounds().getWest() - map.getBounds().getEast(),
);
this.params.maxAllowableOffset = (mapWidth / map.getSize().y) * factor;
return this;
},
orderBy(fieldName, order) {
order = order || "ASC";
this.params.orderByFields = this.params.orderByFields
? `${this.params.orderByFields},`
: "";
this.params.orderByFields += [fieldName, order].join(" ");
return this;
},
run(callback, context) {
this._cleanParams();
// services hosted on ArcGIS Online and ArcGIS Server 10.3.1+ support requesting geojson directly
if (
this.options.isModern ||
(isArcgisOnline(this.options.url) && this.options.isModern === undefined)
) {
this.params.f = "geojson";
return this.request(function (error, response) {
this._trapSQLerrors(error);
callback.call(context, error, response, response);
}, this);
// otherwise convert it in the callback then pass it on
}
return this.request(function (error, response) {
this._trapSQLerrors(error);
callback.call(
context,
error,
response && responseToFeatureCollection(response),
response,
);
}, this);
},
count(callback, context) {
this._cleanParams();
this.params.returnCountOnly = true;
return this.request(function (error, response) {
callback.call(this, error, response && response.count, response);
}, context);
},
ids(callback, context) {
this._cleanParams();
this.params.returnIdsOnly = true;
return this.request(function (error, response) {
callback.call(this, error, response && response.objectIds, response);
}, context);
},
// only valid for Feature Services running on ArcGIS Server 10.3+ or ArcGIS Online
bounds(callback, context) {
this._cleanParams();
this.params.returnExtentOnly = true;
return this.request((error, response) => {
if (response && response.extent && extentToBounds(response.extent)) {
callback.call(
context,
error,
extentToBounds(response.extent),
response,
);
} else {
error = {
message: "Invalid Bounds",
};
callback.call(context, error, null, response);
}
}, context);
},
distinct() {
// geometry must be omitted for queries requesting distinct values
this.params.returnGeometry = false;
this.params.returnDistinctValues = true;
return this;
},
// only valid for image services
pixelSize(rawPoint) {
const castPoint = leaflet.point(rawPoint);
this.params.pixelSize = [castPoint.x, castPoint.y];
return this;
},
// only valid for map services
layer(layer) {
this.path = `${layer}/query`;
return this;
},
_trapSQLerrors(error) {
if (error) {
if (error.code === "400") {
warn(
"one common syntax error in query requests is encasing string values in double quotes instead of single quotes",
);
}
}
},
_cleanParams() {
delete this.params.returnIdsOnly;
delete this.params.returnExtentOnly;
delete this.params.returnCountOnly;
},
_setGeometryParams(geometry) {
this.params.inSR = 4326;
const converted = _setGeometry(geometry);
this.params.geometry = converted.geometry;
this.params.geometryType = converted.geometryType;
},
});
function query(options) {
return new Query(options);
}
const Find = Task.extend({
setters: {
// method name > param name
contains: "contains",
text: "searchText",
fields: "searchFields", // denote an array or single string
spatialReference: "sr",
sr: "sr",
layers: "layers",
returnGeometry: "returnGeometry",
maxAllowableOffset: "maxAllowableOffset",
precision: "geometryPrecision",
dynamicLayers: "dynamicLayers",
returnZ: "returnZ",
returnM: "returnM",
gdbVersion: "gdbVersion",
// skipped implementing this (for now) because the REST service implementation isnt consistent between operations
// 'transform': 'datumTransformations',
token: "token",
},
path: "find",
params: {
sr: 4326,
contains: true,
returnGeometry: true,
returnZ: true,
returnM: false,
},
layerDefs(id, where) {
this.params.layerDefs = this.params.layerDefs
? `${this.params.layerDefs};`
: "";
this.params.layerDefs += [id, where].join(":");
return this;
},
simplify(map, factor) {
const mapWidth = Math.abs(
map.getBounds().getWest() - map.getBounds().getEast(),
);
this.params.maxAllowableOffset = (mapWidth / map.getSize().y) * factor;
return this;
},
run(callback, context) {
return this.request((error, response) => {
callback.call(
context,
error,
r