Upload GPX files and render animated flyover maps with MapLibre and an elevation chart in WordPress.
This plugin adds a Track post type, a simple admin uploader, a REST endpoint serving parsed GPX data, and a front‑end player with play/pause controls, progress bar, and a synced elevation chart.
- Animated flyover map (MapLibre GL) with smooth camera
- Elevation-based route coloring – progressive route changes color based on gradient (flat vs steep sections)
- Direction arrows on route – optional ▶ arrows drawn along the path at configurable intervals (admin setting, default 5 km)
- Photos on the map (thumbnails + fullscreen on cue) with image overlay support during video recording
- Labels for maximum speed and elevation
- HUD overlays (speed, distance, elevation, heading) – toggleable
- Multi-weather heatmap overlays with 4 separate colored layers (snow, rain, fog, clouds) using admin-configurable colors
- Day/Night overlay with configurable colors
- Weather visualizations: colored heatmaps, temperature circles, and wind arrows with configurable radius
- Wind analysis: per-point wind speed/direction, wind impact factor chart, wind rose distribution (16 sectors)
- Multi-tab Chart.js visualizations: Elevation, Biometrics (HR/Cadence), Temperature, Power, Power Zones, Wind Impact, Wind Rose, and All Data
- Simulation tab: compact weather + grade strip with wind arrows, elevation and photo cues.
- Power fallback model: if GPX has no power stream, backend estimates power and the frontend displays an "Estimated power" indicator
- Chart area selection & zoom with reset and synchronized map marker filtering (excludes polar charts)
- Video recording – record MP4/WebM videos of the flyover animation with customizable settings
- Privacy mode (hide first/last N km for playback window only)
- Dark mode‑friendly UI
- Gallery with tile/list view, Searchable, filterable, orderable
- Admin tools: Add New Track page, sortable stats, live preview
- Configurable defaults (height, zoom, pitch, chart colors, elevation coloring)
- Custom styling: inline style.json or vector style URL; OSM raster fallback
- Backend GPX simplification enabled by default with dynamic targets for large tracks
- Admin toggle for tile prefetching (reduce third‑party tile requests/quota usage)
- Lazy-loaded chart data with caching for 60% faster initial render on large tracks
- Shortcode to embed anywhere with per-shortcode feature overrides
- WP-CLI support for batch imports and automation
- REST API + AJAX fallback with caching (2h weather cache)
- Dark splash overlay with Play; hidden immediately on any Play (map or controls)
- User zoom/rotate allowed during playback
- Click progress bar to seek; camera moves to the marker
- Chart: vertical cursor + position dot; secondary speed line (km/h) on right axis
- Optional top x‑axis shows distance (km) while primary x‑axis is time
- Auto zoom‑out to full bounds at the end; default zoom restored on restart
- Initial stopped view fits the full track; on Play, the map smoothly zooms in
Click the image above to watch the demo video (recorded with plugin internal functionality)
- WordPress 6.0+
- PHP 7.4+
- Copy the
flyover-gpxdirectory into your WordPresswp-content/pluginsfolder. - If developing from source, install dependencies:
cd wp-content/plugins/flyover-gpx
composer install --no-interaction --no-dev- Activate the plugin in WordPress → Plugins.
- Go to Settings → Flyover GPX.
- Upload a
.gpxfile (≤ 20MB). The plugin parses it, computes stats, and creates a Track post. - On the Tracks list, use “Copy Shortcode” to embed it in a page or post.
- Optionally open “Preview Map” to quickly verify the flyover.
Embed a track:
[flyover_gpx id="123"]
Parameters:
id(required): The Track post ID.style(optional):default(OSM fallback),url(remote style), orinline(JSON). Default is set in Settings → Flyover GPX. Backward compat:raster→default,vector→url.height(optional): Container height (e.g.625px,60vh). Default625px.zoom(optional): Initial zoom level. Default is set in Settings → Flyover GPX.style_url(optional): A MapLibre-compatible style URL (any provider: MapTiler, Mapbox, custom). Used whenstyle="url". Ignored if inline style JSON is configured in settings.privacy(optional): Override privacy mode for this embed. Acceptstrue|false|1|0|yes|no|on|off. Defaults to the admin setting.privacy_km(optional): Override privacy distance in kilometers for this embed. Example:privacy_km="3". Defaults to the admin setting.hud(optional): Toggle the live HUD overlay (speed/distance/elevation/heading). Acceptstrue|false|1|0|yes|no|on|off. Defaults to admin setting.elevation_coloring(optional): Enable/disable elevation-based route coloring for this embed. Acceptstrue|false|1|0|yes|no|on|off. Defaults to admin setting.speed(optional): Override default playback speed for this embed. Example:speed="50". Defaults to admin setting.gpx_download(optional): Show/hide the GPX download button for this embed. Acceptstrue|false|1|0|yes|no|on|off. Defaults to admin setting.
Additional per-shortcode overrides (all optional, defaulting to admin settings):
- Display & Elevation Coloring
show_labels:true|false– show max elevation/speed labels on chartelevation_color_flat: hex color (e.g.#00ff00)elevation_color_steep: hex color (e.g.#ff0000)
- Chart Colors
speed_chart_color,cadence_chart_color,temperature_chart_color,power_chart_colorwind_impact_chart_color,wind_rose_chart_colorwind_rose_color_north,wind_rose_color_south,wind_rose_color_east,wind_rose_color_west
- Features
photos_enabled:true|falseweather_visible_by_default:true|falsewind_analysis_enabled:true|falsedaynight_enabled:true|false(chart visualization)daynight_map_enabled:true|false(map overlay)daynight_visible_by_default:true|falsedaynight_map_color: hex color (night overlay)
Examples:
[flyover_gpx id="123" height="60vh"]
[flyover_gpx id="123" style="url" style_url="https://api.maptiler.com/maps/outdoor/style.json?key=YOUR_KEY"]
[flyover_gpx id="123" style="inline"]
[flyover_gpx id="123" privacy="true" privacy_km="2.5"]
[flyover_gpx id="123" hud="false"]
[flyover_gpx id="123" elevation_coloring="true" speed="75"]
[flyover_gpx id="123" gpx_download="true"]
[flyover_gpx id="123" show_labels="true" speed_chart_color="#1976d2" power_chart_color="#059669"]
[flyover_gpx id="123" wind_analysis_enabled="true" wind_impact_chart_color="#ff6b35" wind_rose_chart_color="#4ecdc4"]
Notes:
- If a remote style URL fails to load, the player falls back to OSM raster tiles.
- Multiple instances per page are supported. The first container uses
fgpx-app, additional embeds usefgpx-app-N. - Disabling tile prefetching sets MapLibre's
prefetchZoomDeltato 0 and skips prewarm to minimize extra requests.
The plugin resolves map styles in this priority order:
- Inline JSON (highest precedence): If "Inline style JSON" is defined in admin settings, it is always used.
- Remote URL: If style source = "Remote Style URL" and a URL is provided, that URL is fetched and used (any MapLibre-compatible provider).
- Default (OSM Raster) (fallback): If neither inline JSON nor URL is available, the player uses OpenStreetMap raster tiles.
- Default (OSM Raster): Simple raster tiles from OpenStreetMap. No API key required, but limited to OSM's base map. Good for basic use cases and bandwidth-constrained scenarios.
- Remote Style URL: Fetch a complete MapLibre style JSON from a URL. Supports any provider (MapTiler, Mapbox, Maptiles) or custom. Ideal for satellite + terrain, vector overlays, custom branding.
- Inline JSON: Paste a full MapLibre style JSON directly into admin settings. Useful for complex styles, offline development, or provider-specific features (3D buildings, DEM sources, heatmaps, etc.).
You can use "Remote Style URL" (or inline JSON) to set up satellite + hillshade + terrain:
[flyover_gpx id="123" style="url" style_url="https://api.maptiler.com/maps/satellite/style.json?key=YOUR_KEY"]
This will fetch the MapTiler satellite style (which includes terrain/DEM by default) and render it with 3D mountain shading. No need to switch to "Vector" mode—the style URL works with any provider, satellite or vector.
Embed a browsable track gallery with inline player and social sharing:
[flyover_gpx_gallery]
Parameters (all optional):
per_page(optional): Number of tracks shown before "Load more". Range 4–48. Default12.height(optional): Player height when a track is opened. Default625px.style(optional): Map style for the player —raster(default) orvector.style_url(optional): MapLibre style URL whenstyle="vector".show_view_toggle(optional): Show grid/list toggle buttons. Accepts1|0|true|false. Default1.show_search(optional): Show search input. Accepts1|0|true|false. Default1.default_sort(optional): Initial sort key. One ofnewest|distance|duration|gain|title. Defaultnewest.
Default resolution order:
- Shortcode attribute value (if provided)
- Admin Gallery defaults (Settings → Flyover GPX)
- Built-in fallback
Features:
- Grid and list card view with distance, duration, elevation gain, and upload date
- Full-text search across title and metadata
- Sort by newest, distance, duration, elevation gain, or title
- Inline player panel — opens below the list when a track is selected
- Sharing: Facebook, Twitter/X, WhatsApp buttons + copy-link button (copies share URL with
#track-{id}hash) - Shared URLs include a
#track-{id}hash that auto-opens the correct track on page load - Multiple
[flyover_gpx_gallery]shortcodes on the same page are fully isolated - Photo enrichment from embedding posts – When opening a track from the gallery, the player automatically loads photos from the latest post that embeds the track, providing richer visual context
When you open a track in the gallery player, the plugin automatically searches for the most recent published post that contains the same track shortcode and loads photos from that post instead of (or in addition to) the track's own attachments. This feature enriches tracks with contextual photos from blog posts, reviews, or articles that reference them.
How it works:
- Gallery player detects the latest embedding post — the most recent published post that contains
[flyover_gpx id="123"](the track shortcode) - Photos are collected from that post in this order:
- Direct media attachments to the post
- Images in gallery blocks
- Inline images in post content
- Fallback: track's own attached images if the embedding post has no photos
- Each photo includes its source post reference — the player UI indicates "📷 Photo from linked post" when viewing photos enriched from an embedding post
- Cache strategy: Photos are cached for 6 hours. If you delete or unpublish the embedding post, the cache is automatically invalidated
Example:
- Track: "Downtown Bike Trail" has 5 attached photos
- Blog post: "My Saturday Ride" embeds this track with 12 photos
- Gallery result: Player shows the 12 photos from the blog post when opening the track from the gallery
Tips:
- Create posts with
[flyover_gpx id="123"]to embed tracks alongside journey photos, observations, or race reports - The plugin automatically picks the latest post by publish date; if multiple posts embed the same track, the newest one is used
- For consistent photo experience: upload photos to the post that embeds the track using the WordPress gallery block or inline images
- Photos are deduplicated by approximate location (~10m precision) to avoid duplicate markers on the map
Examples:
[flyover_gpx_gallery]
[flyover_gpx_gallery per_page="6" height="500px"]
[flyover_gpx_gallery show_view_toggle="0"]
[flyover_gpx_gallery show_search="0" default_sort="distance"]
You can paste a complete MapLibre style.json into Settings → Flyover GPX → Shortcode Defaults → “Inline style JSON (optional)”.
- If provided, this inline style takes precedence over both remote URL and the default OSM raster.
- If blank, the player respects the "Map style source" setting:
- "Remote Style URL" → fetches from the provided URL
- "Default (OSM Raster)" → uses OpenStreetMap tiles
Example minimal style JSON with OSM raster source (only for dev, does not provide all features):
{
"version": 8,
"name": "FGPX Raster Minimal",
"sources": {
"osm": {
"type": "raster",
"tiles": ["https://tile.openstreetmap.org/{z}/{x}/{y}.png"],
"tileSize": 512,
"minzoom": 0,
"maxzoom": 18,
"attribution": "© OpenStreetMap contributors"
}
},
"layers": [
{ "id": "background", "type": "background", "paint": { "background-color": "#000" } },
{
"id": "osm",
"type": "raster",
"source": "osm",
"paint": { "raster-fade-duration": 0 }
}
]
}Example style for 3D terrain rendering and points of interrest (for all features):
{
"version": 8,
"glyphs": "https://maps.6bes.de/fonts/{fontstack}/{range}.pbf?key={{API_KEY}}",
"sources": {
"terrain": {
"type": "raster-dem",
"url": "https://maps.6bes.de/tiles/terrain-rgb-v2/tiles.json?key={{API_KEY}}",
"tileSize": 512
},
"satellite": {
"type": "raster",
"tiles": [
"https://maps.6bes.de/maps/satellite/{z}/{x}/{y}.jpg?key={{API_KEY}}"
],
"tileSize": 512
},
"openmaptiles": {
"type": "vector",
"url": "https://maps.6bes.de/tiles/v3/tiles.json?key={{API_KEY}}"
}
},
"layers": [
{
"id": "satellite",
"type": "raster",
"source": "satellite"
},
{
"id": "cycleways",
"type": "line",
"source": "openmaptiles",
"source-layer": "transportation",
"filter": [
"any",
["==", ["get", "class"], "cycleway"],
[
"all",
["==", ["get", "class"], "path"],
["in", ["get", "bicycle"], ["literal", ["yes", "designated", "official"]]]
]
],
"paint": {
"line-color": "#00c853",
"line-width": 2
}
},
{
"id": "place-labels",
"type": "symbol",
"source": "openmaptiles",
"source-layer": "place",
"layout": {
"text-field": ["get", "name"],
"text-font": ["Open Sans Bold", "Arial Unicode MS Bold"],
"text-size": 14,
"text-anchor": "center"
},
"paint": {
"text-color": "#ffffff",
"text-halo-color": "#000000",
"text-halo-width": 1
}
},
{
"id": "water-name-labels",
"type": "symbol",
"source": "openmaptiles",
"source-layer": "water_name",
"layout": {
"text-field": ["get", "name"],
"text-font": ["Open Sans Italic", "Arial Unicode MS Regular"],
"text-size": 12,
"symbol-placement": "line"
},
"paint": {
"text-color": "#4FC3F7",
"text-halo-color": "#000000",
"text-halo-width": 0.5
}
}
]
}Downstripped raster style (fewest requests, no API key, no labels)
- Single raster source (OSM), no glyphs/sprites/vector/DEM
- 512px tiles and maxzoom 18 to reduce the number of tile requests
- Best choice when you’re hitting MapTiler limits
- Adds global DEM (Terrarium encoding) to enable 3D terrain without an API key
- Limits DEM to maxzoom 12 and keeps 256px raster base to control request volume
- Note: enabling terrain increases requests versus the minimal style
{
"version": 8,
"name": "FGPX Raster + Terrain (Terrarium)",
"sources": {
"osm": {
"type": "raster",
"tiles": ["https://tile.openstreetmap.org/{z}/{x}/{y}.png"],
"tileSize": 512,
"minzoom": 0,
"maxzoom": 18,
"attribution": "© OpenStreetMap contributors"
},
"terrain": {
"type": "raster-dem",
"tiles": ["https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png"],
"encoding": "terrarium",
"tileSize": 256,
"minzoom": 0,
"maxzoom": 12,
"attribution": "© Mapzen, AWS Terrain Tiles"
}
},
"layers": [
{ "id": "background", "type": "background", "paint": { "background-color": "#000" } },
{
"id": "osm",
"type": "raster",
"source": "osm",
"paint": { "raster-fade-duration": 0 }
}
],
"terrain": { "source": "terrain", "exaggeration": 1.0 }
}Notes
- These styles intentionally remove labels/icons to eliminate glyph and sprite requests.
- Using 512px raster tiles and capping maxzoom saves requests, especially at higher zooms.
- If you need satellite, swap the
tilesURL underosmwith a satellite raster provider, but check usage terms/quotas. - For the fewest requests overall, use the “Raster Minimal” style and disable tile prefetching in the plugin settings.
- Base:
/wp-json/fgpx/v1 - Endpoint:
GET /track/{id}
{
"id": 123,
"name": "my-ride.gpx",
"stats": {
"total_distance_m": 42195.3,
"moving_time_s": 10800,
"average_speed_m_s": 3.9,
"elevation_gain_m": 520,
"min_elevation_m": 120,
"max_elevation_m": 980
},
"geojson": {
"type": "LineString",
"coordinates": [ [lon, lat, ele], ... ],
"properties": {
"timestamps": ["2024-02-01T10:00:00Z", null, ...],
"cumulativeDistance": [0, 12.3, ...],
"heartRates": [152, 154, null, ...],
"cadences": [88, 86, null, ...],
"temperatures": [21.4, 21.6, 21.5, ...],
"powers": [210, 215, 205, ...],
"windSpeeds": [5.2, 4.8, 5.0, ...],
"windDirections": [220, 225, 230, ...],
"windImpacts": [0.92, 1.05, 1.02, ...]
}
},
"bounds": [minLon, minLat, maxLon, maxLat],
"points_count": 12345,
"simplified": true,
"estimatedPower": true,
"photos": [
{
"id": 49785,
"title": "IMG_20250824_112847a",
"caption": "Carolinen Hütte",
"description": "Die Carolinen Hütte bei Rohrbach",
"lat": 49.000894,
"lon": 11.381095,
"timestamp": "2025-08-24T11:28:47+00:00",
"thumbUrl": "https://.../IMG_2025...-225x300.jpg",
"fullUrl": "https://.../IMG_2025...-768x1024.jpg"
}
]
}estimatedPower is true when power values were computed on the backend (instead of read from the GPX stream).
- Enable in Settings → Flyover GPX → “Enable privacy mode”.
- Configure “Privacy distance (km)” (default 3). Playback will start after the first N km and finish N km before the end.
- The map camera, progress line, chart cursor, photo cues, and weather overlays all respect the trimmed window. Stats (distance, time, avg speed, gain) remain computed from the full GPX.
- Shortcode/CLI can override privacy enablement and distance on a per-embed basis.
The plugin includes built-in video recording capabilities to create MP4/WebM videos of your flyover animations:
- Record Button: Available in the player controls during playback
- Format Support: Automatically detects and uses the best supported format (MP4 H.264, WebM VP9, or WebM VP8)
- Image Overlay: Photos and markers are included in the recorded video
- Customizable Settings: Recording quality and frame rate can be configured
- Download: Completed videos are automatically downloaded to your device
To record a video:
- Start playback of your GPX track
- Click the record button in the player controls
- The video will capture the entire flyover animation
- Download begins automatically when recording completes
Notes:
- Recording includes map, route, HUD, chart cursor, and active overlays (photos/weather/day-night)
- Requires a modern browser with MediaRecorder API; codec availability varies by browser/OS
Import GPX Files (creates a Track; optionally inserts a shortcode into an existing post and publishes it):
wp fgpx import --file=/abs/path/ride.gpx [options]Options:
--file=<path> # required, absolute path to GPX file
--post=<id> # post ID to embed shortcode into
--title=<string> # optional track title (defaults to filename)
--privacy=<on|off> # privacy mode toggle
--privacy-km=<float> # privacy distance in km
--hud=<on|off> # HUD overlay toggle
--elevation-coloring=<on|off> # elevation-based coloring toggle
--speed=<int> # default playback speed
--show-labels=<on|off> # show max elev/speed labels
--elevation-color-flat=<hex> # flat terrain color
--elevation-color-steep=<hex> # steep terrain color
--speed-chart-color=<hex> # speed chart color
--cadence-chart-color=<hex> # cadence chart color
--temperature-chart-color=<hex> # temperature chart color
--power-chart-color=<hex> # power chart color
--wind-impact-chart-color=<hex> # wind impact chart color
--wind-rose-chart-color=<hex> # wind rose chart color (default)
--wind-rose-color-north=<hex> # wind rose north color
--wind-rose-color-south=<hex> # wind rose south color
--wind-rose-color-east=<hex> # wind rose east color
--wind-rose-color-west=<hex> # wind rose west color
--photos-enabled=<on|off> # enable photo thumbnails/overlay
--weather-visible-by-default=<on|off> # weather buttons visibility at load
--wind-analysis-enabled=<on|off> # enable wind impact analysis
--daynight-enabled=<on|off> # enable day/night chart visualization
--daynight-map-enabled=<on|off> # enable day/night map overlay
--daynight-visible-by-default=<on|off># default visibility for day/night overlay
--daynight-map-color=<hex> # night overlay color
--publish # publish the target post after shortcode insertionExamples:
# Simple import
wp fgpx import --file=/uploads/ride.gpx
# Import with shortcode insertion and common toggles
wp fgpx import --file=/uploads/ride.gpx --post=123 --publish \
--title="Mountain Ride" --privacy=on --privacy-km=3 --hud=on --elevation-coloring=on --speed=75
# Customize visuals and features per-embed
wp fgpx import --file=/uploads/ride.gpx --post=123 \
--show-labels=on --speed-chart-color="#1976d2" --power-chart-color="#059669" \
--photos-enabled=on --weather-visible-by-default=on --daynight-enabled=on --daynight-map-color="#000080"
# Wind analysis focused
wp fgpx import --file=/uploads/ride.gpx --post=123 \
--wind-analysis-enabled=on --wind-impact-chart-color="#ff6b35" --wind-rose-chart-color="#4ecdc4"- Minimal CSS in
flyover-gpx/assets/css/front.csswith variables like--fgpx-border,--fgpx-card-bgfor light/dark. - The player respects
prefers-color-schemeand adapts colors accordingly.
- Source code uses a small PHP core and a single front‑end JS (
assets/js/front.js). - PHP GPX parsing via
sibyxs/phpgpx(declared incomposer.json). - Autoloading is PSR‑4 (
FGpx\\→includes/). Ifvendor/autoload.phpis missing, the plugin shows an admin notice to run Composer. - Debug logging systems: JavaScript (
DBG) respectsFGPX.debugLogging; PHP usesErrorHandler::debug()/warning()with admin toggle - Performance settings: backend simplification enabled by default with dynamic target; lazy viewport loading; prefetch toggle; asset fallback detection
- Frontend caching: localStorage cache for processed track data with automatic expiry and safe fallback
Build/Install locally:
composer installFolder layout:
flyover-gpx/
flyover-gpx.php
includes/
Options.php
ErrorHandler.php
AssetManager.php
DatabaseOptimizer.php
Plugin.php
Rest.php
Admin.php
CLI.php
assets/
css/front.css
js/front.js
composer.json
- One player instance per page (container id is fixed to
fgpx-app). - Upload limit: 20MB per GPX file.
- Large tracks are simplified on the backend by default; dynamic targets avoid over/under‑simplification.
- Local caching is best‑effort and expires automatically; the player gracefully falls back to live fetch.
- Weather data is cached server‑side (≈2h) to limit API calls.
- Chart zoom is unavailable for polar charts (wind rose) by design.
- Video recording requires a modern browser with MediaRecorder API support.
- Style resolution: Inline JSON (if provided) always takes precedence over remote URL, which takes precedence over OSM fallback.
This plugin was developed with the help of AI coding tools.
MIT. See LICENSE if provided; otherwise, embed licensing details as appropriate for your project.








