Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save hhkaos/3d560cd5f7e50c36db4c56ddd7d88b6f to your computer and use it in GitHub Desktop.
Save hhkaos/3d560cd5f7e50c36db4c56ddd7d88b6f to your computer and use it in GitHub Desktop.
ArcGIS Developer Guide: Snap GPX track points to roads
<!--
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 lang="en">
<head>
<meta charset="utf-8" />
<title>Upload GPX and Snap to Roads</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- ArcGIS Maps SDK for JavaScript -->
<link
rel="stylesheet"
href="https://js.arcgis.com/4.32/esri/themes/light/main.css"
/>
<script src="https://js.arcgis.com/4.32/"></script>
<!-- Calcite Components -->
<script
type="module"
src="https://js.arcgis.com/calcite-components/3.1.0/calcite.esm.js"
></script>
<link
rel="stylesheet"
href="https://js.arcgis.com/calcite-components/3.1.0/calcite.css"
/>
<style>
html,
body,
#viewDiv {
height: 100%;
width: 100%;
margin: 0;
padding: 0;
}
#uiPanel {
z-index: 10;
background: white;
padding: 1rem;
border-radius: 0px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
width: 280px;
}
#fileInput {
display: none;
}
</style>
</head>
<body>
<div id="viewDiv"></div>
<div id="uiPanel" class="esri-widget">
<calcite-button
id="downloadSampleBtn"
icon-start="download"
appearance="outline"
width="full"
style="margin-bottom: 0.5rem;"
>Download sample GPX File</calcite-button
>
<calcite-label>
<calcite-button
id="uploadBtn"
icon-start="upload"
appearance="solid"
width="full"
>Upload GPX File</calcite-button
>
<input id="fileInput" type="file" accept=".gpx" />
</calcite-label>
<calcite-button
id="snapBtn"
icon-start="pin-plus"
appearance="outline"
disabled
width="full"
>Snap points</calcite-button
>
<calcite-button
id="resetBtn"
icon-start="reset"
appearance="outline"
width="full"
style="margin-top: 0.5rem;"
>Reset</calcite-button
>
<div id="layerSwitches" style="display:none; margin-top:1rem;">
<calcite-label layout="inline">
<calcite-switch id="rawPointsSwitch" checked></calcite-switch>
GPS Points
</calcite-label>
<calcite-label layout="inline">
<calcite-switch id="snappedPointsSwitch" checked></calcite-switch>
Snapped Points
</calcite-label>
<calcite-label layout="inline">
<calcite-switch id="snappedRouteSwitch" checked></calcite-switch>
Snapped Route
</calcite-label>
</div>
</div>
<script type="module">
// Import ArcGIS modules
const [
EsriConfig,
EsriMap,
EsriMapView,
EsriGeoprocessor,
EsriGraphicsLayer,
EsriGraphic,
EsriPoint,
EsriPolyline,
EsriFeatureLayer,
] = await $arcgis.import([
"@arcgis/core/config.js",
"@arcgis/core/Map",
"@arcgis/core/views/MapView",
"@arcgis/core/rest/geoprocessor",
"@arcgis/core/layers/GraphicsLayer",
"@arcgis/core/Graphic",
"@arcgis/core/geometry/Point",
"@arcgis/core/geometry/Polyline",
"@arcgis/core/layers/FeatureLayer",
])
// --- Constants ---
const apiKey = "YOUR_ACCESS_TOKEN"
EsriConfig.apiKey = apiKey
const serviceURLs = {
snapToRoads:
"https://route-api.arcgis.com/arcgis/rest/services/World/SnapToRoadsSync/GPServer/SnapToRoads",
travelModes:
"https://route-api.arcgis.com/arcgis/rest/services/World/Utilities/GPServer/GetTravelModes",
}
const US_CENTER = [-98.5795, 39.8283]
const US_ZOOM = 3
// --- Map and Layers ---
const map = new EsriMap({ basemap: "streets-navigation-vector" })
const view = new EsriMapView({
container: "viewDiv",
map,
center: US_CENTER,
zoom: US_ZOOM,
})
const rawPointsLayer = new EsriFeatureLayer({
title: "Raw GPX Points",
source: [],
fields: [
{ name: "ObjectID", type: "oid" },
{ name: "lat", type: "double" },
{ name: "lon", type: "double" },
],
objectIdField: "ObjectID",
geometryType: "point",
spatialReference: { wkid: 4326 },
renderer: {
type: "simple",
symbol: {
type: "simple-marker",
color: "#007ac2",
size: 6,
},
},
})
const snappedPointsLayer = new EsriGraphicsLayer({
title: "Snapped Points",
})
const snappedRouteLayer = new EsriGraphicsLayer({
title: "Snapped Route",
})
map.addMany([rawPointsLayer, snappedRouteLayer, snappedPointsLayer])
// --- UI Elements ---
const uploadBtn = document.getElementById("uploadBtn")
const fileInput = document.getElementById("fileInput")
const snapBtn = document.getElementById("snapBtn")
const resetBtn = document.getElementById("resetBtn")
const downloadSampleBtn = document.getElementById("downloadSampleBtn")
const layerSwitches = document.getElementById("layerSwitches")
const rawPointsSwitch = document.getElementById("rawPointsSwitch")
const snappedPointsSwitch = document.getElementById("snappedPointsSwitch")
const snappedRouteSwitch = document.getElementById("snappedRouteSwitch")
// --- State ---
let gpxPoints = []
let travelMode = null
// --- Event Handlers ---
// Upload button triggers file input
uploadBtn.addEventListener("click", () => fileInput.click())
// Handle GPX file selection
fileInput.addEventListener("change", async e => {
const file = e.target.files[0]
if (!file) return
const text = await file.text()
gpxPoints = parseGPX(text)
if (gpxPoints.length === 0) {
alert("No track points found in GPX file.")
return
}
await clearAllLayers()
// Add raw points to map as features
const features = gpxPoints.map((pt, i) => ({
geometry: { type: "point", longitude: pt.lon, latitude: pt.lat },
attributes: { ObjectID: i + 1, lat: pt.lat, lon: pt.lon },
}))
await rawPointsLayer.applyEdits({ addFeatures: features })
// Zoom to the extent of rawPointsLayer
const extent = await rawPointsLayer.queryExtent()
if (extent && extent.extent) {
view.goTo(extent.extent)
rawPointsLayer.visible = true
}
snapBtn.disabled = false
})
// Snap to Roads button
snapBtn.addEventListener("click", async () => {
if (gpxPoints.length === 0) return
snapBtn.loading = true
snappedPointsLayer.removeAll()
snappedRouteLayer.removeAll()
// Query the raw points layer for features
const features = await rawPointsLayer.queryFeatures()
// Create a feature set
const params = {
points: features,
token: apiKey,
travel_mode: travelMode,
return_lines: true,
return_location_fields: true,
road_properties_on_snapped_points: [
"posted_speed_limit_mph",
"length_miles",
],
road_properties_on_lines: ["posted_speed_limit_mph", "length_miles"],
}
try {
// Execute the geoprocessing service
const result = await EsriGeoprocessor.execute(
serviceURLs.snapToRoads,
params
)
// Get the snapped points
const snappedPoints = result.results[0].value.features
if (snappedPoints.length === 0) {
alert("No roads found for the provided points.")
return
}
const snappedGraphics = snappedPoints.map(
f =>
new EsriGraphic({
geometry: new EsriPoint({
longitude: f.geometry.x,
latitude: f.geometry.y,
}),
symbol: { type: "simple-marker", color: "black", size: 6 },
})
)
snappedPointsLayer.addMany(snappedGraphics)
// Get the snapped route lines
const snappedLines = result.results[1].value.features
if (snappedLines.length === 0) {
alert("No route found for the provided points.")
return
}
const snappedRouteGraphics = snappedLines.map(
f =>
new EsriGraphic({
geometry: f.geometry,
symbol: {
type: "simple-line",
color: [0, 150, 255, 0.75],
width: 3,
},
})
)
snappedRouteLayer.addMany(snappedRouteGraphics)
// Zoom to snapped route
if (snappedGraphics.length > 0) view.goTo(snappedGraphics)
showHideLayerSwitchesAndLayers(true)
} catch (err) {
alert("Snap to Roads failed: " + err.message)
} finally {
snapBtn.loading = false
}
})
// Download sample GPX file button
downloadSampleBtn.addEventListener("click", () => {
const link = document.createElement("a")
link.href = "https://developers.arcgis.com/documentation//data/RedlandsRoute.gpx"
link.download = "sample.gpx"
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
})
// Layer switch handlers
rawPointsSwitch.addEventListener("calciteSwitchChange", e => {
rawPointsLayer.visible = e.target.checked
})
snappedPointsSwitch.addEventListener("calciteSwitchChange", e => {
snappedPointsLayer.visible = e.target.checked
})
snappedRouteSwitch.addEventListener("calciteSwitchChange", e => {
snappedRouteLayer.visible = e.target.checked
})
// Reset button
resetBtn.addEventListener("click", async () => {
// Clear all layers and reset state
await clearAllLayers()
gpxPoints = []
snapBtn.disabled = true
fileInput.value = ""
showHideLayerSwitchesAndLayers(false)
view.goTo({ center: US_CENTER, zoom: US_ZOOM })
})
// --- Helper Functions ---
// Remove all features from all layers
async function clearAllLayers() {
await removeFeatures(rawPointsLayer)
;[snappedPointsLayer, snappedRouteLayer].forEach(layer =>
layer.removeAll()
)
}
// Remove all features from a FeatureLayer
async function removeFeatures(layer) {
const { features } = await layer.queryFeatures()
if (features?.length) {
await layer.applyEdits({ deleteFeatures: features })
}
}
// Parse GPX file for <trkpt> or <wpt> elements
function parseGPX(xmlText) {
const parser = new DOMParser()
const xml = parser.parseFromString(xmlText, "application/xml")
let points = Array.from(xml.getElementsByTagName("trkpt"))
if (!points.length) points = Array.from(xml.getElementsByTagName("wpt"))
if (!points.length) {
console.error("No track points or waypoints found in GPX file.")
return []
}
return points
.map(pt => ({
lat: parseFloat(pt.getAttribute("lat")),
lon: parseFloat(pt.getAttribute("lon")),
}))
.filter(pt => !isNaN(pt.lat) && !isNaN(pt.lon))
}
// Get travel mode by name
async function getTravelModes(modeName) {
const params = { token: apiKey, f: "json" }
const response = await EsriGeoprocessor.execute(
serviceURLs.travelModes,
params
)
let mode = response.results[0].value.features.find(
f => f.attributes.Name === modeName
)
if (!mode) {
console.error(`Travel mode '${modeName}' not found`)
mode = response.results[0].value.features[0]
}
return mode
}
// Show all layer switches and make all layers visible
const showHideLayerSwitchesAndLayers = show => {
layerSwitches.style.display = show ? "block" : "none"
rawPointsSwitch.checked = show
snappedPointsSwitch.checked = show
snappedRouteSwitch.checked = show
rawPointsLayer.visible = show
snappedPointsLayer.visible = show
snappedRouteLayer.visible = show
}
// Map load event handler
view.when(async () => {
view.ui.add(document.getElementById("uiPanel"), "top-right")
const mode = await getTravelModes("Driving Time")
travelMode = mode.attributes.TravelMode
})
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment