< 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 ,