UNPKG

esri-leaflet

Version:

Leaflet plugins for consuming ArcGIS Online and ArcGIS Server services.

1,662 lines (1,402 loc) 155 kB
/* 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