UNPKG

@esri/arcgis-rest-feature-service

Version:

Feature layer query and edit helpers for @esri/arcgis-rest-js

328 lines 12 kB
/** * This code has been adapted from [arcgis-pbf-parser] ([https://github.com/rowanwins/arcgis-pbf-parser]) * Modifications have been made for use in REST JS. */ import { ArcGISRequestError } from "@esri/arcgis-rest-request"; import Pbf from "pbf"; import { readFeatureCollectionPBuffer, FeatureCollectionPBufferFieldType, FeatureCollectionPBufferSQLType } from "./PbfFeatureCollectionV2.js"; export default function pbfToArcGIS(featureCollectionBuffer) { const decodedObject = decode(featureCollectionBuffer); const featureResult = decodedObject.queryResult.featureResult; const transform = decodedObject.queryResult.featureResult.transform; const geometryType = decodedObject.queryResult.featureResult.geometryType; // Assign all the top level properties const out = Object.assign(Object.assign({}, featureResult), { geometryType: getGeometryType(geometryType) }); // istanbul ignore else --@preserve if (out.spatialReference) { // Remove any spatial reference fields with empty values out.spatialReference = removeEmptyValues(out.spatialReference); } // Normalize fields out.fields = decodeFields(featureResult.fields); // Throw error if objectIdField does not exist in the fields if (out.fields.every((field) => field.name !== featureResult.objectIdFieldName)) { throw new ArcGISRequestError(`objectIdField '${featureResult.objectIdFieldName}' was not found.`, 400); } // Get attribute and geometry transformations const attributeFields = featureResult.fields.map((field) => (Object.assign(Object.assign({}, field), { keyName: getKeyName(field) }))); const geometryParser = getGeometryParser(geometryType); // Normalize Features out.features = featureResult.features.map((f) => ({ attributes: collectAttributes(attributeFields, f.attributes), geometry: (f.geometry && geometryParser(f, transform)) || null })); // 4. Purge all properties that are not part of IQueryFeaturesResponse from the output object (optionally retain some if needed) const queryFeaturesResponse = normalizeFeatureResponse(out); return queryFeaturesResponse; } export function decode(featureCollectionBuffer) { let decodedObject; try { decodedObject = readFeatureCollectionPBuffer(new Pbf(featureCollectionBuffer)); } catch (error) { /* istanbul ignore next --@preserve */ throw new Error(`Could not parse arcgis-pbf buffer: ${error}`); } if (!decodedObject.queryResult) { throw new Error("Could not parse arcgis-pbf buffer: Missing queryResult."); } return decodedObject; } export function decodeFields(fields) { // Build lookup maps const fieldTypeMap = buildKeyMap(FeatureCollectionPBufferFieldType); const sqlTypeMap = buildKeyMap(FeatureCollectionPBufferSQLType); return fields.map((field) => // sqlMap exists on response on some feature services but not on the current REST JS IField interface decodeField(field, fieldTypeMap, sqlTypeMap)); } export function buildKeyMap(spec) { const map = {}; for (const [key, val] of Object.entries(spec)) { map[val] = key; } return map; } // Fields: PbfFeatureCollection doesnt decode to IField export function decodeField(field, fieldTypeMap, sqlTypeMap) { // configure getters that return arcgis json default values for optional props const optionalProps = [ ["alias", (f) => f.alias], ["sqlType", (f) => sqlTypeMap[f.sqlType]], ["domain", (f) => (f.domain === "" ? null : JSON.parse(f.domain))], ["length", (f) => (f.length === 0 ? undefined : f.length)], ["editable", (f) => f.editable], ["exactMatch", (f) => f.exactMatch], ["nullable", (f) => f.nullable], ["defaultValue", (f) => (f.defaultValue === "" ? null : f.defaultValue)] ]; // set required properties const out = { name: field.name, type: fieldTypeMap[field.fieldType] }; // set optional properties for (const [key, getter] of optionalProps) { if (field[key] !== undefined) { const val = getter(field); if (val !== undefined) { out[key] = val; } } } return out; } function getKeyName(fields) { switch (fields.fieldType) { case 1: return "sintValue"; case 2: return "floatValue"; case 3: return "doubleValue"; case 4: return "stringValue"; /* istanbul ignore next --@preserve */ case 5: return "sint64Value"; case 6: return "uintValue"; default: return null; } } function collectAttributes(fields, featureAttributes) { const out = {}; for (let i = 0; i < fields.length; i++) { const f = fields[i]; if (featureAttributes[i][featureAttributes[i].value_type] !== undefined) out[f.name] = featureAttributes[i][featureAttributes[i].value_type]; else out[f.name] = null; } return out; } export function removeEmptyValues(obj) { return Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== 0 && value !== "")); } function normalizeFeatureResponse(featureResult) { // List of keys pbf decoder result that are not part of IQueryFeaturesResponse const excludeKeys = [ "serverGens", "geohashFieldName", "transform", "values" // Add any other keys to exclude ]; /** * Keys on arcGIS json response that are not part of IQueryFeaturesResponse that should probably not be excluded * uniqueIdField * geometryProperties */ const out = {}; Object.keys(featureResult).forEach((key) => { // only assign properties that are defined and on the IQueryFeaturesResponse if (!excludeKeys.includes(key) && featureResult[key] !== undefined) { out[key] = featureResult[key]; } }); return out; } // * @property {number} esriGeometryTypePoint=0 esriGeometryTypePoint value // * @property {number} esriGeometryTypeMultipoint=1 esriGeometryTypeMultipoint value // * @property {number} esriGeometryTypePolyline=2 esriGeometryTypePolyline value // * @property {number} esriGeometryTypePolygon=3 esriGeometryTypePolygon value // * @property {number} esriGeometryTypeMultipatch=4 esriGeometryTypeMultipatch value // * @property {number} esriGeometryTypeNone=127 esriGeometryTypeNone value function getGeometryParser(featureType) { switch (featureType) { case 3: return createPolygon; case 2: return createLine; case 0: return createPoint; /* istanbul ignore next --@preserve */ default: return createPolygon; } } function getGeometryType(featureType) { switch (featureType) { case 0: return "esriGeometryPoint"; case 2: return "esriGeometryPolyline"; case 3: return "esriGeometryPolygon"; // istanbul ignore next --@preserve default: throw new ArcGISRequestError("Geometry type not supported.", 501); } } function createPoint(f, transform) { const p = { type: "Point", coordinates: transformTuple(f.geometry.coords, transform) }; const ret = { x: p.coordinates[0], y: p.coordinates[1] }; // structure output according to arcgis point geometry spec // istanbul ignore if else --@preserve if (p.coordinates.length > 2) { return Object.assign(Object.assign({}, ret), { z: p.coordinates[2] }); } return ret; } function createLine(f, transform) { let l = null; const lengths = f.geometry.lengths.length; /* istanbul ignore else if --@preserve */ if (lengths === 1) { l = { type: "LineString", coordinates: createLinearRing(f.geometry.coords, transform, 0, f.geometry.lengths[0] * 2) }; // structure output according to arcgis Polyline geometry spec // https://developers.arcgis.com/javascript/latest/api-reference/esri-geometry-Polyline.html#paths return { paths: [l.coordinates] }; } else if (lengths > 1) { l = { type: "MultiLineString", coordinates: [] }; let startPoint = 0; for (let index = 0; index < lengths; index++) { const stopPoint = startPoint + f.geometry.lengths[index] * 2; const line = createLinearRing(f.geometry.coords, transform, startPoint, stopPoint); l.coordinates.push(line); startPoint = stopPoint; } return { paths: l.coordinates }; } } function createPolygon(f, transform) { const lengths = f.geometry.lengths.length; const p = { type: "Polygon", coordinates: [] }; if (lengths === 1) { p.coordinates.push(createLinearRing(f.geometry.coords, transform, 0, f.geometry.lengths[0] * 2)); } else { p.type = "MultiPolygon"; let startPoint = 0; for (let index = 0; index < lengths; index++) { const stopPoint = startPoint + f.geometry.lengths[index] * 2; const ring = createLinearRing(f.geometry.coords, transform, startPoint, stopPoint); // Check if the ring is clockwise, if so it's an outer ring // If it's counter-clockwise its a hole and so push it to the prev outer ring // This is perhaps a bit naive // see https://github.com/terraformer-js/terraformer/blob/master/packages/arcgis/src/geojson.js // for a fuller example of doing this /* istanbul ignore else if --@preserve */ if (ringIsClockwise(ring)) { p.coordinates.push([ring]); } else if (p.coordinates.length > 0) { p.coordinates[p.coordinates.length - 1].push(ring); } startPoint = stopPoint; } } // structure output according to arcgis polygon geometry spec return { rings: p.coordinates }; } function ringIsClockwise(ringToTest) { let total = 0; let i = 0; const rLength = ringToTest.length; let pt1 = ringToTest[i]; let 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; } function createLinearRing(arr, transform, startPoint, stopPoint) { const out = []; /* istanbul ignore if --@preserve */ if (arr.length === 0) return out; const initialX = arr[startPoint]; const initialY = arr[startPoint + 1]; out.push(transformTuple([initialX, initialY], transform)); let prevX = initialX; let prevY = initialY; for (let i = startPoint + 2; i < stopPoint; i = i + 2) { const x = difference(prevX, arr[i]); const y = difference(prevY, arr[i + 1]); const transformed = transformTuple([x, y], transform); out.push(transformed); prevX = x; prevY = y; } return out; } /* istanbul ignore next --@preserve */ function transformTuple(coords, transform) { let x = coords[0]; let y = coords[1]; let z = coords[2] ? coords[2] : undefined; if (transform.scale) { x *= transform.scale.xScale; y *= -transform.scale.yScale; if (undefined !== z) { z *= transform.scale.zScale; } } if (transform.translate) { x += transform.translate.xTranslate; y += transform.translate.yTranslate; if (undefined !== z) { z += transform.translate.zTranslate; } } const ret = [x, y]; if (undefined !== z) { ret.push(z); } return ret; } function difference(a, b) { return a + b; } //# sourceMappingURL=arcGISPbfParser.js.map