Passed
Pull Request — developer (#16696)
by Arkadiusz
15:40
created

MapCoordinates::updateMapCoordinates()   C

Complexity

Conditions 17
Paths 12

Size

Total Lines 32
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 28
c 0
b 0
f 0
dl 0
loc 32
rs 5.2166
cc 17
nc 12
nop 2

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * Tool file for the field type `MapCoordinates`.
4
 *
5
 * @package App
6
 *
7
 * @copyright YetiForce S.A.
8
 * @license   YetiForce Public License 5.0 (licenses/LicenseEN.txt or yetiforce.com)
9
 * @author    Mariusz Krzaczkowski <[email protected]>
10
 */
11
12
namespace App\Fields;
13
14
/**
15
 * Tool class for the field type `MapCoordinates`.
16
 */
17
class MapCoordinates
18
{
19
	const DECIMAL = 'decimal';
20
	const DEGREES = 'degrees';
21
	const CODE_PLUS = 'codeplus';
22
23
	/** @var string[] Coordinate formats */
24
	const COORDINATE_FORMATS = [
25
		self::DECIMAL => 'LBL_DECIMAL',
26
		self::DEGREES => 'LBL_DEGREES',
27
		self::CODE_PLUS => 'LBL_CODE_PLUS'
28
	];
29
	/** @var array Coordinate format validators */
30
	const VALIDATORS = [
31
		self::DECIMAL => ['lat' => 'Double', 'lon' => 'Double'],
32
		self::DEGREES => ['lat' => 'Text', 'lon' => 'Text'],
33
		self::CODE_PLUS => 'Text',
34
		'type' => 'Standard',
35
	];
36
37
	/**
38
	 * Converting coordinates from formats: {@see self::COORDINATE_FORMATS}.
39
	 *
40
	 * @param string $from
41
	 * @param string $to
42
	 * @param mixed  $value
43
	 *
44
	 * @return mixed
45
	 */
46
	public static function convert(string $from, string $to, $value)
47
	{
48
		if ($from === $to) {
49
			return $value;
50
		}
51
		switch ($from) {
52
			case self::DECIMAL:
53
				['lat' => $lat, 'lon' => $lon] = $value;
54
				break;
55
			case self::DEGREES:
56
				$lat = self::degreesToDecimal($value['lat']);
57
				$lon = self::degreesToDecimal($value['lon']);
58
				break;
59
			case self::CODE_PLUS:
60
				['lat' => $lat, 'lon' => $lon] = self::codePlusToDecimal($value);
61
				break;
62
			default:
63
				throw new \App\Exceptions\AppException('ERR_NOT_ALLOWED_VALUE||' . $from);
64
		}
65
		switch ($to) {
66
			case self::DECIMAL:
67
				$return = ['lat' => $lat, 'lon' => $lon];
68
				break;
69
			case self::DEGREES:
70
				$return = ['lat' => self::decimalToDegrees($lat, 'lat'), 'lon' => self::decimalToDegrees($lon, 'lon')];
71
				break;
72
			case self::CODE_PLUS:
73
				$return = self::decimalToCodePlus($lat, $lon);
74
				break;
75
			default:
76
				throw new \App\Exceptions\AppException('ERR_NOT_ALLOWED_VALUE||' . $to);
77
		}
78
		return $return;
79
	}
80
81
	/**
82
	 * Convert coordinates from decimal to degrees.
83
	 *
84
	 * @param string $coord     Coordinates in decimal, e.g. 52.23155431436567
85
	 * @param string $type      Type: `lat` or `lon`
86
	 * @param int    $precision Precision, default `4`
87
	 *
88
	 * @return string Coordinates in degrees, e.g. `52°13'53.5955"N`
89
	 */
90
	public static function decimalToDegrees(string $coord, string $type, int $precision = 4): string
91
	{
92
		if ('lat' === $type) {
93
			$dir = $coord < 0 ? 'S' : 'N';
94
		} else {
95
			$dir = $coord < 0 ? 'W' : 'E';
96
		}
97
		$vars = explode('.', $coord, 2);
98
		if (isset($vars[1])) {
99
			$val = (float) ('0.' . ($vars[1] ?? 0)) * 3600;
100
			$min = floor($val / 60);
101
			$sec = round($val - ($min * 60), $precision);
102
			if (0 == $sec) {
103
				return sprintf("%s°%02d'%s", $vars[0], $min, $dir);
104
			}
105
			return sprintf("%s°%02d'%s\"%s", $vars[0], $min, $sec, $dir);
106
		}
107
		return sprintf('%s°%s', $vars[0], $dir);
108
	}
109
110
	/**
111
	 * Convert coordinates from degrees to decimal.
112
	 *
113
	 * @param string $coord Coordinates in degrees, e.g. `21°0'17.983"E`
114
	 *
115
	 * @return string|null Coordinates in decimal, e.g. `21.004995277778`
116
	 */
117
	public static function degreesToDecimal(string $coord): ?string
118
	{
119
		if (($dots = substr_count($coord, '.')) > 1) {
120
			if (2 < \count(explode(' ', trim(preg_replace('/[a-zA-Z]/', '', preg_replace('/\./', ' ', $coord, $dots - 1)))))) {
121
				$coord = preg_replace('/\./', ' ', $coord, $dots - 1);
122
			} else {
123
				$coord = str_replace('.', ' ', $coord);
124
			}
125
		}
126
		$coord = trim(str_replace(['º', '°', "'", '"', '  '], ' ', trim($coord)));
127
		$coord = substr($coord, 0, 1) . str_replace('-', ' ', substr($coord, 1));
128
		if ($coord) {
129
			$direction = 1;
130
			if (preg_match('/^(-?\\d{1,3})\\s+(\\d{1,3})\\s*(\\d*(?:\\.\\d*)?)\\s*([nsewoNSEWO]?)$/', $coord, $matches)) {
131
				// `50°12'13.1188" N` , direction at the end of the string
132
				$deg = (int) ($matches[1]);
133
				$min = (int) ($matches[2]);
134
				$sec = (float) ($matches[3]);
135
				$dir = strtoupper($matches[4]);
136
				if ('S' === $dir || 'W' === $dir || $deg < 0) {
137
					$direction = -1;
138
					$deg = abs($deg);
139
				}
140
				$decimal = ($deg + ($min / 60) + ($sec / 3600)) * $direction;
141
			} elseif (preg_match('/^([nsewoNSEWO]?)\\s*(\\d{1,3})\\s+(\\d{1,3})\\s*(\\d*\\.?\\d*)$/', $coord, $matches)) {
142
				// `N 50°12'13.1188"` , direction at the start of the string
143
				$dir = strtoupper($matches[1]);
144
				$deg = (int) ($matches[2]);
145
				$min = (int) ($matches[3]);
146
				$sec = (float) ($matches[4]);
147
				if ('S' === $dir || 'W' === $dir) {
148
					$direction = -1;
149
				}
150
				$decimal = ($deg + ($min / 60) + ($sec / 3600)) * $direction;
151
			} elseif (preg_match('/^(-?\\d+(?:\\.\\d+)?)\\s*([nsewNSEW]?)$/', $coord, $matches)) {
152
				$dir = strtoupper($matches[2]);
153
				if ('S' === $dir || 'W' === $dir) {
154
					$direction = -1;
155
				}
156
				$decimal = $matches[1] * $direction;
157
			} elseif (preg_match('/^([nsewNSEW]?)\\s*(\\d+(?:\\.\\d+)?)$/', $coord, $matches)) {
158
				$dir = strtoupper($matches[1]);
159
				if ('S' === $dir || 'W' === $dir) {
160
					$direction = -1;
161
				}
162
				$decimal = $matches[2] * $direction;
163
			}
164
		}
165
		return isset($decimal) ? preg_replace('/[\x00-\x1F\x80-\xFF]/', '', $decimal) : null;
166
	}
167
168
	/**
169
	 * Convert coordinates from decimal to full Open Location Code.
170
	 *
171
	 * @see https://plus.codes/
172
	 *
173
	 * @param float $lat A latitude in signed decimal degrees.
174
	 *                   Will be clipped to the range -90 to 90, e.g. `52.231313`
175
	 * @param float $lon A longitude in signed decimal degrees.
176
	 *                   Will be normalized to the range -180 to 180, e.g. `21.004562`
177
	 *
178
	 * @return string Full Open Location Code., e.g. `9G4362J3+GR`
179
	 */
180
	public static function decimalToCodePlus(float $lat, float $lon): string
181
	{
182
		return \OpenLocationCode\OpenLocationCode::encode($lat, $lon, 12);
183
	}
184
185
	/**
186
	 * Undocumented function.
187
	 *
188
	 * @see https://plus.codes/
189
	 *
190
	 * @param string $coord Full Open Location Code., e.g. `9G4362J3+GR`
191
	 *
192
	 * @return float[] Coordinates in decimal, e.g. `[lat=>52.2313125,lon=>21.0045625]`
193
	 */
194
	public static function codePlusToDecimal(string $coord): array
195
	{
196
		$return = \OpenLocationCode\OpenLocationCode::decode($coord);
197
		return ['lat' => $return['latitudeCenter'], 'lon' => $return['longitudeCenter']];
198
	}
199
200
	/**
201
	 * Update of coordinates on the map.
202
	 *
203
	 * @param Vtiger_Record_Model $recordModel
0 ignored issues
show
Bug introduced by
The type App\Fields\Vtiger_Record_Model was not found. Did you mean Vtiger_Record_Model? If so, make sure to prefix the type with \.
Loading history...
204
	 * @param string              $fieldName
205
	 */
206
	public static function updateMapCoordinates(\Vtiger_Record_Model $recordModel, $fieldName)
207
	{
208
		$recordId = $recordModel->getId();
209
		$db = \App\Db::getInstance();
210
		$coordinateData = \App\Json::decode($recordModel->get($fieldName));
211
		if (('codeplus' === $coordinateData['type'] && !empty($coordinateData['value'])) || (\in_array($coordinateData['type'], ['degrees', 'decimal']) && !empty($coordinateData['value']['lat'])) && !empty($coordinateData['value']['lon'])) {
212
			switch ($coordinateData['type']) {
213
				case 'degrees':
214
					$coordinate = self::convert('degrees', 'decimal', $coordinateData['value']);
215
					break;
216
				case 'codeplus':
217
					$coordinate = self::convert('codeplus', 'decimal', $coordinateData['value']);
218
					break;
219
				default:
220
				$coordinate = $coordinateData['value'];
221
					break;
222
			}
223
			if (!(new \App\Db\Query())->from(\OpenStreetMap_Module_Model::COORDINATES_TABLE_NAME)
224
				->where(['crmid' => $recordId, 'type' => $fieldName])->exists()) {
225
				$db->createCommand()->insert(\OpenStreetMap_Module_Model::COORDINATES_TABLE_NAME, [
226
					'crmid' => $recordId,
227
					'type' => $fieldName,
228
					'lat' => round($coordinate['lat'], 7),
229
					'lon' => round($coordinate['lon'], 7),
230
				])->execute();
231
			} elseif ($recordModel->getPreviousValue($fieldName)) {
232
				$db->createCommand()->update(\OpenStreetMap_Module_Model::COORDINATES_TABLE_NAME, ['lat' => round($coordinate['lat'], 7), 'lon' => round($coordinate['lon'], 7)], ['crmid' => $recordId, 'type' => $fieldName])->execute();
233
			}
234
		} elseif ($recordModel->getPreviousValue($fieldName) && ('codeplus' === $coordinateData['type'] && empty($coordinateData['value'])) || (\in_array($coordinateData['type'], ['degrees', 'decimal']) && empty($coordinateData['value']['lat']) && empty($coordinateData['value']['lon']))) {
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($recordModel->getPrevio...teData['value']['lon']), Probably Intended Meaning: $recordModel->getPreviou...eData['value']['lon']))
Loading history...
235
			if ((new \App\Db\Query())->from(\OpenStreetMap_Module_Model::COORDINATES_TABLE_NAME)
236
				->where(['crmid' => $recordId, 'type' => $fieldName])->exists()) {
237
				$db->createCommand()->delete(\OpenStreetMap_Module_Model::COORDINATES_TABLE_NAME, ['crmid' => $recordId, 'type' => $fieldName])->execute();
238
			}
239
		}
240
	}
241
}
242