<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<title>Geometry operator - offset analysis | Sample | ArcGIS Maps SDK for JavaScript</title>
<!-- Load the ArcGIS Maps SDK for JavaScript from CDN -->
<script type="module" src="https://js.arcgis.com/5.0/"></script>
<calcite-shell content-behind>
<arcgis-map basemap="topo-vector" zoom="5" center="24, 28"></arcgis-map>
<calcite-shell-panel slot="panel-end" display-mode="float">
<calcite-block expanded id="parametersPanel" heading="Parameters">
<span>Offset distance:</span>
<span id="offsetLabel"></span>
NOTE: for purposes of offset, <code>Polygons</code> are always "oriented"-
counterclockwise rings are reversed to produce uniformly clockwise rings. This means a
negative offset will <b>expand</b> <code>Polygons</code> whereas a positive offset
will <b>contract</b> them.
<calcite-radio-button-group id="offsetJoins" layout="vertical">
<calcite-label layout="inline">
<calcite-radio-button value="miter" checked="true"></calcite-radio-button>
style="--calcite-tooltip-border-color: black"
reference-element="miterLimitSlider">
miter limit (in units of offset distance)
<calcite-label layout="inline">
<calcite-radio-button value="bevel"></calcite-radio-button>
<calcite-label layout="inline">
<calcite-radio-button value="round"></calcite-radio-button>
<calcite-label layout="inline">
<calcite-radio-button value="square"></calcite-radio-button>
</calcite-radio-button-group>
id="joinTypeDemoAngleSlider"
style="--calcite-tooltip-border-color: black"
reference-element="joinTypeDemoAngleSlider">
Measured as the angle between
the tangent vectors of the segments
before and after the vertex
<span id="joinTypeExplanation"></span>
] = await $arcgis.import([
"@arcgis/core/layers/FeatureLayer.js",
"@arcgis/core/Graphic.js",
"@arcgis/core/geometry/Polygon.js",
"@arcgis/core/geometry/Polyline.js",
"@arcgis/core/geometry/SpatialReference.js",
"@arcgis/core/layers/GraphicsLayer.js",
"@arcgis/core/symbols/CIMSymbol.js",
"@arcgis/core/geometry/operators/offsetOperator.js",
// Get a reference to the map component
const viewElement = document.querySelector("arcgis-map");
const tunisia = new Polygon({
spatialReference: SpatialReference.WebMercator,
const aeniad = new Polyline({
spatialReference: SpatialReference.WebMercator,
showOrientationWithArrows: new CIMSymbol({
type: "CIMSymbolReference",
type: "CIMMarkerPlacementAlongLineSameSize", // places same size markers along the line
placementTemplate: [19.5], // determines space between each arrow
angleToLine: true, // symbol will maintain its angle to the line when map is rotated
type: "CIMMarkerGraphic",
// black fill for the arrow symbol
type: "CIMPolygonSymbol",
// white dashed layer at center of the line
enable: true, // must be set to true in order for the symbol layer to be visible
dottedLinesForConstruction: new CIMSymbol({
type: "CIMSymbolReference",
type: "CIMGeometricEffectDashes",
lineDashEnding: "NoConstraint",
color: [200, 115, 0, 255],
const makeFeatureLayer = (geometry, symbol) => {
return new FeatureLayer({
objectIdField: "ObjectID",
const [tunisiaOffsetGraphic, aeniadOffsetGraphic, joinTypeDemoOffsetGraphic] = [
const tunisaFeatureLayer = makeFeatureLayer(tunisia, symbols.showOrientationWithArrows);
const aeniadFeatureLayer = makeFeatureLayer(aeniad, symbols.showOrientationWithArrows);
const joinTypeDemoLayer = makeFeatureLayer(new Polyline(), symbols.showOrientationWithArrows);
const joinTypeConstructionLayer = makeFeatureLayer(
symbols.dottedLinesForConstruction,
const graphicsLayer = new GraphicsLayer({
graphics: [tunisiaOffsetGraphic, aeniadOffsetGraphic, joinTypeDemoOffsetGraphic],
const setup = async () => {
const offsetSlider = document.getElementById("offsetSlider");
const offsetLabel = document.getElementById("offsetLabel");
const offsetJoins = document.getElementById("offsetJoins");
const miterLimitSlider = document.getElementById("miterLimitSlider");
const joinTypeDemoAngleSlider = document.getElementById("joinTypeDemoAngleSlider");
const joinTypeExplanationLabel = document.getElementById("joinTypeExplanation");
offsetSlider.addEventListener("calciteSliderInput", onChange);
miterLimitSlider.addEventListener("calciteSliderInput", onChange);
offsetJoins.addEventListener("calciteRadioButtonGroupChange", onChange);
joinTypeDemoAngleSlider.addEventListener("calciteSliderInput", onChange);
await updateJoinTypeDemo(90, 0);
async function onChange() {
const offset = offsetSlider.value;
const joins = offsetJoins.selectedItem.value;
miterLimitSlider.disabled = joins !== "miter";
const miterLimit = miterLimitSlider.value;
const joinTypeDemoAngle = joinTypeDemoAngleSlider.value;
const joinIsInner = joinTypeDemoAngle > 0 !== offset >= 0;
await updateJoinTypeDemo(joinTypeDemoAngle, offset, joins === "square", joinIsInner);
for (const [offsetGraphic, geometry] of [
[tunisiaOffsetGraphic, tunisia],
[aeniadOffsetGraphic, aeniad],
[joinTypeDemoOffsetGraphic, joinTypeDemoLayer.source.at(0).geometry],
offsetGraphic.symbol.color = offset > 0 ? [0, 0, 200, 255] : [0, 180, 0, 255];
offsetGraphic.geometry = offsetOperator.execute(geometry, offset, {
const sign = offset > 0 ? "positive" : "negative";
const dir = offset > 0 ? "RIGHT" : "LEFT";
const op = offset > 0 ? "CONTRACT" : "EXPAND";
offsetLabel.textContent = `(${sign}; ${dir} side of directed path; ${op}S polygons)`;
joinTypeExplanationLabel.innerHTML = joinIsInner
? "Inner corners are always mitered."
: joinTypeExplanations[joins];
const parallelLinesExplanation = `
A line is constructed parallel to each segment, separated from the
segment by the specified <code>offset</code>.
const joinTypeExplanations = {
${parallelLinesExplanation}
The intersection of these lines is the mitered vertex.
If the distance between the mitered vertex and the original vertex
is greater than <code>offset * miterLimit</code>, the mitered vertex
is replaced with a bevel.
${parallelLinesExplanation}
The original vertex is projected onto each of these lines and the
resulting points are used as the beginning and end of a line segment
${parallelLinesExplanation}
The original vertex is projected onto each of these lines and the
resulting points are used as the beginning and end of a circular arc
centered on the original vertex.
${parallelLinesExplanation}
The original vertex is projected onto each of these lines at a
45degree angle instead of at 90degrees as in <code>bevel</code>. The
resulting points are used as the beginning and end of a line segment.
If the resulting corner is not acute, a miter will be used instead.
async function updateJoinTypeDemo(angle, offset, isSquare, joinIsInner = false) {
angle = (angle / 180) * Math.PI;
const vertex = [2e6, 3e6];
function rotate([x, y]) {
const relx = x - vertex[0];
const rely = y - vertex[1];
vertex[0] + relx * Math.cos(angle) + rely * Math.sin(angle),
vertex[1] + relx * Math.sin(angle) - rely * Math.cos(angle),
function translate([x, y], [dx, dy]) {
const pre = translate(vertex, [0, -1e6]);
const post = rotate(pre);
const offsetPre = translate(pre, [offset, 0]);
const offsetPost = rotate(offsetPre);
const bevelStart = translate(vertex, [offset, 0]);
const bevelEnd = rotate(bevelStart);
const squareStart = translate(vertex, [
Math.abs(offset) * Math.sign(angle),
offset * Math.sign(angle),
const squareEnd = rotate(squareStart);
const extendedPre = translate(bevelStart, [0, 1e6]);
const extendedPost = rotate(extendedPre);
translate(vertex, [-d * Math.sign(offset), 0]),
translate(vertex, [-d * Math.sign(offset), d]),
translate(vertex, [0, d]),
graphic = joinTypeDemoLayer.source.at(0);
graphic.geometry = new Polyline({
paths: [[pre, vertex, post]],
spatialReference: SpatialReference.WebMercator,
joinTypeDemoLayer.applyEdits({ updateFeatures: [graphic] });
const extraConstructionPaths = [
[bevelStart, vertex, bevelEnd],
rightAnglePre.map(rotate),
isSquare && Math.abs(angle) > Math.PI / 2 ? [squareStart, vertex, squareEnd] : [],
graphic = joinTypeConstructionLayer.source.at(0);
graphic.geometry = new Polyline({
[offsetPre, extendedPre],
[extendedPost, offsetPost],
...(joinIsInner ? [] : extraConstructionPaths),
spatialReference: SpatialReference.WebMercator,
joinTypeConstructionLayer.applyEdits({ updateFeatures: [graphic] });
// Listen for when the view is ready
// then start adding layers and setting up the app
await viewElement.viewOnReady();
viewElement.map.addMany([
joinTypeConstructionLayer,