TopoScout is a browser-based terrain analysis tool for finding high points, comparing climbs, visualizing slope, and overlaying GPX routes directly on the map. The app runs fully client-side, so terrain analysis happens in the browser without a custom backend. Its designed to work on mobile devices as well, and can be installed as an app.
🌐 Open the Live demo with GPX store.
- Live center elevation for the current map position.
- Find Highest Points within a configurable search radius.
- Find Climbs by scanning many directions and ranking routes by cumulative ascent.
- Slope Map overlay with opacity and slope-angle filtering.
- GPX route overlay with customizable styling and route stats.
- Points of Interest (POIs) saved to your Google account, with a custom name, description, and color.
- Map tools for overzoom, tilt, and 3D terrain exaggeration.
- Share Map View links that restore language, center, zoom, and selected layer.
- Multiple map sources including topographic, satellite, national, and debug elevation layers.
- PWA install support for desktop and mobile.
- English and Swedish localization.
TopoScout focuses on terrain discovery rather than just displaying a single height sample.
- Highest-point scanning ranks the tallest candidates inside the current search radius.
- Climb analysis estimates the strongest uphill routes by summing positive elevation changes over a chosen distance.
- Slope visualization renders a color-coded raster overlay that highlights shallow terrain, steep hillsides, and very steep ground.
- Water filtering can exclude water-colored areas from analysis to reduce false positives.
The built-in GPX overlay lets you add route context while inspecting the terrain.
- Load a local
.gpxfile directly in the browser. - Customize track color and line width.
- Toggle distance labels in kilometers or miles.
- Color the route by slope.
- Show waypoints and min/max elevation markers.
- View route summary stats including distance, elevation gain/loss, and min/max elevation.
- Open an elevation profile bar for the loaded route: hover or drag to scrub along the track, scroll to zoom the profile, and use the arrow keys to step (hold
Shiftfor larger steps). - Enable Sync Map with Profile to pan the map to a blue marker that follows the profile cursor.
- Download the loaded route back to a
.gpxfile (saved under its current name). - Optionally upload, list, share, rename, and delete GPX routes when the optional backend is running.
- To acess your GPX file you log i with your Google account
Save your own marked spots and keep them on every device.
- Sign in with Google, then tap Add POI and tap the map to drop a pin.
- Give each POI a name, a description (URLs become clickable links), and a color.
- POI pins use a star marker tinted with the chosen color, show the point's elevation, and include a copy-coordinates button.
- Open a POI from the list to recenter the map on it, or move, rename, edit, and delete it.
- POIs are stored per Google account through the optional backend and load automatically wherever you're signed in.
- Toggle all POI pins on or off with the Show POIs checkbox.
- Search by place name or coordinates.
- Jump to your current position with the GPS button.
- Rotate the map with
Ctrl+ drag on desktop or two-finger rotation on touch devices. - Reset north using the compass control.
- Toggle 3D terrain with the 3D button next to the search box.
- Enable overzoom, tilt, and 3D exaggeration from Advanced settings in the About menu.
- Switch between multiple map layers without leaving the current map state.
Built-in layers include:
- OpenTopoMap
- Tracetrack Topo
- ThunderForest Outdoors
- Lantmateriet (Sweden)
- Norgeskart (Norway)
- OpenStreetMap
- Satellite (ESRI)
- Elevation Data (debug view)
Some third-party layers require an API key. When needed, the app prompts for the key and stores it locally in the browser.
Elevation analysis uses Terrarium-format DEM tiles from Mapterhorn.
Optional overlays can be drawn on top of any base layer from the Route Overlay dropdown:
- Waymarked Trails — hiking, cycling, MTB, and skating route networks, with a "Routes in view" legend (click a route to isolate just that trail).
- Strava Global Heatmap — aggregated activity heatmap
- The app loads terrain raster tiles for the current viewport into an off-screen analysis surface.
- Pixel values are decoded with the Terrarium elevation formula:
(R * 256 + G + B / 256) - 32768. - The same viewport data can then be reused by the peak scan, climb scan, and slope renderer.
- Optional water analysis masks out likely water pixels before ranking terrain results.
- The visible analysis surface is sampled for candidate elevations.
- Only candidates inside the selected search radius are kept.
- Candidates are sorted by elevation.
- A minimum-distance filter removes near-duplicates so the result list stays geographically useful.
- The best matches are rendered as numbered markers with result popups.
- Candidate start points are sampled across the analysis surface.
- Multiple headings are tested from each start point.
- Each path is walked in small elevation steps.
- A smoothing pass reduces tile noise.
- The route is scored by cumulative positive ascent.
- The best climbs are drawn on the map with distance, slope, vertical drop, and elevation details.
- The app compares neighboring elevation samples to estimate slope angle.
- Each pixel is assigned a slope class color.
- The overlay can be clipped to the search radius or shown across the full visible viewport.
- Users can filter by minimum and maximum slope angle, then adjust overlay opacity.
- Pick a base layer from the layer selector.
- Search for a place or center the map on your current location.
- Adjust the search radius and decide whether to show or lock it.
- Click the 3D button next to search to turn on 3D terrain relief.
- Use Advanced settings (in the About menu) for Overzoom, Tilt, and 3D Exaggeration.
- Open Find Highest Points to rank peaks inside the active radius.
- Open Find Climbs to look for strong uphill routes over a fixed measurement distance.
- Open Generate Slope Map to paint the terrain by steepness.
- Expand Add Routes and POIs.
- Load a GPX file, or sign in and tap Add POI to drop a saved Point of Interest.
- Tune track styling and visibility options, and toggle pins with Show POIs.
- Compare routes and POIs against peak, climb, and slope results already on the map.
- Click the share button in the header to copy a map-state link.
- Install the app from the About dialog or the mobile install prompt when supported.
TopoScout is a Progressive Web App, so you can install it to your home screen or desktop for a full-screen, app-like experience. Once installed, the core app shell works offline.
- Open toposcout.org in Chrome.
- Tap the ⋮ menu → Install app (or Add to Home screen).
- You can also use the in-app install prompt, or the Install as App button in the About dialog.
- Open toposcout.org in Safari (installing isn't available in other iOS browsers).
- Tap the Share button.
- Scroll down and tap Add to Home Screen, then tap Add.
- Click the install icon in the address bar, or use the Install as App button in the About dialog.
- The app remembers language, map position, zoom, and selected layer in
localStorage. - Shared URLs restore the current language and map state.
- API keys are stored locally in the browser.
- Points of Interest are saved per Google account on the optional backend, so they sync across devices.
- No terrain analysis results are uploaded to a project server.
The frontend works fully on static hosting (GitHub Pages and the live demo) with no backend. An optional FastAPI backend adds GPX upload, a per-browser upload history, shareable ?gpx=<id> links, and saved Points of Interest.
The frontend auto-detects the backend by probing /api/health on load. When it is reachable, the Load GPX Route button opens an upload/history modal and share links include the uploaded route. When it is not reachable, the same button opens the local file picker directly — no upload UI, no errors, and any ?gpx= parameter is stripped silently.
Saved Points of Interest also require the backend: each POI is tied to your Google account through the /api/pois endpoints, so signing in shows your pins on any device. Without the backend, the Add POI flow reports that POIs need the online backend.
Run it locally:
pip install -r requirements.txt
uvicorn main:app --host 0.0.0.0 --port 8000Then open http://localhost:8000/. The backend serves the static files and stores uploads under gpx-files/ (configurable via GPX_UPLOAD_DIR).
Or with Docker:
docker build -t toposcout .
docker run -p 8000:8000 -v "$(pwd)/gpx-files:/app/gpx-files" toposcout- The app can be installed on mobile and desktop.
- A service worker caches the core app shell for faster repeat visits.
- When shipping a new release, bump both the displayed app version and the cache name so clients refresh cleanly.
index.html- application shell and modal markupscript.js- map adapter, terrain analysis, GPX overlay, elevation profile, localization, and app logicstyle.css- control panel, modal, and map stylingservice-worker.js- offline asset cachingmanifest.json- PWA metadatalang/en.js- English stringslang/sv.js- Swedish stringsmain.py- optional FastAPI backend for GPX upload/list/delete/share/renamerequirements.txt- Python dependencies for the optional backendDockerfile- container image for the optional backendgpx-files/- uploaded GPX storage (created at runtime; git-ignored)
- v2.10.0: Added an optional contour lines overlay, toggled by Enable contour line layer under Advanced settings (persisted as
topo_contours). Contours are generated client-side withmaplibre-contourfrom the same Mapterhorn terrarium DEM (tiles.mapterhorn.com,terrarium, maxzoom 15) the app already uses for terrain and hillshade — no extra backend or tile provider. Acontour-sourcevector source feeds a topographic-browncontour-lineslayer (thicker major contours, a low-zoom opacity fade) inserted directly above the basemap/hillshade but below every overlay, route and marker, so it reuses the same layer-ordering pattern as the hillshade. Elevation labels along the major contours are shown by a separatecontour-labelssymbol layer, toggled by Enable contour labels (persisted astopo_contour_labels, default on); rendering them required adding aglyphsfont source to the otherwise raster style, served from a self-hosted, same-origin glyph set bundled underfonts/(Noto Sans Regular, with Open Sans Regular also bundled to compare; precached by the service worker, so labels keep working offline with no third-party font CDN). The contour interval and the labels follow the global Metric/Imperial setting (metre intervals withmlabels, or feet intervals with'labels), regenerating viamap.refreshContours()when units change. The library is loaded from unpkg like MapLibre and degrades gracefully (the overlay simply no-ops) if it fails to load. - v2.9.0: Added a global Metric/Imperial units setting in the About modal (a dropdown directly below the language selector, persisted as
topo_units, migrating the legacy per-routetopo_distance_uniton first load). Metric stays the canonical internal unit; a newgetUnitSystem()drivesgetDistanceUnit(),formatDistance(), and a newformatElevation(), whilegetRadiusMeters()/getClimbDistMeters()/getClimbStepMeters()convert the numeric inputs at the boundary. Switching to Imperial shows distances in mi/ft and all elevations in ft everywhere — live center elevation, peak/climb popups, GPX gain/loss/min-max and the min/max markers, and the elevation-profile axes + readout — and converts the Search Radius (mi), Measure Dist. (ft) and Climb Step Res. (ft) input fields and their labels (with unit-appropriate min/max/step). The old per-route Distance Unit (km/mi) dropdown is removed in favor of this single global control. Also polished in this release: result popups raise theirmaxWidthso long (4-digit) values size the box to fit instead of crowding the right padding, and the Add-routes-and-POIs checkboxes are arranged in a 2-column grid (3 per side). - v2.8.2: Performance and fixes. The service worker now keeps a capped runtime cache (
toposcout-tiles-v1, ~400 tiles, stale-while-revalidate) for cross-origin map/elevation tiles, so revisited areas render instantly and the map keeps working offline; the cache is version-independent and preserved across releases (theactivatecleanup keeps both the shell cache and the tile cache). The render-blocking<script>tags (MapLibre, language files,script.js) are nowdeferred withpreconnect/dns-prefetchhints for the library CDN and the elevation-tile host, the per-framemap.on('move')UI work isrequestAnimationFrame-throttled, and the center/POI elevation lookups now share an LRU tile cache (loadElevationTile, ~64 tiles) instead of refetching a tile per call. Fixes: the popup copy-coordinates tooltip uses the active language instead of hardcoded Swedish, peak/climb popup distances honor the km/mi unit picker (via a sharedformatDistancehelper), the viewport meta no longer disables pinch-zoom (WCAG 1.4.4), the slope filter max is capped at 90°, and the[GPX auth]debug console logging (and its/api/auth/debugprobe) was removed. - v2.8.1: Added a Max tilt angle slider to Advanced settings (0–85°, persisted as
topo_max_pitch, default 60°). It sets the map'smaxPitchso manual pitch gestures can go beyond MapLibre's default 60° cap (up to its 85° hard limit), and the Tilt and 3D buttons now ease to the chosen angle instead of a fixed 60°. While 3D is enabled, dragging the slider re-tilts the view live; the value is clamped to MapLibre's 0–85° range. - v2.8.0: Added an optional hillshade relief layer. A Hillshade toggle button in the search bar (replacing the redundant GPS button there — GPS stays available via the on-map control) enables a MapLibre
hillshadelayer rendered from the existing Mapterhornraster-demsource (elevation-dem), inserted directly above the basemap and below every overlay and marker, so route overlays, climbs, GPX tracks, and POI/GPS markers are unaffected and the basemap stays beneath it across layer switches. An optional on-map opacity slider — shown by Enable Hillshade opacity slider under Advanced settings — adjusts the relief strength viahillshade-exaggeration(0–100%) live. The on/off state (topo_hillshade), slider visibility (topo_hillshade_slider), and strength (topo_hillshade_opacity) persist inlocalStorage, and the layer reuses the shared DEM source so 3D terrain keeps working alongside it. The 3D terrain exaggeration is now adjusted with the same kind of on-map slider (enabled via Enable 3D exaggeration slider, persisted astopo_3d_exaggeration), and the Advanced settings are sorted alphabetically. - v2.7.4: The UI now defaults to Swedish automatically when the browser/device language is Swedish (detected from
navigator.languages/navigator.language). Detection re-runs on every visit until the user picks a language manually from the menu, which sets atopo_lang_chosenflag inlocalStoragethat pins their choice. An explicit?lang=URL parameter still takes precedence over both. - v2.7.3: Added a dynamic accuracy ring around the live GPS marker. The shaded blue ring is sized to the reported margin of error (
pos.coords.accuracy): it shrinks as the fix tightens and disappears entirely for a pinpoint fix (accuracy of 5 m or better). The rendered radius is capped at 1 km so a coarse "Approximate Location" fix doesn't swamp the map. The ring reuses the existing meter-radius circle primitive and is removed when GPS tracking is toggled off. - v2.7.2: Points of Interest now persist on your device. The most recently synced POIs are cached in
localStorage, so their pins stay visible on the map after you sign out of Google or reload the page. Signing in re-syncs and overwrites the cache; creating, editing, moving, and deleting POIs still require a signed-in Google account through the backend (/api/pois). Also fixed the copy-coordinates button in popups, whose clipboard icon had been corrupted into stray text. - v2.7.1: Made the in-app Refresh app button and automatic updates refresh reliably on mobile browsers and the home-screen (PWA) app. The service worker now caches updated files with
cache: 'reload'so a new release never re-caches stale copies from the browser HTTP cache, and the local scripts/styles are version-stamped (?v=) so a refresh can no longer be served stale assets. The service worker matches requests withignoreSearchso the stamped URLs still resolve to their cached entries (offline still works). - v2.7.0: Added saved Points of Interest (POIs). Sign in with Google, then tap the map to drop a colored star pin and give it a name, a description (URLs become clickable links), and a color. POI pins show the point's elevation and a copy-coordinates button, and can be opened (recenters the map), moved, edited, or deleted. POIs are stored per Google account through the optional backend (
/api/pois) and load automatically on every device while you're signed in. - v2.6.2: Renamed the app to TopoScout.
- v2.6.1: Made the "new version available" update prompt far more reliable for the iOS home-screen (PWA) app. The app now re-checks for updates when it's reopened or brought back to the foreground (not only on a cold start), surfaces an update that finished downloading in a previous session (previously it could sit unprompted until the browser's automatic ~24h check), and registers the service worker with
updateViaCache: 'none'so the worker script is always fetched fresh. Also removed a stray reload on first launch and hardened the worker's message handler. - v2.6: Added a Strava Global Heatmap to the Route Overlay dropdown. Tiles are served privately through the optional backend (
/api/heatmap/...). - v2.5.1: Moved the language switcher from the header into the About menu as a Select Language dropdown and removed the flag icons. Placed the Install as App button beside Refresh app, and put the GitHub Project and droidgren.github.io links on one row.
- v2.5.0: The GPS button now toggles live positioning: it drops a moving marker that follows you in real time (tap again to stop). Added a center crosshair you can show/hide, with a selectable high-contrast color (Dark, White, Magenta, Cyan, Yellow, Red, Lime) under Advanced settings. The center dot now shows only when the search radius is locked, so it no longer overlaps the crosshair.
- v2.4.0: Added a Download GPX button (next to Clear Route) that saves the currently loaded route back to a
.gpxfile, and a Rename action for uploaded routes in the GPX upload history (renames the file on the optional backend). Also unified some secondary button colors. - v2.3.0: Redesigned the control icons: replaced all emoji and glyph icons with a crisp, consistent inline SVG icon set that highlights on hover, refreshed the Sweden/UK language flags, switched the collapsible sections and panel toggle to + / − icons, and gave the 3D toggle a clear active state.
- v2.2.0: Added an elevation profile bar for loaded GPX routes (hover/drag to scrub, scroll to zoom, arrow keys to step, with an optional "Sync Map with Profile" marker), and an optional FastAPI backend for uploading, listing, and sharing GPX routes by link. The frontend auto-detects the backend and stays fully functional on static hosting when none is present.
- v2.1.2: Misc GUI fixes: added an Advanced settings section and a 3D-terrain toggle button next to search, simplified the route overlay to a single dropdown (route names always shown, legend collapsed by default), and refined the panel layout, dropdowns, and tutorial.
- v2.1.1: Route-names legend now shows each route's symbol with a manual refresh button, and you can click a route to show only that trail ("Show all" to restore). Plus compass-placement, tutorial, and Find Climbs refinements.
- v2.1: Added a Waymarkedtrails route overlay (hiking, cycling, MTB, skating) and an optional route-names legend that lists the routes in the current view with their official route symbols.
- v2.0.2: Reworked the analysis section accordion so only one section stays open at a time, moved the Search Radius / Show Radius / Lock Radius controls into the active analysis section, and auto-enabled Show Radius when opening analysis sections.
- v2.0.1: Added Manual mode tutorial guidance (including a spotlight step), explained the difference between automatic and manual climb modes in the tutorial, and fixed manual-route ascent smoothing for multi-point routes.
- v2.0: Migrated frontend map rendering to MapLibre GL JS and added overzoom, tilt, 3D terrain, and shareable map views.
- v1.8.2: Added Norgeskart (Norway) map layer.
- v1.8.1: Added map rotation with
Ctrl+ drag and two-finger touch support, plus a compass indicator with reset-north button. - v1.8: Added GPX file upload with route overlay, track styling, distance labels, slope coloring, waypoints, and elevation stats.
- v1.7: Added the Slope Map feature to color-code terrain by steepness, with filter and opacity controls.
- v1.6: Added an interactive tutorial, reordered tutorial steps, and added the GitHub Project link in the info modal.
- v1.5: Added a PWA install button in the info modal and a mobile install prompt bar.
- v1.4: Improved Find Climbs accuracy with cumulative ascent, noise filtering, and higher scan resolution. Added detailed climb stats and new debug settings.
- v1.3: Made the app installable, added custom numbered map pins, improved touch UI for number inputs, and fixed alignment on high-resolution screens.
- v1.2.1: Fixed incorrect results at zoom level 15+ and added toggleable water analysis in debug settings.
- v1.2: Migrated elevation tiles to Mapterhorn with 512 px terrain tiles.
- v1.1: Added Find Climbs, the Lantmateriet map layer, and multilingual support.
- v1.0: Initial release.
TopoScout is client-side by default.
- No location data is sent to the creator's server.
- No search history is stored on a backend.
- API keys are only stored locally in the browser and sent directly to the relevant map provider when used.
- The optional backend only stores the GPX files you explicitly upload, and only on the server you choose to run. The public live demo and static hosting run without it.
I'd love to hear from you — feedback helps shape where TopoScout goes next.
- Ideas, feature requests, and general feedback: start a thread in GitHub Discussions.
- Bug reports: open an issue on GitHub Issues.
Created by droidgren.github.io.
Libraries, services, and data sources used by the project include:
- MapLibre GL JS
- OpenTopoMap
- OpenStreetMap and Nominatim
- Esri World Imagery
- Lantmateriet
- Kartverket / Norgeskart
- ThunderForest
- Tracestrack
- Mapterhorn
- maplibre-contour (client-side contour generation)
- Noto Sans and Open Sans (SIL OFL 1.1 / Apache License 2.0) — bundled glyphs for contour labels
This project is open source. See the repository for the applicable license and distribution terms.