esri-leaflet
Version:
Leaflet plugins for consuming ArcGIS Online and ArcGIS Server services.
1,612 lines (1,388 loc) • 111 kB
JavaScript
/*! esri-leaflet - v1.0.3 - 2016-07-03
* Copyright (c) 2016 Environmental Systems Research Institute, Inc.
* Apache License*/
(function (factory) {
//define an AMD module that relies on 'leaflet'
if (typeof define === 'function' && define.amd) {
define(['leaflet'], function (L) {
return factory(L);
});
//define a common js module that relies on 'leaflet'
} else if (typeof module === 'object' && typeof module.exports === 'object') {
module.exports = factory(require('leaflet'));
}
if(typeof window !== 'undefined' && window.L){
factory(window.L);
}
}(function (L) {
var EsriLeaflet = { //jshint ignore:line
VERSION: '1.0.3',
Layers: {},
Services: {},
Controls: {},
Tasks: {},
Util: {},
Support: {
CORS: !!(window.XMLHttpRequest && 'withCredentials' in new XMLHttpRequest()),
pointerEvents: document.documentElement.style.pointerEvents === ''
}
};
if(typeof window !== 'undefined' && window.L){
window.L.esri = EsriLeaflet;
}
(function(EsriLeaflet){
// normalize request animation frame
var raf = window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function(cb) { return window.setTimeout(cb, 1000 / 60); };
// shallow object clone for feature properties and attributes
// from http://jsperf.com/cloning-an-object/2
function clone(obj) {
var target = {};
for (var i in obj) {
if (obj.hasOwnProperty(i)) {
target[i] = obj[i];
}
}
return target;
}
// 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,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 ( 0 <= ua && ua <= 1 && 0 <= ub && 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;
}
// convert an extent (ArcGIS) to LatLngBounds (Leaflet)
EsriLeaflet.Util.extentToBounds = function(extent){
var sw = new L.LatLng(extent.ymin, extent.xmin);
var ne = new L.LatLng(extent.ymax, extent.xmax);
return new L.LatLngBounds(sw, ne);
};
// convert an LatLngBounds (Leaflet) to extent (ArcGIS)
EsriLeaflet.Util.boundsToExtent = function(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
}
};
};
EsriLeaflet.Util.arcgisToGeojson = function (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) ? EsriLeaflet.Util.arcgisToGeojson(arcgis.geometry) : null;
geojson.properties = (arcgis.attributes) ? clone(arcgis.attributes) : null;
if(arcgis.attributes) {
geojson.id = arcgis.attributes[idAttribute] || arcgis.attributes.OBJECTID || arcgis.attributes.FID;
}
}
return geojson;
};
// GeoJSON -> ArcGIS
EsriLeaflet.Util.geojsonToArcGIS = function(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 = EsriLeaflet.Util.geojsonToArcGIS(geojson.geometry, idAttribute);
}
result.attributes = (geojson.properties) ? clone(geojson.properties) : {};
if(geojson.id){
result.attributes[idAttribute] = geojson.id;
}
break;
case 'FeatureCollection':
result = [];
for (i = 0; i < geojson.features.length; i++){
result.push(EsriLeaflet.Util.geojsonToArcGIS(geojson.features[i], idAttribute));
}
break;
case 'GeometryCollection':
result = [];
for (i = 0; i < geojson.geometries.length; i++){
result.push(EsriLeaflet.Util.geojsonToArcGIS(geojson.geometries[i], idAttribute));
}
break;
}
return result;
};
EsriLeaflet.Util.responseToFeatureCollection = function(response, idAttribute){
var objectIdField;
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 {
objectIdField = 'OBJECTID';
}
var featureCollection = {
type: 'FeatureCollection',
features: []
};
var features = response.features || response.results;
if(features.length){
for (var i = features.length - 1; i >= 0; i--) {
featureCollection.features.push(EsriLeaflet.Util.arcgisToGeojson(features[i], objectIdField));
}
}
return featureCollection;
};
// trim url whitespace and add a trailing slash if needed
EsriLeaflet.Util.cleanUrl = function(url){
//trim leading and trailing spaces, but not spaces inside the url
url = url.replace(/^\s+|\s+$|\A\s+|\s+\z/g, '');
//add a trailing slash to the url if the user omitted it
if(url[url.length-1] !== '/'){
url += '/';
}
return url;
};
EsriLeaflet.Util.isArcgisOnline = function(url){
/* hosted feature services can emit geojson natively.
our check for 'geojson' support will need to be revisted
once the functionality makes its way to ArcGIS Server*/
return (/\.arcgis\.com.*?FeatureServer/g).test(url);
};
EsriLeaflet.Util.geojsonTypeToArcGIS = function (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;
};
EsriLeaflet.Util.requestAnimationFrame = L.Util.bind(raf, window);
EsriLeaflet.Util.warn = function (message) {
if(console && console.warn) {
console.warn(message);
}
};
})(EsriLeaflet);
(function(EsriLeaflet){
var callbacks = 0;
window._EsriLeafletCallbacks = {};
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 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);
}
};
return httpRequest;
}
// AJAX handlers for CORS (modern browsers) or JSONP (older browsers)
EsriLeaflet.Request = {
request: function(url, params, callback, context){
var paramString = serialize(params);
var httpRequest = createRequest(callback, context);
var requestLength = (url + '?' + paramString).length;
// request is less then 2000 characters and the browser supports CORS, make GET request with XMLHttpRequest
if(requestLength <= 2000 && L.esri.Support.CORS){
httpRequest.open('GET', url + '?' + paramString);
httpRequest.send(null);
// request is less more then 2000 characters and the browser supports CORS, make POST request with XMLHttpRequest
} else if (requestLength > 2000 && L.esri.Support.CORS){
httpRequest.open('POST', url);
httpRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
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 && !L.esri.Support.CORS){
return L.esri.Request.get.JSONP(url, params, callback, context);
// request is longer then 2000 characters and the browser does not support CORS, log a warning
} else {
EsriLeaflet.Util.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;
},
post: {
XMLHTTP: function (url, params, callback, context) {
var httpRequest = createRequest(callback, context);
httpRequest.open('POST', url);
httpRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
httpRequest.send(serialize(params));
return httpRequest;
}
},
get: {
CORS: function (url, params, callback, context) {
var httpRequest = createRequest(callback, context);
httpRequest.open('GET', url + '?' + serialize(params), true);
httpRequest.send(null);
return httpRequest;
},
JSONP: function(url, params, callback, context){
var callbackId = 'c' + callbacks;
params.callback = 'window._EsriLeafletCallbacks.' + callbackId;
var script = L.DomUtil.create('script', null, document.body);
script.type = 'text/javascript';
script.src = url + '?' + serialize(params);
script.id = 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;
}
};
callbacks++;
return {
id: callbackId,
url: script.src,
abort: function(){
window._EsriLeafletCallbacks._callback[callbackId]({
code: 0,
message: 'Request aborted.'
});
}
};
}
}
};
// choose the correct AJAX handler depending on CORS support
EsriLeaflet.get = (EsriLeaflet.Support.CORS) ? EsriLeaflet.Request.get.CORS : EsriLeaflet.Request.get.JSONP;
// always use XMLHttpRequest for posts
EsriLeaflet.post = EsriLeaflet.Request.post.XMLHTTP;
// expose a common request method the uses GET\POST based on request length
EsriLeaflet.request = EsriLeaflet.Request.request;
})(EsriLeaflet);
EsriLeaflet.Services.Service = L.Class.extend({
includes: L.Mixin.Events,
options: {
proxy: false,
useCors: EsriLeaflet.Support.CORS
},
initialize: function (options) {
options = options || {};
this._requestQueue = [];
this._authenticating = false;
L.Util.setOptions(this, options);
this.options.url = EsriLeaflet.Util.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;
},
_request: function(method, path, params, callback, context){
this.fire('requeststart', {
url: this.options.url + path,
params: params,
method: method
});
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 EsriLeaflet.Request.get.JSONP(url, params, wrappedCallback);
} else {
return EsriLeaflet[method](url, params, wrappedCallback);
}
}
},
_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)
});
// 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
});
} else {
this.fire('requestsuccess', {
url: this.options.url + path,
params: params,
response: response,
method: method
});
}
this.fire('requestend', {
url: this.options.url + path,
params: params,
method: method
});
}, 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 = [];
}
});
EsriLeaflet.Services.service = function(params){
return new EsriLeaflet.Services.Service(params);
};
EsriLeaflet.Services.FeatureLayerService = EsriLeaflet.Services.Service.extend({
options: {
idAttribute: 'OBJECTID'
},
query: function(){
return new EsriLeaflet.Tasks.Query(this);
},
addFeature: function(feature, callback, context) {
delete feature.id;
feature = EsriLeaflet.Util.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 = EsriLeaflet.Util.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);
}
});
EsriLeaflet.Services.featureLayerService = function(options) {
return new EsriLeaflet.Services.FeatureLayerService(options);
};
EsriLeaflet.Services.MapService = EsriLeaflet.Services.Service.extend({
identify: function () {
return new EsriLeaflet.Tasks.identifyFeatures(this);
},
find: function () {
return new EsriLeaflet.Tasks.Find(this);
},
query: function () {
return new EsriLeaflet.Tasks.Query(this);
}
});
EsriLeaflet.Services.mapService = function(params){
return new EsriLeaflet.Services.MapService(params);
};
EsriLeaflet.Services.ImageService = EsriLeaflet.Services.Service.extend({
query: function () {
return new EsriLeaflet.Tasks.Query(this);
},
identify: function() {
return new EsriLeaflet.Tasks.IdentifyImage(this);
}
});
EsriLeaflet.Services.imageService = function(params){
return new EsriLeaflet.Services.ImageService(params);
};
EsriLeaflet.Tasks.Task = L.Class.extend({
options: {
proxy: false,
useCors: EsriLeaflet.Support.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 = L.esri.Util.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);
} else {
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 EsriLeaflet.Request.get.JSONP(url, params, callback, context);
} else{
return EsriLeaflet[method](url, params, callback, context);
}
}
});
EsriLeaflet.Tasks.Query = EsriLeaflet.Tasks.Task.extend({
setters: {
'offset': 'offset',
'limit': 'limit',
'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();
// if the service is hosted on arcgis online request geojson directly
if(EsriLeaflet.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 && EsriLeaflet.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 && EsriLeaflet.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'){
EsriLeaflet.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 = EsriLeaflet.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 = EsriLeaflet.Util.geojsonToArcGIS(geometry);
this.params.geometryType = EsriLeaflet.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') {
this.params.geometry = EsriLeaflet.Util.geojsonToArcGIS(geometry);
this.params.geometryType = EsriLeaflet.Util.geojsonTypeToArcGIS(geometry.type);
return;
}
// warn the user if we havn't found a
/* global console */
EsriLeaflet.Util.warn('invalid geometry passed to spatial query. Should be an L.LatLng, L.LatLngBounds or L.Marker or a GeoJSON Point Line or Polygon object');
return;
}
});
EsriLeaflet.Tasks.query = function(params){
return new EsriLeaflet.Tasks.Query(params);
};
EsriLeaflet.Tasks.Find = EsriLeaflet.Tasks.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 && EsriLeaflet.Util.responseToFeatureCollection(response)), response);
}, context);
}
});
EsriLeaflet.Tasks.find = function (params) {
return new EsriLeaflet.Tasks.Find(params);
};
EsriLeaflet.Tasks.Identify = EsriLeaflet.Tasks.Task.extend({
path: 'identify',
between: function(start, end){
this.params.time = [start.valueOf(), end.valueOf()];
return this;
}
});
EsriLeaflet.Tasks.IdentifyImage = EsriLeaflet.Tasks.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 = EsriLeaflet.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;
}
});
EsriLeaflet.Tasks.identifyImage = function(params){
return new EsriLeaflet.Tasks.IdentifyImage(params);
};
EsriLeaflet.Tasks.IdentifyFeatures = EsriLeaflet.Tasks.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 = EsriLeaflet.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 = EsriLeaflet.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);
}
});
}
});
EsriLeaflet.Tasks.identifyFeatures = function(params){
return new EsriLeaflet.Tasks.IdentifyFeatures(params);
};
(function(EsriLeaflet){
var tileProtocol = (window.location.protocol !== 'https:') ? 'http:' : 'https:';
EsriLeaflet.Layers.BasemapLayer = L.TileLayer.extend({
statics: {
TILES: {
Streets: {
urlTemplate: tileProtocol + '//{s}.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}',
attributionUrl: 'https://static.arcgis.com/attribution/World_Street_Map',
options: {
hideLogo: false,
logoPosition: 'bottomright',
minZoom: 1,
maxZoom: 19,
subdomains: ['server', 'services'],
attribution: 'Esri'
}
},
Topographic: {
urlTemplate: tileProtocol + '//{s}.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}',
attributionUrl: 'https://static.arcgis.com/attribution/World_Topo_Map',
options: {
hideLogo: false,
logoPosition: 'bottomright',
minZoom: 1,
maxZoom: 19,
subdomains: ['server', 'services'],
attribution: 'Esri'
}
},
Oceans: {
urlTemplate: tileProtocol + '//{s}.arcgisonline.com/arcgis/rest/services/Ocean/World_Ocean_Base/MapServer/tile/{z}/{y}/{x}',
attributionUrl: 'https://static.arcgis.com/attribution/Ocean_Basemap',
options: {
hideLogo: false,
logoPosition: 'bottomright',
minZoom: 1,
maxZoom: 16,
subdomains: ['server', 'services'],
attribution: 'Esri'
}
},
OceansLabels: {
urlTemplate: tileProtocol + '//{s}.arcgisonline.com/arcgis/rest/services/Ocean/World_Ocean_Reference/MapServer/tile/{z}/{y}/{x}',
options: {
hideLogo: true,
logoPosition: 'bottomright',
//pane: 'esri-label',
minZoom: 1,
maxZoom: 16,
subdomains: ['server', 'services']
}
},
NationalGeographic: {
urlTemplate: tileProtocol + '//{s}.arcgisonline.com/ArcGIS/rest/services/NatGeo_World_Map/MapServer/tile/{z}/{y}/{x}',
options: {
hideLogo: false,
logoPosition: 'bottomright',
minZoom: 1,
maxZoom: 16,
subdomains: ['server', 'services'],
attribution: 'Esri'
}
},
DarkGray: {
urlTemplate: tileProtocol + '//{s}.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Dark_Gray_Base/MapServer/tile/{z}/{y}/{x}',
options: {
hideLogo: false,
logoPosition: 'bottomright',
minZoom: 1,
maxZoom: 16,
subdomains: ['server', 'services'],
attribution: 'Esri, DeLorme, HERE'
}
},
DarkGrayLabels: {
urlTemplate: tileProtocol + '//{s}.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Dark_Gray_Reference/MapServer/tile/{z}/{y}/{x}',
options: {
hideLogo: true,
logoPosition: 'bottomright',
//pane: 'esri-label',
minZoom: 1,
maxZoom: 16,
subdomains: ['server', 'services']
}
},
Gray: {
urlTemplate: tileProtocol + '//{s}.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Base/MapServer/tile/{z}/{y}/{x}',
options: {
hideLogo: false,
logoPosition: 'bottomright',
minZoom: 1,
maxZoom: 16,
subdomains: ['server', 'services'],
attribution: 'Esri, NAVTEQ, DeLorme'
}
},
GrayLabels: {
urlTemplate: tileProtocol + '//{s}.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Reference/MapServer/tile/{z}/{y}/{x}',
options: {
hideLogo: true,
logoPosition: 'bottomright',
//pane: 'esri-label',
minZoom: 1,
maxZoom: 16,
subdomains: ['server', 'services']
}
},
Imagery: {
urlTemplate: tileProtocol + '//{s}.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
options: {
hideLogo: false,
logoPosition: 'bottomright',
minZoom: 1,
maxZoom: 19,
subdomains: ['server', 'services'],
attribution: 'Esri, DigitalGlobe, GeoEye, i-cubed, USDA, USGS, AEX, Getmapping, Aerogrid, IGN, IGP, swisstopo, and the GIS User Community'
}
},
ImageryLabels: {
urlTemplate: tileProtocol + '//{s}.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}',
options: {
hideLogo: true,
logoPosition: 'bottomright',
//pane: 'esri-label',
minZoom: 1,
maxZoom: 19,
subdomains: ['server', 'services']
}
},
ImageryTransportation: {
urlTemplate: tileProtocol + '//{s}.arcgisonline.com/ArcGIS/rest/services/Reference/World_Transportation/MapServer/tile/{z}/{y}/{x}',
//pane: 'esri-label',
options: {
hideLogo: true,
logoPosition: 'bottomright',
minZoom: 1,
maxZoom: 19,
subdomains: ['server', 'services']
}
},
ShadedRelief: {
urlTemplate: tileProtocol + '//{s}.arcgisonline.com/ArcGIS/rest/services/World_Shaded_Relief/MapServer/tile/{z}/{y}/{x}',
options: {
hideLogo: false,
logoPosition: 'bottomright',
minZoom: 1,
maxZoom: 13,
subdomains: ['server', 'services'],
attribution: 'Esri, NAVTEQ, DeLorme'
}
},
ShadedReliefLabels: {
urlTemplate: tileProtocol + '//{s}.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places_Alternate/MapServer/tile/{z}/{y}/{x}',
options: {
hideLogo: true,
logoPosition: 'bottomright',
//pane: 'esri-label',
minZoom: 1,
maxZoom: 12,
subdomains: ['server', 'services']
}
},
Terrain: {
urlTemplate: tileProtocol + '//{s}.arcgisonline.com/ArcGIS/rest/services/World_Terrain_Base/MapServer/tile/{z}/{y}/{x}',
options: {
hideLogo: false,
logoPosition: 'bottomright',
minZoom: 1,
maxZoom: 13,
subdomains: ['server', 'services'],
attribution: 'Esri, USGS, NOAA'
}
},
TerrainLabels: {
urlTemplate: tileProtocol + '//{s}.arcgisonline.com/ArcGIS/rest/services/Reference/World_Reference_Overlay/MapServer/tile/{z}/{y}/{x}',
options: {
hideLogo: true,
logoPosition: 'bottomright',
//pane: 'esri-label',
minZoom: 1,
maxZoom: 13,
subdomains: ['server', 'services']
}
}
}
},
initialize: function(key, options){
var config;
// set the config variable with the appropriate config object
if (typeof key === 'object' && key.urlTemplate && key.options){
config = key;
} else if(typeof key === 'string' && EsriLeaflet.BasemapLayer.TILES[key]){
config = EsriLeaflet.BasemapLayer.TILES[key];
} else {
throw new Error('L.esri.BasemapLayer: Invalid parameter. Use one of "St