1 /**
2  * @file
3  */
4 
5 /*
6 Copyright (C) 2002-2013 UFO: Alien Invasion.
7 
8 This program is free software; you can redistribute it and/or
9 modify it under the terms of the GNU General Public License
10 as published by the Free Software Foundation; either version 2
11 of the License, or (at your option) any later version.
12 
13 This program is distributed in the hope that it will be useful,
14 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
16 
17 See the GNU General Public License for more details.
18 
19 You should have received a copy of the GNU General Public License
20 along with this program; if not, write to the Free Software
21 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
22 
23 */
24 
25 #include "../ui_nodes.h"
26 #include "../ui_input.h"
27 #include "../ui_parse.h"
28 #include "../ui_behaviour.h"
29 #include "../ui_actions.h"
30 #include "../ui_render.h"
31 #include "ui_node_geoscape.h"
32 #include "../../renderer/r_framebuffer.h"
33 #include "../../renderer/r_geoscape.h"
34 #include "../../cl_shared.h"
35 #include "../../cgame/cl_game.h"
36 #include "../../input/cl_input.h"
37 #include "../../input/cl_keys.h"
38 
39 #define EXTRADATA_TYPE mapExtraData_t
40 #define EXTRADATA(node) UI_EXTRADATA(node, EXTRADATA_TYPE)
41 #define EXTRADATACONST(node) UI_EXTRADATACONST(node, EXTRADATA_TYPE)
42 
43 /**
44  * Status of the node
45  */
46 enum mapDragMode_t {
47 	/**
48 	 * No interaction
49 	 */
50 	MODE_NULL,
51 	/**
52 	 * Mouse captured to move the 2D geoscape
53 	 */
54 	MODE_SHIFT2DMAP,
55 	/**
56 	 * Mouse captured to move the 3D geoscape
57 	 */
58 	MODE_SHIFT3DMAP,
59 	/**
60 	 * Mouse captured to zoom on the geoscape
61 	 */
62 	MODE_ZOOMMAP
63 };
64 
65 /**
66  * Old X-location of the mouse, when the node is captured.
67  * It is related to a captured node (only one at a time), that why it is global.
68  */
69 static int oldMousePosX = 0;
70 
71 /**
72  * Old y-location of the mouse, when the node is captured.
73  * It is related to a captured node (only one at a time), that why it is global.
74  */
75 static int oldMousePosY = 0;
76 
77 /**
78  * Status of the node.
79  * It is related to a captured node (only one at a time), that why it is global.
80  */
81 static mapDragMode_t mode = MODE_NULL;
82 static const float ROTATE_SPEED	= 0.5f;
83 static const float GLOBE_ROTATE = -90.0f;
84 static const float SMOOTHING_STEP_2D = 0.02f;
85 static const float SMOOTHACCELERATION = 0.06f;		/**< the acceleration to use during a smooth motion (This affects the speed of the smooth motion) */
86 
87 static cvar_t* cl_3dmap;						/**< 3D geoscape or flat geoscape */
88 static cvar_t* cl_3dmapAmbient;
89 static cvar_t* cl_mapzoommax;
90 static cvar_t* cl_mapzoommin;
91 static cvar_t* cl_geoscape_overlay;
92 
93 // FIXME: don't make this static
94 static uiNode_t* geoscapeNode;
95 
96 /** this is the mask that is used to display day/night on (2d-)geoscape */
97 image_t* r_dayandnightTexture;
98 
99 image_t* r_radarTexture;				/**< radar texture */
100 image_t* r_xviTexture;					/**< XVI alpha mask texture */
101 
102 #define DAWN		0.03
103 
104 /**
105  * @brief smooth rotation of the 3D geoscape
106  * @note updates slowly values of @c angles and @c zoom so that its value goes to @c smoothFinalGlobeAngle
107  */
smoothRotate(uiNode_t * node)108 void uiGeoscapeNode::smoothRotate (uiNode_t* node)
109 {
110 	vec3_t diff;
111 	const float diffZoom = UI_MAPEXTRADATACONST(node).smoothFinalZoom - UI_MAPEXTRADATACONST(node).zoom;
112 
113 	VectorSubtract(UI_MAPEXTRADATACONST(node).smoothFinalGlobeAngle, UI_MAPEXTRADATACONST(node).angles, diff);
114 
115 	if (UI_MAPEXTRADATACONST(node).smoothDeltaLength > UI_MAPEXTRADATACONST(node).smoothDeltaZoom) {
116 		/* when we rotate (and zoom) */
117 		const float diffAngle = VectorLength(diff);
118 		const float epsilon = 0.1f;
119 		if (diffAngle > epsilon) {
120 			float rotationSpeed;
121 			/* Append the old speed to the new speed if this is the first half of a new rotation, but never exceed the max speed.
122 			 * This allows the globe to rotate at maximum speed when the button is held down. */
123 			rotationSpeed = sin(3.05f * diffAngle / UI_MAPEXTRADATACONST(node).smoothDeltaLength) * diffAngle;
124 			if (diffAngle / UI_MAPEXTRADATACONST(node).smoothDeltaLength > 0.5)
125 				rotationSpeed = std::min(diffAngle, UI_MAPEXTRADATACONST(node).curRotationSpeed + rotationSpeed * 0.5f);
126 
127 			UI_MAPEXTRADATA(node).curRotationSpeed = rotationSpeed;
128 			VectorScale(diff, SMOOTHACCELERATION / diffAngle * rotationSpeed, diff);
129 			VectorAdd(UI_MAPEXTRADATACONST(node).angles, diff, UI_MAPEXTRADATA(node).angles);
130 			UI_MAPEXTRADATA(node).zoom = UI_MAPEXTRADATACONST(node).zoom + SMOOTHACCELERATION * diffZoom / diffAngle * rotationSpeed;
131 			return;
132 		}
133 	} else {
134 		const float epsilonZoom = 0.01f;
135 		/* when we zoom only */
136 		if (fabsf(diffZoom) > epsilonZoom) {
137 			float speed;
138 			/* Append the old speed to the new speed if this is the first half of a new zoom operation, but never exceed the max speed.
139 			 * This allows the globe to zoom at maximum speed when the button is held down. */
140 			if (fabsf(diffZoom) / UI_MAPEXTRADATACONST(node).smoothDeltaZoom > 0.5f) {
141 				const float maxSpeed = SMOOTHACCELERATION * 2.0f;
142 				const float newSpeed = UI_MAPEXTRADATACONST(node).curZoomSpeed + sin(3.05 * (fabs(diffZoom) / UI_MAPEXTRADATACONST(node).smoothDeltaZoom)) * SMOOTHACCELERATION;
143 				speed = std::min(maxSpeed, newSpeed);
144 			} else {
145 				speed = sin(3.05 * (fabs(diffZoom) / UI_MAPEXTRADATACONST(node).smoothDeltaZoom)) * SMOOTHACCELERATION * 2.0;
146 			}
147 			UI_MAPEXTRADATA(node).curZoomSpeed = speed;
148 			UI_MAPEXTRADATA(node).zoom = UI_MAPEXTRADATACONST(node).zoom + diffZoom * speed;
149 			return;
150 		}
151 	}
152 
153 	/* if we reach this point, that means that movement is over */
154 	VectorCopy(UI_MAPEXTRADATACONST(node).smoothFinalGlobeAngle, UI_MAPEXTRADATA(node).angles);
155 	UI_MAPEXTRADATA(node).smoothRotation = false;
156 	UI_MAPEXTRADATA(node).zoom = UI_MAPEXTRADATACONST(node).smoothFinalZoom;
157 }
158 
159 /**
160  * @brief smooth translation of the 2D geoscape
161  * @note updates slowly values of @c center so that its value goes to @c smoothFinal2DGeoscapeCenter
162  * @note updates slowly values of @c zoom so that its value goes to @c ZOOM_LIMIT
163  */
smoothTranslate(uiNode_t * node)164 void uiGeoscapeNode::smoothTranslate (uiNode_t* node)
165 {
166 	const float dist1 = UI_MAPEXTRADATACONST(node).smoothFinal2DGeoscapeCenter[0] - UI_MAPEXTRADATACONST(node).center[0];
167 	const float dist2 = UI_MAPEXTRADATACONST(node).smoothFinal2DGeoscapeCenter[1] - UI_MAPEXTRADATACONST(node).center[1];
168 	const float length = sqrt(dist1 * dist1 + dist2 * dist2);
169 
170 	if (length < SMOOTHING_STEP_2D) {
171 		UI_MAPEXTRADATA(node).center[0] = UI_MAPEXTRADATACONST(node).smoothFinal2DGeoscapeCenter[0];
172 		UI_MAPEXTRADATA(node).center[1] = UI_MAPEXTRADATACONST(node).smoothFinal2DGeoscapeCenter[1];
173 		UI_MAPEXTRADATA(node).zoom = UI_MAPEXTRADATACONST(node).smoothFinalZoom;
174 		UI_MAPEXTRADATA(node).smoothRotation = false;
175 	} else {
176 		const float diffZoom = UI_MAPEXTRADATACONST(node).smoothFinalZoom - UI_MAPEXTRADATACONST(node).zoom;
177 		UI_MAPEXTRADATA(node).center[0] = UI_MAPEXTRADATACONST(node).center[0] + SMOOTHING_STEP_2D * dist1 / length;
178 		UI_MAPEXTRADATA(node).center[1] = UI_MAPEXTRADATACONST(node).center[1] + SMOOTHING_STEP_2D * dist2 / length;
179 		UI_MAPEXTRADATA(node).zoom = UI_MAPEXTRADATACONST(node).zoom + SMOOTHING_STEP_2D * diffZoom;
180 	}
181 }
182 
183 /**
184  * @brief Applies alpha values to the night overlay image for 2d geoscape
185  * @param[in] node The current menuNode we have clicked on (3dmap or map)
186  * @param[in] q The angle the sun is standing against the equator on earth
187  */
calcAndUploadDayAndNightTexture(uiNode_t * node,float q)188 void uiGeoscapeNode::calcAndUploadDayAndNightTexture (uiNode_t* node, float q)
189 {
190 	int x, y;
191 	const float dphi = (float) 2 * M_PI / DAN_WIDTH;
192 	const float da = M_PI / 2 * (HIGH_LAT - LOW_LAT) / DAN_HEIGHT;
193 	const float sin_q = sin(q);
194 	const float cos_q = cos(q);
195 	float sin_phi[DAN_WIDTH], cos_phi[DAN_WIDTH];
196 	byte* px;
197 
198 	for (x = 0; x < DAN_WIDTH; x++) {
199 		const float phi = x * dphi - q;
200 		sin_phi[x] = sin(phi);
201 		cos_phi[x] = cos(phi);
202 	}
203 
204 	/* calculate */
205 	px = UI_MAPEXTRADATA(node).r_dayandnightAlpha;
206 	for (y = 0; y < DAN_HEIGHT; y++) {
207 		const float a = sin(M_PI / 2 * HIGH_LAT - y * da);
208 		const float root = sqrt(1 - a * a);
209 		for (x = 0; x < DAN_WIDTH; x++) {
210 			const float pos = sin_phi[x] * root * sin_q - (a * SIN_ALPHA + cos_phi[x] * root * COS_ALPHA) * cos_q;
211 
212 			if (pos >= DAWN)
213 				*px++ = 255;
214 			else if (pos <= -DAWN)
215 				*px++ = 0;
216 			else
217 				*px++ = (byte) (128.0 * (pos / DAWN + 1));
218 		}
219 	}
220 
221 	/* upload alpha map into the r_dayandnighttexture */
222 	R_UploadAlpha(r_dayandnightTexture, UI_MAPEXTRADATA(node).r_dayandnightAlpha);
223 }
224 
draw(uiNode_t * node)225 void uiGeoscapeNode::draw (uiNode_t* node)
226 {
227 	vec2_t screenPos;
228 
229 	geoscapeNode = node;
230 	UI_MAPEXTRADATA(node).flatgeoscape = cl_3dmap->integer == 0;
231 	UI_MAPEXTRADATA(node).overlayMask = cl_geoscape_overlay->integer;
232 	UI_MAPEXTRADATA(node).ambientLightFactor = cl_3dmapAmbient->value;
233 	UI_MAPEXTRADATA(node).mapzoommin = cl_mapzoommin->value;
234 	UI_MAPEXTRADATA(node).mapzoommax = cl_mapzoommax->value;
235 
236 	UI_GetNodeAbsPos(node, UI_MAPEXTRADATA(node).mapPos);
237 	Vector2Copy(node->box.size, UI_MAPEXTRADATA(node).mapSize);
238 	if (!UI_MAPEXTRADATACONST(node).flatgeoscape) {
239 		/* remove the left padding */
240 		UI_MAPEXTRADATA(node).mapSize[0] -= UI_MAPEXTRADATACONST(node).paddingRight;
241 	}
242 
243 	/* Draw geoscape */
244 	UI_GetNodeScreenPos(node, screenPos);
245 	UI_PushClipRect(screenPos[0], screenPos[1], node->box.size[0], node->box.size[1]);
246 
247 	if (UI_MAPEXTRADATACONST(node).smoothRotation) {
248 		if (UI_MAPEXTRADATACONST(node).flatgeoscape)
249 			smoothTranslate(node);
250 		else
251 			smoothRotate(node);
252 	}
253 
254 	geoscapeData_t& data = *UI_MAPEXTRADATA(node).geoscapeData;
255 	data.geoscapeNode = node;
256 	GAME_DrawMap(&data);
257 	if (!data.active)
258 		return;
259 
260 	const char* map = data.map;
261 	date_t& date = data.date;
262 
263 	/* Draw the map and markers */
264 	if (UI_MAPEXTRADATACONST(node).flatgeoscape) {
265 		/* the last q value for the 2d geoscape night overlay */
266 		static float lastQ = 0.0f;
267 
268 		/* the sun is not always in the plane of the equator on earth - calculate the angle the sun is at */
269 		const float q = (date.day % DAYS_PER_YEAR + (float)(date.sec / (SECONDS_PER_HOUR * 6)) / 4) * 2 * M_PI / DAYS_PER_YEAR - M_PI;
270 		if (lastQ != q) {
271 			calcAndUploadDayAndNightTexture(node, q);
272 			lastQ = q;
273 		}
274 		R_DrawFlatGeoscape(UI_MAPEXTRADATACONST(node).mapPos, UI_MAPEXTRADATACONST(node).mapSize, (float) date.sec / SECONDS_PER_DAY,
275 				UI_MAPEXTRADATACONST(node).center[0], UI_MAPEXTRADATACONST(node).center[1], 0.5 / UI_MAPEXTRADATACONST(node).zoom, map,
276 				data.nationOverlay, data.xviOverlay, data.radarOverlay, r_dayandnightTexture, r_xviTexture, r_radarTexture);
277 
278 		GAME_DrawMapMarkers(node);
279 	} else {
280 		bool disableSolarRender = false;
281 		if (UI_MAPEXTRADATACONST(node).zoom > 3.3)
282 			disableSolarRender = true;
283 
284 		R_EnableRenderbuffer(true);
285 
286 		R_Draw3DGlobe(UI_MAPEXTRADATACONST(node).mapPos, UI_MAPEXTRADATACONST(node).mapSize, date.day, date.sec,
287 				UI_MAPEXTRADATACONST(node).angles, UI_MAPEXTRADATACONST(node).zoom, map, disableSolarRender,
288 				UI_MAPEXTRADATACONST(node).ambientLightFactor, UI_MAPEXTRADATA(node).overlayMask & OVERLAY_NATION,
289 				UI_MAPEXTRADATA(node).overlayMask & OVERLAY_XVI, UI_MAPEXTRADATA(node).overlayMask & OVERLAY_RADAR, r_xviTexture, r_radarTexture,
290 				true);
291 
292 		GAME_DrawMapMarkers(node);
293 
294 		R_DrawBloom();
295 		R_EnableRenderbuffer(false);
296 	}
297 
298 	UI_PopClipRect();
299 }
300 
onCapturedMouseMove(uiNode_t * node,int x,int y)301 void uiGeoscapeNode::onCapturedMouseMove (uiNode_t* node, int x, int y)
302 {
303 	switch (mode) {
304 	case MODE_SHIFT2DMAP:
305 	{
306 		int i;
307 		const float zoom = 0.5 / UI_MAPEXTRADATACONST(node).zoom;
308 		/* shift the map */
309 		UI_MAPEXTRADATA(node).center[0] -= (float) (mousePosX - oldMousePosX) / (node->box.size[0] * UI_MAPEXTRADATACONST(node).zoom);
310 		UI_MAPEXTRADATA(node).center[1] -= (float) (mousePosY - oldMousePosY) / (node->box.size[1] * UI_MAPEXTRADATACONST(node).zoom);
311 		for (i = 0; i < 2; i++) {
312 			/* clamp to min/max values */
313 			while (UI_MAPEXTRADATACONST(node).center[i] < 0.0)
314 				UI_MAPEXTRADATA(node).center[i] += 1.0;
315 			while (UI_MAPEXTRADATACONST(node).center[i] > 1.0)
316 				UI_MAPEXTRADATA(node).center[i] -= 1.0;
317 		}
318 		if (UI_MAPEXTRADATACONST(node).center[1] < zoom)
319 			UI_MAPEXTRADATA(node).center[1] = zoom;
320 		if (UI_MAPEXTRADATACONST(node).center[1] > 1.0 - zoom)
321 			UI_MAPEXTRADATA(node).center[1] = 1.0 - zoom;
322 		break;
323 	}
324 
325 	case MODE_SHIFT3DMAP:
326 		/* rotate a model */
327 		UI_MAPEXTRADATA(node).angles[PITCH] += ROTATE_SPEED * (mousePosX - oldMousePosX) / UI_MAPEXTRADATACONST(node).zoom;
328 		UI_MAPEXTRADATA(node).angles[YAW] -= ROTATE_SPEED * (mousePosY - oldMousePosY) / UI_MAPEXTRADATACONST(node).zoom;
329 
330 		/* clamp the UI_MAPEXTRADATACONST(node).angles */
331 		while (UI_MAPEXTRADATACONST(node).angles[YAW] > 0.0)
332 			UI_MAPEXTRADATA(node).angles[YAW] = 0.0;
333 		while (UI_MAPEXTRADATACONST(node).angles[YAW] < -180.0)
334 			UI_MAPEXTRADATA(node).angles[YAW] = -180.0;
335 
336 		while (UI_MAPEXTRADATACONST(node).angles[PITCH] > 180.0)
337 			UI_MAPEXTRADATA(node).angles[PITCH] -= 360.0;
338 		while (UI_MAPEXTRADATACONST(node).angles[PITCH] < -180.0)
339 			UI_MAPEXTRADATA(node).angles[PITCH] += 360.0;
340 		break;
341 	case MODE_ZOOMMAP:
342 	{
343 		const float zoom = 0.5 / UI_MAPEXTRADATACONST(node).zoom;
344 		/* zoom the map */
345 		UI_MAPEXTRADATA(node).zoom *= pow(0.995, mousePosY - oldMousePosY);
346 		if (UI_MAPEXTRADATACONST(node).zoom < UI_MAPEXTRADATACONST(node).mapzoommin)
347 			UI_MAPEXTRADATA(node).zoom = UI_MAPEXTRADATACONST(node).mapzoommin;
348 		else if (UI_MAPEXTRADATACONST(node).zoom > UI_MAPEXTRADATACONST(node).mapzoommax)
349 			UI_MAPEXTRADATA(node).zoom = UI_MAPEXTRADATACONST(node).mapzoommax;
350 
351 		if (UI_MAPEXTRADATACONST(node).center[1] < zoom)
352 			UI_MAPEXTRADATA(node).center[1] = zoom;
353 		if (UI_MAPEXTRADATACONST(node).center[1] > 1.0 - zoom)
354 			UI_MAPEXTRADATA(node).center[1] = 1.0 - zoom;
355 		break;
356 	}
357 	default:
358 		assert(false);
359 		break;
360 	}
361 	oldMousePosX = x;
362 	oldMousePosY = y;
363 }
364 
startMouseShifting(uiNode_t * node,int x,int y)365 void uiGeoscapeNode::startMouseShifting (uiNode_t* node, int x, int y)
366 {
367 	UI_SetMouseCapture(node);
368 	if (UI_MAPEXTRADATACONST(node).flatgeoscape)
369 		mode = MODE_SHIFT2DMAP;
370 	else
371 		mode = MODE_SHIFT3DMAP;
372 	UI_MAPEXTRADATA(node).smoothRotation = false;
373 	oldMousePosX = x;
374 	oldMousePosY = y;
375 }
376 
377 /**
378  * @brief Return longitude and latitude of a point of the screen for 2D geoscape
379  * @param[in] node The current menuNode we have clicked on (3dmap or map)
380  * @param[in] x X coordinate on the screen that was clicked on
381  * @param[in] y Y coordinate on the screen that was clicked on
382  * @param[out] pos vec2_t was filled with longitude and latitude
383  */
screenToMap(const uiNode_t * node,int x,int y,vec2_t pos)384 void uiGeoscapeNode::screenToMap (const uiNode_t* node, int x, int y, vec2_t pos)
385 {
386 	pos[0] = (((UI_MAPEXTRADATACONST(node).mapPos[0] - x) / UI_MAPEXTRADATACONST(node).mapSize[0] + 0.5) / UI_MAPEXTRADATACONST(node).zoom
387 			- (UI_MAPEXTRADATACONST(node).center[0] - 0.5)) * 360.0;
388 	pos[1] = (((UI_MAPEXTRADATACONST(node).mapPos[1] - y) / UI_MAPEXTRADATACONST(node).mapSize[1] + 0.5) / UI_MAPEXTRADATACONST(node).zoom
389 			- (UI_MAPEXTRADATACONST(node).center[1] - 0.5)) * 180.0;
390 
391 	while (pos[0] > 180.0)
392 		pos[0] -= 360.0;
393 	while (pos[0] < -180.0)
394 		pos[0] += 360.0;
395 }
396 
397 /**
398  * @brief Return longitude and latitude of a point of the screen for 3D geoscape (globe)
399  * @param[in] node The current menuNode we have clicked on (3dmap or map)
400  * @param[in] x X coordinate on the screen that was clicked on
401  * @param[in] y Y coordinate on the screen that was clicked on
402  * @param[out] pos vec2_t was filled with longitude and latitude
403  * @sa MAP_3DMapToScreen
404  */
screenTo3DMap(const uiNode_t * node,int x,int y,vec2_t pos)405 void uiGeoscapeNode::screenTo3DMap (const uiNode_t* node, int x, int y, vec2_t pos)
406 {
407 	vec2_t mid;
408 	vec3_t v, v1, rotationAxis;
409 	float dist;
410 	const float radius = GLOBE_RADIUS;
411 
412 	/* set mid to the coordinates of the center of the globe */
413 	Vector2Set(mid, UI_MAPEXTRADATACONST(node).mapPos[0] + UI_MAPEXTRADATACONST(node).mapSize[0] / 2.0f,
414 			UI_MAPEXTRADATACONST(node).mapPos[1] + UI_MAPEXTRADATACONST(node).mapSize[1] / 2.0f);
415 
416 	/* stop if we click outside the globe (distance is the distance of the point to the center of the globe) */
417 	dist = sqrt((x - mid[0]) * (x - mid[0]) + (y - mid[1]) * (y - mid[1]));
418 	if (dist > radius) {
419 		Vector2Set(pos, -1.0, -1.0);
420 		return;
421 	}
422 
423 	/* calculate the coordinates in the local frame
424 	 * this frame is the frame of the screen.
425 	 * v[0] is the vertical axis of the screen
426 	 * v[1] is the horizontal axis of the screen
427 	 * v[2] is the axis perpendicular to the screen - we get its value knowing that norm of v is egal to radius
428 	 *  (because the point is on the globe) */
429 	v[0] = - (y - mid[1]);
430 	v[1] = - (x - mid[0]);
431 	v[2] = - sqrt(radius * radius - (x - mid[0]) * (x - mid[0]) - (y - mid[1]) * (y - mid[1]));
432 	VectorNormalize(v);
433 
434 	/* rotate the vector to switch of reference frame
435 	 * note the ccs.angles[ROLL] is always 0, so there is only 2 rotations and not 3
436 	 * and that GLOBE_ROTATE is already included in ccs.angles[YAW]
437 	 * first rotation is along the horizontal axis of the screen, to put north-south axis of the earth
438 	 * perpendicular to the screen */
439 	VectorSet(rotationAxis, 0, 1, 0);
440 	RotatePointAroundVector(v1, rotationAxis, v, UI_MAPEXTRADATACONST(node).angles[YAW]);
441 
442 	/* second rotation is to rotate the earth around its north-south axis
443 	 * so that Greenwich meridian is along the vertical axis of the screen */
444 	VectorSet(rotationAxis, 0, 0, 1);
445 	RotatePointAroundVector(v, rotationAxis, v1, UI_MAPEXTRADATACONST(node).angles[PITCH]);
446 
447 	/* we therefore got in v the coordinates of the point in the static frame of the earth
448 	 * that we can convert in polar coordinates to get its latitude and longitude */
449 	VecToPolar(v, pos);
450 }
451 
onLeftClick(uiNode_t * node,int x,int y)452 void uiGeoscapeNode::onLeftClick (uiNode_t* node, int x, int y)
453 {
454 	if (mode != MODE_NULL)
455 		return;
456 
457 	vec2_t pos;
458 
459 	/* get map position */
460 	if (!UI_MAPEXTRADATACONST(node).flatgeoscape)
461 		screenTo3DMap(node, x, y, pos);
462 	else
463 		screenToMap(node, x, y, pos);
464 
465 	GAME_MapClick(node, x, y, pos);
466 }
467 
onStartDragging(uiNode_t * node,int startX,int startY,int x,int y,int button)468 bool uiGeoscapeNode::onStartDragging (uiNode_t* node, int startX, int startY, int x, int y, int button)
469 {
470 	switch (button) {
471 	case K_MOUSE1:
472 	case K_MOUSE3:
473 		startMouseShifting(node, startX, startY);
474 		return true;
475 	case K_MOUSE2:
476 		UI_SetMouseCapture(node);
477 		mode = MODE_ZOOMMAP;
478 		oldMousePosX = startX;
479 		oldMousePosY = startY;
480 		return true;
481 	}
482 	return false;
483 }
484 
onMouseUp(uiNode_t * node,int x,int y,int button)485 void uiGeoscapeNode::onMouseUp (uiNode_t* node, int x, int y, int button)
486 {
487 	if (mode != MODE_NULL) {
488 		UI_MouseRelease();
489 		mode = MODE_NULL;
490 	}
491 }
492 
493 /**
494  * @brief Called when the node have lost the captured node
495  * We clean cached data
496  */
onCapturedMouseLost(uiNode_t * node)497 void uiGeoscapeNode::onCapturedMouseLost (uiNode_t* node)
498 {
499 	mode = MODE_NULL;
500 }
501 
502 /**
503  * Zoom on the node
504  * @todo it should use an int param for smooth zoom
505  */
zoom(uiNode_t * node,bool out)506 void uiGeoscapeNode::zoom (uiNode_t* node, bool out)
507 {
508 	UI_MAPEXTRADATA(node).zoom *= pow(0.995, (out ? 10: -10));
509 	if (UI_MAPEXTRADATACONST(node).zoom < UI_MAPEXTRADATACONST(node).mapzoommin)
510 		UI_MAPEXTRADATA(node).zoom = UI_MAPEXTRADATACONST(node).mapzoommin;
511 	else if (UI_MAPEXTRADATACONST(node).zoom > UI_MAPEXTRADATACONST(node).mapzoommax)
512 		UI_MAPEXTRADATA(node).zoom = UI_MAPEXTRADATACONST(node).mapzoommax;
513 
514 	if (UI_MAPEXTRADATACONST(node).flatgeoscape) {
515 		if (UI_MAPEXTRADATACONST(node).center[1] < 0.5 / UI_MAPEXTRADATACONST(node).zoom)
516 			UI_MAPEXTRADATA(node).center[1] = 0.5 / UI_MAPEXTRADATACONST(node).zoom;
517 		if (UI_MAPEXTRADATACONST(node).center[1] > 1.0 - 0.5 / UI_MAPEXTRADATACONST(node).zoom)
518 			UI_MAPEXTRADATA(node).center[1] = 1.0 - 0.5 / UI_MAPEXTRADATACONST(node).zoom;
519 	}
520 	UI_MAPEXTRADATA(node).smoothRotation = false;
521 }
522 
onScroll(uiNode_t * node,int deltaX,int deltaY)523 bool uiGeoscapeNode::onScroll (uiNode_t* node, int deltaX, int deltaY)
524 {
525 	bool down = deltaY > 0;
526 	if (deltaY == 0)
527 		return false;
528 	zoom(node, down);
529 	return true;
530 }
531 
532 /**
533  * @brief Called before loading. Used to set default attribute values
534  */
onLoading(uiNode_t * node)535 void uiGeoscapeNode::onLoading (uiNode_t* node)
536 {
537 	Vector4Set(node->color, 1, 1, 1, 1);
538 
539 	OBJZERO(EXTRADATA(node));
540 	EXTRADATA(node).angles[YAW] = GLOBE_ROTATE;
541 	EXTRADATA(node).center[0] = EXTRADATA(node).center[1] = 0.5;
542 	EXTRADATA(node).zoom = 1.0;
543 	Vector2Set(EXTRADATA(node).smoothFinal2DGeoscapeCenter, 0.5, 0.5);
544 	VectorSet(EXTRADATA(node).smoothFinalGlobeAngle, 0, GLOBE_ROTATE, 0);
545 
546 	/* @todo: allocate this on a per node basis - and remove the global variable geoscapeData */
547 	EXTRADATA(node).geoscapeData = &geoscapeData;
548 	/* EXTRADATA(node).geoscapeData = Mem_AllocType(geoscapeData_t); */
549 
550 	/** this is the data that is used with r_dayandnightTexture */
551 	EXTRADATA(node).r_dayandnightAlpha = Mem_AllocTypeN(byte, DAN_WIDTH * DAN_HEIGHT);
552 
553 	r_dayandnightTexture = R_LoadImageData("***r_dayandnighttexture***", nullptr, DAN_WIDTH, DAN_HEIGHT, it_effect);
554 	r_radarTexture = R_LoadImageData("***r_radarTexture***", nullptr, RADAR_WIDTH, RADAR_HEIGHT, it_effect);
555 	r_xviTexture = R_LoadImageData("***r_xvitexture***", nullptr, XVI_WIDTH, XVI_HEIGHT, it_effect);
556 }
557 
UI_GeoscapeNodeZoomIn(uiNode_t * node,const uiCallContext_t * context)558 static void UI_GeoscapeNodeZoomIn (uiNode_t* node, const uiCallContext_t* context)
559 {
560 	uiGeoscapeNode* m = static_cast<uiGeoscapeNode*>(node->behaviour->manager.get());
561 	m->zoom(node, false);
562 }
563 
UI_GeoscapeNodeZoomOut(uiNode_t * node,const uiCallContext_t * context)564 static void UI_GeoscapeNodeZoomOut (uiNode_t* node, const uiCallContext_t* context)
565 {
566 	uiGeoscapeNode* m = static_cast<uiGeoscapeNode*>(node->behaviour->manager.get());
567 	m->zoom(node, true);
568 }
569 
570 /**
571  * @brief Command binding for map zooming
572  * @todo convert into node method
573  */
UI_GeoscapeNodeZoom_f(void)574 static void UI_GeoscapeNodeZoom_f (void)
575 {
576 	const char* cmd;
577 	const float zoomAmount = 50.0f;
578 
579 	if (Cmd_Argc() != 2) {
580 		Com_Printf("Usage: %s <in|out>\n", Cmd_Argv(0));
581 		return;
582 	}
583 
584 	cmd = Cmd_Argv(1);
585 	uiNode_t* node = geoscapeNode;
586 	if (!node)
587 		return;
588 
589 	switch (cmd[0]) {
590 	case 'i':
591 		UI_MAPEXTRADATA(node).smoothFinalZoom = UI_MAPEXTRADATACONST(node).zoom * powf(0.995, -zoomAmount);
592 		break;
593 	case 'o':
594 		UI_MAPEXTRADATA(node).smoothFinalZoom = UI_MAPEXTRADATACONST(node).zoom * powf(0.995, zoomAmount);
595 		break;
596 	default:
597 		Com_Printf("UI_GeoscapeNodeZoom_f: Invalid parameter: %s\n", cmd);
598 		return;
599 	}
600 
601 	if (UI_MAPEXTRADATACONST(node).smoothFinalZoom < UI_MAPEXTRADATACONST(node).mapzoommin)
602 		UI_MAPEXTRADATA(node).smoothFinalZoom = UI_MAPEXTRADATACONST(node).mapzoommin;
603 	else if (UI_MAPEXTRADATACONST(node).smoothFinalZoom > UI_MAPEXTRADATACONST(node).mapzoommax)
604 		UI_MAPEXTRADATA(node).smoothFinalZoom = UI_MAPEXTRADATACONST(node).mapzoommax;
605 
606 	if (UI_MAPEXTRADATACONST(node).flatgeoscape) {
607 		UI_MAPEXTRADATA(node).zoom = UI_MAPEXTRADATACONST(node).smoothFinalZoom;
608 		if (UI_MAPEXTRADATACONST(node).center[1] < 0.5 / UI_MAPEXTRADATACONST(node).zoom)
609 			UI_MAPEXTRADATA(node).center[1] = 0.5 / UI_MAPEXTRADATACONST(node).zoom;
610 		if (UI_MAPEXTRADATACONST(node).center[1] > 1.0 - 0.5 / UI_MAPEXTRADATACONST(node).zoom)
611 			UI_MAPEXTRADATA(node).center[1] = 1.0 - 0.5 / UI_MAPEXTRADATACONST(node).zoom;
612 	} else {
613 		VectorCopy(UI_MAPEXTRADATACONST(node).angles, UI_MAPEXTRADATA(node).smoothFinalGlobeAngle);
614 		UI_MAPEXTRADATA(node).smoothDeltaLength = 0;
615 		UI_MAPEXTRADATA(node).smoothRotation = true;
616 		UI_MAPEXTRADATA(node).smoothDeltaZoom = fabs(UI_MAPEXTRADATACONST(node).smoothFinalZoom - UI_MAPEXTRADATACONST(node).zoom);
617 	}
618 }
619 
620 /**
621  * @brief Command binding for map scrolling
622  * @todo convert into node method
623  */
UI_GeoscapeNodeScroll_f(void)624 static void UI_GeoscapeNodeScroll_f (void)
625 {
626 	const char* cmd;
627 	float scrollX = 0.0f, scrollY = 0.0f;
628 	const float scrollAmount = 80.0f;
629 
630 	if (Cmd_Argc() != 2) {
631 		Com_Printf("Usage: %s <up|down|left|right>\n", Cmd_Argv(0));
632 		return;
633 	}
634 
635 	cmd = Cmd_Argv(1);
636 
637 	uiNode_t* node = geoscapeNode;
638 	if (!node)
639 		return;
640 
641 	switch (cmd[0]) {
642 	case 'l':
643 		scrollX = scrollAmount;
644 		break;
645 	case 'r':
646 		scrollX = -scrollAmount;
647 		break;
648 	case 'u':
649 		scrollY = scrollAmount;
650 		break;
651 	case 'd':
652 		scrollY = -scrollAmount;
653 		break;
654 	default:
655 		Com_Printf("UI_GeoscapeNodeScroll_f: Invalid parameter\n");
656 		return;
657 	}
658 
659 	if (!UI_MAPEXTRADATACONST(node).flatgeoscape) {
660 		/* case 3D geoscape */
661 		vec3_t diff;
662 
663 		VectorCopy(UI_MAPEXTRADATACONST(node).angles, UI_MAPEXTRADATA(node).smoothFinalGlobeAngle);
664 
665 		/* rotate a model */
666 		UI_MAPEXTRADATA(node).smoothFinalGlobeAngle[PITCH] += ROTATE_SPEED * (scrollX) / UI_MAPEXTRADATACONST(node).zoom;
667 		UI_MAPEXTRADATA(node).smoothFinalGlobeAngle[YAW] -= ROTATE_SPEED * (scrollY) / UI_MAPEXTRADATACONST(node).zoom;
668 
669 		while (UI_MAPEXTRADATACONST(node).smoothFinalGlobeAngle[YAW] < -180.0) {
670 			UI_MAPEXTRADATA(node).smoothFinalGlobeAngle[YAW] = -180.0;
671 		}
672 		while (UI_MAPEXTRADATACONST(node).smoothFinalGlobeAngle[YAW] > 0.0) {
673 			UI_MAPEXTRADATA(node).smoothFinalGlobeAngle[YAW] = 0.0;
674 		}
675 
676 		while (UI_MAPEXTRADATACONST(node).smoothFinalGlobeAngle[PITCH] > 180.0) {
677 			UI_MAPEXTRADATA(node).smoothFinalGlobeAngle[PITCH] -= 360.0;
678 			UI_MAPEXTRADATA(node).angles[PITCH] -= 360.0;
679 		}
680 		while (UI_MAPEXTRADATACONST(node).smoothFinalGlobeAngle[PITCH] < -180.0) {
681 			UI_MAPEXTRADATA(node).smoothFinalGlobeAngle[PITCH] += 360.0;
682 			UI_MAPEXTRADATA(node).angles[PITCH] += 360.0;
683 		}
684 		VectorSubtract(UI_MAPEXTRADATACONST(node).smoothFinalGlobeAngle, UI_MAPEXTRADATACONST(node).angles, diff);
685 		UI_MAPEXTRADATA(node).smoothDeltaLength = VectorLength(diff);
686 
687 		UI_MAPEXTRADATA(node).smoothFinalZoom = UI_MAPEXTRADATACONST(node).zoom;
688 		UI_MAPEXTRADATA(node).smoothDeltaZoom = 0.0f;
689 		UI_MAPEXTRADATA(node).smoothRotation = true;
690 	} else {
691 		int i;
692 		/* shift the map */
693 		UI_MAPEXTRADATA(node).center[0] -= (float) (scrollX) / (UI_MAPEXTRADATACONST(node).mapSize[0] * UI_MAPEXTRADATACONST(node).zoom);
694 		UI_MAPEXTRADATA(node).center[1] -= (float) (scrollY) / (UI_MAPEXTRADATACONST(node).mapSize[1] * UI_MAPEXTRADATACONST(node).zoom);
695 		for (i = 0; i < 2; i++) {
696 			while (UI_MAPEXTRADATACONST(node).center[i] < 0.0)
697 				UI_MAPEXTRADATA(node).center[i] += 1.0;
698 			while (UI_MAPEXTRADATACONST(node).center[i] > 1.0)
699 				UI_MAPEXTRADATA(node).center[i] -= 1.0;
700 		}
701 		if (UI_MAPEXTRADATACONST(node).center[1] < 0.5 / UI_MAPEXTRADATACONST(node).zoom)
702 			UI_MAPEXTRADATA(node).center[1] = 0.5 / UI_MAPEXTRADATACONST(node).zoom;
703 		if (UI_MAPEXTRADATACONST(node).center[1] > 1.0 - 0.5 / UI_MAPEXTRADATACONST(node).zoom)
704 			UI_MAPEXTRADATA(node).center[1] = 1.0 - 0.5 / UI_MAPEXTRADATACONST(node).zoom;
705 	}
706 }
707 
UI_RegisterGeoscapeNode(uiBehaviour_t * behaviour)708 void UI_RegisterGeoscapeNode (uiBehaviour_t* behaviour)
709 {
710 	behaviour->name = "geoscape";
711 	behaviour->manager = UINodePtr(new uiGeoscapeNode());
712 	behaviour->extraDataSize = sizeof(EXTRADATA_TYPE);
713 
714 	/* Use a right padding. */
715 	UI_RegisterExtradataNodeProperty(behaviour, "padding-right", V_FLOAT, EXTRADATA_TYPE, paddingRight);
716 	/* Call it to zoom out of the map */
717 	UI_RegisterNodeMethod(behaviour, "zoomin", UI_GeoscapeNodeZoomIn);
718 	/* Call it to zoom into the map */
719 	UI_RegisterNodeMethod(behaviour, "zoomout", UI_GeoscapeNodeZoomOut);
720 
721 	Cmd_AddCommand("map_zoom", UI_GeoscapeNodeZoom_f);
722 	Cmd_AddCommand("map_scroll", UI_GeoscapeNodeScroll_f);
723 
724 	cl_3dmap = Cvar_Get("cl_3dmap", "1", CVAR_ARCHIVE, "3D geoscape or flat geoscape");
725 	cl_3dmapAmbient = Cvar_Get("cl_3dmapAmbient", "0", CVAR_ARCHIVE, "3D geoscape ambient lighting factor");
726 	cl_mapzoommax = Cvar_Get("cl_mapzoommax", "6.0", CVAR_ARCHIVE, "Maximum geoscape zooming value");
727 	cl_mapzoommin = Cvar_Get("cl_mapzoommin", "1.0", CVAR_ARCHIVE, "Minimum geoscape zooming value");
728 	cl_geoscape_overlay = Cvar_Get("cl_geoscape_overlay", "0", 0, "Geoscape overlays - Bitmask");
729 }
730