'use strict';
// TODO revisit if we still need rv- in these constants.
const states = { // these are used as css classes; hence the `rv` prefix
NEW: 'rv-new',
REFRESH: 'rv-refresh',
LOADING: 'rv-loading',
LOADED: 'rv-loaded', // TODO maybe loaded and default are the same?
DEFAULT: 'rv-default', // TODO it appears this is not being used?
ERROR: 'rv-error'
};
// these match strings in the client.
const clientLayerType = {
ESRI_GRAPHICS: 'esriGraphics',
ESRI_DYNAMIC: 'esriDynamic',
ESRI_FEATURE: 'esriFeature',
ESRI_IMAGE: 'esriImage',
ESRI_TILE: 'esriTile',
ESRI_GROUP: 'esriGroup',
ESRI_RASTER: 'esriRaster',
OGC_WMS: 'ogcWms',
OGC_WFS: 'ogcWfs',
UNRESOLVED: 'unresolved',
UNKNOWN: 'unknown'
};
const dataSources = {
ESRI: 'esri',
WFS: 'wfs',
WMS: 'wms',
FILE: 'file'
}
const filterType = {
SYMBOL: 'symbol',
API: 'api',
GRID: 'grid',
EXTENT: 'extent'
}
/**
* Takes an array of (possibly pending) legend data and constructs an array of default
* symbology objects. As each legend item loads, the symbology objects are updated.
*
* @function makeSymbologyArray
* @param {Array} legendData list of promises that resolve with legend data (svg and labels)
* @returns {Array} a list of symbology objects.
*/
function makeSymbologyArray(legendData) {
return legendData.map(item => {
// items are promises. they resolve when the svg has been renderer.
// after that happens, we update the internal properties of the symbologyItem
const symbologyItem = {
svgcode: null,
name: null,
definitionClause: null,
drawPromise: item.then(data => {
symbologyItem.svgcode = data.svgcode;
symbologyItem.name = data.label || '';
symbologyItem.definitionClause = data.definitionClause;
})
};
return symbologyItem;
});
}
/**
* Splits an indexed map server url into an object with .rootUrl and .index
* properties.
*
* @function parseUrlIndex
* @param {String} url an indexed map server url
* @returns {Object} the url split into the server root and the index.
*/
function parseUrlIndex(url) {
// break url into root and index
// note we are returning index as a string for now.
const result = {
rootUrl: url,
index: '0'
};
const re = /\/(\d+)\/?$/;
const matches = url.match(re);
if (matches) {
result.index = matches[1];
result.rootUrl = url.substr(0, url.length - matches[0].length); // will drop trailing slash
} else {
// give up, dont crash with error.
// default configuration will make sense for non-feature urls,
// even though they should not be using this.
console.warn('Cannot extract layer index from url ' + url);
}
return result;
}
/**
* Takes a specific layer state and determines if the layer can be considered
* loaded or not.
*
* @function layerLoaded
* @param {String} state a layer state
* @returns {Boolean} if the layer is loaded or not
*/
function layerLoaded(state) {
switch (state) {
case states.ERROR:
case states.LOADING:
case states.NEW:
return false;
default:
return true;
}
}
/**
* Takes an extent. If extent has problematic boundaries, adjust the extent inwards.
*
* @function makeSafeExtent
* @param {Object} extent an extent. Param may be modified in place
* @return {Object} an extent that has been adjusted if it's too big
*/
function makeSafeExtent(extent) {
// TODO add more cases to check for as we find them
// we modify the parameter in-place due to lazyness (i.e. not wanting to generate
// a new prototyped extent object). If we find this to be a problem, change
// the code to make a proper copy (might need some shenanigans to get the
// extent constructor function in here)
// if lat/long, back off if too close to poles or anti-prime-meridian
if (extent.spatialReference.wkid === 4326) {
const squish = (ext, prop, limit, direction) => {
if (((ext[prop]) * direction) > (limit * direction)) {
ext[prop] = limit;
}
};
[['xmin', -179, -1], ['xmax', 179, 1], ['ymin', -89, -1], ['ymax', 89, 1]].forEach(nugget => {
squish(extent, ...nugget);
});
}
return extent;
}
/**
* @class IdentifyResult
*/
class IdentifyResult {
/**
* @param {Object} proxy proxy to the logical layer containing the results (i.e. a feature class)
*/
constructor (proxy) {
// TODO revisit what should be in this class, and what belongs in the app
// also what can be abstacted to come from layerRec
this.isLoading = true;
this.requestId = -1;
this.requester = {
proxy
};
this.data = [];
}
}
/**
* @class FakeEvent
*/
class FakeEvent {
constructor () {
this._listeners = [];
}
/**
* Triggers the event (i.e. notifies all listeners)
*
* @function fireEvent
* @private
* @param {...Object} eventParams arbitrary set of parameters to pass to the event handler functions
*/
fireEvent (...eventParams) {
// if we don't copy the array we could be looping on an array
// that is being modified as it is being read
this._listeners.slice(0).forEach(l => l(...eventParams));
}
/**
* Register a function to listen to this event.
*
* @function addListener
* @param {Function} listenerCallback function to call when the event fires
* @returns {Function} the input function (for fun and reference)
*/
addListener (listenerCallback) {
this._listeners.push(listenerCallback);
return listenerCallback;
}
/**
* Remove a mouse filter listener.
*
* @function removeListener
* @param {Function} listenerCallback function to not call when a filter event happens
*/
removeListener (listenerCallback) {
const idx = this._listeners.indexOf(listenerCallback);
if (idx < 0) {
throw new Error('Attempting to remove a listener which is not registered.');
}
this._listeners.splice(idx, 1);
}
get listenerCount () { return this._listeners.length; }
}
/**
* Determines if two extents are the same.
*
* @function areExtentsSame
* @param {Extent} e1 an extent.
* @param {Extent} e2 another extent.
* @return {Boolean} indicates if input extents are the same
*/
function areExtentsSame(e1, e2) {
if (!(e1 && e2)) {
// a param was empty/nothing
return false;
}
return e1.xmin === e2.xmin && e1.ymin === e2.ymin && e1.xmax === e2.xmax && e1.ymax === e2.ymax;
}
/**
* Returns array of common elements. Assumes each array has no duplicates (e.g. no [1,1,2] type arrays).
* This is mainly used for arrays of object ids
*
* @function arrayIntersect
* @param {Array} a1 an array.
* @param {Array} a2 another array.
* @return {Array} array that has elements common to both input arrays
*/
function arrayIntersect(a1, a2) {
return a1.filter(e => -1 !== a2.indexOf(e));
}
module.exports = () => ({
states,
clientLayerType,
dataSources,
filterType,
makeSymbologyArray,
IdentifyResult,
parseUrlIndex,
layerLoaded,
makeSafeExtent,
areExtentsSame,
arrayIntersect,
FakeEvent
});