defmodule LocationSimulator.Worker do
@moduledoc """
Worker simulator that generates synthetic GPS data.
Each worker picks a travel direction (or randomises one if not supplied),
then emits GPS events at the configured interval until the event count is
exhausted, the callback requests a stop, or a group-stop message arrives.
See `LocationSimulator.Event` for the callback contract.
"""
require Logger
import LocationSimulator.Gps
alias LocationSimulator.WorkerBase
# ── OTP ────────────────────────────────────────────────────────────────────
@spec start_link(map()) :: {:ok, pid()}
def start_link(arg), do: WorkerBase.start_link(__MODULE__, arg)
@spec child_spec(map()) :: map()
def child_spec(opts), do: WorkerBase.child_spec(__MODULE__, opts)
# ── Entry point ────────────────────────────────────────────────────────────
@doc "Entry point called by `start_link/1`."
@spec init(map()) :: :ok
def init(config) do
state = WorkerBase.init_state()
config = resolve_direction(config)
config = WorkerBase.fire_start!(config, state)
:rand.seed(:exsss)
gps = initial_gps(config)
state = Map.put(state, :gps, gps)
counter = Map.get(config, :event, :infinity)
WorkerBase.maybe_register(config)
Logger.debug(
"Worker #{inspect(Map.get(config, :id))}: starting loop, events=#{inspect(counter)}"
)
loop(config, state, counter)
end
# ── Private: direction ─────────────────────────────────────────────────────
@directions [:north, :south, :east, :west, :north_east, :north_west, :south_east, :south_west]
# Explicit :random → pick one
defp resolve_direction(%{direction: :random} = config) do
Map.put(config, :direction, Enum.random(@directions))
end
# Already a specific direction → keep it
defp resolve_direction(%{direction: _} = config), do: config
# Key missing entirely → same as :random
defp resolve_direction(config) do
Map.put(config, :direction, Enum.random(@directions))
end
# ── Private: initial GPS ───────────────────────────────────────────────────
defp initial_gps(%{started_gps: {lat, lon, ele}}) do
%{timestamp: 0, lat: lat, lon: lon, ele: ele}
end
defp initial_gps(config) do
{lat, lon} = generate_pos()
ele = Map.get(config, :elevation, 0)
%{timestamp: 0, lat: lat, lon: lon, ele: ele}
end
# ── Private: loop (terminal) ───────────────────────────────────────────────
defp loop(config, state, 0) do
WorkerBase.fire_stop!(config, state)
:ok
end
# ── Private: loop (active) ─────────────────────────────────────────────────
defp loop(%{id: id} = config, %{gps: last_gps} = state, counter) do
sleep_ms = sleep_duration(config)
Process.sleep(sleep_ms)
new_gps = next_gps(config, last_gps, sleep_ms)
state = Map.put(state, :gps, new_gps)
{config, state, next_counter} = dispatch_event(config, state, counter)
case next_counter do
:stop ->
loop(config, state, 0)
:infinity ->
receive do
{:stop_worker, ^id} ->
Logger.debug("Worker #{inspect(id)}: stop received from group manager")
loop(config, state, 0)
after
0 ->
loop(config, state, :infinity)
end
n ->
receive do
{:stop_worker, ^id} ->
Logger.debug("Worker #{inspect(id)}: stop received from group manager")
loop(config, state, 0)
after
0 ->
loop(config, state, n - 1)
end
end
end
# ── Private: GPS generation ────────────────────────────────────────────────
defp next_gps(config, last, sleep_ms) do
# direction is guaranteed present after resolve_direction/1
direction = Map.get(config, :direction, :north)
{lat, lon} =
generate_next_pos(
last.lat,
last.lon,
lat_step(direction),
lon_step(direction)
)
ele = next_elevation(Map.get(config, :elevation_way, :no_up_down), last.ele)
%{timestamp: last.timestamp + sleep_ms, lat: lat, lon: lon, ele: ele}
end
defp next_elevation(:up, ele), do: ele + Enum.random(0..2)
defp next_elevation(:down, ele), do: ele - Enum.random(0..2)
defp next_elevation(_, ele), do: ele
# ── Private: step helpers ──────────────────────────────────────────────────
# Longitude step (east/west component)
defp lon_step(dir) do
case dir do
d when d in [:north_east, :south_east, :east] -> Enum.random(1..5)
d when d in [:north_west, :south_west, :west] -> Enum.random(-5..-1//1)
_ -> 0
end
end
# Latitude step (north/south component)
defp lat_step(dir) do
case dir do
d when d in [:north_east, :north_west, :north] -> Enum.random(1..5)
d when d in [:south_east, :south_west, :south] -> Enum.random(-5..-1//1)
_ -> 0
end
end
# ── Private: event dispatch ────────────────────────────────────────────────
defp dispatch_event(%{callback: mod} = config, state, counter) do
case mod.event(config, state) do
{:ok, new_config} -> {new_config, Map.update!(state, :success, &(&1 + 1)), counter}
{:error, _reason} -> {config, Map.update!(state, :failed, &(&1 + 1)), counter}
{:stop, _reason} -> {config, state, :stop}
end
end
defp sleep_duration(%{interval: interval, random_range: range}) do
interval + Enum.random(0..range)
end
defp sleep_duration(%{interval: interval}), do: interval
end