Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 166 additions & 1 deletion web/client/api/ArcGIS.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ import axios from '../libs/ajax';
import Proj4js from 'proj4';
import { reprojectBbox } from '../utils/CoordinatesUtils';
import trimEnd from 'lodash/trimEnd';
import { isFeatureServerUrl, esriGeometryTypeToGeoJSON } from '../utils/ArcGISUtils';
import {
isFeatureServerUrl,
esriGeometryTypeToGeoJSON,
esriFieldTypeToPrimitive
} from '../utils/ArcGISUtils';

let _cache = {};

Expand Down Expand Up @@ -222,3 +226,164 @@ export const getCapabilities = (url, startPosition, maxRecords, text, info) => {
return { numberOfRecordsMatched, numberOfRecordsReturned, records };
});
};

const buildFeatureLayerUrl = (url, name) => {
const baseUrl = trimEnd(url, '/');
const layerId = name !== undefined && name !== null ? name : 0;
return `${baseUrl}/${layerId}`;
};

let _schemaCache = {};
let _pendingSchemaFetch = {};
/**
* Fetch a FeatureService layer's schema
* @param {string} url FeatureService root url
* @param {string|number} name layer id within the service
* @param {object} [opts]
* @param {string} [opts.authSourceId] security source id forwarded to axios
* @param {boolean} [opts.force] bypass cache
* @returns {object} promise resolving with
* - {array} fields - raw ESRI fields metadata
* - {object} properties - sampled `{name: 0|''}` primitives
* - {string} geometryType - GeoJSON geometry type
*/
export const getFeatureLayerSchema = (url, name, opts = {}) => {
const layerUrl = buildFeatureLayerUrl(url, name);
if (!opts.force && _schemaCache[layerUrl]) {
return Promise.resolve(_schemaCache[layerUrl]);
}
if (_pendingSchemaFetch[layerUrl]) {
return _pendingSchemaFetch[layerUrl];
}
const request = axios.get(layerUrl, {
params: { f: 'json' },
_msAuthSourceId: opts.authSourceId
}).then(({ data }) => {
const fields = Array.isArray(data?.fields) ? data.fields : [];
// Sample primitive value so getVectorLayerAttributes' isNumber/isString filter accepts it.
const properties = fields.reduce((acc, f) => {
if (!f?.name) return acc;
const primitive = esriFieldTypeToPrimitive(f.type);
if (primitive === 'number') acc[f.name] = 0;
else if (primitive === 'string') acc[f.name] = '';
return acc;
}, {});
const result = {
fields,
properties,
geometryType: data?.geometryType
? esriGeometryTypeToGeoJSON(data.geometryType)
: undefined
};
_schemaCache[layerUrl] = result;
delete _pendingSchemaFetch[layerUrl];
return result;
}).catch((e) => {
delete _pendingSchemaFetch[layerUrl];
throw e;
});
_pendingSchemaFetch[layerUrl] = request;
return request;
};

const DEFAULT_FEATURE_PAGE_SIZE = 1000;

/**
* Paginated fetch of features from a FeatureService layer as GeoJSON.
* @param {string} url FeatureService root url
* @param {string|number} name layer id within the service
* @param {object} [opts]
* @param {object} [opts.params] extra query params merged into the request
* @param {number} [opts.pageSize] resultRecordCount per page (default 1000)
* @param {number} [opts.maxRecordCount] caps the page size (server-side limit)
* @param {number} [opts.maxFeatures] maximum number of features to return
* @param {string} [opts.authSourceId] security source id forwarded to axios
* @returns {object} promise resolving with a GeoJSON FeatureCollection
* - {string} type - always `FeatureCollection`
* - {array} features - accumulated GeoJSON features
*/
export const fetchFeatureLayerCollection = (url, name, opts = {}) => {
const queryUrl = `${buildFeatureLayerUrl(url, name)}/query`;
const baseParams = {
where: '1=1',
outFields: '*',
outSR: 4326,
f: 'geojson',
...(opts.params || {})
};
// Use server-advertised maxRecordCount as page size (may exceed 1000)
const recordCount = opts.pageSize
|| opts.maxRecordCount
|| DEFAULT_FEATURE_PAGE_SIZE;
const allFeatures = [];
const seenIds = new Set();
const fetchPage = (offset) => {
return axios.get(queryUrl, {
params: {
...baseParams,
resultOffset: offset,
resultRecordCount: recordCount
},
_msAuthSourceId: opts.authSourceId
}).then((response) => {
const data = response?.data;
const pageFeatures = data?.features || [];
const newFeatures = pageFeatures.filter((f) => {
const id = f.id ?? f.properties?.OBJECTID;
if (id !== null && id !== undefined && seenIds.has(id)) return false;
if (id !== null && id !== undefined) seenIds.add(id);
return true;
});
if (newFeatures.length) {
allFeatures.push(...newFeatures);
}
const reachedCap = opts.maxFeatures && allFeatures.length >= opts.maxFeatures;
const exceeded = data?.exceededTransferLimit
|| data?.properties?.exceededTransferLimit;
if (!reachedCap && exceeded && newFeatures.length > 0) {
return fetchPage(offset + pageFeatures.length);
}
const features = opts.maxFeatures
? allFeatures.slice(0, opts.maxFeatures)
: allFeatures;
return { type: 'FeatureCollection', features };
}).catch(() => ({ type: 'FeatureCollection', features: allFeatures }));
};
return fetchPage(0);
};

const DEFAULT_CLASSIFICATION_SAMPLE = 5000;
let _classificationCache = {};

/**
* Fetch a feature collection suitable for client-side
* classification of a FeatureService layer
* @param {object} layer layer config
* @param {object} [opts]
* @param {number} [opts.sampleSize] maximum number of features to return
* @param {boolean} [opts.force] force re-fetch
* @returns {object} promise resolving with a GeoJSON FeatureCollection
* - {string} type - always `FeatureCollection`
* - {array} features - fetched features
*/
export const queryFeatureLayerForClassification = (layer, opts = {}) => {
const layerUrl = buildFeatureLayerUrl(layer?.url, layer?.name);
const sampleSize = opts.sampleSize || DEFAULT_CLASSIFICATION_SAMPLE;
const cacheKey = `${layerUrl}::${sampleSize}`;
if (!opts.force && _classificationCache[cacheKey]) {
return _classificationCache[cacheKey];
}
const request = fetchFeatureLayerCollection(layer?.url, layer?.name, {
authSourceId: layer?.security?.sourceId,
maxRecordCount: layer?.maxRecordCount,
maxFeatures: sampleSize
}).then((collection) => {
// Empty result usually means a rejected request — remove so next call retries.
if (!collection?.features?.length) {
delete _classificationCache[cacheKey];
}
return collection;
});
_classificationCache[cacheKey] = request;
return request;
};
Loading
Loading