Replay real GPS tracks from GPX files instead of generating synthetic data.

Quick start

defmodule MyApp.TrackReplay do
  use LocationSimulator.Pipeline

  source :gpx, gpx_file: "tracks/*.gpx"

  plug LocationSimulator.Plug.Callback, callback: MyApp.Handler
end

LocationSimulator.start(MyApp.TrackReplay, %{worker: 3, interval: :gpx_time})

Source options

source :gpx, gpx_file: "data/**/*.gpx"
OptionRequiredDescription
gpx_fileyesGlob pattern matching GPX files
started_gpsno{lat, lon, ele} to override first point

Interval modes

Sleeps for the exact time delta between consecutive track points, preserving the original timing.

Fixed integer (for faster replay)

LocationSimulator.start(MyApp.TrackReplay, %{worker: 1, interval: 500})

Each point is emitted 500ms apart, regardless of the original timestamps.

Multiple files

When the glob matches multiple files, workers are assigned files in order, cycling through if there are more workers than files.

tracks/
 morning.gpx     Worker 1
 afternoon.gpx   Worker 2
 evening.gpx     Worker 3

With 5 workers and 3 files: Worker 4 gets morning.gpx, Worker 5 gets afternoon.gpx.

GPX point structure

Each replayed point has the following keys:

%{
  lat: 21.0278,           # float — latitude
  lon: 105.8342,          # float — longitude
  ele: 10.5,              # float — elevation in metres
  timestamp: 1700000000,  # integer — wall-clock ms (:os.system_time(:millisecond))
  time: ~N[2024-01-01 12:00:00]  # NaiveDateTime — original GPX timestamp
}

Handling GPX without elevation

If a GPX track point lacks an <ele> element, the elevation defaults to 0.

Custom interval logic

Combine with your own plug for custom timing (timing is owned by the worker loop; this adds extra pacing on top):

defmodule MyApp.PacingPlug do
  @behaviour LocationSimulator.Plug

  @impl true
  def init(opts), do: opts

  @impl true
  def call(pipeline, _opts) do
    # Apply random jitter to the configured interval
    interval = Map.get(pipeline.config, :interval, 1_000)
    jitter = max(floor(interval * 0.1), 1)
    Process.sleep(interval + Enum.random(0..jitter))
    pipeline
  end
end

Legacy API

For a flat config map approach without a pipeline module:

LocationSimulator.start(%{
  worker: 3,
  interval: :gpx_time,
  gpx_file: "data/*.gpx",
  callback: MyApp.Handler
})