defmodule LocationSimulator.WorkerGpx do
@moduledoc """
Worker is a simulator that use for generating GPS data.
Worker will call client follow :callback setting in config.
Two start/stop event is used for start or init data and clean if need.
`event` api is used for put GPS data to client.
Callbacks are declared in `LocationSimulator.Event` module.
"""
require Logger
alias LocationSimulator.Gpx
@doc """
Support for starting from supervisor.
"""
@spec start_link(any) :: {:ok, pid}
def start_link(arg) do
pid = spawn_link(__MODULE__, :init, [arg])
{:ok, pid}
end
@spec child_spec(map()) :: %{
id: any(),
restart: any(),
shutdown: any(),
start: {LocationSimulator.WorkerGpx, :start_link, [map(), ...]},
type: :worker
}
@doc """
Generate child spec for supervisor.
"""
def child_spec(opts) when is_map(opts) do
%{
id: Map.get(opts, :id, __MODULE__),
start: {__MODULE__, :start_link, [opts]},
type: :worker,
restart: Map.get(opts, :restart, :temporary),
shutdown: Map.get(opts, :shutdown, :brutal_kill)
}
end
@doc """
Entry point of worker, start event will be fired from here.
Init some data and go to loop function for generating GPS data.
Root GPS data will be generated in this function.
"""
@spec init(map) :: :ok
def init(config) when is_map(config) do
state = %{
start_time: get_timestamp(),
success: 0,
failed: 0,
error: 0
}
# load GPS data from GPX file
gps_data = Gpx.read_gpx(config.gpx_file)
id = Map.get(config, :id, self())
%{callback: mod} = config
Logger.debug("#{inspect id}, call start event")
# send start event
config = case mod.start(config, state) do
{:ok, new_config} ->
new_config
{:error, reason} ->
err = "call start event failed: #{inspect reason}"
Logger.error(err)
raise err
end
:rand.seed(:exsss)
# get first gps data
[gps | gps_data] = gps_data
config = Map.put(config, :gps_data, gps_data)
new_gps = %{
timestamp: 0,
time: gps.time,
lon: gps.lon,
lat: gps.lat,
ele: gps.ele
}
state = Map.put(state, :gps, new_gps)
Logger.debug("start loop with gps data from file : #{inspect config.gpx_file}")
loop_event(config, state)
end
# Main function of worker, GPS data will be generated in here then trigger client's api.
# After fired all GPS data/ client send signal to stop, the function will fire stop event.
defp loop_event(%{gps_data: []} = config, state) do
stop_time = get_timestamp()
state = Map.put(state, :stop_time, stop_time)
%{callback: mod} = config
# send stop event
config = case mod.stop(config, state) do
{:ok, new_config} ->
new_config
{:error, reason} ->
err = "call start event failed: #{inspect reason}"
Logger.error(err)
raise err
end
%{start_time: start_time} = state
%{success: s} = state
%{failed: f} = state
%{error: e} = state
id = Map.get(config, :id, self())
Logger.debug "Worker(#{inspect id}) DONE!, time: #{stop_time - start_time}s, success: #{s}, failed: #{f}, error: #{e}"
end
# in case of gpx_time, we will use timestamp from gpx file.
defp loop_event(%{interval: :gpx_time} = config, %{gps: last_gps} = state) do
# get next gps data
[gps | gps_data] = config.gps_data
config = Map.put(config, :gps_data, gps_data)
# sleep time between two gps data
# TO-DO: Move sleep to send event for can cancel sleep.
interval = NaiveDateTime.diff(gps.time, last_gps.time, :second) * 1_000
Process.sleep(interval)
new_gps = %{
timestamp: interval + last_gps.timestamp,
time: gps.time,
lon: gps.lon,
lat: gps.lat,
ele: gps.ele
}
state = Map.put(state, :gps, new_gps)
%{callback: mod} = config
{config, state, is_stop} =
case mod.event(config, state) do
{:ok, config} ->
{config, Map.update!(state, :success, &(&1 + 1)), :continue}
{:error, reason} ->
Logger.debug "failed to post data return code: #{inspect reason}"
{config, Map.update!(state, :failed, &(&1 + 1)), :continue}
{:stop, reason} ->
Logger.debug "stop worker by client, reason: #{inspect reason}"
{config, state, :stop}
end
case is_stop do
:stop ->
id = Map.get(config, :id, self())
Logger.debug("#{inspect id} stop by callback, id: #{inspect id}")
loop_event(Map.put(config, :gps_data, []), state)
_ ->
loop_event(config, state)
end
end
defp loop_event(config, %{gps: last_gps} = state) do
# get next gps data
[gps | gps_data] = config.gps_data
config = Map.put(config, :gps_data, gps_data)
Process.sleep(config.interval)
new_gps = %{
timestamp: config.interval + last_gps.timestamp,
lon: gps.lon,
lat: gps.lat,
ele: gps.ele
}
state = Map.put(state, :gps, new_gps)
%{callback: mod} = config
{config, state, is_stop} =
case mod.event(config, state) do
{:ok, config} ->
{config, Map.update!(state, :success, &(&1 + 1)), :continue}
{:error, reason} ->
Logger.debug "failed to post data return code: #{inspect reason}"
{config, Map.update!(state, :failed, &(&1 + 1)), :continue}
{:stop, reason} ->
Logger.debug "stop worker by client, reason: #{inspect reason}"
{config, state, :stop}
end
case is_stop do
:stop ->
id = Map.get(config, :id, self())
Logger.debug("#{inspect id} stop by callback, id: #{inspect id}")
loop_event(Map.put(config, :gps_data, []), state)
_ ->
loop_event(config, state)
end
end
defp get_timestamp do
:os.system_time(:seconds)
end
end