Packages
A library for generating fake GPS data or load GPS from GPX file/string using for dev/test. The library generates GPSs for simulating a route and scalable for test workload.
Current section
Files
Jump to
Current section
Files
lib/location_simulator.ex
defmodule LocationSimulator do
@moduledoc """
Entry-point API for the LocationSimulator library.
## Quick start
iex> LocationSimulator.start()
## Custom config
iex> LocationSimulator.start(%{
...> worker: 3,
...> event: 100,
...> interval: 1_000,
...> random_range: 200,
...> direction: :north,
...> elevation: 50,
...> elevation_way: :up,
...> callback: MyApp.GpsHandler
...> })
## Config keys
| Key | Type / Values | Default |
|-----------------|----------------------------------------|---------------------------------|
| `:worker` | `pos_integer()` | `1` |
| `:event` | `pos_integer() \| :infinity` | `1` |
| `:interval` | `pos_integer() \| :gpx_time` | `100` ms |
| `:random_range` | `non_neg_integer()` | `10` ms extra jitter |
| `:direction` | see `t:direction/0` | `:random` |
| `:elevation` | `integer()` (metres) | `0` |
| `:elevation_way`| `:up \| :down \| :no_up_down` | `:no_up_down` |
| `:callback` | module implementing `Event` behaviour | `LocationSimulator.LoggerEvent` |
| `:group_id` | `any()` | `nil` (no group) |
| `:gpx_file` | glob string e.g. `"tracks/*.gpx"` | `nil` (synthetic GPS) |
| `:started_gps` | `{lat, lon, ele}` floats | `nil` (random start) |
`:id` is reserved and set automatically per worker.
Any additional keys are forwarded unchanged to your callback module.
"""
require Logger
alias LocationSimulator.DynamicSupervisor, as: Sup
@app_config Application.compile_env(:location_simulator, :default_config, %{})
@type direction ::
:north
| :south
| :east
| :west
| :north_east
| :north_west
| :south_east
| :south_west
| :random
# ── Public API ─────────────────────────────────────────────────────────────
@doc "Starts workers using `config`. See the module doc for key reference."
@spec start(map()) :: :ok
def start(config) when is_map(config) do
config
|> build_worker_specs()
|> Sup.start_simulator()
end
@doc "Starts one worker with the default / application config."
@spec start() :: :ok
def start do
default_config() |> build_worker_specs() |> Sup.start_simulator()
end
@doc """
Stops all workers that belong to `group_id`.
Workers must have been started with a matching `:group_id` in their config.
"""
@spec stop(group_id :: any()) :: :ok
def stop(group_id) do
Registry.dispatch(LocationSimulator.Registry, group_id, fn entries ->
Logger.debug("Stopping #{length(entries)} workers in group #{inspect(group_id)}")
for {pid, id} <- entries do
Logger.debug("Sending stop cmd for #{inspect id} (#{inspect pid})")
send(pid, {:stop_worker, id})
end
end)
end
@doc "Returns the merged default config (application config overrides built-in defaults)."
@spec default_config() :: map()
def default_config do
%{
worker: fetch(:worker, 1),
event: fetch(:event, 1),
interval: fetch(:interval, 100),
random_range: fetch(:random_range, 10),
direction: fetch(:direction, :random),
elevation: fetch(:elevation, 0),
elevation_way: fetch(:elevation_way, :no_up_down),
callback: fetch(:callback, LocationSimulator.LoggerEvent)
}
end
# ── Private ────────────────────────────────────────────────────────────────
# Reads a key from the application config with a fallback default.
defp fetch(key, default) do
Keyword.get(@app_config, key, default)
end
# Dispatch to GPX or synthetic worker builder.
@spec build_worker_specs(map()) :: [{module(), map()}]
defp build_worker_specs(%{gpx_file: glob} = config) do
files = Path.wildcard(glob)
if files == [], do: raise("No GPX files matched: #{glob}")
build_gpx_specs(config.worker, config, files, files, [])
end
defp build_worker_specs(config) do
build_specs(config.worker, config, [])
end
# Build specs for synthetic workers (tail-recursive).
defp build_specs(0, _config, acc), do: acc
defp build_specs(n, config, acc) do
build_specs(n - 1, config, [{LocationSimulator.Worker, Map.put(config, :id, n)} | acc])
end
# Build specs for GPX workers, cycling through available files (tail-recursive).
defp build_gpx_specs(0, _config, _cur, _all, acc), do: acc
defp build_gpx_specs(n, config, [], all, acc),
do: build_gpx_specs(n, config, all, all, acc)
defp build_gpx_specs(n, config, [file | rest], all, acc) do
spec = {LocationSimulator.WorkerGpx, config |> Map.put(:id, n) |> Map.put(:gpx_file, file)}
build_gpx_specs(n - 1, config, rest, all, [spec | acc])
end
end