Skip to content

Commit 6d611f9

Browse files
authored
Merge pull request #1304 from Esri/parser-edge-cases
PBF Parser geometry Z coordinates and M values
2 parents 3cc3bed + b93cd03 commit 6d611f9

32 files changed

Lines changed: 2616 additions & 235 deletions

packages/arcgis-rest-feature-service/src/pbf-parser/PbfFeatureCollectionV2.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -635,8 +635,9 @@ export function readFeatureCollectionPBufferFeatureResult(pbf: any, end: any) {
635635
geometryType: 0,
636636
spatialReference: undefined,
637637
exceededTransferLimit: false,
638-
hasZ: false,
639-
hasM: false,
638+
// the following 2 fields can be removed as they are populated later but may break tests that test for their presence incorrectly as their presence does not match arcgis responses functionality.
639+
// hasZ: false,
640+
// hasM: false,
640641
transform: undefined,
641642
fields: [],
642643
values: [],

packages/arcgis-rest-feature-service/src/pbf-parser/arcGISPbfParser.ts

Lines changed: 151 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,17 @@ export default function pbfToArcGIS(
5757
keyName: getKeyName(field)
5858
}));
5959
const geometryParser = getGeometryParser(geometryType);
60+
const hasZ = featureResult.hasZ === true;
61+
const hasM = featureResult.hasM === true;
6062

6163
// Normalize Features
6264
out.features = featureResult.features.map(
6365
(f: any) =>
6466
({
6567
attributes: collectAttributes(attributeFields, f.attributes),
66-
geometry: ((f.geometry && geometryParser(f, transform)) as any) || null
68+
geometry:
69+
((f.geometry && geometryParser(f, transform, hasZ, hasM)) as any) ||
70+
null
6771
} as IFeature)
6872
);
6973

@@ -218,13 +222,23 @@ function getGeometryParser(featureType: number) {
218222
case 3:
219223
return createPolygon;
220224
case 2:
221-
return createLine;
225+
return createPolyLine;
222226
case 0:
223227
return createPoint;
224228
/* istanbul ignore next --@preserve */
225229
default:
226-
return createPolygon;
230+
throw new ArcGISRequestError("Geometry type not supported.", 501);
231+
}
232+
}
233+
234+
function getCoordinateDimensions(hasZ: boolean, hasM: boolean) {
235+
if (hasZ && hasM) {
236+
return 4;
237+
}
238+
if (hasZ || hasM) {
239+
return 3;
227240
}
241+
return 2;
228242
}
229243

230244
function getGeometryType(featureType: number): GeometryType {
@@ -241,183 +255,184 @@ function getGeometryType(featureType: number): GeometryType {
241255
}
242256
}
243257

244-
function createPoint(f: any, transform: any) {
245-
const p = {
246-
type: "Point",
247-
coordinates: transformTuple(f.geometry.coords, transform)
248-
};
249-
const ret = {
250-
x: p.coordinates[0],
251-
y: p.coordinates[1]
258+
function createPoint(f: any, transform: any, hasZ: boolean, hasM: boolean) {
259+
const dimensions = getCoordinateDimensions(hasZ, hasM);
260+
const coordinates = transformTuple(
261+
f.geometry.coords.slice(0, dimensions),
262+
transform,
263+
hasZ,
264+
hasM
265+
);
266+
267+
const point: any = {
268+
x: coordinates[0],
269+
y: coordinates[1]
252270
};
253-
// structure output according to arcgis point geometry spec
254-
// istanbul ignore if else --@preserve
255-
if (p.coordinates.length > 2) {
256-
return { ...ret, z: p.coordinates[2] };
271+
272+
if (coordinates.length > 2) {
273+
if (hasZ) {
274+
point.z = coordinates[2];
275+
}
276+
if (hasM) {
277+
point.m = coordinates[hasZ ? 3 : 2];
278+
}
257279
}
258-
return ret;
280+
281+
return point;
259282
}
260283

261-
function createLine(f: any, transform: any) {
262-
let l = null;
263-
const lengths = f.geometry.lengths.length;
284+
function createPolyLine(f: any, transform: any, hasZ: boolean, hasM: boolean) {
285+
const paths = [];
286+
let startPoint = 0;
287+
const dimensions = getCoordinateDimensions(hasZ, hasM);
264288

265-
/* istanbul ignore else if --@preserve */
266-
if (lengths === 1) {
267-
l = {
268-
type: "LineString",
269-
coordinates: createLinearRing(
270-
f.geometry.coords,
271-
transform,
272-
0,
273-
f.geometry.lengths[0] * 2
274-
)
275-
};
276-
// structure output according to arcgis Polyline geometry spec
277-
// https://developers.arcgis.com/javascript/latest/api-reference/esri-geometry-Polyline.html#paths
278-
return {
279-
paths: [l.coordinates]
280-
};
281-
} else if (lengths > 1) {
282-
l = {
283-
type: "MultiLineString",
284-
coordinates: []
285-
};
286-
let startPoint = 0;
287-
for (let index = 0; index < lengths; index++) {
288-
const stopPoint = startPoint + f.geometry.lengths[index] * 2;
289-
const line = createLinearRing(
289+
for (let i = 0; i < f.geometry.lengths.length; i++) {
290+
const stopPoint = startPoint + f.geometry.lengths[i] * dimensions;
291+
paths.push(
292+
genericPartDecoder(
290293
f.geometry.coords,
291294
transform,
292295
startPoint,
293-
stopPoint
294-
);
295-
l.coordinates.push(line);
296-
startPoint = stopPoint;
297-
}
298-
return {
299-
paths: l.coordinates
300-
};
296+
stopPoint,
297+
dimensions,
298+
hasZ,
299+
hasM
300+
)
301+
);
302+
startPoint = stopPoint;
301303
}
304+
305+
return { paths };
302306
}
303307

304-
function createPolygon(f: any, transform: any) {
308+
function createPolygon(f: any, transform: any, hasZ: boolean, hasM: boolean) {
309+
const rings = [] as any[];
305310
const lengths = f.geometry.lengths.length;
306-
307-
const p = {
308-
type: "Polygon",
309-
coordinates: [] as any[]
310-
};
311-
312-
if (lengths === 1) {
313-
p.coordinates.push(
314-
createLinearRing(
315-
f.geometry.coords,
316-
transform,
317-
0,
318-
f.geometry.lengths[0] * 2
319-
)
311+
let startPoint = 0;
312+
const dimensions = getCoordinateDimensions(hasZ, hasM);
313+
314+
for (let index = 0; index < lengths; index++) {
315+
const stopPoint = startPoint + f.geometry.lengths[index] * dimensions;
316+
const ring = genericPartDecoder(
317+
f.geometry.coords,
318+
transform,
319+
startPoint,
320+
stopPoint,
321+
dimensions,
322+
hasZ,
323+
hasM
320324
);
321-
} else {
322-
p.type = "MultiPolygon";
323325

324-
let startPoint = 0;
325-
for (let index = 0; index < lengths; index++) {
326-
const stopPoint = startPoint + f.geometry.lengths[index] * 2;
327-
const ring = createLinearRing(
328-
f.geometry.coords,
329-
transform,
330-
startPoint,
331-
stopPoint
332-
);
333-
334-
// Check if the ring is clockwise, if so it's an outer ring
335-
// If it's counter-clockwise its a hole and so push it to the prev outer ring
336-
// This is perhaps a bit naive
337-
// see https://github.com/terraformer-js/terraformer/blob/master/packages/arcgis/src/geojson.js
338-
// for a fuller example of doing this
339-
/* istanbul ignore else if --@preserve */
340-
if (ringIsClockwise(ring)) {
341-
p.coordinates.push([ring]);
342-
} else if (p.coordinates.length > 0) {
343-
p.coordinates[p.coordinates.length - 1].push(ring);
344-
}
345-
startPoint = stopPoint;
326+
/* istanbul ignore else --@preserve */
327+
if (ring.length > 0) {
328+
rings.push(closeRing(ring));
346329
}
347-
}
348-
// structure output according to arcgis polygon geometry spec
349-
return {
350-
rings: p.coordinates
351-
};
352-
}
353330

354-
function ringIsClockwise(ringToTest: any) {
355-
let total = 0;
356-
let i = 0;
357-
const rLength = ringToTest.length;
358-
let pt1 = ringToTest[i];
359-
let pt2;
360-
for (i; i < rLength - 1; i++) {
361-
pt2 = ringToTest[i + 1];
362-
total += (pt2[0] - pt1[0]) * (pt2[1] + pt1[1]);
363-
pt1 = pt2;
331+
startPoint = stopPoint;
364332
}
365-
return total >= 0;
333+
334+
return { rings };
366335
}
367336

368-
function createLinearRing(
337+
/* istanbul ignore next --@preserve */
338+
function genericPartDecoder(
369339
arr: number[],
370340
transform: any,
371341
startPoint: number,
372-
stopPoint: number
342+
stopPoint: number,
343+
dimensions: number,
344+
hasZ: boolean,
345+
hasM: boolean
373346
) {
374347
const out = [] as any[];
375-
/* istanbul ignore if --@preserve */
376348
if (arr.length === 0) return out;
377349

378-
const initialX = arr[startPoint];
379-
const initialY = arr[startPoint + 1];
380-
out.push(transformTuple([initialX, initialY], transform));
381-
let prevX = initialX;
382-
let prevY = initialY;
383-
for (let i = startPoint + 2; i < stopPoint; i = i + 2) {
384-
const x = difference(prevX, arr[i]);
385-
const y = difference(prevY, arr[i + 1]);
386-
const transformed = transformTuple([x, y], transform);
350+
let prevCoords = arr.slice(startPoint, startPoint + dimensions);
351+
if (prevCoords.length < 2) return out;
352+
353+
out.push(transformTuple(prevCoords, transform, hasZ, hasM));
354+
355+
for (let i = startPoint + dimensions; i < stopPoint; i = i + dimensions) {
356+
const delta = arr.slice(i, i + dimensions);
357+
if (delta.length < 2) {
358+
continue;
359+
}
360+
361+
const currentCoords = prevCoords.map((coordinate, index) =>
362+
// x and y values are relative to the previous coordinate, while z and m values are absolute
363+
// if idx is >= 2, we are handling z or m values so we should return the value only
364+
// if idx is < 2, we are handling x and y values so we should sum the delta to the previous coordinate value
365+
// since x and y values are encoded as deltas in the pbf
366+
index < 2 ? sum(coordinate, delta[index]) : delta[index]
367+
);
368+
369+
const transformed = transformTuple(currentCoords, transform, hasZ, hasM);
387370
out.push(transformed);
388-
prevX = x;
389-
prevY = y;
371+
prevCoords = currentCoords;
390372
}
391373
return out;
392374
}
393375

394376
/* istanbul ignore next --@preserve */
395-
function transformTuple(coords: any, transform: any) {
377+
function closeRing(ring: any[]) {
378+
const first = ring[0];
379+
const last = ring[ring.length - 1];
380+
if (!first || !last) return ring;
381+
if (first[0] === last[0] && first[1] === last[1]) return ring;
382+
return [...ring, [...first]];
383+
}
384+
385+
/* istanbul ignore next --@preserve */
386+
function transformTuple(
387+
coords: any,
388+
transform: any,
389+
hasZ: boolean,
390+
hasM: boolean
391+
) {
392+
const scale = transform?.scale || {};
393+
const translate = transform?.translate || {};
394+
395+
const xScale = scale.xScale ?? 1;
396+
const yScale = scale.yScale ?? 1;
397+
const zScale = scale.zScale ?? 1;
398+
const mScale = scale.mScale ?? 1;
399+
400+
const xTranslate = translate.xTranslate ?? 0;
401+
const yTranslate = translate.yTranslate ?? 0;
402+
const zTranslate = translate.zTranslate ?? 0;
403+
const mTranslate = translate.mTranslate ?? 0;
404+
396405
let x = coords[0];
397406
let y = coords[1];
398407

399-
let z = coords[2] ? coords[2] : undefined;
400-
if (transform.scale) {
401-
x *= transform.scale.xScale;
402-
y *= -transform.scale.yScale;
403-
if (undefined !== z) {
404-
z *= transform.scale.zScale;
405-
}
408+
let z = hasZ ? coords[2] : undefined;
409+
let m = hasM ? coords[hasZ ? 3 : 2] : undefined;
410+
411+
x = x * xScale + xTranslate;
412+
y = y * -yScale + yTranslate;
413+
414+
if (undefined !== z) {
415+
z = z * zScale + zTranslate;
406416
}
407-
if (transform.translate) {
408-
x += transform.translate.xTranslate;
409-
y += transform.translate.yTranslate;
410-
if (undefined !== z) {
411-
z += transform.translate.zTranslate;
417+
418+
if (undefined !== m) {
419+
if (m === 0) {
420+
m = null;
421+
} else {
422+
m = m * mScale + mTranslate;
412423
}
413424
}
425+
414426
const ret = [x, y];
415427
if (undefined !== z) {
416428
ret.push(z);
417429
}
430+
if (undefined !== m) {
431+
ret.push(m);
432+
}
418433
return ret;
419434
}
420435

421-
function difference(a: any, b: any) {
436+
function sum(a: any, b: any) {
422437
return a + b;
423438
}

0 commit comments

Comments
 (0)