I've recently upgraded to ArcGIS JS API version 4.32. After the update I've noticed that some of the geometry operators now return true curves.
For example the `minimumBoundingCircleOperator` now returns a polygon with curved rings. This is breaking my logic as I was previously using the centroid and extent of the polygon which now showing as null in the geometry.
Is there a way of flagging that I want to produce normal geometries? Currently I can't see much guidance of how to work with curved rings etc.
Solved! Go to Solution.
Hi @JonathanDawe_BAS thanks for the feedback. Yes, the correct approach is to use the densifyOperator to remove curves from the output geometries at version 4.32+. There's also a code snippet in the densifyOperator API reference:
// Returns a polyline where all curve segments have been densified.
if (polyline.curvePaths) {
const densified = densifyOperator.execute(polyline, 0, { maxAngleInDegrees: 1 });
}
We'll add more clarification to the minimumBoundingCircleOperator documentation that it returns a polygon with curves, even when using input geometries that don't have curves.
My current work around is to send it through the densifyOperator. This conceptually makes sense I guess, but does feel a bit undocumented?
The complete code is here for reference:
import * as densifyOperator from '@arcgis/core/geometry/operators/densifyOperator.js';
import * as minimumBoundingCircleOperator from '@arcgis/core/geometry/operators/minimumBoundingCircleOperator.js';
import * as projectOperator from '@arcgis/core/geometry/operators/projectOperator.js';
import { useDebouncedValue } from '@mantine/hooks';
import React, { type JSX } from 'react';
import { useArcState } from '@/features/arcgis/hooks';
import { LineGraphic } from '@/types';
import { isDefined } from '@/types/typeGuards';
type Point = [number, number];
export interface SvgPreviewOptions {
rotation?: number;
size?: number;
padding?: number;
bgColor?: string;
lineColor?: string;
lineWidth?: number;
}
/**
* Scales and centers polyline points to fit within a circular viewport
* @param polyline - Input polyline geometry
* @param size - SVG viewport size
* @param padding - Padding inside circle
* @returns Normalized points scaled to fit viewport
*/
function normalizePoints(polyline: __esri.Polyline, size: number, padding: number): Point[][] {
console.log(minimumBoundingCircleOperator.supportsCurves);
const boundingCircle = minimumBoundingCircleOperator.execute(polyline);
const boundingPolygonCircle = densifyOperator.execute(boundingCircle, 100) as __esri.Polygon;
const { extent, centroid } = boundingPolygonCircle;
if (!isDefined(centroid) || !isDefined(extent)) {
return [];
}
const radius = extent.width / 2;
const availableRadius = size / 2 - padding;
const scale = radius === 0 ? 1 : availableRadius / radius;
return polyline.paths.map((path) => {
return path.map(([x, y]) => [
((x ?? 0) - centroid.x) * scale + size / 2,
size - (((y ?? 0) - centroid.y) * scale + size / 2),
]);
});
}
/**
* Converts array of point paths to SVG path data strings
*/
function generatePathData(paths: Point[][]): string[] {
return paths.map((path) =>
path.map((point, index) => `${index === 0 ? 'M' : 'L'} ${point[0]},${point[1]}`).join(' '),
);
}
function PolylineSVG({
polyline,
options,
}: {
polyline: __esri.Polyline;
options: SvgPreviewOptions;
}): JSX.Element {
const {
rotation = 0,
size = 100,
padding = 10,
bgColor = '#f0f0f0',
lineColor = '#000000',
lineWidth = 2,
} = options;
const normalizedPoints = normalizePoints(polyline, size, padding);
const pathData = generatePathData(normalizedPoints);
return (
<svg
width={size}
height={size}
viewBox={`0 0 ${size} ${size}`}
xmlns="http://www.w3.org/2000/svg"
style={{
transform: `rotate(${rotation}deg)`,
}}
>
<circle cx={size / 2} cy={size / 2} r={size / 2} fill={bgColor} />
{pathData.map((path, index) => (
<path
key={index}
d={path}
stroke={lineColor}
strokeWidth={lineWidth}
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
/>
))}
</svg>
);
}
export function MapGraphicPolylinePreviewSVG({
mapView,
graphic,
options,
}: {
mapView: __esri.MapView;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
graphic: LineGraphic<any>;
options: SvgPreviewOptions;
}): JSX.Element {
const [mapRotation] = useArcState(mapView, 'rotation');
const [geometry] = useArcState(graphic, 'geometry');
const [debouncedGeometry] = useDebouncedValue(geometry, 50);
const projectedPolyline = React.useMemo(
() => projectOperator.execute(debouncedGeometry, mapView.spatialReference) as __esri.Polyline,
[debouncedGeometry, mapView.spatialReference],
);
return (
<PolylineSVG
polyline={projectedPolyline ?? debouncedGeometry}
options={{ ...options, rotation: mapRotation }}
/>
);
}
Hi @JonathanDawe_BAS thanks for the feedback. Yes, the correct approach is to use the densifyOperator to remove curves from the output geometries at version 4.32+. There's also a code snippet in the densifyOperator API reference:
// Returns a polyline where all curve segments have been densified.
if (polyline.curvePaths) {
const densified = densifyOperator.execute(polyline, 0, { maxAngleInDegrees: 1 });
}
We'll add more clarification to the minimumBoundingCircleOperator documentation that it returns a polygon with curves, even when using input geometries that don't have curves.
Thanks Andy, that makes sense I missed that code snippet! I'll update my logic in this case to densify by angle, rather than using a max segment length as this makes more sense for my use case.
My other feedback would be that the documentation for the densify operator is really hard to read. A lot of the sentences in the overview are really hard to parse and could be rewritten in more plain english.
For example:
When the maxAngleInDegrees is not zero, vertices are introduced at points on the curve where the included angle between tangents at those point is maxAngleInDegrees. The new vertices are connected with line segments.
If maxSegmentLength is zero, the operation only applies to curves and geometries that do not have curves will be passed through unchanged. The operation always starts from the highest point on segment, thus guaranteeing that geometries that equal segments are densified exactly the same.
Could become:
If maxAngleInDegrees is not zero, vertices are added at points along the curve where the angle between tangent segments reaches this value. These vertices are then connected by straight-line segments.
If maxSegmentLength is zero, only curves are affected. Other geometries remain unchanged. The process always starts from the highest point on a segment, so identical segments will be densified in the same way.
Cheers for the help!
Good catch on the wording, I'll blend your suggestions in. We agree that some of the wording could use "fine tuning". The first phase was to get all 60+ new operators into the API, now we are in the enhancement phase.