1########################################################
2# routines to set up, transform and manage weather tiles
3# Thorsten Renk, March 2011
4########################################################
5
6# function			purpose
7#
8# tile_management_loop		to decide if a tile is created, removed or considered current
9# generate_tile			to decide on orientation and type and set up all information for tile creation
10# remove_tile			to delete a tile by index
11# change_active_tile		to change the tile the aircraft is currently in and to generate neighbour info
12# copy_entry			to copy tile information from one node to another
13# create_neighbour		to set up information for a new neighbouring tile
14# create_neighbours		to initialize the 8 neighbours of the initial tile
15# buffer_loop			to manage the buffering of faraway clouds in an array
16# housekeeping_loop		to shift clouds from the scenery into the buffer
17# remove_impostors		to delete a ring of impostors to mimick distant clouds
18# create_impostors		to create a ring of impostors to mimick distant clouds
19# shadow_management_loop	to manage cloud shadow information
20# thunderstorm_management_loop	to manage information on thunderstorm position
21# lightning_strike		to get the timing for a lightning strike to the property tree
22# watchdog loop			(debug helping structure)
23# calc_geo			to get local Cartesian geometry for latitude conversion
24# get_lat			to get latitude from Cartesian coordinates
25# get_lon			to get longitude from Cartesian coordinates
26# relangle			to compute the relative angle between two directions, normalized to [0:180]
27# norm_relangle			to compute the relative angle between two directions, normalized to [0:360]
28# delete_from_vector		to delete an element 'n' from a vector
29
30# object			purpose
31#
32# cloud				to provide the data hash for the new cloud rendering system
33# cloudBuffer			to store a cloud in a Nasal buffer, to provide methods to move it
34# cloudScenery			to store info for clouds in scenery, to provide methods to move and evolve them
35# cloudImpostor			to provide the hash data for an impostor cloud sheet
36
37
38###################################
39# tile management loop
40###################################
41
42var tile_management_loop = func {
43
44
45if (local_weather.local_weather_running_flag == 0) {return;}
46
47var tNode = props.globals.getNode(lw~"tiles", 1).getChildren("tile");
48var viewpos = geo.aircraft_position(); # using viewpos here triggers massive tile ops for tower view...
49var code = getprop(lw~"tiles/tile[4]/code");
50var i = 0;
51var d_min = 100000.0;
52var i_min = 0;
53var current_visibility = local_weather.interpolated_conditions.visibility_m;
54var current_heading = getprop("orientation/heading-deg");
55var loading_flag = getprop(lw~"tmp/asymmetric-tile-loading-flag");
56var this_frame_action_flag = 0; # use this flag to avoid overlapping tile operations
57
58setsize(active_tile_list,0);
59#append(active_tile_list,0); # tile zero formally containing static objects is always active
60
61var distance_to_load = current_visibility + 10000.0;
62
63if (distance_to_load > 65000.0) {distance_to_load = 65000.0;}
64if (distance_to_load < 29000.0) {distance_to_load = 29000.0;}
65
66
67
68
69var distance_to_remove = distance_to_load + 20000.0;
70if (distance_to_remove > 65500.0) {distance_to_remove = 65500.0;}
71
72
73# check here if we have a new weather station if METAR is running
74
75if ((local_weather.metar_flag == 1) and (getprop(lw~"METAR/station-id") != getprop("/environment/metar/station-id")))
76	{
77	weather_tiles.set_METAR_weather_station();
78	}
79
80# compute the averaged framerate and see if cloud visibility needs to be adjusted
81
82if (local_weather.fps_control_flag == 1)
83	{
84	local_weather.fps_average = local_weather.fps_sum/local_weather.fps_samples;
85
86	# print("Average framerate: ", local_weather.fps_average);
87
88	local_weather.fps_sum = 0.0;
89	local_weather.fps_samples = 0;
90
91	if (local_weather.fps_average > 1.1 * local_weather.target_framerate)
92		{
93		var target_cloud_view_distance = cloud_view_distance * 1.1;
94		if (target_cloud_view_distance > 45000.0)
95			{target_cloud_view_distance = 45000.0;}
96		setprop(lw~"config/clouds-visible-range-m", target_cloud_view_distance);
97		}
98	if (local_weather.fps_average < 0.9 * local_weather.target_framerate)
99		{
100		var target_cloud_view_distance = cloud_view_distance * 0.9;
101		if (target_cloud_view_distance < 15000.0)
102			{target_cloud_view_distance = 15000.0;}
103		setprop(lw~"config/clouds-visible-range-m", target_cloud_view_distance);
104		}
105
106	}
107
108
109
110
111foreach (var t; tNode) {
112
113	var tpos = geo.Coord.new();
114	tpos.set_latlon(t.getNode("latitude-deg").getValue(),t.getNode("longitude-deg").getValue(),0.0);
115	var d = viewpos.distance_to(tpos);
116	if (d < d_min) {d_min = d; i_min = i;}
117	var flag = t.getNode("generated-flag").getValue();
118
119	if ((flag ==2) or (flag ==1)) {append(active_tile_list,t.getNode("tile-index").getValue());}
120
121	var dir = viewpos.course_to(tpos);
122	var d_load = distance_to_load;
123	var d_remove = distance_to_remove;
124	if (loading_flag == 1)
125		{
126		var angle = abs(dir-current_heading);
127		#if (i==7) {print(angle);}
128		if ((angle > 135.0) and (angle < 225.0))
129			{
130			d_load = 0.7 * d_load;
131			d_remove = 0.7 * d_remove;
132			}
133		}
134
135	# the tile needs to be generated, unless it already has been
136	# and if no other tile has been generated in this loop cycle
137	# and the thread and convective system are idle
138	# (we want to avoid overlapping tile generation)
139
140	if ((d < d_load) and (flag==0) and (this_frame_action_flag == 0) and (getprop(lw~"tmp/thread-status") == "idle") and (getprop(lw~"tmp/convective-status") == "idle") and (getprop(lw~"tmp/presampling-status") == "idle"))
141		{
142		this_frame_action_flag = 1;
143		setprop(lw~"tiles/tile-counter",getprop(lw~"tiles/tile-counter")+1);
144		if (local_weather.debug_output_flag == 1)
145			{print("Building tile unique index ",getprop(lw~"tiles/tile-counter"), " in direction ",i);}
146		append(active_tile_list,getprop(lw~"tiles/tile-counter"));
147
148		if (local_weather.dynamics_flag == 1)
149			{
150			var quadtree = [];
151			weather_dynamics.generate_quadtree_structure(0, quadtree);
152			append(weather_dynamics.cloudQuadtrees,quadtree);
153			}
154
155		t.getNode("generated-flag").setValue(1);
156		t.getNode("timestamp-sec").setValue(weather_dynamics.time_lw);
157		t.getNode("tile-index",1).setValue(getprop(lw~"tiles/tile-counter"));
158		generate_tile(code, tpos.lat(), tpos.lon(),i);
159
160		}
161
162	if ((d > d_remove) and (flag == 2) and (this_frame_action_flag == 0)) # the tile needs to be deleted if it exists
163		{
164		if (local_weather.debug_output_flag == 1)
165			{print("Removing tile, unique index ", t.getNode("tile-index").getValue()," direction ",i);}
166		remove_tile(t.getNode("tile-index").getValue());
167		t.getNode("generated-flag").setValue(0);
168		this_frame_action_flag = 1;
169		}
170	i = i + 1;
171	} # end foreach
172
173	#print("Minimum distance to: ",i_min);
174
175	var presampling_status = getprop(lw~"tmp/presampling-status");
176	var convective_status = getprop(lw~"tmp/convective-status");
177	var thread_status = getprop(lw~"tmp/thread-status");
178
179	if ((presampling_status == "idle") and (convective_status == "idle") and (thread_status == "idle"))
180		{
181		var system_status = "idle";
182		}
183	else
184		{system_status = "computing";}
185
186	#  and (this_frame_action_flag == 0) and (presampling_status == "idle") and (convective_status=="idle"))
187
188	# check if we've entered a different tile and if no operation is in progress
189
190	# var gen_flag = tNode[i_min].getNode("generated-flag").getValue();
191	if ((i_min != 4) and (system_status == "idle"))
192		{
193
194		var gen_flag = tNode[i_min].getNode("generated-flag").getValue();
195		if (gen_flag != 2)  {
196			logprint(DEV_WARN, "Tile direction ",i_min, " not generated!");
197			logprint(DEV_WARN, "Flag: ",gen_flag);
198		}
199
200		if (local_weather.debug_output_flag == 1)
201			{logprint(LOG_DEBUG, "Changing active tile to direction ", i_min);}
202		change_active_tile(i_min);
203
204		}
205
206
207
208if (getprop(lw~"tile-loop-flag") ==1) {settimer(tile_management_loop, 4.0);}
209
210}
211
212
213###################################
214# tile generation call
215###################################
216
217var generate_tile = func (code, lat, lon, dir_index) {
218
219
220# the code should never be NIL, but this appears to happen under certain conditions
221# so just to be on the safe side make sure it is set to current tile code if
222# it actually is NIL
223
224if (code == "")
225	{
226	print("No tile code - falling back on default!");
227	code = getprop(lw~"tiles/code");
228	}
229
230setprop(lw~"tiles/tmp/latitude-deg", lat);
231setprop(lw~"tiles/tmp/longitude-deg",lon);
232setprop(lw~"tiles/tmp/code",code);
233setprop(lw~"tiles/tmp/dir-index",dir_index);
234
235
236# do windspeed and orientation before presampling check, but test not to do it again
237
238if (((local_weather.presampling_flag == 1) and (getprop(lw~"tmp/presampling-status") == "idle")) or (local_weather.presampling_flag == 0))
239	{
240
241	var alpha = getprop(lw~"tmp/tile-orientation-deg");
242
243
244	if ((local_weather.wind_model_flag == 2) or (local_weather.wind_model_flag ==4))
245		{
246
247		if (local_weather.metar_flag == 0)
248			{
249			alpha = alpha + 2.0 * (rand()-0.5) * 10.0;
250			# account for the systematic spin of weather systems around a low pressure
251			# core dependent on hemisphere
252			if (lat >0.0) {alpha = alpha -3.0;}
253			else {alpha = alpha +3.0;}
254			}
255		else
256			{
257
258			var step = 20.0;
259
260			var alpha_test = getprop("/environment/metar/base-wind-dir-deg");
261
262
263			if (local_weather.debug_output_flag == 1)
264				{print("alpha: ", alpha, " alpha_test: ", alpha_test, " relangle: ", relangle(alpha, alpha_test));}
265
266
267			var coordinate_rotation_angle = norm_relangle(alpha, alpha_test);
268
269
270			if (coordinate_rotation_angle < 45.0)
271				{
272				var system_rotation_angle = 0;
273				var displacement_angle = coordinate_rotation_angle;
274				}
275			else if (coordinate_rotation_angle < 135.0)
276				{
277				var system_rotation_angle = 90.0;
278				var displacement_angle = coordinate_rotation_angle - 90.0;
279				}
280			else if (coordinate_rotation_angle < 225.0)
281				{
282				var system_rotation_angle = 180.0;
283				var displacement_angle = coordinate_rotation_angle - 180.0;
284				}
285			else if (coordinate_rotation_angle < 315.0)
286				{
287				var system_rotation_angle = 270.0;
288				var displacement_angle = coordinate_rotation_angle - 270.0;
289				}
290			else
291				{
292				var system_rotation_angle = 0;
293				var displacement_angle = coordinate_rotation_angle - 360.0;
294				}
295
296
297			if (displacement_angle < -step)
298				{
299				print("Coordinate rotation by more than ",step," deg... compensating");
300				displacement_angle = -step;
301				}
302			else if (displacement_angle > step)
303				{
304				print("Coordinate rotation by more than ",step," deg... compensating");
305				displacement_angle = step;
306				}
307
308			alpha = alpha + system_rotation_angle + displacement_angle;
309
310
311
312			#if (relangle(alpha, alpha_test) > step)
313			#	{
314			#	print("Coordinate rotation by more than ",step," deg... compensating");
315			#	if (relangle(alpha + step, alpha_test) < relangle(alpha-step, alpha_test))
316			#		{
317			#		alpha = alpha + step;
318			#		}
319			#	else
320			#		{
321			#		alpha = alpha - step;
322			#		}
323			#	}
324			#else
325			#	{
326			#	alpha = alpha_test;
327			#	}
328			}
329
330
331
332
333		setprop(lw~"tmp/tile-orientation-deg",alpha);
334
335		# compute the new windspeed
336
337		var windspeed = 0;
338		if (local_weather.metar_flag == 0)
339			{
340			windspeed = getprop(lw~"tmp/windspeed-kt");
341			windspeed = windspeed + 2.0 * (rand()-0.5) * 2.0;
342			if (windspeed < 0) {windspeed = rand();}
343			}
344		else
345			{
346			var boundary_correction = 1.0/local_weather.get_slowdown_fraction();
347			windspeed = boundary_correction * getprop("/environment/metar/base-wind-speed-kt");
348			}
349
350		setprop(lw~"tmp/windspeed-kt",windspeed);
351
352		# store the tile orientation and wind strength in an array for fast processing
353
354		append(weather_dynamics.tile_wind_direction, alpha);
355		append(weather_dynamics.tile_wind_speed, windspeed);
356
357		}
358	else if (local_weather.wind_model_flag ==5) # alpha and windspeed are calculated
359		{
360		var res = local_weather.wind_interpolation(lat,lon,0.0);
361
362		var step = 20.0;
363		var alpha_test = res[0];
364
365
366		if (local_weather.debug_output_flag == 1)
367				{print("alpha: ", alpha, " alpha_test: ", alpha_test, " relangle: ", relangle(alpha, alpha_test));}
368
369
370		var coordinate_rotation_angle = norm_relangle(alpha, alpha_test);
371
372		#print("Norm_relangle : ", norm_relangle(alpha, alpha_test));
373
374
375		if (coordinate_rotation_angle < 45.0)
376			{
377			var system_rotation_angle = 0;
378			var displacement_angle = coordinate_rotation_angle;
379			}
380		else if (coordinate_rotation_angle < 135.0)
381			{
382			var system_rotation_angle = 90.0;
383			var displacement_angle = coordinate_rotation_angle - 90.0;
384			}
385		else if (coordinate_rotation_angle < 225.0)
386			{
387			var system_rotation_angle = 180.0;
388			var displacement_angle = coordinate_rotation_angle - 180.0;
389			}
390		else if (coordinate_rotation_angle < 315.0)
391			{
392			var system_rotation_angle = 270.0;
393			var displacement_angle = coordinate_rotation_angle - 270.0;
394			}
395		else
396			{
397			var system_rotation_angle = 0;
398			var displacement_angle = coordinate_rotation_angle - 360.0;
399			}
400
401		#print("Displacement angle: ", displacement_angle);
402
403		if (displacement_angle < -step)
404			{
405			print("Coordinate rotation by more than ",step," deg... compensating");
406			displacement_angle = -step;
407			}
408		else if (displacement_angle > step)
409			{
410			print("Coordinate rotation by more than ",step," deg... compensating");
411			displacement_angle = step;
412			}
413
414		#print("Normalized displacement angle: ", displacement_angle);
415
416		alpha = alpha + system_rotation_angle + displacement_angle;
417
418		#print("alpha_out: ", alpha);
419
420		#if (relangle(alpha, alpha_test) > step)
421		#	{
422		#	print("Coordinate rotation by more than ",step," deg... compensating");
423		#	if (relangle(alpha + step, alpha_test) < relangle(alpha-step, alpha_test))
424		#		{
425		#		alpha = alpha + step;
426		#		}
427		#	else
428		#		{
429		#		alpha = alpha - step;
430		#		}
431		#	}
432		#else
433		#	{
434		#	alpha = alpha_test;
435		#	}
436
437		#alpha = alpha_test;
438
439
440
441		setprop(lw~"tmp/tile-orientation-deg",alpha);
442		var windspeed = res[1];
443		setprop(lw~"tmp/windspeed-kt",windspeed);
444
445		append(weather_dynamics.tile_wind_direction,res[0]);
446		append(weather_dynamics.tile_wind_speed,res[1]);
447
448		}
449
450
451	props.globals.getNode(lw~"tiles").getChild("tile",dir_index).getNode("orientation-deg").setValue(alpha);
452	}
453
454
455
456# now see if we need to presample the terrain
457
458if ((local_weather.presampling_flag == 1) and (getprop(lw~"tmp/presampling-status") == "idle") and (compat_layer.features.terrain_presampling_active == 0))
459	{
460	local_weather.terrain_presampling_start(lat, lon, 1000, 40000, getprop(lw~"tmp/tile-orientation-deg"));
461	return;
462	}
463else if (compat_layer.features.terrain_presampling_active == 1)# we have hard-coded values available and use those
464	{local_weather.terrain_presampling_analysis();}
465
466
467if (local_weather.debug_output_flag == 1)
468	{print("Current tile type: ", code);}
469
470if (getprop(lw~"tmp/tile-management") == "repeat tile")
471	{
472	if (code == "altocumulus_sky"){weather_tiles.set_altocumulus_tile();}
473	else if (code == "broken_layers") {weather_tiles.set_broken_layers_tile();}
474	else if (code == "stratus") {weather_tiles.set_overcast_stratus_tile();}
475	else if (code == "cumulus_sky") {weather_tiles.set_fair_weather_tile();}
476	else if (code == "gliders_sky") {weather_tiles.set_gliders_sky_tile();}
477	else if (code == "blue_thermals") {weather_tiles.set_blue_thermals_tile();}
478	else if (code == "summer_rain") {weather_tiles.set_summer_rain_tile();}
479	else if (code == "high_pressure_core") {weather_tiles.set_high_pressure_core_tile();}
480	else if (code == "high_pressure") {weather_tiles.set_high_pressure_tile();}
481	else if (code == "high_pressure_border") {weather_tiles.set_high_pressure_border_tile();}
482	else if (code == "low_pressure_border") {weather_tiles.set_low_pressure_border_tile();}
483	else if (code == "low_pressure") {weather_tiles.set_low_pressure_tile();}
484	else if (code == "low_pressure_core") {weather_tiles.set_low_pressure_core_tile();}
485	else if (code == "cold_sector") {weather_tiles.set_cold_sector_tile();}
486	else if (code == "warm_sector") {weather_tiles.set_warm_sector_tile();}
487	else if (code == "tropical_weather") {weather_tiles.set_tropical_weather_tile();}
488	else if (code == "thunderstorms") {weather_tiles.set_thunderstorms_tile();}
489	else if (code == "test") {weather_tiles.set_4_8_stratus_tile();}
490	else
491		{
492		print("Repeat tile not implemented with this tile type!");
493		setprop("/sim/messages/pilot", "Local weather: Repeat tile not implemented with this tile type!");
494		}
495	}
496else if (getprop(lw~"tmp/tile-management") == "realistic weather")
497	{
498	var rn = rand() * getprop(lw~"config/large-scale-persistence");
499
500	if (code == "low_pressure_core")
501		{
502		if (rn > 0.1) {weather_tiles.set_low_pressure_core_tile();}
503		else {weather_tiles.set_low_pressure_tile();}
504		}
505	else if (code == "low_pressure")
506		{
507		if (rn > 0.1) {weather_tiles.set_low_pressure_tile();}
508		else if (rn > 0.05) {weather_tiles.set_low_pressure_core_tile();}
509		else {weather_tiles.set_low_pressure_border_tile();}
510		}
511	else if (code == "low_pressure_border")
512		{
513		if (rn > 0.2) {weather_tiles.set_low_pressure_border_tile();}
514		else if (rn > 0.15) {weather_tiles.set_cold_sector_tile();}
515		else if (rn > 0.1) {weather_tiles.set_warm_sector_tile();}
516		else if (rn > 0.05) {weather_tiles.set_low_pressure_tile();}
517		else {weather_tiles.set_high_pressure_border_tile();}
518		}
519	else if (code == "high_pressure_border")
520		{
521		if (rn > 0.2) {weather_tiles.set_high_pressure_border_tile();}
522		else if (rn > 0.15) {weather_tiles.set_cold_sector_tile();}
523		else if (rn > 0.1) {weather_tiles.set_warm_sector_tile();}
524		else if (rn > 0.05) {weather_tiles.set_high_pressure_tile();}
525		else {weather_tiles.set_low_pressure_border_tile();}
526		}
527	else if (code == "high_pressure")
528		{
529		if (rn > 0.1) {weather_tiles.set_high_pressure_tile();}
530		else if (rn > 0.05) {weather_tiles.set_high_pressure_border_tile();}
531		else {weather_tiles.set_high_pressure_core_tile();}
532		}
533	else if (code == "high_pressure_core")
534		{
535		if (rn > 0.1) {weather_tiles.set_high_pressure_core_tile();}
536		else {weather_tiles.set_high_pressure_tile();}
537		}
538	else if (code == "cold_sector")
539		{
540		if (rn > 0.15) {weather_tiles.set_cold_sector_tile();}
541		else if (rn > 0.1)
542			{
543			if ((dir_index ==0) or (dir_index ==1) or (dir_index==2))
544				{weather_tiles.set_warmfront1_tile();}
545			else if ((dir_index ==3) or (dir_index ==5))
546				{weather_tiles.set_cold_sector_tile();}
547			else if ((dir_index ==6) or (dir_index ==7) or (dir_index==8))
548				{weather_tiles.set_coldfront_tile();}
549			}
550		else if (rn > 0.05) {weather_tiles.set_low_pressure_border_tile();}
551		else {weather_tiles.set_high_pressure_border_tile();}
552		}
553	else if (code == "warm_sector")
554		{
555		if (rn > 0.15) {weather_tiles.set_warm_sector_tile();}
556		else if (rn > 0.1)
557			{
558			if ((dir_index ==0) or (dir_index ==1) or (dir_index==2))
559				{weather_tiles.set_coldfront_tile();}
560			else if ((dir_index ==3) or (dir_index ==5))
561				{weather_tiles.set_warm_sector_tile();}
562			else if ((dir_index ==6) or (dir_index ==7) or (dir_index==8))
563				{weather_tiles.set_warmfront4_tile();}
564			}
565		else if (rn > 0.05) {weather_tiles.set_low_pressure_border_tile();}
566		else {weather_tiles.set_high_pressure_border_tile();}
567		}
568	else if (code == "warmfront1")
569		{
570		if ((dir_index ==0) or (dir_index ==1) or (dir_index==2))
571			{weather_tiles.set_warmfront2_tile();}
572		else if ((dir_index ==3) or (dir_index ==5))
573			{
574			if (rand() > 0.15)
575				{weather_tiles.set_warmfront1_tile();}
576			else
577				{weather_tiles.set_high_pressure_border_tile();}
578			}
579		else if ((dir_index ==6) or (dir_index ==7) or (dir_index==8))
580			{weather_tiles.set_cold_sector_tile();}
581		}
582	else if (code == "warmfront2")
583		{
584		if ((dir_index ==0) or (dir_index ==1) or (dir_index==2))
585			{weather_tiles.set_warmfront3_tile();}
586		if ((dir_index ==3) or (dir_index ==5))
587			{
588			if (rand() > 0.15)
589				{weather_tiles.set_warmfront2_tile();}
590			else
591				{weather_tiles.set_high_pressure_border_tile();}
592			}
593		if ((dir_index ==6) or (dir_index ==7) or (dir_index==8))
594			{weather_tiles.set_warmfront1_tile();}
595		}
596	else if (code == "warmfront3")
597		{
598		if ((dir_index ==0) or (dir_index ==1) or (dir_index==2))
599			{weather_tiles.set_warmfront4_tile();}
600		if ((dir_index ==3) or (dir_index ==5))
601			{
602			if (rand() > 0.15)
603				{weather_tiles.set_warmfront3_tile();}
604			else
605				{weather_tiles.set_low_pressure_border_tile();}
606			}
607		if ((dir_index ==6) or (dir_index ==7) or (dir_index==8))
608			{weather_tiles.set_warmfront2_tile();}
609		}
610	else if (code == "warmfront4")
611		{
612		if ((dir_index ==0) or (dir_index ==1) or (dir_index==2))
613			{weather_tiles.set_warm_sector_tile();}
614		if ((dir_index ==3) or (dir_index ==5))
615			{
616			if (rand() > 0.15)
617				{weather_tiles.set_warmfront4_tile();}
618			else
619				{weather_tiles.set_low_pressure_tile();}
620			}
621		if ((dir_index ==6) or (dir_index ==7) or (dir_index==8))
622			{weather_tiles.set_warmfront3_tile();}
623		}
624	else if (code == "coldfront")
625		{
626		if ((dir_index ==0) or (dir_index ==1) or (dir_index==2))
627			{weather_tiles.set_cold_sector_tile();}
628		else if ((dir_index ==3) or (dir_index ==5))
629			{
630			if (rand() > 0.15)
631				{weather_tiles.set_coldfront_tile();}
632			else
633				{weather_tiles.set_high_pressure_border_tile();}
634			}
635		else if ((dir_index ==6) or (dir_index ==7) or (dir_index==8))
636			{weather_tiles.set_warm_sector_tile();}
637		}
638	else
639		{
640		print("Realistic weather not implemented with this tile type!");
641		setprop("/sim/messages/pilot", "Local weather: Realistic weather not implemented with this tile type!");
642		}
643
644	} # end if mode == realistic weather
645else if (getprop(lw~"tmp/tile-management") == "METAR")
646	{
647	weather_tiles.set_METAR_tile();
648	}
649}
650
651
652###################################
653# tile removal call
654###################################
655
656var remove_tile = func (index) {
657
658# remove tile from active list
659
660var s = size(active_tile_list);
661
662for (var j = 0; j < s; j=j+1)
663	{
664	if (index == active_tile_list[j])
665		{
666		active_tile_list = delete_from_vector(active_tile_list,j);
667		break;
668		}
669	}
670
671settimer( func { props.globals.getNode("local-weather/clouds", 1).removeChild("tile",index) },100);
672
673
674var effectNode = props.globals.getNode("local-weather/effect-volumes").getChildren("effect-volume");
675
676var ecount = 0;
677
678for (var i = 0; i < local_weather.n_effectVolumeArray; i = i + 1)
679	{
680	ev = local_weather.effectVolumeArray[i];
681	if (ev.index == index)
682		{
683		local_weather.effectVolumeArray = delete_from_vector(local_weather.effectVolumeArray,i);
684		local_weather.n_effectVolumeArray = local_weather.n_effectVolumeArray - 1;
685		i = i - 1;
686		ecount = ecount + 1;
687		}
688	else if (ev.index == 0) # use the opportunity to check if static effects should also be removed
689		{
690		if (ev.get_distance() > 80000.0)
691			{
692			local_weather.effectVolumeArray = delete_from_vector(local_weather.effectVolumeArray,i);
693			local_weather.n_effectVolumeArray = local_weather.n_effectVolumeArray - 1;
694			i = i - 1;
695			ecount = ecount + 1;
696			}
697		}
698	}
699
700
701setprop(lw~"effect-volumes/number",getprop(lw~"effect-volumes/number")- ecount);
702
703# set placement indices to zero to reinitiate search for free positions
704
705setprop(lw~"clouds/placement-index",0);
706setprop(lw~"clouds/model-placement-index",0);
707setprop(lw~"effect-volumes/effect-placement-index",0);
708
709# remove quadtree structures
710
711if (local_weather.dynamics_flag ==1)
712	{
713	settimer( func {setsize(weather_dynamics.cloudQuadtrees[index-1],0);},1.0);
714	}
715
716# rebuild effect volume vector
717
718local_weather.assemble_effect_array();
719
720if (local_weather.wxradar_support_flag == 1)
721	{
722	local_weather.remove_wxradar_echos();
723	}
724
725}
726
727
728
729###################################
730# active tile change and neighbour
731# recomputation
732###################################
733
734var change_active_tile = func (index) {
735
736var t = props.globals.getNode(lw~"tiles").getChild("tile",index,0);
737
738var lat = t.getNode("latitude-deg").getValue();
739var lon = t.getNode("longitude-deg").getValue();
740# var alpha = getprop(lw~"tmp/tile-orientation-deg");
741
742var alpha_old = getprop(lw~"tiles/tile[4]/orientation-deg");
743var alpha = t.getNode("orientation-deg").getValue();
744
745var coordinate_rotation_angle = norm_relangle(alpha_old, alpha);
746
747if (coordinate_rotation_angle < 45.0)
748	{
749	var system_rotation_angle = 0;
750	var displacement_angle = coordinate_rotation_angle;
751	}
752else if (coordinate_rotation_angle < 135.0)
753	{
754	var system_rotation_angle = 90.0;
755	var displacement_angle = coordinate_rotation_angle - 90.0;
756	}
757else if (coordinate_rotation_angle < 225.0)
758	{
759	var system_rotation_angle = 180.0;
760	var displacement_angle = coordinate_rotation_angle - 180.0;
761	}
762else if (coordinate_rotation_angle < 315.0)
763	{
764	var system_rotation_angle = 270.0;
765	var displacement_angle = coordinate_rotation_angle - 270.0;
766	}
767else
768	{
769	var system_rotation_angle = 0;
770	var displacement_angle = coordinate_rotation_angle - 360.0;
771	}
772
773alpha = alpha_old + displacement_angle;
774
775if (index == 0)
776	{
777	copy_entry(4,8);
778	copy_entry(3,7);
779	copy_entry(1,5);
780	copy_entry(0,4);
781	create_neighbour(lat,lon,0,alpha);
782	create_neighbour(lat,lon,1,alpha);
783	create_neighbour(lat,lon,2,alpha);
784	create_neighbour(lat,lon,3,alpha);
785	create_neighbour(lat,lon,6,alpha);
786	}
787else if (index == 1)
788	{
789	copy_entry(3,6);
790	copy_entry(4,7);
791	copy_entry(5,8);
792	copy_entry(0,3);
793	copy_entry(1,4);
794	copy_entry(2,5);
795	create_neighbour(lat,lon,0,alpha);
796	create_neighbour(lat,lon,1,alpha);
797	create_neighbour(lat,lon,2,alpha);
798	}
799else if (index == 2)
800	{
801	copy_entry(4,6);
802	copy_entry(1,3);
803	copy_entry(2,4);
804	copy_entry(5,7);
805	create_neighbour(lat,lon,0,alpha);
806	create_neighbour(lat,lon,1,alpha);
807	create_neighbour(lat,lon,2,alpha);
808	create_neighbour(lat,lon,5,alpha);
809	create_neighbour(lat,lon,8,alpha);
810	}
811else if (index == 3)
812	{
813	copy_entry(1,2);
814	copy_entry(4,5);
815	copy_entry(7,8);
816	copy_entry(0,1);
817	copy_entry(3,4);
818	copy_entry(6,7);
819	create_neighbour(lat,lon,0,alpha);
820	create_neighbour(lat,lon,3,alpha);
821	create_neighbour(lat,lon,6,alpha);
822	}
823else if (index == 5)
824	{
825	copy_entry(1,0);
826	copy_entry(4,3);
827	copy_entry(7,6);
828	copy_entry(2,1);
829	copy_entry(5,4);
830	copy_entry(8,7);
831	create_neighbour(lat,lon,2,alpha);
832	create_neighbour(lat,lon,5,alpha);
833	create_neighbour(lat,lon,8,alpha);
834	}
835else if (index == 6)
836	{
837	copy_entry(4,2);
838	copy_entry(3,1);
839	copy_entry(6,4);
840	copy_entry(7,5);
841	create_neighbour(lat,lon,0,alpha);
842	create_neighbour(lat,lon,3,alpha);
843	create_neighbour(lat,lon,6,alpha);
844	create_neighbour(lat,lon,7,alpha);
845	create_neighbour(lat,lon,8,alpha);
846	}
847else if (index == 7)
848	{
849	copy_entry(3,0);
850	copy_entry(4,1);
851	copy_entry(5,2);
852	copy_entry(6,3);
853	copy_entry(7,4);
854	copy_entry(8,5);
855	create_neighbour(lat,lon,6,alpha);
856	create_neighbour(lat,lon,7,alpha);
857	create_neighbour(lat,lon,8,alpha);
858	}
859else if (index == 8)
860	{
861	copy_entry(4,0);
862	copy_entry(7,3);
863	copy_entry(8,4);
864	copy_entry(5,1);
865	create_neighbour(lat,lon,2,alpha);
866	create_neighbour(lat,lon,5,alpha);
867	create_neighbour(lat,lon,6,alpha);
868	create_neighbour(lat,lon,7,alpha);
869	create_neighbour(lat,lon,8,alpha);
870	}
871
872
873
874
875if (system_rotation_angle > 0.0)
876	{
877	if (local_weather.debug_output_flag == 1)
878		{print("Rotating coordinate system by ", system_rotation_angle, " degrees");}
879
880	# create a buffer entry for rotation, this is deleted in the rotation routine
881
882	create_neighbour(lat, lon, 9, alpha);
883	rotate_tile_scheme(system_rotation_angle);
884	}
885
886# ready the system for creating impostors
887
888impostor_trigger = 1;
889
890}
891
892
893###################################
894# rotate tile scheme
895###################################
896
897var rotate_tile_scheme = func (angle) {
898
899if (angle < 45.0)
900	{
901	return;
902	}
903else if (angle < 135)
904	{
905
906
907	copy_entry(2,9);
908	copy_entry(8,2);
909	copy_entry(6,8);
910	copy_entry(0,6);
911	copy_entry(9,0);
912	copy_entry(5,9);
913	copy_entry(7,5);
914	copy_entry(3,7);
915	copy_entry(1,3);
916	copy_entry(9,1);
917
918	props.globals.getNode(lw~"tiles").removeChild("tile",9);
919	}
920else if (angle < 225)
921	{
922	copy_entry(8,9);
923	copy_entry(0,8);
924	copy_entry(9,0);
925
926	copy_entry(7,9);
927	copy_entry(1,7);
928	copy_entry(9,1);
929
930	copy_entry(6,9);
931	copy_entry(2,6);
932	copy_entry(9,2);
933
934	copy_entry(5,9);
935	copy_entry(3,5);
936	copy_entry(9,3);
937
938	props.globals.getNode(lw~"tiles").removeChild("tile",9);
939	}
940else if (angle < 315)
941	{
942	copy_entry(0,9);
943	copy_entry(6,0);
944	copy_entry(8,6);
945	copy_entry(2,8);
946	copy_entry(9,2);
947	copy_entry(3,9);
948	copy_entry(7,3);
949	copy_entry(5,7);
950	copy_entry(1,5);
951	copy_entry(9,1);
952
953	props.globals.getNode(lw~"tiles").removeChild("tile",9);
954	}
955else
956	{
957	return;
958	}
959}
960
961
962#####################################
963# copy tile info in neighbour matrix
964#####################################
965
966var copy_entry = func (from_index, to_index) {
967
968var tNode = props.globals.getNode(lw~"tiles");
969
970var f = tNode.getChild("tile",from_index,0);
971var t = tNode.getChild("tile",to_index,0);
972
973t.getNode("latitude-deg").setValue(f.getNode("latitude-deg").getValue());
974t.getNode("longitude-deg").setValue(f.getNode("longitude-deg").getValue());
975t.getNode("generated-flag").setValue(f.getNode("generated-flag").getValue());
976t.getNode("tile-index").setValue(f.getNode("tile-index").getValue());
977t.getNode("timestamp-sec").setValue(f.getNode("timestamp-sec").getValue());
978t.getNode("orientation-deg").setValue(f.getNode("orientation-deg").getValue());
979t.getNode("code").setValue(f.getNode("code").getValue());
980
981
982}
983
984#####################################
985# create adjacent tile coordinates
986#####################################
987
988var create_neighbour = func (blat, blon, index, alpha) {
989
990var x = 0.0;
991var y = 0.0;
992var phi = alpha * math.pi/180.0;
993
994calc_geo(blat);
995
996if ((index == 0) or (index == 3) or (index == 6)) {x =-40000.0;}
997if ((index == 2) or (index == 5) or (index == 8)) {x = 40000.0;}
998
999if ((index == 0) or (index == 1) or (index == 2)) {y = 40000.0;}
1000if ((index == 6) or (index == 7) or (index == 8)) {y = -40000.0;}
1001
1002var t = props.globals.getNode(lw~"tiles").getChild("tile",index,1);
1003
1004# use the last built tile code as default, in case a tile isn't formed when reached,
1005# the code is not empty but has a plausible value
1006
1007var default_code = getprop(lw~"tiles/code");
1008
1009t.getNode("latitude-deg",1).setValue(blat + get_lat(x,y,phi));
1010t.getNode("longitude-deg",1).setValue(blon + get_lon(x,y,phi));
1011t.getNode("generated-flag",1).setValue(0);
1012t.getNode("tile-index",1).setValue(-1);
1013t.getNode("code",1).setValue(default_code);
1014t.getNode("timestamp-sec",1).setValue(weather_dynamics.time_lw);
1015t.getNode("orientation-deg",1).setValue(0.0);
1016}
1017
1018#####################################
1019# find the 8 adjacent tile coordinates
1020# after the initial setup call
1021#####################################
1022
1023var create_neighbours = func (blat, blon, alpha)	{
1024
1025var x = 0.0;
1026var y = 0.0;
1027var phi = alpha * math.pi/180.0;
1028
1029calc_geo(blat);
1030
1031x = -40000.0; y = 40000.0;
1032setprop(lw~"tiles/tile[0]/latitude-deg",blat + get_lat(x,y,phi));
1033setprop(lw~"tiles/tile[0]/longitude-deg",blon + get_lon(x,y,phi));
1034setprop(lw~"tiles/tile[0]/generated-flag",0);
1035setprop(lw~"tiles/tile[0]/tile-index",-1);
1036setprop(lw~"tiles/tile[0]/code","");
1037setprop(lw~"tiles/tile[0]/timestamp-sec",weather_dynamics.time_lw);
1038setprop(lw~"tiles/tile[0]/orientation-deg",alpha);
1039
1040x = 0.0; y = 40000.0;
1041setprop(lw~"tiles/tile[1]/latitude-deg",blat + get_lat(x,y,phi));
1042setprop(lw~"tiles/tile[1]/longitude-deg",blon + get_lon(x,y,phi));
1043setprop(lw~"tiles/tile[1]/generated-flag",0);
1044setprop(lw~"tiles/tile[1]/tile-index",-1);
1045setprop(lw~"tiles/tile[1]/code","");
1046setprop(lw~"tiles/tile[1]/timestamp-sec",weather_dynamics.time_lw);
1047setprop(lw~"tiles/tile[1]/orientation-deg",alpha);
1048
1049x = 40000.0; y = 40000.0;
1050setprop(lw~"tiles/tile[2]/latitude-deg",blat + get_lat(x,y,phi));
1051setprop(lw~"tiles/tile[2]/longitude-deg",blon + get_lon(x,y,phi));
1052setprop(lw~"tiles/tile[2]/generated-flag",0);
1053setprop(lw~"tiles/tile[2]/tile-index",-1);
1054setprop(lw~"tiles/tile[2]/code","");
1055setprop(lw~"tiles/tile[2]/timestamp-sec",weather_dynamics.time_lw);
1056setprop(lw~"tiles/tile[2]/orientation-deg",alpha);
1057
1058x = -40000.0; y = 0.0;
1059setprop(lw~"tiles/tile[3]/latitude-deg",blat + get_lat(x,y,phi));
1060setprop(lw~"tiles/tile[3]/longitude-deg",blon + get_lon(x,y,phi));
1061setprop(lw~"tiles/tile[3]/generated-flag",0);
1062setprop(lw~"tiles/tile[3]/tile-index",-1);
1063setprop(lw~"tiles/tile[3]/code","");
1064setprop(lw~"tiles/tile[3]/timestamp-sec",weather_dynamics.time_lw);
1065setprop(lw~"tiles/tile[3]/orientation-deg",alpha);
1066
1067# this is the current tile
1068x = 0.0; y = 0.0;
1069setprop(lw~"tiles/tile[4]/latitude-deg",blat + get_lat(x,y,phi));
1070setprop(lw~"tiles/tile[4]/longitude-deg",blon + get_lon(x,y,phi));
1071setprop(lw~"tiles/tile[4]/generated-flag",1);
1072setprop(lw~"tiles/tile[4]/tile-index",1);
1073setprop(lw~"tiles/tile[4]/code","");
1074setprop(lw~"tiles/tile[4]/timestamp-sec",weather_dynamics.time_lw);
1075setprop(lw~"tiles/tile[4]/orientation-deg",getprop(lw~"tmp/tile-orientation-deg"));
1076
1077
1078x = 40000.0; y = 0.0;
1079setprop(lw~"tiles/tile[5]/latitude-deg",blat + get_lat(x,y,phi));
1080setprop(lw~"tiles/tile[5]/longitude-deg",blon + get_lon(x,y,phi));
1081setprop(lw~"tiles/tile[5]/generated-flag",0);
1082setprop(lw~"tiles/tile[5]/tile-index",-1);
1083setprop(lw~"tiles/tile[5]/code","");
1084setprop(lw~"tiles/tile[5]/timestamp-sec",weather_dynamics.time_lw);
1085setprop(lw~"tiles/tile[5]/orientation-deg",alpha);
1086
1087x = -40000.0; y = -40000.0;
1088setprop(lw~"tiles/tile[6]/latitude-deg",blat + get_lat(x,y,phi));
1089setprop(lw~"tiles/tile[6]/longitude-deg",blon + get_lon(x,y,phi));
1090setprop(lw~"tiles/tile[6]/generated-flag",0);
1091setprop(lw~"tiles/tile[6]/tile-index",-1);
1092setprop(lw~"tiles/tile[6]/code","");
1093setprop(lw~"tiles/tile[6]/timestamp-sec",weather_dynamics.time_lw);
1094setprop(lw~"tiles/tile[6]/orientation-deg",alpha);
1095
1096x = 0.0; y = -40000.0;
1097setprop(lw~"tiles/tile[7]/latitude-deg",blat + get_lat(x,y,phi));
1098setprop(lw~"tiles/tile[7]/longitude-deg",blon + get_lon(x,y,phi));
1099setprop(lw~"tiles/tile[7]/generated-flag",0);
1100setprop(lw~"tiles/tile[7]/tile-index",-1);
1101setprop(lw~"tiles/tile[7]/code","");
1102setprop(lw~"tiles/tile[7]/timestamp-sec",weather_dynamics.time_lw);
1103setprop(lw~"tiles/tile[7]/orientation-deg",alpha);
1104
1105x = 40000.0; y = -40000.0;
1106setprop(lw~"tiles/tile[8]/latitude-deg",blat + get_lat(x,y,phi));
1107setprop(lw~"tiles/tile[8]/longitude-deg",blon + get_lon(x,y,phi));
1108setprop(lw~"tiles/tile[8]/generated-flag",0);
1109setprop(lw~"tiles/tile[8]/tile-index",-1);
1110setprop(lw~"tiles/tile[8]/code","");
1111setprop(lw~"tiles/tile[8]/timestamp-sec",weather_dynamics.time_lw);
1112setprop(lw~"tiles/tile[8]/orientation-deg",alpha);
1113}
1114
1115
1116
1117
1118###############################
1119# housekeeping loop
1120###############################
1121
1122var housekeeping_loop = func (index, index1) {
1123
1124if (local_weather.local_weather_running_flag == 0) {return;}
1125
1126# supply a few properties which are used more generally
1127
1128lwObserverLat = getprop("/position/latitude-deg");
1129lwObserverLon = getprop("/position/longitude-deg");
1130
1131var heading = getprop("/orientation/heading-deg");
1132var offset =  getprop("/sim/current-view/heading-offset-deg");
1133lwViewDir = (heading - offset) * math.pi/180.0 ;
1134setsize(lwViewVec,0);
1135append(lwViewVec,math.cos(lwViewDir));
1136append(lwViewVec,-math.sin(lwViewDir));
1137
1138
1139var n = 5;
1140var n_max = size(cloudSceneryArray);
1141n_cloudSceneryArray = n_max;
1142var s = size(active_tile_list);
1143
1144var m_max = size(cloudArray);
1145
1146setprop(lw~"clouds/cloud-scenery-count",n_max+m_max);
1147
1148# don't do anything as long as the array is empty
1149
1150if ((n_max == 0) and (m_max == 0)) # nothing to do, loop over
1151	{if (getprop(lw~"housekeeping-loop-flag") ==1) {settimer( func {housekeeping_loop(index, index1)}, 0);} return;}
1152
1153# parse the flags
1154
1155
1156# now process the Scenery array
1157
1158if (index > n_max-1) {index = 0;}
1159
1160var i_max = index + n;
1161if (i_max > n_max) {i_max = n_max;}
1162
1163for (var i = index; i < i_max; i = i+1)
1164	{
1165	var c = cloudSceneryArray[i];
1166
1167	var flag = 0;
1168
1169	for (var j = 0; j < s; j = j+1)
1170		{
1171		if (active_tile_list[j] == c.index) {flag = 1; break;}
1172		if (c.index == 0) {flag =1; break;} # clouds in tile index 0 are special
1173		}
1174
1175	if (flag == 0)
1176		{
1177		c.removeNodes();
1178		cloudSceneryArray = delete_from_vector(cloudSceneryArray,i);
1179		i = i -1; i_max = i_max - 1; n_max = n_max - 1;
1180		n_cloudSceneryArray = n_cloudSceneryArray -1;
1181		continue;
1182		}
1183	}
1184
1185
1186# now process the hard coded cloud array and see a tile has been removed
1187
1188if (index1 > m_max-1) {index1 = 0;}
1189
1190var j_max = index1 + n;
1191if (j_max > m_max) {j_max = m_max;}
1192
1193for (var j = index1; j < j_max; j = j+1)
1194	{
1195	var c = cloudArray[j];
1196
1197	var flag = 0;
1198
1199	for (var k = 0; k < s; k = k+1)
1200		{
1201		if (active_tile_list[k] == c.index) {flag = 1; break;}
1202		}
1203
1204	if (flag == 0)
1205		{
1206		c.remove();
1207		cloudArray = delete_from_vector(cloudArray,j);
1208		j = j -1; j_max = j_max - 1; m_max = m_max - 1;
1209		continue;
1210		}
1211	}
1212
1213if (getprop(lw~"housekeeping-loop-flag") ==1) {settimer( func {housekeeping_loop(i,j)}, 0);}
1214}
1215
1216
1217###############################
1218# impostor handline routines
1219###############################
1220
1221var impostor_trigger = 0;
1222
1223var impostor_type_map = {"low_pressure_core" : "Nimbus", "low_pressure" : "broken", "low_pressure_border": "broken", "high_pressure_border" : "scattered", "high_pressure" : "few", "high_pressure_core": "few"};
1224
1225var remove_impostors = func {
1226
1227foreach (entry;cloudImpostorArray)
1228	{
1229	entry.removeNodes();
1230	}
1231setsize(cloudImpostorArray,0);
1232}
1233
1234var create_impostors = func {
1235
1236var visibility = getprop("/environment/visibility-m");
1237var cloud_range = getprop("/sim/rendering/clouds3d-vis-range");
1238
1239if ((visibility < 80000.0) or (cloud_range < 70000.0)) {return;}
1240
1241if (visibility < cloud_range) {var range = visibility;}
1242else	{var range = cloud_range;}
1243
1244var n = int ((range - 60000.0)/40000.0);
1245
1246var lat = getprop(lw~"tiles/tile[4]/latitude-deg");
1247var lon = getprop(lw~"tiles/tile[4]/longitude-deg");
1248var alpha = getprop(lw~"tiles/tile[4]/orientation-deg");
1249var code = getprop(lw~"tiles/tile[4]/code");
1250var index = getprop(lw~"tiles/tile[4]/tile-index");
1251var alt_offset = getprop(lw~"tmp/tile-alt-offset-ft");
1252var alt = weather_dynamics.tile_convective_altitude[index-1] + 1000.0 + alt_offset;
1253
1254if (contains(impostor_type_map,code)) {var type = impostor_type_map[code];}
1255else {var type = "scattered";}
1256
1257if (local_weather.debug_output_flag == 1)
1258	{
1259	printf("Creating impostor ring...");
1260	print(lat, " ", lon, " ", alt, " ", alpha, " ", type, " ", n);
1261	}
1262
1263weather_tiles.create_impostor_ring(lat, lon, alt, alpha, type, n);
1264}
1265
1266##################################
1267# Thunderstorm positon management
1268##################################
1269
1270var thunderstorm_management_loop = func {
1271
1272if (local_weather.local_weather_running_flag == 0) {return;}
1273
1274# compute some general-purpose stuff for the loop
1275
1276var eyeLat = lwObserverLat;
1277var eyeLon = lwObserverLon;
1278
1279var n = size(thunderstormArray);
1280
1281#print("We have ",n," storms.");
1282
1283for (var i = 0; i < n; i=i+1)
1284	{
1285	var tstorm = thunderstormArray[i];
1286
1287	var rn = rand();
1288	#if (i == 0) {rn = 0;} else {rn = 1;}
1289	if (rn < tstorm.strength)
1290		{
1291		#print("Lightning strike storm ", i,"!");
1292
1293		var diffx = -(tstorm.lat - eyeLat) * local_weather.lat_to_m;
1294		var diffy = (tstorm.lon - eyeLon) * local_weather.lon_to_m ;
1295		var offset_x = -3000.0 + rand() * 6000.0;
1296		var offset_y = -3000.0 + rand() * 6000.0;
1297
1298		var dist = math.sqrt(diffx * diffx + diffy * diffy) ;
1299
1300		setprop("/environment/lightning/lightning-pos-x", diffx + offset_x);
1301		setprop("/environment/lightning/lightning-pos-y", diffy + offset_y);
1302		setprop("/environment/lightning/lightning-range", tstorm.size);
1303		setprop("/local-weather/lightning/latitude-deg", tstorm.lat - offset_x * local_weather.m_to_lat);
1304		setprop("/local-weather/lightning/longitude-deg", tstorm.lon + offset_y * local_weather.m_to_lon);
1305		setprop("/local-weather/lightning/altitude-ft", tstorm.alt);
1306
1307		var rn = rand();
1308		var type = 0;
1309		if (rn > 0.7) {type = 1;}
1310		else if (rn > 0.3) {type = 2;}
1311		setprop("/local-weather/lightning/model-index", type);
1312
1313		lightning_strike();
1314
1315		if (dist > 50000.0)
1316			{
1317			thunderstormArray = delete_from_vector(thunderstormArray,i);
1318			print("Removing storm ", i);
1319			break;
1320			}
1321		}
1322
1323
1324	}
1325
1326
1327if (getprop(lw~"thunderstorm-loop-flag") ==1) {settimer( func {thunderstorm_management_loop()}, 1.0);}
1328}
1329
1330var lightning_strike = func {
1331
1332var rn = rand();
1333
1334var repeat = 1;
1335
1336if (rn > 0.5) {repeat = 2;}
1337
1338var duration = 0.1 + 0.1 * rand();
1339var strength = 0.5 + 1.0 * rand();
1340
1341setprop("/environment/lightning/flash", strength);
1342settimer( func{ setprop("/environment/lightning/flash", 0.0);}, duration);
1343
1344var duration1 = 0.1 +  0.1 * rand();
1345
1346if (repeat == 2)
1347	{
1348	settimer( func{ setprop("/environment/lightning/flash", strength);}, duration + 0.1);
1349	settimer( func{ setprop("/environment/lightning/flash", 0.0);}, duration + 0.1 + duration1);
1350	}
1351
1352}
1353
1354###############################
1355# Cloud shadow management
1356###############################
1357
1358
1359
1360var shadow_management_loop = func (index) {
1361
1362if (local_weather.local_weather_running_flag == 0) {return;}
1363
1364var n = 50;
1365var n_max = size(cloudShadowCandidateArray);
1366var s = size(active_tile_list);
1367
1368# don't do anything as long as the array is empty
1369
1370if (n_max == 0)  # nothing to do, loop over
1371	{
1372	setprop("/local-weather/cloud-shadows/cloud-shadow-flag",0);
1373	if (getprop(lw~"shadow-loop-flag") ==1) {settimer( func {shadow_management_loop(index)}, 0);}
1374	return;
1375	}
1376else
1377	{
1378	setprop("/local-weather/cloud-shadows/cloud-shadow-flag",1);
1379	}
1380
1381
1382
1383# compute some general-purpose stuff for the loop
1384
1385var eyeLat = lwObserverLat;
1386var eyeLon = lwObserverLon;
1387
1388#var sec_to_rad = 2.0 * math.pi/86400;
1389var time = getprop("/sim/time/utc/day-seconds") + getprop("/sim/time/local-offset");
1390var sun_angle = getprop("/sim/time/sun-angle-rad");
1391var cloud_alt = getprop("/environment/ground-haze-thickness-m");
1392var offset_mag = cloud_alt * math.tan(sun_angle);
1393var offset_x = -math.cos(time * local_weather.sec_to_rad) * math.cos(eyeLat) * offset_mag;
1394var offset_y = -math.sin(time * local_weather.sec_to_rad) * math.sin(eyeLat) * offset_mag;
1395
1396
1397# indexing, we don't want to run through hundreds of candidates per frame
1398
1399if (index > n_max-1) {index = 0;}
1400
1401var i_max = index + n;
1402if (i_max > n_max) {i_max = n_max;}
1403
1404for (var i = index; i < i_max; i = i+1)
1405	{
1406	var shadow = cloudShadowCandidateArray[i];
1407
1408	# housekeeping - delete shadows from deleted tiles
1409
1410	var flag = 0;
1411
1412	for (var j = 0; j < s; j = j+1)
1413		{
1414		if (active_tile_list[j] == shadow.index) {flag = 1; break;}
1415		if (shadow.index == 0) {flag =1; break;} # clouds in tile index 0 are special
1416		}
1417	if (flag == 0)
1418		{
1419		#print("Shadow management housekeeping!");
1420		cloudShadowCandidateArray = delete_from_vector(cloudShadowCandidateArray,i);
1421		i = i -1; i_max = i_max - 1; n_max = n_max - 1;
1422		continue;
1423		}
1424
1425	# find nearest shadows
1426
1427	if (shadow.shadow_flag == 0)
1428		{
1429		var diffx = (shadow.lat - eyeLat) * local_weather.lat_to_m + offset_x;
1430		var diffy = -(shadow.lon - eyeLon) * local_weather.lon_to_m + offset_y;
1431		var dist = math.sqrt(diffx * diffx + diffy * diffy) ;
1432		if (getprop("/local-weather/cloud-shadows/cloud-shadow-fov-flag")==1)
1433			{
1434			var viewDotPos = (diffx * lwViewVec[0] + diffy * lwViewVec[1])/dist;
1435			if (viewDotPos <0.7) {dist = dist -(viewDotPos - 0.7) * 10000.0;}
1436			}
1437		if (dist < cloudShadowMaxDist)
1438			{
1439			#print("Shadow management:");
1440			#print("Max. dist is now: ", dist);
1441			#print("Adding cloud");
1442			#print("Array size is now: ", size(cloudShadowArray));
1443			#print("CloudShadowMinIndex is now: ", cloudShadowMinIndex);
1444			cloudShadowMaxDist = dist;
1445			if (size(cloudShadowArray)>cloudShadowArraySize-1)
1446				{
1447				cloudShadowArray[cloudShadowMinIndex].shadow_flag = 0;
1448				cloudShadowArray = delete_from_vector(cloudShadowArray,cloudShadowMinIndex);
1449				}
1450			append(cloudShadowArray,shadow);
1451			shadow.shadow_flag = 1;
1452			break;
1453			}
1454		}
1455	}
1456
1457var index_max = -1;
1458var index_min = -1;
1459var dist_max = -1.0;
1460var dist_min = 1000000.0;
1461
1462var counter = 0;
1463
1464var diffx = 0.0;
1465var diffy = 0.0;
1466
1467foreach(s; cloudShadowArray)
1468	{
1469	diffx = (s.lat - eyeLat) * local_weather.lat_to_m + offset_x;
1470	diffy = -(s.lon - eyeLon) * local_weather.lon_to_m + offset_y;
1471
1472	var dist = math.sqrt(diffx*diffx + diffy*diffy);
1473	if (getprop("/local-weather/cloud-shadows/cloud-shadow-fov-flag")==1)
1474		{
1475		var viewDotPos = (diffx * lwViewVec[0] + diffy * lwViewVec[1])/dist;
1476		if (viewDotPos <0.7) {dist = dist  -(viewDotPos - 0.7) * 10000.0;}
1477		}
1478	if (dist > dist_max) {dist_max = dist; index_max = counter;}
1479	if (dist < dist_min) {dist_min = dist; index_min = counter;}
1480
1481	setprop("/local-weather/cloud-shadows/cloudpos-x["~counter~"]",int(diffx) + s.size);
1482	setprop("/local-weather/cloud-shadows/cloudpos-y["~counter~"]",int(diffy) + s.strength );
1483	counter = counter+1;
1484	}
1485
1486	# now write out the closest cloud for the detail effects
1487	if (index_min != -1) {
1488	    var s = cloudShadowArray[index_min];
1489
1490	    diffx = (s.lat - eyeLat) * local_weather.lat_to_m + offset_x;
1491	    diffy = -(s.lon - eyeLon) * local_weather.lon_to_m + offset_y;
1492
1493	    setprop("/local-weather/cloud-shadows/nearest-cloudpos-x",int(diffx) + s.size);
1494	    setprop("/local-weather/cloud-shadows/nearest-cloudpos-y",int(diffy) + s.strength );
1495
1496
1497	    #print("Dist_max:", dist_max, " index_max: ", index_max);
1498	    cloudShadowMinIndex = index_max;
1499	    if (dist_max > 0.0) {cloudShadowMaxDist = dist_max;}
1500	}
1501
1502    settimer( func {shadow_management_loop(i)}, 0);
1503}
1504
1505
1506
1507###############################
1508# watchdog loop for debugging
1509###############################
1510
1511
1512var watchdog_loop = func {
1513
1514var winddir = getprop("/environment/wind-from-heading-deg");
1515var windspeed = getprop("/environment/wind-speed-kt");
1516
1517print (windspeed, " ", winddir);
1518
1519
1520if (0==1)
1521{var tNode = props.globals.getNode(lw~"tiles", 1).getChildren("tile");
1522
1523var i = 0;
1524
1525print("====================");
1526
1527var viewpos = geo.aircraft_position();
1528var n_stations = size(local_weather.weatherStationArray);
1529
1530var sum_T = 0.0;
1531var sum_p = 0.0;
1532var sum_D = 0.0;
1533var sum_norm = 0.0;
1534
1535var sum_wind = [0,0];
1536
1537var wsize = size(local_weather.windIpointArray);
1538
1539var alt = getprop("position/altitude-ft");
1540
1541for (var i = 0; i < wsize; i=i+1) {
1542
1543
1544	var w = local_weather.windIpointArray[i];
1545
1546	var wpos = geo.Coord.new();
1547	wpos.set_latlon(w.lat,w.lon,1000.0);
1548
1549
1550
1551	var d = viewpos.distance_to(wpos);
1552	if (d <100.0) {d = 100.0;} # to prevent singularity at zero
1553
1554	sum_norm = sum_norm + (1./d);
1555
1556	var res = local_weather.wind_altitude_interpolation(alt,w);
1557
1558	sum_wind = local_weather.add_vectors(sum_wind[0], sum_wind[1], res[0], (res[1]/d));
1559
1560	print(i, " dir: ", res[0], " speed: ", res[1], " d: ",d);
1561	}
1562
1563print("dir_int: ", sum_wind[0], " speed_int: ", sum_wind[1]/sum_norm);
1564}
1565
1566
1567if (0==1)
1568{
1569for (var i = 0; i < n_stations; i=i+1) {
1570
1571	s = local_weather.weatherStationArray[i];
1572
1573
1574	var stpos = geo.Coord.new();
1575	stpos.set_latlon(s.lat,s.lon,0.0);
1576
1577	var d = viewpos.distance_to(stpos);
1578	if (d <100.0) {d = 100.0;} # to prevent singularity at zero
1579
1580	sum_norm = sum_norm + 1./d * s.weight;
1581
1582	sum_T = sum_T + (s.T/d) * s.weight;
1583	sum_D = sum_D + (s.D/d) * s.weight;
1584	sum_p = sum_p + (s.p/d) * s.weight;
1585
1586	print(i, " p: ", s.p, " T: ", s.T, " D: ", s.D, " d: ",d);
1587
1588	}
1589
1590print("p_int: ", sum_p/sum_norm, " T_int: ", sum_T/sum_norm, " D_int: ", sum_D/sum_norm);
1591}
1592
1593if (0==1)
1594{
1595
1596foreach(t; tNode)
1597	{
1598	var code = t.getNode("code").getValue();
1599	var index = t.getNode("tile-index").getValue();
1600	var flag = t.getNode("generated-flag").getValue();
1601	var alpha = t.getNode("orientation-deg").getValue();
1602
1603	print(i,": code: ", code, " unique id: ", index, " flag: ", flag, " alpha: ",alpha);
1604
1605	i = i + 1;
1606	}
1607print("alpha: ",getprop(lw~"tmp/tile-orientation-deg"));
1608
1609var lat = getprop("/position/latitude-deg");
1610var lon = getprop("/position/longitude-deg");
1611
1612var res = local_weather.wind_interpolation(lat,lon,0.0);
1613
1614print("Wind: ", res[0], " tile alpha: ", getprop(lw~"tiles/tile[4]/orientation-deg"));
1615print("Mismatch: ", relangle(res[0], getprop(lw~"tiles/tile[4]/orientation-deg")));
1616}
1617
1618
1619#print("====================");
1620
1621settimer(watchdog_loop, 4.0);
1622}
1623
1624
1625
1626###################
1627# global variables
1628###################
1629
1630# these already exist in different namespace, but for ease of typing we define them here as well
1631
1632var lat_to_m = 110952.0; # latitude degrees to meters
1633var m_to_lat = 9.01290648208234e-06; # meters to latitude degrees
1634var ft_to_m = 0.30480;
1635var m_to_ft = 1.0/ft_to_m;
1636var inhg_to_hp = 33.76389;
1637var hp_to_inhg = 1.0/inhg_to_hp;
1638var lon_to_m = 0.0; #local_weather.lon_to_m;
1639var m_to_lon = 0.0; # local_weather.m_to_lon;
1640var lw = "/local-weather/";
1641var cloud_shadow_flag = 0;
1642
1643var cloud_view_distance = getprop(lw~"config/clouds-visible-range-m");
1644
1645var modelArrays = [];
1646var active_tile_list = [];
1647var thunderstormArray = [];
1648
1649# a bunch of variables to be updated per frame used by different
1650# routines, managed by the housekeeping loop
1651
1652var lwObserverLat = 0.0;
1653var lwObserverLon = 0.0;
1654var lwViewDir = 0.0;
1655var lwViewVec = [];
1656var lwTileIndex = 0;
1657
1658
1659#####################################################
1660# hashes to manage clouds in scenery or in the buffer
1661#####################################################
1662
1663
1664
1665var cloudBufferArray = [];
1666
1667
1668var cloudBuffer = {
1669	new: func(lat, lon, alt, path, orientation, index, type) {
1670	        var c = { parents: [cloudBuffer] };
1671	        c.lat = lat;
1672		c.lon = lon;
1673		c.alt = alt;
1674		c.path = path;
1675		c.orientation = orientation;
1676		c.index = index;
1677		c.type = type;
1678	        return c;
1679	},
1680	get_distance: func {
1681		var pos = geo.aircraft_position();
1682		var cpos = geo.Coord.new();
1683		cpos.set_latlon(me.lat,me.lon,0.0);
1684		return pos.distance_to(cpos);
1685	},
1686	get_course: func {
1687		var pos = geo.aircraft_position();
1688		var cpos = geo.Coord.new();
1689		cpos.set_latlon(me.lat,me.lon,0.0);
1690		return pos.course_to(cpos);
1691	},
1692	show: func {
1693		print("lat: ",me.lat," lon: ",me.lon," alt: ",me.alt);
1694		print("path: ",me.path);
1695
1696	},
1697	move: func {
1698		var windfield = weather_dynamics.get_windfield(me.index);
1699		var dt = weather_dynamics.time_lw - me.timestamp;
1700		me.lat = me.lat + windfield[1] * dt * local_weather.m_to_lat;
1701		me.lon = me.lon + windfield[0] * dt * local_weather.m_to_lon;
1702		me.timestamp = weather_dynamics.time_lw;
1703	},
1704
1705};
1706
1707
1708var cloudImpostorArray = [];
1709
1710var cloudImpostor = {
1711	new: func(modelNode) {
1712	        var c = { parents: [cloudImpostor] };
1713		c.modelNode = modelNode;
1714	        return c;
1715	},
1716	removeNodes: func {
1717		me.modelNode.remove();
1718	},
1719};
1720
1721
1722var cloudShadowArray = [];
1723var cloudShadowCandidateArray = [];
1724var cloudShadowArraySize = 20;
1725var cloudShadowMaxDist = 100000.0;
1726var cloudShadowMinIndex = -1;
1727
1728var cloudShadow = {
1729	new: func (lat, lon, size, strength) {
1730			var s = {parents: [cloudShadow] };
1731		s.lat = lat;
1732		s.lon = lon;
1733		s.size = size;
1734		s.strength = strength;
1735		s.shadow_flag = 0;
1736		return s;
1737	},
1738};
1739
1740var thunderstormHash = {
1741	new: func (lat, lon, alt, size, strength) {
1742			var t = {parents: [thunderstormHash] };
1743		t.lat = lat;
1744		t.lon = lon;
1745		t.alt = alt;
1746		t.size = size;
1747		t.strength = strength;
1748		return t;
1749	},
1750};
1751
1752
1753var cloudSceneryArray = [];
1754var n_cloudSceneryArray = 0;
1755
1756var cloudScenery = {
1757	new: func(index, type, cloudNode, modelNode) {
1758	        var c = { parents: [cloudScenery] };
1759		c.index = index;
1760		c.type = type;
1761		c.cloudNode = cloudNode;
1762		c.modelNode = modelNode;
1763		c.calt = cloudNode.getNode("position/altitude-ft");
1764		c.clat = cloudNode.getNode("position/latitude-deg");
1765		c.clon = cloudNode.getNode("position/longitude-deg");
1766		c.alt = c.calt.getValue();
1767		c.lat = c.clat.getValue();
1768		c.lon = c.clon.getValue();
1769	        return c;
1770	},
1771	removeNodes: func {
1772		me.modelNode.remove();
1773		me.cloudNode.remove();
1774	},
1775	to_buffer: func {
1776		var path = me.modelNode.getNode("path").getValue();
1777		var orientation = me.cloudNode.getNode("orientation/true-heading-deg").getValue();
1778		var b = cloudBuffer.new(me.lat, me.lon, me.alt, path, orientation, me.index, me.type);
1779
1780		if (local_weather.dynamics_flag == 1)
1781			{
1782			b.timestamp = me.timestamp;
1783
1784			if (me.type !=0) # Cumulus clouds get some extra info
1785				{
1786				b.flt = me.flt;
1787				b.rel_alt = me.rel_alt;
1788				b.evolution_timestamp = me.evolution_timestamp;
1789				}
1790			}
1791
1792		me.removeNodes();
1793		return b;
1794	},
1795	get_distance: func {
1796		var pos = geo.aircraft_position();
1797		var cpos = geo.Coord.new();
1798		var lat = me.clat.getValue();
1799		var lon = me.clon.getValue();
1800		cpos.set_latlon(lat,lon,0.0);
1801		return pos.distance_to(cpos);
1802	},
1803	get_course: func {
1804		var pos = geo.aircraft_position();
1805		var cpos = geo.Coord.new();
1806		var lat = me.clat.getValue();
1807		var lon = me.clon.getValue();
1808		cpos.set_latlon(lat,lon,0.0);
1809		return pos.course_to(cpos);
1810	},
1811	get_altitude: func {
1812		return me.calt.getValue();
1813	},
1814	correct_altitude: func {
1815		var lat = me.lat;
1816		var lon = me.lon;
1817		var convective_alt = weather_dynamics.tile_convective_altitude[me.index-1] + local_weather.alt_20_array[me.index-1];
1818		var elevation = compat_layer.get_elevation(lat, lon);
1819
1820		if (local_weather.detailed_terrain_interaction_flag == 1)
1821			{
1822			var phi = local_weather.get_wind_direction(me.index) * math.pi/180.0;
1823			var grad = local_weather.get_terrain_gradient(lat, lon, elevation, phi, 1000.0);
1824			}
1825		else
1826			{var grad = 0.0;}
1827
1828		var alt_new = local_weather.get_convective_altitude(convective_alt, elevation, me.index, grad);
1829		me.target_alt = alt_new + me.rel_alt;
1830	},
1831	correct_altitude_and_age: func {
1832		var lat = me.lat;
1833		var lon = me.lon;
1834		var convective_alt = weather_dynamics.tile_convective_altitude[me.index-1] + local_weather.alt_20_array[me.index-1];
1835
1836		# get terrain elevation and landcover
1837
1838		var elevation = -1.0; var p_cover = 0.2;# defaults if there is no info
1839		var info = geodinfo(lat, lon);
1840		if (info != nil)
1841			{
1842			elevation = info[0] * local_weather.m_to_ft;
1843			if (info[1] != nil)
1844				{
1845         			var landcover = info[1].names[0];
1846	 			if (contains(local_weather.landcover_map,landcover)) {p_cover = local_weather.landcover_map[landcover];}
1847				else {p_cover = 0.2;}
1848				}
1849			}
1850
1851		if (local_weather.detailed_terrain_interaction_flag == 1)
1852			{
1853			var phi = local_weather.get_wind_direction(me.index) * math.pi/180.0;
1854			var grad = local_weather.get_terrain_gradient(lat, lon, elevation, phi, 1000.0);
1855			var lee_bias = local_weather.get_lee_bias(grad);
1856			}
1857		else
1858			{
1859			var grad = 0.0;
1860			var lee_bias = 1.0;
1861			}
1862
1863		# correct the altitude
1864		var alt_new = local_weather.get_convective_altitude(convective_alt, elevation, me.index, grad);
1865		me.target_alt = alt_new + me.rel_alt;
1866
1867		# correct fractional lifetime based on terrain below
1868
1869		var current_lifetime = math.sqrt(p_cover * lee_bias)/math.sqrt(0.35) * weather_dynamics.cloud_convective_lifetime_s;
1870		var fractional_increase = (weather_dynamics.time_lw - me.evolution_timestamp)/current_lifetime;
1871		me.flt = me.flt + fractional_increase;
1872		me.evolution_timestamp = weather_dynamics.time_lw;
1873	},
1874	to_target_alt: func {
1875		if (me.type ==0) {return;}
1876		var alt_diff = me.target_alt - me.alt;
1877		if (alt_diff == 0.0) {return;}
1878		var max_vertical_movement_ft = weather_dynamics.dt_lw * weather_dynamics.cloud_max_vertical_speed_fts;
1879		if (abs(alt_diff) < max_vertical_movement_ft)
1880			{
1881			me.alt = me.target_alt;
1882			}
1883		else if (alt_diff < 0)
1884			{
1885			me.alt = me.alt -max_vertical_movement_ft;
1886			}
1887		else
1888			{
1889			me.alt = me.alt + max_vertical_movement_ft;
1890			}
1891		setprop(lw~"clouds/tile["~me.index~"]/cloud["~me.write_index~"]/position/altitude-ft", me.alt);
1892	},
1893	move: func {
1894
1895
1896		var windfield = weather_dynamics.windfield;
1897		var dt = weather_dynamics.time_lw - me.timestamp;
1898
1899		me.lat = me.lat + windfield[1] * dt * local_weather.m_to_lat;
1900		me.lon = me.lon + windfield[0] * dt * local_weather.m_to_lon;
1901
1902		setprop(lw~"clouds/tile["~me.index~"]/cloud["~me.write_index~"]/position/latitude-deg", me.lat);
1903		setprop(lw~"clouds/tile["~me.index~"]/cloud["~me.write_index~"]/position/longitude-deg", me.lon);
1904
1905		me.timestamp = weather_dynamics.time_lw;
1906
1907	},
1908	show: func {
1909		var lat = me.clat.getValue();
1910		var lon = me.clon.getValue();
1911		var alt = me.calt.getValue();
1912
1913		var convective_alt = weather_dynamics.tile_convective_altitude[me.index-1] + local_weather.alt_20_array[me.index-1];
1914		var elevation = compat_layer.get_elevation(lat, lon);
1915		print("lat :", lat, " lon: ", lon, " alt: ", alt);
1916		print("path: ", me.modelNode.getNode("path").getValue());
1917		print("elevation: ", compat_layer.get_elevation(lat, lon), " cloudbase: ", convective_alt);
1918		if (me.type !=0) {print("relative: ", me.rel_alt, "target: ", me.target_alt);}
1919	},
1920};
1921
1922var cloudArray = [];
1923
1924var cloud = {
1925	new: func(type, subtype) {
1926	        var c = { parents: [cloud] };
1927		c.type = type;
1928		c.subtype = subtype;
1929		c.tracer_flag = 0;
1930	        return c;
1931	},
1932	remove: func {
1933		var p = props.Node.new({ "layer" : 0,
1934                         "index": me.cloud_index });
1935		fgcommand("del-cloud", p);
1936	},
1937	move: func {
1938		# this doesn't move a cloud in the scenery, but updates its position in internal space
1939		var windfield = local_weather.windfield;
1940		var dt = local_weather.time_lw - me.timestamp;
1941
1942		me.lat = me.lat + windfield[1] * dt * local_weather.m_to_lat;
1943		me.lon = me.lon + windfield[0] * dt * local_weather.m_to_lon;
1944		me.timestamp = weather_dynamics.time_lw;
1945
1946	},
1947	correct_altitude: func {
1948		var convective_alt = weather_dynamics.tile_convective_altitude[me.index-1] + local_weather.alt_20_array[me.index-1];
1949		var elevation = compat_layer.get_elevation(me.lat, me.lon);
1950
1951		if (local_weather.detailed_terrain_interaction_flag == 1)
1952			{
1953			var phi = local_weather.get_wind_direction(me.index) * math.pi/180.0;
1954			var grad = local_weather.get_terrain_gradient(me.lat, me.lon, elevation, phi, 1000.0);
1955			}
1956		else
1957			{var grad = 0.0;}
1958
1959		var alt_new = local_weather.get_convective_altitude(convective_alt, elevation, me.index, grad);
1960		var target_alt = alt_new + me.rel_alt;
1961
1962		var p = props.Node.new({ "layer" : 0,
1963                         "index": me.cloud_index,
1964 			 "lat-deg": me.lat,
1965                         "lon-deg": me.lon,
1966			 "alt-ft": target_alt
1967			 });
1968		fgcommand("move-cloud",p);
1969
1970		me.alt = target_alt;
1971	},
1972};
1973
1974
1975###################
1976# helper functions
1977###################
1978
1979var calc_geo = func(clat) {
1980
1981lon_to_m  = math.cos(clat*math.pi/180.0) * lat_to_m;
1982m_to_lon = 1.0/lon_to_m;
1983}
1984
1985var get_lat = func (x,y,phi) {
1986
1987return (y * math.cos(phi) - x * math.sin(phi)) * m_to_lat;
1988}
1989
1990var get_lon = func (x,y,phi) {
1991
1992return (x * math.cos(phi) + y * math.sin(phi)) * m_to_lon;
1993}
1994
1995
1996var relangle = func (alpha, beta) {
1997
1998var angdiff = abs (alpha - beta);
1999
2000if ((360.0 - angdiff) < angdiff)
2001	{angdiff = 360.0 - angdiff};
2002
2003return angdiff;
2004}
2005
2006
2007var norm_relangle = func (alpha, beta) {
2008
2009var angdiff = beta - alpha;
2010
2011while (angdiff < 0.0)
2012	{angdiff = angdiff + 360.0;}
2013
2014while (angdiff > 360.0)
2015	{angdiff = angdiff - 360.0;}
2016
2017if (angdiff == 360.0)
2018	{angdiff = 0.0;}
2019
2020return angdiff;
2021}
2022
2023
2024var delete_from_vector = func(vec, index) {
2025
2026var n = index+1;
2027
2028var vec_end = subvec(vec, n);
2029
2030setsize(vec, n-1);
2031return vec~vec_end;
2032
2033}
2034