A Pen by Raul Jimenez Ortega on CodePen.
Created
July 2, 2025 09:20
-
-
Save hhkaos/03f533d415f4b939970265a302a12166 to your computer and use it in GitHub Desktop.
ArcGIS Developer Guide: Snap to roads
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- | |
To run this demo, you need to replace 'YOUR_ACCESS_TOKEN' with an access token from ArcGIS that has the correct privileges. | |
To get started, sign up for a free ArcGIS Location Platform account or a free trial of ArcGIS Online and create developer credentials. | |
https://developers.arcgis.com/documentation/security-and-authentication/get-started/ | |
--> | |
<html> | |
<head> | |
<meta charset="utf-8" /> | |
<meta | |
name="viewport" | |
content="initial-scale=1, maximum-scale=1, user-scalable=no" | |
/> | |
<title>ArcGIS Developer Guide: Snap to Roads</title> | |
<style> | |
html, | |
body, | |
#map { | |
padding: 0; | |
margin: 0; | |
height: 100%; | |
width: 100%; | |
} | |
.button-container { | |
position: absolute; | |
top: 15px; | |
right: 15px; | |
z-index: 1000; | |
background-color: #ffffff; | |
padding: 6px; | |
} | |
</style> | |
<link | |
rel="stylesheet" | |
href="https://cdn.jsdelivr.net/npm/[email protected]/ol.css" | |
type="text/css" | |
/> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/ol.js"></script> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/olms.js"></script> | |
<link | |
rel="stylesheet" | |
href="https://unpkg.com/[email protected]/src/ol-popup.css" | |
/> | |
<script src="https://unpkg.com/[email protected]/dist/ol-popup.js"></script> | |
<script | |
type="module" | |
src="https://js.arcgis.com/calcite-components/3.2.0/calcite.esm.js" | |
></script> | |
</head> | |
<body> | |
<div class="button-container"> | |
<calcite-button id="snapPointsButton">Snap GPS points</calcite-button> | |
<calcite-button | |
id="resetButton" | |
icon-start="refresh" | |
label="Reset application" | |
></calcite-button> | |
</div> | |
<div id="map"></div> | |
<script type="module"> | |
/* Use for API key authentication */ | |
const accessToken = "YOUR_ACCESS_TOKEN" | |
const snapToRoadsServiceURL = | |
"https://route-api.arcgis.com/arcgis/rest/services/World/SnapToRoadsSync/GPServer/SnapToRoads/execute" | |
const trackPointsURL = `https://services3.arcgis.com/GVgbJbqm8hXASVYi/arcgis/rest/services/EsriToRanchoCostcoEdit/FeatureServer/0` | |
const trackRouteURL = `https://services3.arcgis.com/GVgbJbqm8hXASVYi/arcgis/rest/services/ActualRouteLine/FeatureServer/0` | |
const basemapId = "arcgis/light-gray" | |
const basemapURL = `https://basemapstyles-api.arcgis.com/arcgis/rest/services/styles/v2/styles/${basemapId}?token=${accessToken}` | |
/** | |
* Returns a new layer created from a service | |
*/ | |
const getVectorLayerFromFeatureService = (url, style) => { | |
const queryParams = new URLSearchParams({ | |
where: "1=1", | |
outFields: "*", | |
returnGeometry: true, | |
f: "geojson", | |
token: accessToken, | |
}) | |
const layerURL = `${url}/query?${queryParams.toString()}` | |
const layerSource = new ol.source.Vector({ | |
format: new ol.format.GeoJSON(), | |
url: layerURL, | |
}) | |
return new ol.layer.Vector({ source: layerSource, style }) | |
} | |
/** | |
* Returns a new layer created from GeoJSON | |
*/ | |
const getVectorLayer = (features, style) => { | |
const layerSource = new ol.source.Vector({ | |
features, | |
}) | |
const layer = new ol.layer.Vector({ | |
source: layerSource, | |
style, | |
}) | |
return layer | |
} | |
/** | |
* Update attribution | |
*/ | |
const addAttribution = () => { | |
// Add Esri attribution | |
// Learn more in https://esriurl.com/attribution | |
const source = map | |
.getLayers() | |
.item(0) | |
.getSource() | |
const poweredByEsriString = | |
"Powered by <a href='https://www.esri.com/en-us/home' target='_blank'>Esri</a> | " | |
const attributionFn = source.getAttributions() | |
if (attributionFn) { | |
source.setAttributions(ViewStateLayerStateExtent => { | |
return [ | |
poweredByEsriString, | |
...attributionFn(ViewStateLayerStateExtent), | |
] | |
}) | |
} else source.setAttributions(poweredByEsriString) | |
} | |
/** | |
* Send request to the snap to roads service | |
*/ | |
const snapPointsButtonWasClicked = async () => { | |
document.getElementById("snapPointsButton").loading = true | |
// remove any previous results | |
snappedRouteLayer.getSource().clear(true) | |
snappedPointsLayer.getSource().clear(true) | |
// get features | |
const sourceFeatures = trackPointsLayer.getSource().getFeatures() | |
// convert input features to Esri format | |
const esriFormat = new ol.format.EsriJSON() | |
const esriFeatures = esriFormat.writeFeatures(sourceFeatures, { | |
dataProjection: "EPSG:3857", | |
}) | |
// add authorization | |
// const headerParams = { | |
// Authorization: `Bearer ${accessToken}`, | |
// } | |
// Service parameters | |
// https://developers.arcgis.com/rest/services-reference/enterprise/snap-to-roads/#required-parameters | |
const returnRoadProperties = JSON.stringify([ | |
"posted_speed_limit_mph", | |
"length_miles", | |
]) | |
const queryParams = new URLSearchParams({ | |
f: "json", | |
token: accessToken, | |
points: esriFeatures, | |
return_lines: true, | |
return_location_fields: true, | |
road_properties_on_snapped_points: returnRoadProperties, | |
road_properties_on_lines: returnRoadProperties, | |
context: JSON.stringify({ outSR: { wkid: 3857 } }), // request returned features in the maps spatial reference | |
}) | |
// send the request | |
const srvcResponse = await fetch(snapToRoadsServiceURL, { | |
method: "POST", | |
// headers: headerParams, | |
body: queryParams, | |
}) | |
// get the response | |
const esriResults = await srvcResponse.json() | |
// convert results from Esri format to openlayers Features | |
const snappedPoints = esriFormat.readFeatures( | |
esriResults.results[0].value, | |
{ dataProjection: "EPSG:3857" } | |
) | |
const snappedLines = esriFormat.readFeatures( | |
esriResults.results[1].value, | |
{ dataProjection: "EPSG:3857" } | |
) | |
// add results to our sources | |
snappedPointsLayer.getSource().addFeatures(snappedPoints) | |
snappedRouteLayer.getSource().addFeatures(snappedLines) | |
document.getElementById("snapPointsButton").loading = false | |
} | |
const resetButtonWasClicked = async () => { | |
// remove any previous results | |
snappedRouteLayer.getSource().clear(true) | |
snappedPointsLayer.getSource().clear(true) | |
} | |
// Instantiate a new map object | |
const popup = new Popup() | |
const map = new ol.Map({ target: "map", overlays: [popup] }) | |
// Set the maps initial center and scale level | |
map.setView( | |
new ol.View({ | |
center: ol.proj.fromLonLat([-117.32366341353183, 34.06637751117044]), | |
zoom: 18, | |
}) | |
) | |
/** | |
* use plugin to add our basemap style to the map | |
*/ | |
await olms.apply(map, basemapURL) | |
addAttribution() | |
const trackRouteLayer = getVectorLayerFromFeatureService(trackRouteURL, { | |
"stroke-color": "rgba(0, 0, 0, 0.5)", | |
"stroke-width": 1, | |
"stroke-line-dash": [6, 6, 6, 6], | |
}) | |
map.addLayer(trackRouteLayer) | |
const trackPointsLayer = getVectorLayerFromFeatureService( | |
trackPointsURL, | |
{ | |
"circle-radius": 6, | |
"circle-fill-color": "rgba(0, 122, 194, 0.75)", | |
"circle-stroke-color": "#ffffff", | |
"circle-stroke-width": 2, | |
} | |
) | |
map.addLayer(trackPointsLayer) | |
const snappedRouteLayer = getVectorLayer([], { | |
"stroke-color": "rgba(0, 122, 194, 0.8)", | |
"stroke-width": 4, | |
}) | |
map.addLayer(snappedRouteLayer) | |
const snappedPointsLayer = getVectorLayer([], { | |
"circle-radius": 4, | |
"circle-fill-color": "rgba(0, 0, 0, 0.8)", | |
"circle-stroke-color": "#ffffff", | |
"circle-stroke-width": 1, | |
}) | |
map.addLayer(snappedPointsLayer) | |
const getPopupContent = forProps => { | |
const t = document.createElement("calcite-table") | |
t.striped = true | |
t.bordered = true | |
const hr = document.createElement("calcite-table-row") | |
hr.slot = "table-header" | |
t.appendChild(hr) | |
const nameth = document.createElement("calcite-table-header") | |
nameth.heading = "Name" | |
const valueth = document.createElement("calcite-table-header") | |
valueth.heading = "Value" | |
hr.appendChild(nameth) | |
hr.appendChild(valueth) | |
Object.keys(forProps).forEach(p => { | |
const r = document.createElement("calcite-table-row") | |
const nc = document.createElement("calcite-table-cell") | |
const vc = document.createElement("calcite-table-cell") | |
let fields = ["ObjectID", "posted_speed_limit_mph", "length_miles"] | |
if (p.geometry.getType() === "Point") { | |
fields = ["confidence", "line_id", ...fields] | |
} else { | |
fields = ["line_type", ...fields] | |
} | |
}) | |
return t | |
} | |
// give some feedback if we are over a snapped route feature | |
map.on("pointermove", e => { | |
const featureHit = map.hasFeatureAtPixel(e.pixel, { | |
layerFilter: lyr => | |
lyr === snappedRouteLayer || lyr === snappedPointsLayer, | |
}) | |
map.getTargetElement().style.cursor = featureHit ? "pointer" : "" | |
}) | |
// show feature properties | |
map.on("singleclick", async e => { | |
map.forEachFeatureAtPixel( | |
e.pixel, | |
(f, lyr) => { | |
console.log(lyr) | |
}, | |
{ | |
layerFilter: lyr => | |
lyr === snappedRouteLayer || lyr === snappedPointsLayer, | |
} | |
) | |
// if (tl && tl.length > 0) { | |
// const props = getPopupContent(tl[0].getProperties()) | |
// popup.show(e.coordinate, props) | |
// return | |
// } | |
// popup.hide() | |
}) | |
// handle button click events | |
document | |
.getElementById("snapPointsButton") | |
.addEventListener("click", snapPointsButtonWasClicked) | |
document | |
.getElementById("resetButton") | |
.addEventListener("click", resetButtonWasClicked) | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment