esri-leaflet
Version:
Leaflet plugins for consuming ArcGIS Online and ArcGIS Server services.
1,627 lines (1,389 loc) • 131 kB
JavaScript
/* esri-leaflet - v2.3.0 - Wed Jul 17 2019 15:35:19 GMT-0500 (Central Daylight Time)
* Copyright (c) 2019 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) :
(factory((global.L = global.L || {}, global.L.esri = {}),global.L));
}(this, (function (exports,leaflet) { 'use strict';
var version = "2.3.0";
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 (params.hasOwnProperty(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;
}
}
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;
}
}
// 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 http://esri.github.io/esri-leaflet/api-reference/request.html');
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;
// export the Request object to call the different handlers for debugging
var Request = {
request: request,
get: get,
post: xmlHttpPost
};
/*
* Copyright 2017 Esri
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// checks if 2 x,y points are equal
function pointsEqual (a, b) {
for (var i = 0; i < a.length; i++) {
if (a[i] !== b[i]) {
return false;
}
}
return true;
}
// checks if the first and last points of a ring are equal and closes the ring
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
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);
}
// ported from terraformer.js https://github.com/Esri/Terraformer/blob/master/terraformer.js#L504-L519
function vertexIntersectsVertex (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;
}
// ported from terraformer.js https://github.com/Esri/Terraformer/blob/master/terraformer.js#L521-L531
function arrayIntersectsArray (a, b) {
for (var i = 0; i < a.length - 1; i++) {
for (var j = 0; j < b.length - 1; j++) {
if (vertexIntersectsVertex(a[i], a[i + 1], b[j], b[j + 1])) {
return true;
}
}
}
return false;
}
// ported from terraformer.js https://github.com/Esri/Terraformer/blob/master/terraformer.js#L470-L480
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;
}
// ported from terraformer-arcgis-parser.js https://github.com/Esri/terraformer-arcgis-parser/blob/master/terraformer-arcgis-parser.js#L106-L113
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
// ported from terraformer-arcgis-parser.js https://github.com/Esri/terraformer-arcgis-parser/blob/master/terraformer-arcgis-parser.js#L117-L172
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
};
}
}
// This function ensures that rings are oriented in the right directions
// outer rings are clockwise, holes are counterclockwise
// used for converting GeoJSON Polygons to ArcGIS Polygons
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
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;
}
// shallow object clone for feature properties and attributes
// from http://jsperf.com/cloning-an-object/2
function shallowClone (obj) {
var target = {};
for (var i in obj) {
if (obj.hasOwnProperty(i)) {
target[i] = obj[i];
}
}
return target;
}
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');
}
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;
}
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];
result.spatialReference = spatialReference;
break;
case 'MultiPoint':
result.points = geojson.coordinates.slice(0);
result.spatialReference = spatialReference;
break;
case 'LineString':
result.paths = [geojson.coordinates.slice(0)];
result.spatialReference = spatialReference;
break;
case 'MultiLineString':
result.paths = geojson.coordinates.slice(0);
result.spatialReference = spatialReference;
break;
case 'Polygon':
result.rings = orientRings(geojson.coordinates.slice(0));
result.spatialReference = spatialReference;
break;
case 'MultiPolygon':
result.rings = flattenMultiPolygonRings(geojson.coordinates.slice(0));
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;
}
function geojsonToArcGIS$1 (geojson, idAttr) {
return geojsonToArcGIS(geojson, idAttr);
}
function arcgisToGeoJSON$1 (arcgis, idAttr) {
return arcgisToGeoJSON(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.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$1(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$$1) {
if (options$$1.url.indexOf('?') !== -1) {
options$$1.requestParams = options$$1.requestParams || {};
var queryString = options$$1.url.substring(options$$1.url.indexOf('?') + 1);
options$$1.url = options$$1.url.split('?')[0];
options$$1.requestParams = JSON.parse('{"' + decodeURI(queryString).replace(/"/g, '\\"').replace(/&/g, '","').replace(/=/g, '":"') + '"}');
}
options$$1.url = cleanUrl(options$$1.url.split('?')[0]);
return options$$1;
}
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 warn () {
if (console && console.warn) {
console.warn.apply(console, arguments);
}
}
function calcAttributionWidth (map) {
// either crop at 55px or user defined buffer
return (map.getSize().x - options.attributionWidthOffset) + 'px';
}
function setEsriAttribution (map) {
if (map.attributionControl && !map.attributionControl._esriAttributionAdded) {
map.attributionControl.setPrefix('<a href="http://leafletjs.com" title="A JS library for interactive maps">Leaflet</a> | Powered by <a href="https://www.esri.com">Esri</a>');
var hoverAttributionStyle = document.createElement('style');
hoverAttributionStyle.type = 'text/css';
hoverAttributionStyle.innerHTML = '.esri-truncated-attribution:hover {' +
'white-space: normal;' +
'}';
document.getElementsByTagName('head')[0].appendChild(hoverAttributionStyle);
leaflet.DomUtil.addClass(map.attributionControl._container, 'esri-truncated-attribution:hover');
// 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);
leaflet.DomUtil.addClass(map.attributionControl._container, 'esri-truncated-attribution');
// update the width used to truncate when the map itself is resized
map.on('resize', function (e) {
map.attributionControl._container.style.maxWidth = calcAttributionWidth(e.target);
});
map.attributionControl._esriAttributionAdded = true;
}
}
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$1(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$1(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');
return;
}
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$1,
arcgisToGeoJSON: arcgisToGeoJSON$1,
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;
},
// 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.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
},
layerDefs: function (id, where) {
this.params.layerDefs = (this.params.layerDefs) ? this.params.layerDefs + ';' : '';
this.params.layerDefs += ([id, where]).join(':');
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;
},
run: function (callback, context) {
return this.request(function (error, response) {
callback.call(context, error, (response && responseToFeatureCollection(response)), response);
}, context);
}
});
function find (options) {
return new Find(options);
}
var Identify = Task.extend({
path: 'identify',
between: function (start, end) {
this.params.time = [start.valueOf(), end.valueOf()];
return this;
}
});
function identify (options) {
return new Identify(options);
}
var IdentifyFeatures = Identify.extend({
setters: {
'layers': 'layers',
'precision': 'geometryPrecision',
'tolerance': 'tolerance',
// skipped implementing this (for now) because the REST service implementation isnt consistent between operations.
// 'transform': 'datumTransformations'
'returnGeometry': 'returnGeometry'
},
params: {
sr: 4326,
layers: 'all',
tolerance: 3,
returnGeometry: true
},
on: function (map) {
var extent = boundsToExtent(map.getBounds());
var size = map.getSize();
this.params.imageDisplay = [size.x, size.y, 96];
this.params.mapExtent = [extent.xmin, extent.ymin, extent.xmax, extent.ymax];
return this;
},
at: function (geometry) {
// cast lat, long pairs in raw array form manually
if (geometry.length === 2) {
geometry = leaflet.latLng(geometry);
}
this._setGeometryParams(geometry);
return this;
},
layerDef: function (id, where) {
this.params.layerDefs = (this.params.layerDefs) ? this.params.layerDefs + ';' : '';
this.params.layerDefs += ([id, where]).join(':');
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;
},
run: function (callback, context) {
return this.request(function (error, response) {
// immediately invoke with an error
if (error) {
callback.call(context, error, undefined, response);
return;
// ok no error lets just assume we have features...
} else {
var featureCollection = responseToFeatureCollection(response);
response.results = response.results.reverse();
for (var i = 0; i < featureCollection.features.length; i++) {
var feature = featureCollection.features[i];
feature.layerId = response.results[i].layerId;
}
callback.call(context, undefined, featureCollection, response);
}
});
},
_setGeometryParams: function (geometry) {
var converted = _setGeometry(geometry);
this.params.geometry = converted.geometry;
this.params.geometryType = converted.geometryType;
}
});
function identifyFeatures (options) {
return new IdentifyFeatures(options);
}
var IdentifyImage = Identify.extend({
setters: {
'setMosaicRule': 'mosaicRule',
'setRenderingRule': 'renderingRule',
'setPixelSize': 'pixelSize',
'returnCatalogItems': 'returnCatalogItems',
'returnGeometry': 'returnGeometry'
},
params: {
returnGeometry: false
},
at: function (latlng) {
latlng = leaflet.latLng(latlng);
this.params.geometry = JSON.stringify({
x: latlng.lng,
y: latlng.lat,
spatialReference: {
wkid: 4326
}
});
this.params.geometryType = 'esriGeometryPoint';
return this;
},
getMosaicRule: function () {
return this.params.mosaicRule;
},
getRenderingRule: function () {
return this.params.renderingRule;
},
getPixelSize: function () {
return this.params.pixelSize;
},
run: function (callback, context) {
return this.request(function (error, response) {
callback.call(context, error, (response && this._responseToGeoJSON(response)), response);
}, this);
},
// get pixel data and return as geoJSON point
// populate catalog items (if any)
// merging in any catalogItemVisibilities as a propery of each feature
_responseToGeoJSON: function (response) {
var location = response.location;
var catalogItems = response.catalogItems;
var catalogItemVisibilities = response.catalogItemVisibilities;
var geoJSON = {
'pixel': {
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [location.x, location.y]
},
'crs': {
'type': 'EPSG',
'properties': {
'code': location.spatialReference.wkid
}
},
'properties': {
'OBJECTID': response.objectId,
'name': response.name,
'value': response.value
},
'id': response.objectId
}
};
if (response.properties && response.properties.Values) {
geoJSON.pixel.properties.values = response.properties.Values;
}
if (catalogItems && catalogItems.features) {
geoJSON.catalogItems = responseToFeatureCollection(catalogItems);
if (catalogItemVisibilities && catalogItemVisibilities.length === geoJSON.catalogItems.features.length) {
for (var i = catalogItemVisibilities.length - 1; i >= 0; i--) {
geoJSON.catalogItems.features[i].properties.catalogItemVisibility = catalogItemVisibilities[i];
}
}
}
return geoJSON;
}
});
function identifyImage (params) {
return new IdentifyImage(params);
}
var Service = leaflet.Evented.extend({
options: {
proxy: false,
useCors: cors,
timeout: 0
},
initialize: function (options) {
options = options || {};
this._requestQueue = [];
this._authenticating = false;
leaflet.Util.setOptions(this, options);
this.options.url = cleanUrl(this.options.url);
},
get: function (path, params, callback, context) {
return this._request('get', path, params, callback, context);
},
post: function (path, params, callback, context) {
return this._request('post', path, params, callback, context);
},
request: function (path, params, callback, context) {
return this._request('request', path, params, callback, context);
},
metadata: function (callback, context) {
return this._request('get', '', {}, callback, context);
},
authenticate: function (token) {
this._authenticating = false;
this.options.token = token;
this._runQueue();
return this;
},
getTimeout: function () {
return this.options.timeout;
},
setTimeout: function (timeout) {
this.options.timeout = timeout;
},
_request: function (method, path, params, callback, context) {
this.fire('requeststart', {
url: this.options.url + path,
params: params,
method: method
}, true);
var wrappedCallback = this._createServiceCallback(method, path, params, callback, context);
if (this.options.token) {
params.token = this.options.token;
}
if (this.options.requestParams) {
leaflet.Util.extend(params, this.options.requestParams);
}
if (this._authenticating) {
this._requestQueue.push([method, path, params, callback, context]);
return;
} else {
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, wrappedCallback, context);
} else {
return Request[method](url, params, wrappedCallback, context);
}
}
},
_createServiceCallback: function (method, path, params, callback, context) {
return leaflet.Util.bind(function (error, response) {
if (error && (error.code === 499 || error.code === 498)) {
this._authenticating = true;
this._requestQueue.push([method, path, params, callback, context]);
// fire an event for users to handle and re-authenticate
this.fire('authenticationrequired', {
authenticate: leaflet.Util.bind(this.authenticate, this)
}, true);
// if the user has access to a callback they can handle the auth