esri-leaflet
Version:
Leaflet plugins for consuming ArcGIS Online and ArcGIS Server services.
1,648 lines (1,398 loc) • 461 kB
JavaScript
/* esri-leaflet - v2.0.4 - Tue Oct 18 2016 16:32:16 GMT-0700 (PDT)
* Copyright (c) 2016 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.esri || {}),global.L));
}(this, function (exports,L) { 'use strict';
L = 'default' in L ? L['default'] : L;
var version = "2.0.4";
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 = L.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 = L.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');
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;
// get around ie10/11 bug which requires that 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');
}
if (typeof context !== 'undefined' && context !== null) {
if (typeof context.options !== 'undefined') {
httpRequest.timeout = context.options.timeout;
}
}
// request is less then 2000 characters and the browser supports CORS, make GET request with XMLHttpRequest
if (requestLength <= 2000 && Support.cors) {
httpRequest.send(null);
// request is less more then 2000 characters and the browser supports CORS, make POST request with XMLHttpRequest
} else if (requestLength > 2000 && Support.cors) {
httpRequest.send(paramString);
// request is less more then 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 = L.DomUtil.create('script', null, document.body);
script.type = 'text/javascript';
script.src = url + '?' + serialize(params);
script.id = callbackId;
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 2015 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 Liscense.
*/
// 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 ];
outerRings.push(polygon); // push to outer rings
} else {
holes.push(ring); // counterclockwise push to holes
}
}
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$1 (obj) {
var target = {};
for (var i in obj) {
if (obj.hasOwnProperty(i)) {
target[i] = obj[i];
}
}
return target;
}
function arcgisToGeoJSON$1 (arcgis, idAttribute) {
var geojson = {};
if (typeof arcgis.x === 'number' && typeof arcgis.y === 'number') {
geojson.type = 'Point';
geojson.coordinates = [arcgis.x, arcgis.y];
}
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 (arcgis.geometry || arcgis.attributes) {
geojson.type = 'Feature';
geojson.geometry = (arcgis.geometry) ? arcgisToGeoJSON$1(arcgis.geometry) : null;
geojson.properties = (arcgis.attributes) ? shallowClone$1(arcgis.attributes) : null;
if (arcgis.attributes) {
geojson.id = arcgis.attributes[idAttribute] || arcgis.attributes.OBJECTID || arcgis.attributes.FID;
}
}
return geojson;
}
function geojsonToArcGIS$1 (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$1(geojson.geometry, idAttribute);
}
result.attributes = (geojson.properties) ? shallowClone$1(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$1(geojson.features[i], idAttribute));
}
break;
case 'GeometryCollection':
result = [];
for (i = 0; i < geojson.geometries.length; i++) {
result.push(geojsonToArcGIS$1(geojson.geometries[i], idAttribute));
}
break;
}
return result;
}
function geojsonToArcGIS (geojson, idAttr) {
return geojsonToArcGIS$1(geojson, idAttr);
}
function arcgisToGeoJSON (arcgis, idAttr) {
return arcgisToGeoJSON$1(arcgis, idAttr);
}
// 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;
}
// convert an extent (ArcGIS) to LatLngBounds (Leaflet)
function extentToBounds (extent) {
var sw = L.latLng(extent.ymin, extent.xmin);
var ne = L.latLng(extent.ymax, extent.xmax);
return L.latLngBounds(sw, ne);
}
// convert an LatLngBounds (Leaflet) to extent (ArcGIS)
function boundsToExtent (bounds) {
bounds = L.latLngBounds(bounds);
return {
'xmin': bounds.getSouthWest().lng,
'ymin': bounds.getSouthWest().lat,
'xmax': bounds.getNorthEast().lng,
'ymax': bounds.getNorthEast().lat,
'spatialReference': {
'wkid': 4326
}
};
}
function responseToFeatureCollection (response, idAttribute) {
var objectIdField;
var features = response.features || response.results;
var count = features.length;
if (idAttribute) {
objectIdField = idAttribute;
} else if (response.objectIdFieldName) {
objectIdField = response.objectIdFieldName;
} else if (response.fields) {
for (var j = 0; j <= response.fields.length - 1; j++) {
if (response.fields[j].type === 'esriFieldTypeOID') {
objectIdField = response.fields[j].name;
break;
}
}
} else if (count) {
/* as a last resort, check for common ID fieldnames in the first feature returned
not foolproof. identifyFeatures can returned a mixed array of features. */
for (var key in features[0].attributes) {
if (key.match(/^(OBJECTID|FID|OID|ID)$/i)) {
objectIdField = key;
break;
}
}
}
var featureCollection = {
type: 'FeatureCollection',
features: []
};
if (count) {
for (var i = features.length - 1; i >= 0; i--) {
var feature = arcgisToGeoJSON(features[i], objectIdField);
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 = L.Util.trim(url);
// add a trailing slash to the url if the user omitted it
if (url[url.length - 1] !== '/') {
url += '/';
}
return url;
}
function isArcgisOnline (url) {
/* hosted feature services can emit geojson natively. */
return (/\.arcgis\.com.*?FeatureServer/g).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);
L.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);
L.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 _getAttributionData (url, map) {
jsonp(url, {}, L.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 = L.latLng(coverageArea.bbox[0], coverageArea.bbox[1]);
var northEast = L.latLng(coverageArea.bbox[2], coverageArea.bbox[3]);
map._esriAttributions.push({
attribution: contributor.attribution,
score: coverageArea.score,
bounds: L.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 && oldAttributions) {
var newAttributions = '';
var bounds = map.getBounds();
var wrappedBounds = L.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);
var attributionElement = map.attributionControl._container.querySelector('.esri-dynamic-attribution');
attributionElement.innerHTML = newAttributions;
attributionElement.style.maxWidth = calcAttributionWidth(map);
map.fire('attributionupdated', {
attribution: newAttributions
});
}
}
var Util = {
shallowClone: shallowClone,
warn: warn,
cleanUrl: cleanUrl,
isArcgisOnline: isArcgisOnline,
geojsonTypeToArcGIS: geojsonTypeToArcGIS,
responseToFeatureCollection: responseToFeatureCollection,
geojsonToArcGIS: geojsonToArcGIS,
arcgisToGeoJSON: arcgisToGeoJSON,
boundsToExtent: boundsToExtent,
extentToBounds: extentToBounds,
calcAttributionWidth: calcAttributionWidth,
setEsriAttribution: setEsriAttribution,
_getAttributionData: _getAttributionData,
_updateMapAttribution: _updateMapAttribution
};
var Task = L.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 L.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;
L.Util.setOptions(this, endpoint.options);
} else {
L.Util.setOptions(this, endpoint);
this.options.url = cleanUrl(endpoint.url);
}
// clone default params into this object
this.params = L.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;
},
request: function (callback, context) {
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) {
return new Task(options);
}
var Query = Task.extend({
setters: {
'offset': 'resultOffset',
'limit': 'resultRecordCount',
'fields': 'outFields',
'precision': 'geometryPrecision',
'featureIds': 'objectIds',
'returnGeometry': 'returnGeometry',
'token': 'token'
},
path: 'query',
params: {
returnGeometry: true,
where: '1=1',
outSr: 4326,
outFields: '*'
},
within: function (geometry) {
this._setGeometry(geometry);
this.params.spatialRel = 'esriSpatialRelContains'; // will make code read layer within geometry, to the api this will reads geometry contains layer
return this;
},
intersects: function (geometry) {
this._setGeometry(geometry);
this.params.spatialRel = 'esriSpatialRelIntersects';
return this;
},
contains: function (geometry) {
this._setGeometry(geometry);
this.params.spatialRel = 'esriSpatialRelWithin'; // will make code read layer contains geometry, to the api this will reads geometry within layer
return this;
},
crosses: function (geometry) {
this._setGeometry(geometry);
this.params.spatialRel = 'esriSpatialRelCrosses';
return this;
},
touches: function (geometry) {
this._setGeometry(geometry);
this.params.spatialRel = 'esriSpatialRelTouches';
return this;
},
overlaps: function (geometry) {
this._setGeometry(geometry);
this.params.spatialRel = 'esriSpatialRelOverlaps';
return this;
},
// only valid for Feature Services running on ArcGIS Server 10.3 or ArcGIS Online
nearby: function (latlng, radius) {
latlng = L.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 also support requesting geojson directly
if (this.options.isModern || Util.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 && Util.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) {
callback.call(context, error, (response && response.extent && Util.extentToBounds(response.extent)), response);
}, context);
},
// only valid for image services
pixelSize: function (point) {
point = L.point(point);
this.params.pixelSize = [point.x, point.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') {
Util.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;
},
_setGeometry: function (geometry) {
this.params.inSr = 4326;
// convert bounds to extent and finish
if (geometry instanceof L.LatLngBounds) {
// set geometry + geometryType
this.params.geometry = Util.boundsToExtent(geometry);
this.params.geometryType = 'esriGeometryEnvelope';
return;
}
// convert L.Marker > L.LatLng
if (geometry.getLatLng) {
geometry = geometry.getLatLng();
}
// convert L.LatLng to a geojson point and continue;
if (geometry instanceof L.LatLng) {
geometry = {
type: 'Point',
coordinates: [geometry.lng, geometry.lat]
};
}
// handle L.GeoJSON, pull out the first geometry
if (geometry instanceof L.GeoJSON) {
// reassign geometry to the GeoJSON value (we are assuming that only one feature is present)
geometry = geometry.getLayers()[0].feature.geometry;
this.params.geometry = Util.geojsonToArcGIS(geometry);
this.params.geometryType = Util.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') {
this.params.geometry = Util.geojsonToArcGIS(geometry);
this.params.geometryType = Util.geojsonTypeToArcGIS(geometry.type);
return;
}
// warn the user if we havn't found an appropriate object
Util.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 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',
'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 && Util.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',
'returnGeometry': 'returnGeometry'
},
params: {
sr: 4326,
layers: 'all',
tolerance: 3,
returnGeometry: true
},
on: function (map) {
var extent = Util.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 (latlng) {
latlng = L.latLng(latlng);
this.params.geometry = [latlng.lng, latlng.lat];
this.params.geometryType = 'esriGeometryPoint';
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) * (1 - 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 = Util.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);
}
});
}
});
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 = L.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 = Util.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 = L.Evented.extend({
options: {
proxy: false,
useCors: cors,
timeout: 0
},
initialize: function (options) {
options = options || {};
this._requestQueue = [];
this._authenticating = false;
L.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._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 L.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: L.Util.bind(this.authenticate, this)
}, true);
// if the user has access to a callback they can handle the auth error
error.authenticate = L.Util.bind(this.authenticate, this);
}
callback.call(context, error, response);
if (error) {
this.fire('requesterror', {
url: this.options.url + path,
params: params,
message: error.message,
code: error.code,
method: method
}, true);
} else {
this.fire('requestsuccess', {
url: this.options.url + path,
params: params,
response: response,
method: method
}, true);
}
this.fire('requestend', {
url: this.options.url + path,
params: params,
method: method
}, true);
}, this);
},
_runQueue: function () {
for (var i = this._requestQueue.length - 1; i >= 0; i--) {
var request = this._requestQueue[i];
var method = request.shift();
this[method].apply(this, request);
}
this._requestQueue = [];
}
});
function service (options) {
return new Service(options);
}
var MapService = Service.extend({
identify: function () {
return identifyFeatures(this);
},
find: function () {
return find(this);
},
query: function () {
return query(this);
}
});
function mapService (options) {
return new MapService(options);
}
var ImageService = Service.extend({
query: function () {
return query(this);
},
identify: function () {
return identifyImage(this);
}
});
function imageService (options) {
return new ImageService(options);
}
var FeatureLayerService = Service.extend({
options: {
idAttribute: 'OBJECTID'
},
query: function () {
return query(this);
},
addFeature: function (feature, callback, context) {
delete feature.id;
feature = geojsonToArcGIS(feature);
return this.post('addFeatures', {
features: [feature]
}, function (error, response) {
var result = (response && response.addResults) ? response.addResults[0] : undefined;
if (callback) {
callback.call(context, error || response.addResults[0].error, result);
}
}, context);
},
updateFeature: function (feature, callback, context) {
feature = geojsonToArcGIS(feature, this.options.idAttribute);
return this.post('updateFeatures', {
features: [feature]
}, function (error, response) {
var result = (response && response.updateResults) ? response.updateResults[0] : undefined;
if (callback) {
callback.call(context, error || response.updateResults[0].error, result);
}
}, context);
},
deleteFeature: function (id, callback, context) {
return this.post('deleteFeatures', {
objectIds: id
}, function (error, response) {
var result = (response && response.deleteResults) ? response.deleteResults[0] : undefined;
if (callback) {
callback.call(context, error || response.deleteResults[0].error, result);
}
}, context);
},
deleteFeatures: function (ids, callback, context) {
return this.post('deleteFeatures', {
objectIds: ids
}, function (error, response) {
// pass back the entire array
var result = (response && response.deleteResults) ? response.deleteResults : undefined;
if (callback) {
callback.call(context, error || response.deleteResults[0].error, result);
}
}, context);
}
});
function featureLayerService (options) {
return new FeatureLayerService(options);
}
var tileProtocol = (window.location.protocol !== 'https:') ? 'http:' : 'https:';
var BasemapLayer = L.TileLayer.extend({
statics: {
TILES: {
Streets: {
urlTemplate: tileProtocol + '//{s}.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}',
options: {
minZoom: 1,
maxZoom: 19,
subdomains: ['server', 'services'],
attribution: 'USGS, NOAA',
attributionUrl: 'https://static.arcgis.com/attribution/World_Street_Map'
}
},
Topographic: {
urlTemplate: tileProtocol + '//{s}.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}',
options: {
minZoom: 1,
maxZoom: 19,
subdomains: ['server', 'services'],
attribution: 'USGS, NOAA',
attributionUrl: 'https://static.arcgis.com/attribution/World_Topo_Map'
}
},
Oceans: {
urlTemplate: tileProtocol + '//{s}.arcgisonline.com/arcgis/rest/services/Ocean/World_Ocean_Base/MapServer/tile/{z}/{y}/{x}',
options: {
minZoom: 1,
maxZoom: 16,
subdomains: ['server', 'services'],
attribution: 'USGS, NOAA',
attributionUrl: 'https://static.arcgis.com/attribution/Ocean_Basemap'
}
},
OceansLabels: {
urlTemplate: tileProtocol + '//{s}.arcgisonline.com/arcgis/rest/services/Ocean/World_Ocean_Reference/MapServer/tile/{z}/{y}/{x}',
options: {
minZoom: 1,
maxZoom: 16,
subdomains: ['server', 'services'],
pane: (pointerEvents) ? 'esri-labels' : 'tilePane'
}
},
NationalGeographic: {
urlTemplate: tileProtocol + '//{s}.arcgisonline.com/ArcGIS/rest/services/NatGeo_World_Map/MapServer/tile/{z}/{y}/{x}',
options: {
minZoom: 1,
maxZoom: 16,
subdomains: ['server', 'services'],
attribution: 'National Geographic, DeLorme, HERE, UNEP-WCMC, USGS, NASA, ESA, METI, NRCAN, GEBCO, NOAA, increment P Corp.'
}
},
DarkGray: {
urlTemplate: tileProtocol + '//{s}.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Dark_Gray_Base/MapServer/tile/{z}/{y}/{x}',
options: {
minZo