esri-leaflet
Version:
Leaflet plugins for consuming ArcGIS Online and ArcGIS Server services.
1,540 lines (1,281 loc) • 158 kB
JavaScript
/* esri-leaflet - v3.0.10 - Tue Jan 17 2023 09:24:13 GMT-0600 (Central Standard Time)
* Copyright (c) 2023 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.10";
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",
"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",
rollup: "^2.79.1",
semistandard: "^14.2.3",
sinon: "^15.0.1",
"sinon-chai": "3.7.0",
snazzy: "^9.0.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: "semistandard | snazzy",
prebuild: "mkdirp dist",
pretest: "npm run build",
precommit: "npm run lint",
fix: "semistandard --fix",
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 semistandard = {
globals: [
"expect",
"L",
"XMLHttpRequest",
"sinon",
"xhr",
"proj4"
]
};
var unpkg = "dist/esri-leaflet-debug.js";
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,
semistandard: semistandard,
unpkg: unpkg
};
var cors = ((window.XMLHttpRequest && 'withCredentials' in new window.XMLHttpRequest()));
var pointerEvents = document.documentElement.style.pointerEvents === '';
var Support = {
cors: cors,
pointerEvents: pointerEvents
};
var options = {
attributionWidthOffset: 55
};
var callbacks = 0;
function serialize (params) {
var data = '';
params.f = params.f || 'json';
for (var key in params) {
if (Object.prototype.hasOwnProperty.call(params, key)) {
var param = params[key];
var type = Object.prototype.toString.call(param);
var 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);
}
}
return data;
}
function createRequest (callback, context) {
var httpRequest = new window.XMLHttpRequest();
httpRequest.onerror = function (e) {
httpRequest.onreadystatechange = leaflet.Util.falseFn;
callback.call(context, {
error: {
code: 500,
message: 'XMLHttpRequest error'
}
}, null);
};
httpRequest.onreadystatechange = function () {
var response;
var 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) {
var 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) {
var 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) {
var paramString = serialize(params);
var httpRequest = createRequest(callback, context);
var 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 || {};
var callbackId = 'c' + callbacks;
params.callback = 'window._EsriLeafletCallbacks.' + callbackId;
window._EsriLeafletCallbacks[callbackId] = function (response) {
if (window._EsriLeafletCallbacks[callbackId] !== true) {
var error;
var 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;
}
};
var 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
var 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: function () {
window._EsriLeafletCallbacks._callback[callbackId]({
code: 0,
message: 'Request aborted.'
});
}
};
}
var get = ((Support.cors) ? xmlHttpGet : jsonp);
get.CORS = xmlHttpGet;
get.JSONP = jsonp;
function warn () {
if (console && console.warn) {
console.warn.apply(console, arguments);
}
}
// export the Request object to call the different handlers for debugging
var Request = {
request: request,
get: 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;
};
var BASE_LEAFLET_ATTRIBUTION_STRING = '<a href="http://leafletjs.com" title="A JS library for interactive maps">Leaflet</a>';
var 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') {
var sw = leaflet.latLng(extent.ymin, extent.xmin);
var ne = leaflet.latLng(extent.ymax, extent.xmax);
return leaflet.latLngBounds(sw, ne);
} else {
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
}
};
}
var knownFieldNames = /^(OBJECTID|FID|OID|ID)$/i;
// Attempts to find the ID Field from response
function _findIdAttributeFromResponse (response) {
var result;
if (response.objectIdFieldName) {
// Find Id Field directly
result = response.objectIdFieldName;
} else if (response.fields) {
// Find ID Field based on field type
for (var 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 (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 (var key in feature.attributes) {
if (key.match(knownFieldNames)) {
return key;
}
}
}
function responseToFeatureCollection (response, idAttribute) {
var objectIdField;
var features = response.features || response.results;
var count = features && features.length;
if (idAttribute) {
objectIdField = idAttribute;
} else {
objectIdField = _findIdAttributeFromResponse(response);
}
var featureCollection = {
type: 'FeatureCollection',
features: []
};
if (count) {
for (var i = features.length - 1; i >= 0; i--) {
var 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 || {};
var 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) {
var 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) {
var 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
var 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', function (e) {
if (map.attributionControl) {
map.attributionControl._container.style.maxWidth = calcAttributionWidth(e.target);
}
});
map.attributionControl._esriAttributionAddedOnce = true;
}
map.attributionControl.setPrefix(BASE_LEAFLET_ATTRIBUTION_STRING + ' | ' + POWERED_BY_ESRI_ATTRIBUTION_STRING);
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 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) {
map.attributionControl.setPrefix(BASE_LEAFLET_ATTRIBUTION_STRING);
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) {
var 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(function (error, attributions) {
if (error) { return; }
map._esriAttributions = [];
for (var c = 0; c < attributions.contributors.length; c++) {
var contributor = attributions.contributors[c];
for (var i = 0; i < contributor.coverageAreas.length; i++) {
var coverageArea = contributor.coverageAreas[i];
var southWest = leaflet.latLng(coverageArea.bbox[0], coverageArea.bbox[1]);
var 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(function (a, b) {
return b.score - a.score;
});
// pass the same argument as the map's 'moveend' event
var obj = { target: map };
_updateMapAttribution(obj);
}, this));
}
}
function _updateMapAttribution (evt) {
var map = evt.target;
var oldAttributions = map._esriAttributions;
if (!map || !map.attributionControl) return;
var attributionElement = map.attributionControl._container.querySelector('.esri-dynamic-attribution');
if (attributionElement && oldAttributions) {
var newAttributions = '';
var bounds = map.getBounds();
var wrappedBounds = leaflet.latLngBounds(
bounds.getSouthWest().wrap(),
bounds.getNorthEast().wrap()
);
var zoom = map.getZoom();
for (var i = 0; i < oldAttributions.length; i++) {
var attribution = oldAttributions[i];
var text = attribution.attribution;
if (!newAttributions.match(text) && attribution.bounds.intersects(wrappedBounds) && zoom >= attribution.minZoom && zoom <= attribution.maxZoom) {
newAttributions += (', ' + text);
}
}
newAttributions = newAttributions.substr(2);
attributionElement.innerHTML = newAttributions;
attributionElement.style.maxWidth = calcAttributionWidth(map);
map.fire('attributionupdated', {
attribution: newAttributions
});
}
}
var EsriUtil = {
warn: warn,
cleanUrl: cleanUrl,
getUrlParams: getUrlParams,
isArcgisOnline: isArcgisOnline,
geojsonTypeToArcGIS: geojsonTypeToArcGIS,
responseToFeatureCollection: responseToFeatureCollection,
geojsonToArcGIS: geojsonToArcGIS,
arcgisToGeoJSON: arcgisToGeoJSON,
boundsToExtent: boundsToExtent,
extentToBounds: extentToBounds,
calcAttributionWidth: calcAttributionWidth,
setEsriAttribution: setEsriAttribution,
_setGeometry: _setGeometry,
_getAttributionData: _getAttributionData,
_updateMapAttribution: _updateMapAttribution,
_findIdAttributeFromFeature: _findIdAttributeFromFeature,
_findIdAttributeFromResponse: _findIdAttributeFromResponse
};
var Task = leaflet.Class.extend({
options: {
proxy: false,
useCors: cors
},
// Generate a method for each methodName:paramName in the setters for this task.
generateSetter: function (param, context) {
return leaflet.Util.bind(function (value) {
this.params[param] = value;
return this;
}, context);
},
initialize: function (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 (var setter in this.setters) {
var param = this.setters[setter];
this[setter] = this.generateSetter(param, this);
}
}
},
token: function (token) {
if (this._service) {
this._service.authenticate(token);
} else {
this.params.token = token;
}
return this;
},
apikey: function (apikey) {
return this.token(apikey);
},
// ArcGIS Server Find/Identify 10.5+
format: function (boolean) {
// use double negative to expose a more intuitive positive method name
this.params.returnUnformattedValues = !boolean;
return this;
},
request: function (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: function (method, path, params, callback, context) {
var 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);
}
var 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: function (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: function (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: function (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: function (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: function (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: function (geometry) {
this._setGeometryParams(geometry);
this.params.spatialRel = 'esriSpatialRelOverlaps';
return this;
},
// Returns a feature if the envelope of the two shapes intersects.
bboxIntersects: function (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: function (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: function (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: function (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: function (start, end) {
this.params.time = [start.valueOf(), end.valueOf()];
return this;
},
simplify: function (map, factor) {
var mapWidth = Math.abs(map.getBounds().getWest() - map.getBounds().getEast());
this.params.maxAllowableOffset = (mapWidth / map.getSize().y) * factor;
return this;
},
orderBy: function (fieldName, order) {
order = order || 'ASC';
this.params.orderByFields = (this.params.orderByFields) ? this.params.orderByFields + ',' : '';
this.params.orderByFields += ([fieldName, order]).join(' ');
return this;
},
run: function (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
} else {
return this.request(function (error, response) {
this._trapSQLerrors(error);
callback.call(context, error, (response && responseToFeatureCollection(response)), response);
}, this);
}
},
count: function (callback, context) {
this._cleanParams();
this.params.returnCountOnly = true;
return this.request(function (error, response) {
callback.call(this, error, (response && response.count), response);
}, context);
},
ids: function (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: function (callback, context) {
this._cleanParams();
this.params.returnExtentOnly = true;
return this.request(function (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: function () {
// 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: function (rawPoint) {
var castPoint = leaflet.point(rawPoint);
this.params.pixelSize = [castPoint.x, castPoint.y];
return this;
},
// only valid for map services
layer: function (layer) {
this.path = layer + '/query';
return this;
},
_trapSQLerrors: function (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: function () {
delete this.params.returnIdsOnly;
delete this.params.returnExtentOnly;
delete this.params.returnCountOnly;
},
_setGeometryParams: function (geometry) {
this.params.inSR = 4326;
var converted = _setGeometry(geometry);
this.params.geometry = converted.geometry;
this.params.geometryType = converted.geometryType;
}
});
function query (options) {
return new Query(options);
}
var 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
},
la