%% -*- coding: utf-8 -*-
%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 1996-2012. All Rights Reserved.
%%
%% The contents of this file are subject to the Erlang Public License,
%% Version 1.1, (the "License"); you may not use this file except in
%% compliance with the License. You should have received a copy of the
%% Erlang Public License along with this software. If not, it can be
%% retrieved online at http://www.erlang.org/.
%%
%% Software distributed under the License is distributed on an "AS IS"
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
%% the License for the specific language governing rights and limitations
%% under the License.
%%
%% %CopyrightEnd%
%%
-module(mandel).
-compile([{nowarn_deprecated_function,{gs,assq,2}},
{nowarn_deprecated_function,{gs,config,2}},
{nowarn_deprecated_function,{gs,create,4}},
{nowarn_deprecated_function,{gs,image,2}},
{nowarn_deprecated_function,{gs,start,0}}]).
-author('(mbj,eklas)@erlang.ericsson.se').
%% User's interface
-export([start/0,start/1]).
%% Internal exports:
-export([start_client/2,refresher/1,start_server/1,respond/2]).
%%%-----------------------------------------------------------------
%%% Distributed Mandelbrot program.
%%% Originally written i C++/rpc/lwp/interviews by Klas Eriksson.(1200 lines)
%%% Rewritten in Erlang by Klas Eriksson and Martin Björklund.
%%%-----------------------------------------------------------------
%% unix>erl -sname foo (all nodes will get the same name)
%% (foo@data)1>mandel:start([{hosts,["computer1","computer2"]},{window,400}]).
%% unix>erl
%% 1> mandel:start().
-record(state,{image,width,height,xmax,ymax,range,
maxiter,colortable,zoomstep}).
-record(job,{left,right,ymin,ymax,height,width,maxiter,data=[]}).
-define(JOBWIDTH,10).
%%-----------------------------------------------------------------
%% This is the client start function.
%%-----------------------------------------------------------------
start() ->
start([]).
%%----------------------------------------------------------------------
%% Option is list of Option. Option is:
%% {xmax,float()}|{ymax,float()}|{range,float()}|
%% {maxiter,integer()}|{window,integer()}|{zoomstep,float()}|
%% {hosts,(list of string())|all_found_nodes}
%%----------------------------------------------------------------------
start(Opts) ->
Nodes1 = nodes(),
Nodes = case get_option(hosts,Opts,all_found_nodes) of
all_found_nodes when Nodes1 == [] ->
N = [node()],
spawn(mandel,start_server,[N]),
N;
all_found_nodes ->
start_nodes(dir(),Nodes1),
Nodes1;
Hosts ->
start_slaves(Hosts),
start_nodes(dir(),Nodes1),
Nodes1
end,
spawn(mandel,start_client,[Opts,Nodes]).
%% This is not an application so we don't have their way of knowing
%% a private data directory where the GIF files are located (this directory).
%% We can find GS and makes it relative from there /kgb
-define(EbinFromGsPriv,"../contribs/ebin").
dir()->
GsPrivDir = code:priv_dir(gs),
filename:join(GsPrivDir,?EbinFromGsPriv).
start_slaves([]) -> ok;
start_slaves([Host|Hs]) ->
{ok,Name}=slave:start(Host),
io:format("host ~p is up~n", [Name]),
start_slaves(Hs).
start_nodes(_Dir,[]) -> ok;
start_nodes(Dir,[Node|Nodes]) ->
rpc:call(Node,code,add_path,[Dir]), % hack? should be done in .erlang
spawn_link(Node,mandel,start_server,[[node()]]),
io:format("started mandelserver at node: ~p~n", [Node]),
start_nodes(Dir,Nodes).
start_client(Opts,Nodes) ->
Wt = get_option(window,Opts,100) div ?JOBWIDTH * ?JOBWIDTH,
Ht = get_option(window,Opts,100) div ?JOBWIDTH * ?JOBWIDTH,
S=gs:start(),
Win=gs:create(window,win1,S,[{title,"Mandel"},{width,Wt-1},{height,Ht-1},
{configure,true}]),
Canvas=gs:create(canvas,can1,Win,[{width,Wt},{height,Ht}]),
Image=gs:image(Canvas,[{buttonpress,true}]),
MaxIters = get_option(maxiter,Opts,100),
timer:apply_after(8000,mandel,refresher,[Image]),
CT = make_color_table(MaxIters),
State2=#state{image=Image,width=Wt,height=Ht,
xmax=try_random(get_option(xmax,Opts,2),-2,2),
ymax=try_random(get_option(ymax,Opts,2),-2,2),
range=try_random(get_option(range,Opts,4),0,4),
maxiter=MaxIters,colortable=CT,
zoomstep=get_option(zoomstep,Opts,1.7)},
ToDo = make_jobs(State2),
gs:config(Win,[{map,true}]),
main(State2, [], Nodes, ToDo).
try_random(random,Low,High) ->
random:uniform()*(High-Low)+Low;
try_random(Float,_Low,_High) when is_number(Float) -> Float.
%%-----------------------------------------------------------------
%% Distribute work to the nodes. When a node returns, that
%% node is the first to be used if there's any job left.
%%-----------------------------------------------------------------
main(State, [], PassiveNodes, []) ->
wait_event(State,[],PassiveNodes,[]);
main(State, ActiveNodes, PassiveNodes, []) ->
% No jobs left, but some nodes are still active.
% Wait_Event for their results
wait_event(State,ActiveNodes,PassiveNodes,[]);
main(State, ActiveNodes, [Node|PassiveNodes], [Job|ToDo]) ->
% We have work to do, and at least one passive node.
% Let him do it.
distribute_job(Node, Job),
main(State, [Node|ActiveNodes], PassiveNodes, ToDo);
main(State, ActiveNodes, [], ToDo) ->
% We have work to do, but all nodes are active.
_Node = wait_event(State,ActiveNodes,[],ToDo).
wait_event(State,ActiveNodes,PassiveNodes,ToDo) ->
receive
{calculation_done, {Node, Job}} ->
if % a small hack. we want to discard data for old pictures
Job#job.ymax==State#state.ymax ->
draw(State, Node, Job);
true -> true
end,
main(State,lists:delete(Node,ActiveNodes),[Node|PassiveNodes],ToDo);
{gs,_Img,buttonpress,_Data,[_Butt,X,Y|_]} ->
#state{width=W,height=H,ymax=Ymax,xmax=Xmax,range=R,zoomstep=ZS} =
State,
RX = Xmax-R+(X/W)*R,
RY = Ymax-R+(1-(Y/H))*R,
R2 = R/ZS,
Xmax2 = RX + R2/2,
Ymax2 = RY + R2/2,
State2 = State#state{xmax=Xmax2,ymax=Ymax2,range=R2},
io:format("{xmax,~w},{ymax,~w},{range,~w}~n", [Xmax2,Ymax2,R2]),
ToDo2=make_jobs(State2),
main(State2,ActiveNodes,PassiveNodes,ToDo2);
{gs,_Win,destroy,_,_} ->
kill_nodes(lists:append(ActiveNodes,PassiveNodes));
{gs,_Win,configure,_Data,[W,H|_]}
when State#state.width==W+1, State#state.height==H+1->
main(State,ActiveNodes,PassiveNodes,ToDo);
{gs,_Win,configure,_Data,[W|_]} ->
gs:config(can1,[{width,W},{height,W}]),
gs:config(win1,{configure,false}),
gs:config(win1,[{width,W-1},{height,W-1}]),
gs:config(win1,{configure,true}),
State2 = State#state{width=W,height=W},
ToDo2=make_jobs(State2),
main(State2,ActiveNodes,PassiveNodes,ToDo2)
end.
kill_nodes([]) ->
done;
kill_nodes([Node|Nodes]) ->
exit(rpc:call(Node,erlang,whereis,[mandel_server]),kill),
kill_nodes(Nodes).
distribute_job(Node, Job) ->
{mandel_server, Node} ! {mandel_job, {self(), Job}}.
draw(#state{image=Image, width=Wt, height=Ht, xmax=Xmax,
maxiter=MI,colortable=ColorTable,range=R}, Node, Job) ->
#job{left=Left,data=Data}=Job,
io:format("Got data from node ~30w~n", [Node]),
%% PixelX = K * RealX + M
%% 0 = K * Xmin + M
%% Width-1= K * Xmax + M
K=(1-Wt)/-R,
M=Wt-1-K*Xmax,
Xbegin = round(Left*K+M),
draw_cols(Image, Xbegin, Ht, lists:reverse(Data),MI,ColorTable).
draw_cols(Image, X, Ht, [H|T],MaxIter,ColorTable) ->
draw_col(Image, X, 0, H,MaxIter,ColorTable),
draw_cols(Image, X+1, Ht, T,MaxIter,ColorTable);
draw_cols(_Image, _X, _, [],_MaxIter,_ColorTable) ->
done.
draw_col(_Image, _X,_Y,[{no_first_color,0}],_MaxIter,_ColorTable) ->
done;
draw_col(Image, X,Y,[{Color,1}|T],MaxIter,ColorTable) ->
gs:config(Image,[{pix_val,{{X,Y},
element(Color+1,ColorTable)}}]),
draw_col(Image, X,Y+1,T,MaxIter,ColorTable);
draw_col(Image, X,Y,[{Color,Height}|T],MaxIter,ColorTable) ->
gs:config(Image,[{pix_val,{{{X,Y},{X+1,Y+Height}},
element(Color+1,ColorTable)}}]),
draw_col(Image, X,Y+Height,T,MaxIter,ColorTable).
make_jobs(#state{width=W,height=H,range=R,
xmax=Xmax,ymax=Ymax,maxiter=MI}) ->
make_jobs(Xmax-R,Xmax,Ymax-R,Ymax,H,W,MI).
make_jobs(Xmin,Xmax,Ymin,Ymax,Ht,Wt,MaxIter) ->
NoJobs = Wt/?JOBWIDTH, % Each job is ?JOBWIDTH pixel-col
DX = (Xmax - Xmin)/NoJobs,
make_jobs(DX,Xmin,Xmax,#job{ymin=Ymin,ymax=Ymax,height=Ht,width=Wt/NoJobs,
maxiter=MaxIter},[]).
make_jobs(DX,Left,Xmax,JobSkel,Res) when Left =< Xmax ->
Right = Left + DX,
Job = JobSkel#job{left=Left,right=Right},
make_jobs(DX,Right,Xmax,JobSkel,[Job | Res]);
make_jobs(_DX,_Left,_Xmax,_JobSkel,Res) -> Res.
%%----------------------------------------------------------------------
%% A small process that refreshes the screen now and then.
%%----------------------------------------------------------------------
refresher(Image) ->
gs:config(Image,flush),
timer:apply_after(8000,mandel,refresher,[Image]).
%%-----------------------------------------------------------------
%% This is the server start function.
%%-----------------------------------------------------------------
start_server([ClientNode]) ->
register(mandel_server, self()),
erlang:monitor_node(ClientNode, true),
server_loop().
server_loop() ->
receive
{mandel_job, {Pid, Job}} ->
spawn_link(mandel, respond, [Pid, Job]),
server_loop()
end.
respond(Pid, Job) ->
Data = do_job(Job),
Pid ! {calculation_done, {node(), Data}}.
do_job(Job) ->
calculate_area(Job).
calculate_area(Job) ->
#job{ymin=Ymin,ymax=Ymax,height=Ht,width=Wt,left=Xmin,right=Xmax}=Job,
Job#job{data=x_loop(0,[],Wt,(Xmax-Xmin)/Wt,(Ymax-Ymin)/Ht,Xmin,Job)}.
x_loop(IX,Res,Wt,Dx,Dy,X,Job) when IX < Wt ->
#job{ymin=Ymin,height=Ht,maxiter=MaxIter}=Job,
Cols = y_loop(0,Ht,[],MaxIter,Dy,X,Ymin,no_first_color,0),
x_loop(IX+1,[Cols|Res],Wt,Dx,Dy,X+Dx,Job);
x_loop(_,Res,_,_,_,_,_) ->
Res.
y_loop(IY,Ht,Res,MaxIter,Dy,X,Y,PrevColor,NprevColor) when IY < Ht ->
Color = color_loop(1,MaxIter,0,0,0,0,X,Y),
if
Color == PrevColor ->
y_loop(IY+1,Ht,Res,MaxIter,Dy,X,Y+Dy,PrevColor,NprevColor+1);
true ->
y_loop(IY+1,Ht,[{PrevColor,NprevColor}|Res],MaxIter,
Dy,X,Y+Dy,Color,1)
end;
y_loop(_,_,Res,_,_,_,_,PC,N) ->
[{PC,N}|Res].
color_loop(Color,MaxIter,Za,Zb,Za2,Zb2,X,Y)
when Za2 + Zb2 < 4, Color < MaxIter->
Ztmp = Za2 - Zb2 + X,
ZbN = 2 * Za * Zb + Y,
color_loop(Color+1,MaxIter,Ztmp,ZbN,Ztmp * Ztmp,ZbN * ZbN,X,Y);
color_loop(MaxIter,MaxIter,_Za,_Zb,_Za2,_Zb2,_X,_Y) ->
0; % black
color_loop(Color,_,_,_,_,_,_,_) ->
Color.
%%----------------------------------------------------------------------
%% The "colormodel".
%%----------------------------------------------------------------------
make_color_table(MaxColors) ->
list_to_tuple([{0,0,0}|colors(MaxColors)]).
colors(Ncolors) ->
{A,B,C}=erlang:now(),
random:seed(A,B,C),
Colors = random_colors(Ncolors),
Colors2 = best_insert([hd(Colors)],tl(Colors)),
Colors2.
random_colors(0) -> [];
random_colors(N) ->
R = random:uniform(256)-1,
G = random:uniform(256)-1,
B = random:uniform(256)-1,
[{R,G,B}|random_colors(N-1)].
best_insert(Sorted,[RGB|Unsorted]) ->
best_insert(insert_at(best_pos(RGB,Sorted),RGB,Sorted),Unsorted);
best_insert(Sorted,[]) -> Sorted.
insert_at(1,Elem,L) -> [Elem|L];
insert_at(N,Elem,[H|T]) -> [H|insert_at(N-1,Elem,T)].
best_pos(RGB, Sorted) ->
D = distances(RGB,Sorted),
pos_for_smallest_distance(D,1,1000,-1).
pos_for_smallest_distance([],_CurPos,_SmallestDist,Pos) -> Pos;
pos_for_smallest_distance([Dist|T],CurPos,SmallDist,_Pos)
when Dist < SmallDist ->
pos_for_smallest_distance(T,CurPos+1,Dist,CurPos);
pos_for_smallest_distance([_|T],CurPos,Smallest,Pos) ->
pos_for_smallest_distance(T,CurPos+1,Smallest,Pos).
distances(_RGB,[]) ->
[];
distances({R,G,B},[{R2,G2,B2}|T]) ->
[lists:max([abs(R-R2),abs(G-G2),abs(B-B2)])|distances({R,G,B},T)].
get_option(Option, Options, Default) ->
case gs:assq(Option, Options) of
{value, Val} -> Val;
false -> Default
end.