Passed
Branch master (380e00)
by Greg
20:17
created

GoogleMapsModule   F

Complexity

Total Complexity 333

Size/Duplication

Total Lines 3310
Duplicated Lines 3.38 %

Coupling/Cohesion

Components 1
Dependencies 27

Importance

Changes 0
Metric Value
dl 112
loc 3310
rs 0.5217
c 0
b 0
f 0
wmc 333
lcom 1
cbo 27

45 Methods

Rating   Name   Duplication   Size   Complexity  
A getTitle() 0 3 1
A getDescription() 0 3 1
C modAction() 0 46 13
A getConfigLink() 0 8 1
A defaultTabOrder() 0 3 1
A getPreLoadContent() 0 18 1
A canLoadAjax() 0 3 1
B getTabContent() 0 34 5
A hasTabContent() 0 3 2
A isGrayedOut() 0 3 1
A getChartMenu() 0 8 1
A getBoxChartMenu() 0 3 1
B config() 28 185 6
A googleMapsScript() 0 5 1
F pedigreeMap() 14 574 36
B checkMapData() 0 22 4
B removePrefixFromPlaceName() 0 11 5
B removeSuffixFromPlaceName() 0 11 5
B removePrefixAndSuffixFromPlaceName() 0 13 9
A createPossiblePlaceNames() 0 11 2
B getLatitudeAndLongitudeFromPlaceLocation() 0 32 6
B getPlaceData() 0 42 6
F buildIndividualMap() 0 252 22
B getPlaceLocationId() 12 32 6
B getPlaceId() 12 31 5
A setPlaceIdMap() 0 13 3
A setLevelMap() 0 17 4
B createMap() 0 34 4
A printHowManyPeople() 0 21 4
C printGoogleMapMarkers() 16 67 19
C mapScripts() 0 164 8
A placeIdToHierarchy() 0 11 2
A getHighestIndex() 0 3 1
A getHighestLevel() 0 3 1
B getPlaceListLocation() 0 70 6
B outputLevel() 0 24 5
A findFiles() 0 20 4
B adminUploadForm() 0 111 3
A adminDeleteAction() 0 19 2
A adminImportAction() 0 2 1
F adminUploadAction() 3 174 47
F adminDownload() 0 46 10
B adminPlaceSave() 0 34 2
F adminPlaceEdit() 0 550 23
D adminPlaces() 27 280 42

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like GoogleMapsModule often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use GoogleMapsModule, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * webtrees: online genealogy
4
 * Copyright (C) 2017 webtrees development team
5
 * This program is free software: you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation, either version 3 of the License, or
8
 * (at your option) any later version.
9
 * This program is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
 * GNU General Public License for more details.
13
 * You should have received a copy of the GNU General Public License
14
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15
 */
16
namespace Fisharebest\Webtrees\Module;
17
18
use Fisharebest\Webtrees\Auth;
19
use Fisharebest\Webtrees\Bootstrap4;
20
use Fisharebest\Webtrees\Controller\ChartController;
21
use Fisharebest\Webtrees\Controller\PageController;
22
use Fisharebest\Webtrees\Database;
23
use Fisharebest\Webtrees\DebugBar;
24
use Fisharebest\Webtrees\Fact;
25
use Fisharebest\Webtrees\Family;
26
use Fisharebest\Webtrees\Filter;
27
use Fisharebest\Webtrees\FlashMessages;
28
use Fisharebest\Webtrees\FontAwesome;
29
use Fisharebest\Webtrees\Functions\Functions;
30
use Fisharebest\Webtrees\Functions\FunctionsCharts;
31
use Fisharebest\Webtrees\Functions\FunctionsEdit;
32
use Fisharebest\Webtrees\Html;
33
use Fisharebest\Webtrees\I18N;
34
use Fisharebest\Webtrees\Individual;
35
use Fisharebest\Webtrees\Log;
36
use Fisharebest\Webtrees\Menu;
37
use Fisharebest\Webtrees\Module;
38
use Fisharebest\Webtrees\Session;
39
use Fisharebest\Webtrees\Stats;
40
use Fisharebest\Webtrees\Tree;
41
use PDO;
42
use stdClass;
43
44
/**
45
 * Class GoogleMapsModule
46
 *
47
 * @link http://www.google.com/permissions/guidelines.html
48
 *
49
 * "... an unregistered Google Brand Feature should be followed by
50
 * the superscripted letters TM or SM ..."
51
 *
52
 * Hence, use "Google Maps™"
53
 *
54
 * "... Use the trademark only as an adjective"
55
 *
56
 * "... Use a generic term following the trademark, for example:
57
 * GOOGLE search engine, Google search"
58
 *
59
 * Hence, use "Google Maps™ mapping service" where appropriate.
60
 */
61
class GoogleMapsModule extends AbstractModule implements ModuleConfigInterface, ModuleTabInterface, ModuleChartInterface {
62
	// How to update the database schema for this module
63
	const SCHEMA_TARGET_VERSION   = 6;
64
	const SCHEMA_SETTING_NAME     = 'GM_SCHEMA_VERSION';
65
	const SCHEMA_MIGRATION_PREFIX = '\Fisharebest\Webtrees\Module\GoogleMaps\Schema';
66
67
	const GM_MIN_ZOOM_MINIMUM = 1;
68
	const GM_MIN_ZOOM_DEFAULT = 2;
69
	const GM_MIN_ZOOM_MAXIMUM = 14;
70
71
	const GM_MAX_ZOOM_MINIMUM = 1;
72
	const GM_MAX_ZOOM_DEFAULT = 15;
73
	const GM_MAX_ZOOM_MAXIMUM = 20;
74
75
	/** @var Individual[] of ancestors of root person */
76
	private $ancestors = [];
77
78
	/** @var int Number of nodes in the chart */
79
	private $treesize;
80
81
	/** {@inheritdoc} */
82
	public function getTitle() {
83
		return /* I18N: The name of a module. Google Maps™ is a trademark. Do not translate it? http://en.wikipedia.org/wiki/Google_maps */ I18N::translate('Google Maps™');
84
	}
85
86
	/** {@inheritdoc} */
87
	public function getDescription() {
88
		return /* I18N: Description of the “Google Maps™” module */ I18N::translate('Show the location of places and events using the Google Maps™ mapping service.');
89
	}
90
91
	/**
92
	 * This is a general purpose hook, allowing modules to respond to routes
93
	 * of the form module.php?mod=FOO&mod_action=BAR
94
	 *
95
	 * @param string $mod_action
96
	 */
97
	public function modAction($mod_action) {
98
		Database::updateSchema(self::SCHEMA_MIGRATION_PREFIX, self::SCHEMA_SETTING_NAME, self::SCHEMA_TARGET_VERSION);
99
100
		// Some actions ard admin-only.
101
		if (strpos($mod_action, 'admin') === 0 && !Auth::isAdmin()) {
102
			header('Location: index.php');
103
104
			return;
105
		}
106
107
		switch ($mod_action) {
108
			case 'admin_config':
109
				$this->config();
110
				break;
111
			case 'pedigree_map':
112
				$this->pedigreeMap();
113
				break;
114
			case 'admin_places':
115
				$this->adminPlaces();
116
				break;
117
			case 'admin_place_edit':
118
				$this->adminPlaceEdit();
119
				break;
120
			case 'admin_place_save':
121
				$this->adminPlaceSave();
122
				break;
123
			case 'admin_download':
124
				$this->adminDownload();
125
				break;
126
			case 'admin_upload':
127
				$this->adminUploadForm();
128
				break;
129
			case 'admin_upload_action':
130
				$this->adminUploadAction();
131
				break;
132
			case 'admin_import_action':
133
				$this->adminImportAction();
0 ignored issues
show
Unused Code introduced by
The call to the method Fisharebest\Webtrees\Mod...le::adminImportAction() seems un-needed as the method has no side-effects.

PHP Analyzer performs a side-effects analysis of your code. A side-effect is basically anything that might be visible after the scope of the method is left.

Let’s take a look at an example:

class User
{
    private $email;

    public function getEmail()
    {
        return $this->email;
    }

    public function setEmail($email)
    {
        $this->email = $email;
    }
}

If we look at the getEmail() method, we can see that it has no side-effect. Whether you call this method or not, no future calls to other methods are affected by this. As such code as the following is useless:

$user = new User();
$user->getEmail(); // This line could safely be removed as it has no effect.

On the hand, if we look at the setEmail(), this method _has_ side-effects. In the following case, we could not remove the method call:

$user = new User();
$user->setEmail('email@domain'); // This line has a side-effect (it changes an
                                 // instance variable).
Loading history...
134
				break;
135
			case 'admin_delete_action':
136
				$this->adminDeleteAction();
137
				break;
138
			default:
139
				http_response_code(404);
140
				break;
141
		}
142
	}
143
144
	/** {@inheritdoc} */
145
	public function getConfigLink() {
146
		Database::updateSchema(self::SCHEMA_MIGRATION_PREFIX, self::SCHEMA_SETTING_NAME, self::SCHEMA_TARGET_VERSION);
147
148
		return Html::url('module.php', [
149
			'mod'        => $this->getName(),
150
			'mod_action' => 'admin_config',
151
		]);
152
	}
153
154
	/** {@inheritdoc} */
155
	public function defaultTabOrder() {
156
		return 80;
157
	}
158
159
	/** {@inheritdoc} */
160
	public function getPreLoadContent() {
161
		global $controller;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
162
163
		$controller->addInlineJavascript("
164
		$('head').append('<link type=\"text/css\" href =\"" . WT_MODULES_DIR . "googlemap/css/wt_v3_googlemap.css\" rel=\"stylesheet\">');
165
		");
166
167
		ob_start();
168
		?>
169
		<script src="<?= $this->googleMapsScript() ?>"></script>
170
		<script>
171
			var minZoomLevel   = <?= $this->getPreference('GM_MIN_ZOOM', self::GM_MIN_ZOOM_DEFAULT) ?>;
172
			var maxZoomLevel   = <?= $this->getPreference('GM_MAX_ZOOM', self::GM_MAX_ZOOM_DEFAULT) ?>;
173
			var startZoomLevel = maxZoomLevel;
174
		</script>
175
		<?php
176
		return ob_get_clean();
177
	}
178
179
	/** {@inheritdoc} */
180
	public function canLoadAjax() {
181
		return true;
182
	}
183
184
	/** {@inheritdoc} */
185
	public function getTabContent() {
186
		global $controller;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
187
188
		Database::updateSchema(self::SCHEMA_MIGRATION_PREFIX, self::SCHEMA_SETTING_NAME, self::SCHEMA_TARGET_VERSION);
189
190
		if ($this->checkMapData($controller->record)) {
191
			// This call can return an empty string if no facts with map co-ordinates exist
192
			$mapdata = $this->buildIndividualMap($controller->record);
193
		} else {
194
			$mapdata = '';
195
		}
196
		if ($mapdata) {
197
			$html = '<div id="' . $this->getName() . '_content">';
198
			$html .= '<div class="gm-wrapper">';
199
			$html .= '<div class="gm-map"></div>';
200
			$html .= $mapdata;
201
			$html .= '</div>';
202
			if (Auth::isAdmin()) {
203
				$html .= '<div class="gm-options">';
204
				$html .= '<a href="module.php?mod=' . $this->getName() . '&amp;mod_action=admin_config">' . I18N::translate('Google Maps™ preferences') . '</a>';
205
				$html .= ' | <a href="module.php?mod=' . $this->getName() . '&amp;mod_action=admin_places">' . I18N::translate('Geographic data') . '</a>';
206
				$html .= '</div>';
207
			}
208
			$html .= '<script>loadMap();</script>';
209
			$html .= '</div>';
210
		} else {
211
			$html = '<div>' . I18N::translate('No map data exists for this individual') . '</div>';
212
			if (Auth::isAdmin()) {
213
				$html .= '<div style="text-align: center;"><a href="module.php?mod=googlemap&amp;mod_action=admin_config">' . I18N::translate('Google Maps™ preferences') . '</a></div>';
214
			}
215
		}
216
217
		return $html;
218
	}
219
220
	/** {@inheritdoc} */
221
	public function hasTabContent() {
222
		return Module::getModuleByName('googlemap') || Auth::isAdmin();
223
	}
224
225
	/** {@inheritdoc} */
226
	public function isGrayedOut() {
227
		return false;
228
	}
229
230
	/**
231
	 * Return a menu item for this chart.
232
	 *
233
	 * @param Individual $individual
234
	 *
235
	 * @return Menu
236
	 */
237
	public function getChartMenu(Individual $individual) {
238
		return new Menu(
239
			I18N::translate('Pedigree map'),
240
			'module.php?mod=googlemap&amp;mod_action=pedigree_map&amp;rootid=' . $individual->getXref() . '&amp;ged=' . $individual->getTree()->getNameUrl(),
241
			'menu-chart-pedigreemap',
242
			['rel' => 'nofollow']
243
		);
244
	}
245
246
	/**
247
	 * Return a menu item for this chart - for use in individual boxes.
248
	 *
249
	 * @param Individual $individual
250
	 *
251
	 * @return Menu
252
	 */
253
	public function getBoxChartMenu(Individual $individual) {
254
		return $this->getChartMenu($individual);
255
	}
256
257
	/**
258
	 * A form to edit the module configuration.
259
	 */
260
	private function config() {
261
		$controller = new PageController;
262
		$controller->setPageTitle(I18N::translate('Google Maps™'));
263
264
		if (Filter::post('action') === 'update') {
265
			$this->setPreference('GM_API_KEY', Filter::post('GM_API_KEY'));
266
			$this->setPreference('GM_MIN_ZOOM', Filter::post('GM_MIN_ZOOM'));
267
			$this->setPreference('GM_MAX_ZOOM', Filter::post('GM_MAX_ZOOM'));
268
			$this->setPreference('GM_PLACE_HIERARCHY', Filter::post('GM_PLACE_HIERARCHY'));
269
			$this->setPreference('GM_PH_MARKER', Filter::post('GM_PH_MARKER'));
270
			$this->setPreference('GM_PREFIX_1', Filter::post('GM_PREFIX_1'));
271
			$this->setPreference('GM_PREFIX_2', Filter::post('GM_PREFIX_2'));
272
			$this->setPreference('GM_PREFIX_3', Filter::post('GM_PREFIX_3'));
273
			$this->setPreference('GM_PREFIX_4', Filter::post('GM_PREFIX_4'));
274
			$this->setPreference('GM_PREFIX_5', Filter::post('GM_PREFIX_5'));
275
			$this->setPreference('GM_PREFIX_6', Filter::post('GM_PREFIX_6'));
276
			$this->setPreference('GM_PREFIX_7', Filter::post('GM_PREFIX_7'));
277
			$this->setPreference('GM_PREFIX_8', Filter::post('GM_PREFIX_8'));
278
			$this->setPreference('GM_PREFIX_9', Filter::post('GM_PREFIX_9'));
279
			$this->setPreference('GM_POSTFIX_1', Filter::post('GM_POSTFIX_1'));
280
			$this->setPreference('GM_POSTFIX_2', Filter::post('GM_POSTFIX_2'));
281
			$this->setPreference('GM_POSTFIX_3', Filter::post('GM_POSTFIX_3'));
282
			$this->setPreference('GM_POSTFIX_4', Filter::post('GM_POSTFIX_4'));
283
			$this->setPreference('GM_POSTFIX_5', Filter::post('GM_POSTFIX_5'));
284
			$this->setPreference('GM_POSTFIX_6', Filter::post('GM_POSTFIX_6'));
285
			$this->setPreference('GM_POSTFIX_7', Filter::post('GM_POSTFIX_7'));
286
			$this->setPreference('GM_POSTFIX_8', Filter::post('GM_POSTFIX_8'));
287
			$this->setPreference('GM_POSTFIX_9', Filter::post('GM_POSTFIX_9'));
288
289
			FlashMessages::addMessage(I18N::translate('The preferences for the module “%s” have been updated.', $this->getTitle()), 'success');
290
			header('Location: module.php?mod=googlemap&mod_action=admin_config');
291
292
			return;
293
		}
294
295
		$controller->pageHeader();
296
297
		echo Bootstrap4::breadcrumbs([
298
			route('admin-control-panel') => I18N::translate('Control panel'),
299
			route('admin-modules')       => I18N::translate('Module administration'),
300
		], $controller->getPageTitle());
301
		?>
302
303
		<h2><?= I18N::translate('Google Maps™ preferences') ?></h2>
304
305
		<form class="form-horizontal" method="post" name="configform" action="module.php?mod=googlemap&mod_action=admin_config">
306
			<input type="hidden" name="action" value="update">
307
308
			<div class="row form-group">
309
				<div class="col-sm-3 col-form-label">
310
					<?= I18N::translate('Geographic data') ?>
311
				</div>
312
				<div class="col-sm-9">
313
					<a class="btn btn-primary" href="module.php?mod=googlemap&amp;mod_action=admin_places">
314
						<?= FontAwesome::decorativeIcon('edit') ?>
315
						<?= I18N::translate('edit') ?>
316
					</a>
317
				</div>
318
			</div>
319
320
			<!-- GM_API_KEY -->
321
			<div class="row form-group">
322
				<label class="col-sm-3 col-form-label" for="GM_API_KEY">
323
					<?= /* I18N: https://en.wikipedia.org/wiki/API_key */ I18N::translate('API key') ?>
324
				</label>
325
				<div class="col-sm-9">
326
					<input id="GM_API_KEY" class="form-control" type="text" name="GM_API_KEY" value="<?= $this->getPreference('GM_API_KEY') ?>">
327
					<p class="small text-muted"><?= I18N::translate('Google allows a small number of anonymous map requests per day. If you need more than this, you will need a Google account and an API key.') ?>
328
						<a href="https://developers.google.com/maps/documentation/javascript/get-api-key">
329
							<?= /* I18N: https://en.wikipedia.org/wiki/API_key */ I18N::translate('Get an API key from Google.') ?>
330
						</a>
331
					</p>
332
				</div>
333
			</div>
334
335
			<!-- GM_MIN_ZOOM / GM_MAX_ZOOM -->
336
			<fieldset class="form-group">
337
				<div class="row">
338
					<legend class="col-form-legend col-sm-3">
339
						<?= I18N::translate('Zoom level of map') ?>
340
					</legend>
341
					<div class="col-sm-9">
342
						<div class="row">
343
							<div class="col-sm-6">
344
								<div class="input-group">
345
									<label class="input-group-addon" for="GM_MIN_ZOOM"><?= I18N::translate('minimum') ?></label>
346
									<?= Bootstrap4::select(array_combine(range(self::GM_MIN_ZOOM_MINIMUM, self::GM_MIN_ZOOM_MAXIMUM), range(self::GM_MIN_ZOOM_MINIMUM, self::GM_MIN_ZOOM_MAXIMUM)), $this->getPreference('GM_MIN_ZOOM', self::GM_MIN_ZOOM_DEFAULT), ['id' => 'GM_MIN_ZOOM', 'name' => 'GM_MIN_ZOOM']) ?>
347
								</div>
348
							</div>
349
							<div class="col-sm-6">
350
								<div class="input-group">
351
									<label class="input-group-addon" for="GM_MAX_ZOOM"><?= I18N::translate('maximum') ?></label>
352
									<?= Bootstrap4::select(array_combine(range(self::GM_MAX_ZOOM_MINIMUM, self::GM_MAX_ZOOM_MAXIMUM), range(self::GM_MAX_ZOOM_MINIMUM, self::GM_MAX_ZOOM_MAXIMUM)), $this->getPreference('GM_MAX_ZOOM', self::GM_MAX_ZOOM_DEFAULT), ['id' => 'GM_MAX_ZOOM', 'name' => 'GM_MAX_ZOOM']) ?>
353
								</div>
354
							</div>
355
						</div>
356
						<p class="small text-muted"><?= I18N::translate('Minimum and maximum zoom level for the Google map. 1 is the full map, 15 is single house. Note that 15 is only available in certain areas.') ?></p>
357
					</div>
358
				</div>
359
			</fieldset>
360
361
			<!-- GM_PREFIX / GM_POSTFIX -->
362
			<fieldset class="form-group">
363
				<div class="row">
364
					<legend class="col-form-legend col-sm-3">
365
						<?= I18N::translate('Optional prefixes and suffixes') ?>
366
					</legend>
367
					<div class="col-sm-9">
368
						<div class="row">
369
							<div class ="col-sm-6">
370
								<p class="form-control-static"><strong><?= I18N::translate('Prefixes') ?></strong></p>
371 View Code Duplication
								<?php for ($level = 1; $level < 10; $level++): ?>
372
								<?php
373
								if ($level == 1) {
374
									$label = I18N::translate('Country');
375
								} else {
376
									$label = I18N::translate('Level') . ' ' . $level;
377
								}
378
								?>
379
								<div class="input-group">
380
									<label class="input-group-addon" for="GM_PREFIX_<?= $level ?>"><?= $label ?></label>
381
									<input class="form-control" type="text" name="GM_PREFIX_<?= $level ?>" value="<?= $this->getPreference('GM_PREFIX_' . $level) ?>">
382
								</div>
383
								<?php endfor ?>
384
							</div>
385
							<div class="col-sm-6">
386
								<p class="form-control-static"><strong><?= I18N::translate('Suffixes') ?></strong></p>
387 View Code Duplication
								<?php for ($level = 1; $level < 10; $level++): ?>
388
								<?php
389
								if ($level == 1) {
390
									$label = I18N::translate('Country');
391
								} else {
392
									$label = I18N::translate('Level') . ' ' . $level;
393
								}
394
								?>
395
								<div class="input-group">
396
									<label class="input-group-addon" for="GM_POSTFIX_<?= $level ?>"><?= $label ?></label>
397
									<input class="form-control" type="text" name="GM_POSTFIX_<?= $level ?>" value="<?= $this->getPreference('GM_POSTFIX_' . $level) ?>">
398
								</div>
399
								<?php endfor ?>
400
							</div>
401
						</div>
402
						<p class="small text-muted"><?= I18N::translate('Some place names may be written with optional prefixes and suffixes. For example “Orange” versus “Orange County”. If the family tree contains the full place names, but the geographic database contains the short place names, then you should specify a list of the prefixes and suffixes to be disregarded. Multiple values should be separated with semicolons. For example “County;County of” or “Township;Twp;Twp.”.') ?></p>
403
					</div>
404
				</div>
405
			</fieldset>
406
407
			<h3><?= I18N::translate('Place hierarchy') ?></h3>
408
409
			<!-- GM_PLACE_HIERARCHY -->
410
			<fieldset class="form-group">
411
				<div class="row">
412
					<legend class="col-form-legend col-sm-3">
413
						<?= I18N::translate('Use Google Maps™ for the place hierarchy') ?>
414
					</legend>
415
					<div class="col-sm-9">
416
						<?= Bootstrap4::radioButtons('GM_PLACE_HIERARCHY', [I18N::translate('no'), I18N::translate('yes')], $this->getPreference('GM_PLACE_HIERARCHY', '0'), true) ?>
417
					</div>
418
				</div>
419
			</fieldset>
420
421
			<!-- GM_PH_MARKER -->
422
			<div class="row form-group">
423
				<label class="col-sm-3 col-form-label" for="GM_PH_MARKER">
424
					<?= I18N::translate('Type of place markers in the place hierarchy') ?>
425
				</label>
426
				<div class="col-sm-9">
427
					<?php
428
					echo Bootstrap4::select(['G_DEFAULT_ICON' => I18N::translate('Standard'), 'G_FLAG' => I18N::translate('Flag')], $this->getPreference('GM_PH_MARKER'), ['id' => 'GM_PH_MARKER', 'name' => 'GM_PH_MARKER']);
429
					?>
430
				</div>
431
			</div>
432
433
			<!-- SAVE BUTTON -->
434
			<div class="row form-group">
435
				<div class="offset-sm-3 col-sm-9">
436
					<button type="submit" class="btn btn-primary">
437
						<i class="fa fa-check"></i>
438
						<?= I18N::translate('save') ?>
439
					</button>
440
				</div>
441
			</div>
442
		</form>
443
		<?php
444
	}
445
446
	/**
447
	 * Google Maps API script
448
	 *
449
	 * @return string
450
	 */
451
	private function googleMapsScript() {
452
		$key = $this->getPreference('GM_API_KEY');
453
454
		return 'https://maps.googleapis.com/maps/api/js?v=3&amp;key=' . $key . '&amp;language=' . WT_LOCALE;
455
	}
456
457
	/**
458
	 * Display a map showing the origins of ones ancestors.
459
	 */
460
	private function pedigreeMap() {
461
		global $controller, $WT_TREE;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
462
463
		$controller = new ChartController();
464
		$controller->restrictAccess(Module::isActiveChart($WT_TREE, 'googlemap'));
465
466
		// Limit this to match available number of icons.
467
		// 8 generations equals 255 individuals
468
		$MAX_PEDIGREE_GENERATIONS = $WT_TREE->getPreference('MAX_PEDIGREE_GENERATIONS');
469
		$MAX_PEDIGREE_GENERATIONS = min($MAX_PEDIGREE_GENERATIONS, 8);
470
		$generations              = Filter::getInteger('PEDIGREE_GENERATIONS', 2, $MAX_PEDIGREE_GENERATIONS, $WT_TREE->getPreference('DEFAULT_PEDIGREE_GENERATIONS'));
471
		$this->treesize           = pow(2, $generations) - 1;
0 ignored issues
show
Documentation Bug introduced by
It seems like pow(2, $generations) - 1 can also be of type double. However, the property $treesize is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
472
		$this->ancestors          = array_values($controller->sosaAncestors($generations));
473
474
		// Only generate the content for interactive users (not search robots).
475
		if (Filter::getBool('ajax') && Session::has('initiated')) {
476
			// count records by type
477
			$curgen   = 1;
478
			$priv     = 0;
479
			$count    = 0;
480
			$miscount = 0;
481
			$missing  = [];
482
483
			$latlongval = [];
484
			$lat        = [];
485
			$lon        = [];
486
			for ($i = 0; $i < ($this->treesize); $i++) {
487
				// -- check to see if we have moved to the next generation
488
				if ($i + 1 >= pow(2, $curgen)) {
489
					$curgen++;
490
				}
491
				$person = $this->ancestors[$i];
492
				if (!empty($person)) {
493
					$name = $person->getFullName();
494
					if ($name == I18N::translate('Private')) {
495
						$priv++;
496
					}
497
					$place = $person->getBirthPlace()->getGedcomName();
498
					if (empty($place)) {
499
						$latlongval[$i] = null;
500
					} else {
501
						$latlongval[$i] = $this->getLatitudeAndLongitudeFromPlaceLocation($person->getBirthPlace()->getGedcomName());
502
					}
503
					if ($latlongval[$i]) {
504
						$lat[$i] = strtr($latlongval[$i]->pl_lati, ['N' => '', 'S' => '-', ',' => '.']);
505
						$lon[$i] = strtr($latlongval[$i]->pl_long, ['N' => '', 'S' => '-', ',' => '.']);
506
						if ($lat[$i] && $lon[$i]) {
507
							$count++;
508 View Code Duplication
						} else {
509
							// The place is in the table but has empty values
510
							if ($name) {
511
								$missing[] = '<a href="' . $person->getHtmlUrl() . '">' . $name . '</a>';
512
								$miscount++;
513
							}
514
						}
515 View Code Duplication
					} else {
516
						// There was no place, or not listed in the map table
517
						if ($name) {
518
							$missing[] = '<a href="' . $person->getHtmlUrl() . '">' . $name . '</a>';
519
							$miscount++;
520
						}
521
					}
522
				}
523
			}
524
525
			//<!-- end of count records by type -->
526
			//<!-- start of map display -->
527
			echo '<div class="gm-pedigree-map">';
528
			echo '<div class="gm-wrapper">';
529
			echo '<div class="gm-map wt-ajax-load"></div>';
530
			echo '<div class="gm-ancestors"></div>';
531
			echo '</div>';
532
533
			if (Auth::isAdmin()) {
534
				echo '<div class="gm-options">';
535
				echo '<a href="module.php?mod=' . $this->getName() . '&amp;mod_action=admin_config">' . I18N::translate('Google Maps™ preferences') . '</a>';
536
				echo ' | <a href="module.php?mod=' . $this->getName() . '&amp;mod_action=admin_places">' . I18N::translate('Geographic data') . '</a>';
537
				echo '</div>';
538
			}
539
			// display info under map
540
			echo '<hr>';
541
542
			// print summary statistics
543
			if (isset($curgen)) {
544
				$total = pow(2, $curgen) - 1;
545
				echo '<div>';
546
				echo I18N::plural(
547
					'%1$s individual displayed, out of the normal total of %2$s, from %3$s generations.',
548
					'%1$s individuals displayed, out of the normal total of %2$s, from %3$s generations.',
549
					$count,
550
					I18N::number($count), I18N::number($total), I18N::number($curgen)
551
				);
552
				echo '</div>';
553
				if ($priv) {
554
					echo '<div>' . I18N::plural('%s individual is private.', '%s individuals are private.', $priv, $priv), '</div>';
555
				}
556
				if ($count + $priv != $total) {
557
					if ($miscount == 0) {
558
						echo '<div>' . I18N::translate('No ancestors in the database.'), '</div>';
559
					} else {
560
						echo '<div>' . /* I18N: %1$s is a count of individuals, %2$s is a list of their names */ I18N::plural(
561
								'%1$s individual is missing birthplace map coordinates: %2$s.',
562
								'%1$s individuals are missing birthplace map coordinates: %2$s.',
563
								$miscount, I18N::number($miscount), implode(I18N::$list_separator, $missing)),
564
						'</div>';
565
					}
566
				}
567
			}
568
569
			echo '</div>';
570
			echo '</div>';
571
			?>
572
			<script>
573
				function initialiZePedigreeMap() {
574
					// this variable will collect the html which will eventually be placed in the side bar
575
					var gm_ancestors_html = "";
576
					// arrays to hold copies of the markers and html used by the side bar
577
					// because the function closure trick doesnt work there
578
					var gmarkers = [];
579
					var index = 0;
580
					var lastlinkid;
581
					var infowindow = new google.maps.InfoWindow({});
582
					// === Create an associative array of GIcons()
583
					var gicons = [];
584
					gicons["1"]  = {
585
						url: WT_MODULES_DIR + "googlemap/images/icon1.png"
586
					};
587
					gicons["2"]  = {
588
						url: WT_MODULES_DIR + "googlemap/images/icon2.png"
589
					};
590
					gicons["2L"] = {
591
						url: WT_MODULES_DIR + "googlemap/images/icon2L.png",
592
						size: new google.maps.Size(32, 32),
593
						origin: new google.maps.Point(0, 0),
594
						anchor: new google.maps.Point(28, 28)
595
					};
596
					gicons["2R"] = {
597
						url: WT_MODULES_DIR + "googlemap/images/icon2R.png",
598
						size:  new google.maps.Size(32, 32),
599
						origin: new google.maps.Point(0, 0),
600
						anchor: new google.maps.Point(4, 28)
601
					};
602
					gicons["2Ls"] = {
603
						url: WT_MODULES_DIR+"googlemap/images/icon2Ls.png",
604
						size:  new google.maps.Size(24, 24),
605
						origin: new google.maps.Point(0, 0),
606
						anchor: new google.maps.Point(22, 22)
607
					};
608
					gicons["2Rs"] = {
609
						url: WT_MODULES_DIR + "googlemap/images/icon2Rs.png",
610
						size: new google.maps.Size(24, 24),
611
						origin: new google.maps.Point(0, 0),
612
						anchor: new google.maps.Point(2, 22)
613
					};
614
					gicons["3"]   = {
615
						url: WT_MODULES_DIR + "googlemap/images/icon3.png"
616
					};
617
					gicons["3L"] = {
618
						url: WT_MODULES_DIR + "googlemap/images/icon3L.png",
619
						size: new google.maps.Size(32, 32),
620
						origin: new google.maps.Point(0, 0),
621
						anchor: new google.maps.Point(28, 28)
622
					};
623
					gicons["3R"]  = {
624
						url: WT_MODULES_DIR + "googlemap/images/icon3R.png",
625
						size: new google.maps.Size(32, 32),
626
						origin: new google.maps.Point(0, 0),
627
						anchor: new google.maps.Point(4, 28)
628
					};
629
					gicons["3Ls"] = {
630
						url: WT_MODULES_DIR + "googlemap/images/icon3Ls.png",
631
						size: new google.maps.Size(24, 24),
632
						origin: new google.maps.Point(0, 0),
633
						anchor: new google.maps.Point(22, 22)
634
					};
635
					gicons["3Rs"] = {
636
						url: WT_MODULES_DIR + "googlemap/images/icon3Rs.png",
637
						size: new google.maps.Size(24, 24),
638
						origin: new google.maps.Point(0, 0),
639
						anchor: new google.maps.Point(2, 22)
640
					};
641
					gicons["4"]   = {
642
						url: WT_MODULES_DIR + "googlemap/images/icon4.png"
643
					};
644
					gicons["4L"]  = {
645
						url: WT_MODULES_DIR + "googlemap/images/icon4L.png",
646
						size: new google.maps.Size(32, 32),
647
						origin: new google.maps.Point(0, 0),
648
						anchor: new google.maps.Point(28, 28)
649
					};
650
					gicons["4R"] = {
651
						url: WT_MODULES_DIR + "googlemap/images/icon4R.png",
652
						size: new google.maps.Size(32, 32),
653
						origin: new google.maps.Point(0, 0),
654
						anchor: new google.maps.Point(4, 28)
655
					};
656
					gicons["4Ls"] = {
657
						url: WT_MODULES_DIR + "googlemap/images/icon4Ls.png",
658
						size: new google.maps.Size(24, 24),
659
						origin: new google.maps.Point(0, 0),
660
						anchor: new google.maps.Point(22, 22)
661
					};
662
					gicons["4Rs"] = {
663
						url: WT_MODULES_DIR + "googlemap/images/icon4Rs.png",
664
						size: new google.maps.Size(24, 24),
665
						origin: new google.maps.Point(0, 0),
666
						anchor: new google.maps.Point(2, 22)
667
					};
668
					gicons["5"] = {
669
						url: WT_MODULES_DIR + "googlemap/images/icon5.png"
670
					};
671
					gicons["5L"] = {
672
						url: WT_MODULES_DIR + "googlemap/images/icon5L.png",
673
						size: new google.maps.Size(32, 32),
674
						origin: new google.maps.Point(0, 0),
675
						anchor: new google.maps.Point(28, 28)
676
					};
677
					gicons["5R"] = {
678
						url: WT_MODULES_DIR + "googlemap/images/icon5R.png",
679
						size: new google.maps.Size(32, 32),
680
						origin: new google.maps.Point(0, 0),
681
						anchor: new google.maps.Point(4, 28)
682
					};
683
					gicons["5Ls"] = {
684
						url: WT_MODULES_DIR + "googlemap/images/icon5Ls.png",
685
						size: new google.maps.Size(24, 24),
686
						origin: new google.maps.Point(0, 0),
687
						anchor: new google.maps.Point(22, 22)
688
					};
689
					gicons["5Rs"] = {
690
						url: WT_MODULES_DIR + "googlemap/images/icon5Rs.png",
691
						size: new google.maps.Size(24, 24),
692
						origin: new google.maps.Point(0, 0),
693
						anchor: new google.maps.Point(2, 22)
694
					};
695
					gicons["6"] = {
696
						url: WT_MODULES_DIR + "googlemap/images/icon6.png"
697
					};
698
					gicons["6L"] = {
699
						url: WT_MODULES_DIR + "googlemap/images/icon6L.png",
700
						size: new google.maps.Size(32, 32),
701
						origin: new google.maps.Point(0, 0),
702
						anchor: new google.maps.Point(28, 28)
703
					};
704
					gicons["6R"] = {
705
						url: WT_MODULES_DIR + "googlemap/images/icon6R.png",
706
						size: new google.maps.Size(32, 32),
707
						origin: new google.maps.Point(0, 0),
708
						anchor: new google.maps.Point(4, 28)
709
					};
710
					gicons["6Ls"] = {
711
						url: WT_MODULES_DIR + "googlemap/images/icon6Ls.png",
712
						size: new google.maps.Size(24, 24),
713
						origin: new google.maps.Point(0, 0),
714
						anchor: new google.maps.Point(22, 22)
715
					};
716
					gicons["6Rs"] = {
717
						url: WT_MODULES_DIR + "googlemap/images/icon6Rs.png",
718
						size: new google.maps.Size(24, 24),
719
						origin: new google.maps.Point(0, 0),
720
						anchor: new google.maps.Point(2, 22)
721
					};
722
					gicons["7"]   = {
723
						url: WT_MODULES_DIR + "googlemap/images/icon7.png"
724
					};
725
					gicons["7L"]  = {
726
						url: WT_MODULES_DIR + "googlemap/images/icon7L.png",
727
						size: new google.maps.Size(32, 32),
728
						origin: new google.maps.Point(0, 0),
729
						anchor: new google.maps.Point(28, 28)
730
					};
731
					gicons["7R"]  = {
732
						url: WT_MODULES_DIR + "googlemap/images/icon7R.png",
733
						size: new google.maps.Size(32, 32),
734
						origin: new google.maps.Point(0, 0),
735
						anchor: new google.maps.Point(4, 28)
736
					};
737
					gicons["7Ls"] = {
738
						url: WT_MODULES_DIR + "googlemap/images/icon7Ls.png",
739
						size: new google.maps.Size(24, 24),
740
						origin: new google.maps.Point(0, 0),
741
						anchor: new google.maps.Point(22, 22)
742
					};
743
					gicons["7Rs"] = {
744
						url: WT_MODULES_DIR + "googlemap/images/icon7Rs.png",
745
						size: new google.maps.Size(24, 24),
746
						origin: new google.maps.Point(0, 0),
747
						anchor: new google.maps.Point(2, 22)
748
					};
749
					gicons["8"]   = {
750
						url: WT_MODULES_DIR + "googlemap/images/icon8.png"
751
					};
752
					gicons["8L"]  = {
753
						url: WT_MODULES_DIR + "googlemap/images/icon8L.png",
754
						size: new google.maps.Size(32, 32),
755
						origin: new google.maps.Point(0, 0),
756
						anchor: new google.maps.Point(28, 28)
757
					};
758
					gicons["8R"]  = {
759
						url: WT_MODULES_DIR + "googlemap/images/icon8R.png",
760
						size: new google.maps.Size(32, 32),
761
						origin: new google.maps.Point(0, 0),
762
						anchor: new google.maps.Point(4, 28)
763
					};
764
					gicons["8Ls"] = {
765
						url: WT_MODULES_DIR + "googlemap/images/icon8Ls.png",
766
						size: new google.maps.Size(24, 24),
767
						origin: new google.maps.Point(0, 0),
768
						anchor: new google.maps.Point(22, 22)
769
					};
770
					gicons["8Rs"] = {
771
						url: WT_MODULES_DIR + "googlemap/images/icon8Rs.png",
772
						size: new google.maps.Size(24, 24),
773
						origin: new google.maps.Point(0, 0),
774
						anchor: new google.maps.Point(2, 22)
775
					};
776
					// / A function to create the marker and set up the event window
777
					function createMarker(point, name, html, mhtml, icontype) {
778
						// Create a marker with the requested icon
779
						var marker = new google.maps.Marker({
780
							icon:     gicons[icontype],
781
							map:      pm_map,
782
							position: point,
783
							id:       index,
784
							zIndex:   0
785
						});
786
						google.maps.event.addListener(marker, "click", function() {
787
							infowindow.close();
788
							infowindow.setContent(mhtml);
789
							infowindow.open(pm_map, marker);
790
							var el = $(".gm-ancestor[data-marker=" + marker.id + "]");
791
							if(el.hasClass("person_box")) {
792
								el
793
									.removeClass("person_box")
794
									.addClass("gm-ancestor-visited");
795
								infowindow.close();
796
							} else {
797
								el
798
									.addClass("person_box")
799
									.removeClass("gm-ancestor-visited");
800
							}
801
							var anchor = infowindow.getAnchor();
802
							lastlinkid = anchor ? anchor.id : null;
803
						});
804
						// save the info we need to use later for the side bar
805
						gmarkers[index] = marker;
806
						gm_ancestors_html += "<div data-marker =" + index++ + " class=\"gm-ancestor\">" + html +"</div>";
807
808
						return marker;
809
					}
810
					// create the map
811
					var myOptions = {
812
						zoom:                     6,
813
						minZoom:                  <?= $this->getPreference('GM_MIN_ZOOM', self::GM_MIN_ZOOM_DEFAULT) ?>,
814
						maxZoom:                  <?= $this->getPreference('GM_MAX_ZOOM', self::GM_MAX_ZOOM_DEFAULT) ?>,
815
						center:                   new google.maps.LatLng(0, 0),
816
						mapTypeId:                google.maps.MapTypeId.TERRAIN,  // ROADMAP, SATELLITE, HYBRID, TERRAIN
817
						mapTypeControlOptions:    {
818
							style: google.maps.MapTypeControlStyle.DROPDOWN_MENU  // DEFAULT, DROPDOWN_MENU, HORIZONTAL_BAR
819
						},
820
						navigationControlOptions: {
821
							position: google.maps.ControlPosition.TOP_RIGHT,  // BOTTOM, BOTTOM_LEFT, LEFT, TOP, etc
822
							style:    google.maps.NavigationControlStyle.SMALL   // ANDROID, DEFAULT, SMALL, ZOOM_PAN
823
						},
824
						scrollwheel:              true
825
					};
826
					var pm_map = new google.maps.Map(document.querySelector(".gm-map"), myOptions);
827
					google.maps.event.addListener(pm_map, "click", function() {
828
						$(".gm-ancestor.person_box")
829
							.removeClass("person_box")
830
							.addClass("gm-ancestor-visited");
831
						infowindow.close();
832
						lastlinkid = null;
833
					});
834
					// create the map bounds
835
					var bounds = new google.maps.LatLngBounds();
836
					<?php
837
					// add the points
838
					$curgen       = 1;
839
					$count        = 0;
840
					$colored_line = [
841
						'1' => '#FF0000',
842
						'2' => '#0000FF',
843
						'3' => '#00FF00',
844
						'4' => '#FFFF00',
845
						'5' => '#00FFFF',
846
						'6' => '#FF00FF',
847
						'7' => '#C0C0FF',
848
						'8' => '#808000',
849
					];
850
					$lat        = [];
851
					$lon        = [];
852
					$latlongval = [];
853
					for ($i = 0; $i < $this->treesize; $i++) {
854
						// moved up to grab the sex of the individuals
855
						$person = $this->ancestors[$i];
856
						if ($person) {
857
							$name = $person->getFullName();
858
859
							// -- check to see if we have moved to the next generation
860
							if ($i + 1 >= pow(2, $curgen)) {
861
								$curgen++;
862
							}
863
864
							$relationship = FunctionsCharts::getSosaName($i + 1);
865
866
							// get thumbnail image
867
							if ($person->getTree()->getPreference('SHOW_HIGHLIGHT_IMAGES')) {
868
								$image = $person->displayImage(40, 50, 'crop', []);
869
							} else {
870
								$image = '';
871
							}
872
873
							$event = '<img src="' . WT_MODULES_DIR . 'googlemap/images/sq' . $curgen . '.png" width="10" height="10"> ';
874
							$event .= '<strong>' . $relationship . '</strong>';
875
876
							$birth = $person->getFirstFact('BIRT');
877
							$data  = addslashes($image . '<div class="gm-ancestor-link">' . $event . ' <span><a href="' . $person->getHtmlUrl() . '">' . $name . '</a></span>');
878
							$data .= $birth ? addslashes($birth->summary()) : '';
879
							$data .= '</div>';
880
881
							$latlongval[$i] = $this->getLatitudeAndLongitudeFromPlaceLocation($person->getBirthPlace()->getGedcomName());
882
							if ($latlongval[$i]) {
883
								$lat[$i] = (float) strtr($latlongval[$i]->pl_lati, ['N' => '', 'S' => '-', ',' => '.']);
884
								$lon[$i] = (float) strtr($latlongval[$i]->pl_long, ['E' => '', 'W' => '-', ',' => '.']);
885
								if ($lat[$i] || $lon[$i]) {
886
									$marker_number = $curgen;
887
									$dups          = 0;
888
									for ($k = 0; $k < $i; $k++) {
889
										if ($latlongval[$i] == $latlongval[$k]) {
890
											$dups++;
891
											switch ($dups) {
892
												case 1:
893
													$marker_number = $curgen . 'L';
894
													break;
895
												case 2:
896
													$marker_number = $curgen . 'R';
897
													break;
898
												case 3:
899
													$marker_number = $curgen . 'Ls';
900
													break;
901
												case 4:
902
													$marker_number = $curgen . 'Rs';
903
													break;
904
												case 5: //adjust position where markers have same coodinates
905
												default:
906
													$marker_number = $curgen;
907
													$lon[$i] += 0.0025;
908
													$lat[$i] += 0.0025;
909
													break;
910
											}
911
										}
912
									}
913
914
									?>
915
									var point = new google.maps.LatLng(<?= $lat[$i] ?>, <?= $lon[$i] ?>);
916
									var marker = createMarker(point, "<?= addslashes($name) ?>", "<?= $data ?>", "<div class=\"gm-info-window\"><?= $data ?></div>", "<?= $marker_number ?>");
917
									<?php
918
									// Construct the polygon lines
919
									$to_child = (intval(($i - 1) / 2)); // Draw a line from parent to child
920
									if (array_key_exists($to_child, $lat) && $lat[$to_child] != 0 && $lon[$to_child] != 0) {
921
										?>
922
										var linecolor;
923
										var plines;
924
										var lines = [
925
											new google.maps.LatLng(<?= $lat[$i] ?>, <?= $lon[$i] ?>),
926
											new google.maps.LatLng(<?= $lat[$to_child] ?>, <?= $lon[$to_child] ?>)
927
										];
928
										linecolor = "<?= $colored_line[$curgen] ?>";
929
										plines = new google.maps.Polygon({
930
											paths: lines,
931
											strokeColor: linecolor,
932
											strokeOpacity: 0.8,
933
											strokeWeight: 3,
934
											fillColor: "#FF0000",
935
											fillOpacity: 0.1
936
										});
937
										plines.setMap(pm_map);
938
										<?php
939
									}
940
									// Extend and fit marker bounds
941
942
									?>
943
									bounds.extend(point);
944
									<?php
945
									$count++;
946
								}
947
							}
948
						} else {
949
							$latlongval[$i] = null;
950
						}
951
					}
952
					?>
953
					pm_map.setCenter(bounds.getCenter());
954
					pm_map.fitBounds(bounds);
955
					google.maps.event.addListenerOnce(pm_map, "bounds_changed", function(event) {
956
						var maxZoom = <?= $this->getPreference('GM_MAX_ZOOM', self::GM_MAX_ZOOM_DEFAULT) ?>;
957
						if (this.getZoom() > maxZoom) {
958
							this.setZoom(maxZoom);
959
						}
960
					});
961
962
					// Close the sidebar highlight when the infowindow is closed
963
					google.maps.event.addListener(infowindow, "closeclick", function() {
964
						$(".gm-ancestor[data-marker=" + lastlinkid + "]").toggleClass("gm-ancestor-visited person_box");
965
						lastlinkid = null;
966
					});
967
					// put the assembled gm_ancestors_html contents into the gm-ancestors div
968
					document.querySelector(".gm-ancestors").innerHTML = gm_ancestors_html;
969
970
					$(".gm-ancestor-link")
971
						.on("click", "a", function(e) {
972
							e.stopPropagation();
973
						})
974
						.on("click", function(e) {
975
							if (lastlinkid !== null) {
976
								$(".gm-ancestor[data-marker=" + lastlinkid + "]").toggleClass("person_box gm-ancestor-visited");
977
							}
978
							var target = $(this).closest(".gm-ancestor").data("marker");
979
							google.maps.event.trigger(gmarkers[target], "click");
980
						});
981
				}
982
			</script>
983
			<script src="<?= $this->googleMapsScript() ?>&amp;callback=initialiZePedigreeMap"></script>
984
			<?php
985
986
			return;
987
		}
988
989
		$controller
990
			->setPageTitle(/* I18N: %s is an individual’s name */ I18N::translate('Pedigree map of %s', $controller->root->getFullName()))
991
			/* prepending the module css in the page head allows the theme to over-ride it*/
992
			->addInlineJavascript('$("head").prepend(\'<link type="text/css" href ="' . WT_MODULES_DIR . 'googlemap/css/wt_v3_googlemap.css" rel="stylesheet">\');')
993
			->addInlineJavascript('$(".wt-page-content").load(location.search + "&ajax=1");')
994
			->pageHeader();
995
?>
996
997
	<div id="pedigreemap-page">
998
		<h2><?= $controller->getPageTitle() ?></h2>
999
1000
		<form class="wt-page-options wt-page-options-pedigree-map d-print-none">
1001
			<input type="hidden" name="ged" value="<?= $WT_TREE->getNameHtml() ?>">
1002
			<input type="hidden" name="mod" value="googlemap">
1003
			<input type="hidden" name="mod_action" value="pedigree_map">
1004
1005
			<div class="row form-group">
1006
				<label class="col-sm-3 col-form-label wt-page-options-label" for="rootid">
1007
					<?= I18N::translate('Individual') ?>
1008
				</label>
1009
				<div class="col-sm-9 wt-page-options-value">
1010
					<?= FunctionsEdit::formControlIndividual($controller->root, ['id' => 'rootid', 'name' => 'rootid']) ?>
1011
				</div>
1012
			</div>
1013
1014
			<div class="row form-group">
1015
				<label class="col-sm-3 col-form-label wt-page-options-label" for="PEDIGREE_GENERATIONS">
1016
					<?= I18N::translate('Generations') ?>
1017
				</label>
1018
				<div class="col-sm-9 wt-page-options-value">
1019
					<?= Bootstrap4::select(FunctionsEdit::numericOptions(range(2, $WT_TREE->getPreference('MAX_PEDIGREE_GENERATIONS'))), $generations, ['id' => 'PEDIGREE_GENERATIONS', 'name' => 'PEDIGREE_GENERATIONS']) ?>
1020
				</div>
1021
			</div>
1022
1023
			<div class="row form-group">
1024
				<div class="col-sm-3 wt-page-options-label"></div>
1025
				<div class="col-sm-9 wt-page-options-value">
1026
					<input class="btn btn-primary" type="submit" value="<?= /* I18N: A button label. */ I18N::translate('view') ?>">
1027
				</div>
1028
			</div>
1029
		</form>
1030
1031
		<div class="wt-ajax-load wt-page-content"></div>
1032
		<?php
1033
	}
1034
1035
	/**
1036
	 * Does an individual (or their spouse-families) have any facts with places?
1037
	 *
1038
	 * @param Individual $individual
1039
	 *
1040
	 * @return bool
1041
	 */
1042
	private function checkMapData(Individual $individual) {
1043
		$statement = Database::prepare(
1044
			"SELECT COUNT(*) FROM `##placelinks` WHERE pl_gid = :xref AND pl_file = :tree_id"
1045
		);
1046
		$args = [
1047
			'xref'    => $individual->getXref(),
1048
			'tree_id' => $individual->getTree()->getTreeId(),
1049
		];
1050
1051
		if ($statement->execute($args)->fetchOne()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $statement->execute($args)->fetchOne() of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1052
			return true;
1053
		}
1054
1055
		foreach ($individual->getSpouseFamilies() as $family) {
1056
			$args['xref'] = $family->getXref();
1057
			if ($statement->execute($args)->fetchOne()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $statement->execute($args)->fetchOne() of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1058
				return true;
1059
			}
1060
		}
1061
1062
		return false;
1063
	}
1064
1065
	/**
1066
	 * Remove prefixes from a place name to allow it to be matched.
1067
	 *
1068
	 * @param string   $prefix_list
1069
	 * @param string   $place
1070
	 * @param string[] $placelist
1071
	 *
1072
	 * @return string[]
1073
	 */
1074
	private function removePrefixFromPlaceName($prefix_list, $place, $placelist) {
1075
		if ($prefix_list) {
1076
			foreach (explode(';', $prefix_list) as $prefix) {
1077
				if ($prefix && substr($place, 0, strlen($prefix) + 1) == $prefix . ' ') {
1078
					$placelist[] = substr($place, strlen($prefix) + 1);
1079
				}
1080
			}
1081
		}
1082
1083
		return $placelist;
1084
	}
1085
1086
	/**
1087
	 * Remove suffixes from a place name to allow it to be matched.
1088
	 *
1089
	 * @param string   $suffix_list
1090
	 * @param string   $place
1091
	 * @param string[] $placelist
1092
	 *
1093
	 * @return string[]
1094
	 */
1095
	private function removeSuffixFromPlaceName($suffix_list, $place, $placelist) {
1096
		if ($suffix_list) {
1097
			foreach (explode(';', $suffix_list) as $postfix) {
1098
				if ($postfix && substr($place, -strlen($postfix) - 1) == ' ' . $postfix) {
1099
					$placelist[] = substr($place, 0, strlen($place) - strlen($postfix) - 1);
1100
				}
1101
			}
1102
		}
1103
1104
		return $placelist;
1105
	}
1106
1107
	/**
1108
	 * Remove prefixes and sufixes to allow place names to be matched.
1109
	 *
1110
	 * @param string   $prefix_list
1111
	 * @param string   $suffix_list
1112
	 * @param string   $place
1113
	 * @param string[] $placelist
1114
	 *
1115
	 * @return string[]
1116
	 */
1117
	private function removePrefixAndSuffixFromPlaceName($prefix_list, $suffix_list, $place, $placelist) {
1118
		if ($prefix_list && $suffix_list) {
1119
			foreach (explode(';', $prefix_list) as $prefix) {
1120
				foreach (explode(';', $suffix_list) as $postfix) {
1121
					if ($prefix && $postfix && substr($place, 0, strlen($prefix) + 1) == $prefix . ' ' && substr($place, -strlen($postfix) - 1) == ' ' . $postfix) {
1122
						$placelist[] = substr($place, strlen($prefix) + 1, strlen($place) - strlen($prefix) - strlen($postfix) - 2);
1123
					}
1124
				}
1125
			}
1126
		}
1127
1128
		return $placelist;
1129
	}
1130
1131
	/**
1132
	 * Match placenames with different prefixes and suffixes.
1133
	 *
1134
	 * @param string $placename
1135
	 * @param int    $level
1136
	 *
1137
	 * @return string[]
1138
	 */
1139
	private function createPossiblePlaceNames($placename, $level) {
1140
		$retlist = [];
1141
		if ($level <= 9) {
1142
			$retlist = $this->removePrefixAndSuffixFromPlaceName($this->getPreference('GM_PREFIX_' . $level), $this->getPreference('GM_POSTFIX_' . $level), $placename, $retlist); // Remove both
1143
			$retlist = $this->removePrefixFromPlaceName($this->getPreference('GM_PREFIX_' . $level), $placename, $retlist); // Remove prefix
1144
			$retlist = $this->removeSuffixFromPlaceName($this->getPreference('GM_POSTFIX_' . $level), $placename, $retlist); // Remove suffix
1145
		}
1146
		$retlist[] = $placename; // Exact
1147
1148
		return $retlist;
1149
	}
1150
1151
	/**
1152
	 * Get the map co-ordinates of a place.
1153
	 *
1154
	 * @param string $place
1155
	 *
1156
	 * @return null|\stdClass
0 ignored issues
show
Documentation introduced by
Should the return type not be stdClass|array|null? Also, consider making the array more specific, something like array<String>, or String[].

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

If the return type contains the type array, this check recommends the use of a more specific type like String[] or array<String>.

Loading history...
1157
	 */
1158
	private function getLatitudeAndLongitudeFromPlaceLocation($place) {
1159
		$parent     = explode(',', $place);
1160
		$parent     = array_reverse($parent);
1161
		$place_id   = 0;
1162
		$num_parent = count($parent);
1163
		for ($i = 0; $i < $num_parent; $i++) {
1164
			$parent[$i] = trim($parent[$i]);
1165
			if (empty($parent[$i])) {
1166
				$parent[$i] = 'unknown'; // GoogleMap module uses "unknown" while GEDCOM uses , ,
1167
			}
1168
			$placelist = $this->createPossiblePlaceNames($parent[$i], $i + 1);
1169
			foreach ($placelist as $placename) {
1170
				$pl_id = Database::prepare(
1171
					"SELECT pl_id FROM `##placelocation` WHERE pl_level=? AND pl_parent_id=? AND pl_place LIKE ? ORDER BY pl_place"
1172
				)->execute([$i, $place_id, $placename])->fetchOne();
1173
				if (!empty($pl_id)) {
1174
					break;
1175
				}
1176
			}
1177
			if (empty($pl_id)) {
1178
				break;
1179
			}
1180
			$place_id = $pl_id;
1181
		}
1182
1183
		return Database::prepare(
1184
			"SELECT pl_lati, pl_long, pl_zoom, pl_icon, pl_level" .
1185
			" FROM `##placelocation`" .
1186
			" WHERE pl_id = ?" .
1187
			" ORDER BY pl_place"
1188
		)->execute([$place_id])->fetchOneRow();
1189
	}
1190
1191
	/**
1192
	 * @param Fact $fact
1193
	 *
1194
	 * @return array
1195
	 */
1196
	private function getPlaceData(Fact $fact) {
1197
		$result = [];
1198
1199
		$has_latitude  = preg_match('/\n4 LATI (.+)/', $fact->getGedcom(), $match1);
1200
		$has_longitude = preg_match('/\n4 LONG (.+)/', $fact->getGedcom(), $match2);
1201
1202
		// If co-ordinates are stored in the GEDCOM then use them
1203
		if ($has_latitude && $has_longitude) {
1204
			$result = [
1205
				'index'   => 'ID' . $match1[1] . $match2[1],
1206
				'mapdata' => [
1207
					'class'   => 'optionbox',
1208
					'place'   => $fact->getPlace()->getFullName(),
1209
					'tooltip' => $fact->getPlace()->getGedcomName(),
1210
					'lat'     => strtr($match1[1], ['N' => '', 'S' => '-', ',' => '.']),
1211
					'lng'     => strtr($match2[1], ['E' => '', 'W' => '-', ',' => '.']),
1212
					'pl_icon' => '',
1213
					'pl_zoom' => '0',
1214
					'events'  => '',
1215
				],
1216
			];
1217
		} else {
1218
			$place_location = $this->getLatitudeAndLongitudeFromPlaceLocation($fact->getPlace()->getGedcomName());
1219
			if ($place_location && $place_location->pl_lati && $place_location->pl_long) {
1220
				$result = [
1221
					'index'   => 'ID' . $place_location->pl_lati . $place_location->pl_long,
1222
					'mapdata' => [
1223
						'class'   => 'optionbox',
1224
						'place'   => $fact->getPlace()->getFullName(),
1225
						'tooltip' => $fact->getPlace()->getGedcomName(),
1226
						'lat'     => strtr($place_location->pl_lati, ['N' => '', 'S' => '-', ',' => '.']),
1227
						'lng'     => strtr($place_location->pl_long, ['E' => '', 'W' => '-', ',' => '.']),
1228
						'pl_icon' => $place_location->pl_icon,
1229
						'pl_zoom' => $place_location->pl_zoom,
1230
						'events'  => '',
1231
					],
1232
				];
1233
			}
1234
		}
1235
1236
		return $result;
1237
	}
1238
1239
	/**
1240
	 * Build a map for an individual.
1241
	 *
1242
	 * @param Individual $indi
1243
	 *
1244
	 * @return string
1245
	 */
1246
	private function buildIndividualMap(Individual $indi) {
1247
		$GM_MAX_ZOOM = $this->getPreference('GM_MAX_ZOOM', self::GM_MAX_ZOOM_DEFAULT);
1248
		$facts       = $indi->getFacts();
1249
		foreach ($indi->getSpouseFamilies() as $family) {
1250
			$facts = array_merge($facts, $family->getFacts());
1251
			// Add birth of children from this family to the facts array
1252
			foreach ($family->getChildren() as $child) {
1253
				foreach ($child->getFacts(WT_EVENTS_BIRT, true) as $fact) {
1254
					if ($fact->getPlace() !== null) {
1255
						$facts[] = $fact;
1256
						break;
1257
					}
1258
				}
1259
			}
1260
		}
1261
1262
		Functions::sortFacts($facts);
1263
1264
		// At this point we have an array of valid sorted facts
1265
		// so now build the data structures needed for the map display
1266
		$events        = [];
1267
		$unique_places = [];
1268
1269
		foreach ($facts as $fact) {
1270
			$place_data = $this->getPlaceData($fact);
1271
1272
			if (!empty($place_data)) {
1273
				$index = $place_data['index'];
1274
1275
				if ($place_data['mapdata']['pl_zoom']) {
1276
					$GM_MAX_ZOOM = min($GM_MAX_ZOOM, $place_data['mapdata']['pl_zoom']);
1277
				}
1278
				// Produce the html for the sidebar
1279
				$parent = $fact->getParent();
1280
				if ($parent instanceof Individual && $parent->getXref() !== $indi->getXref()) {
1281
					// Childs birth
1282
					$name   = '<a href="' . $parent->getHtmlUrl() . '">' . $parent->getFullName() . '</a>';
1283
					$label  = strtr($parent->getSex(), ['F' => I18N::translate('Birth of a daughter'), 'M' => I18N::translate('Birth of a son'), 'U' => I18N::translate('Birth of a child')]);
1284
					$class  = 'wt-gender-' . $parent->getSex();
1285
					$evtStr = '<div class="gm-event">' . $label . '<div><strong>' . $name . '</strong></div>' . $fact->getDate()->display(true) . '</div>';
1286
				} else {
1287
					$spouse = $parent instanceof Family ? $parent->getSpouse($indi) : null;
1288
					$name   = $spouse ? '<a href="' . $spouse->getHtmlUrl() . '">' . $spouse->getFullName() . '</a>' : '';
1289
					$label  = $fact->getLabel();
1290
					$class  = '';
1291
					if ($fact->getValue() && $spouse) {
1292
						$evtStr = '<div class="gm-event">' . $label . '<div>' . $fact->getValue() . '</div><strong>' . $name . '</strong>' . $fact->getDate()->display(true) . '</div>';
1293
					} elseif ($spouse) {
1294
						$evtStr = '<div class="gm-event">' . $label . '<div><strong>' . $name . '</strong></div>' . $fact->getDate()->display(true) . '</div>';
1295
					} elseif ($fact->getValue()) {
1296
						$evtStr = '<div class="gm-event">' . $label . '<div> ' . $fact->getValue() . '</div>' . $fact->getDate()->display(true) . '</div>';
1297
					} else {
1298
						$evtStr = '<div class="gm-event">' . $label . '<div>' . $fact->getDate()->display(true) . '</div></div>';
1299
					}
1300
				}
1301
1302
				if (empty($unique_places[$index])) {
1303
					$unique_places[$index] = $place_data['mapdata'];
1304
				}
1305
				$unique_places[$index]['events'] .= $evtStr;
1306
				$events[] = [
1307
					'class'      => $class,
1308
					'fact_label' => $label,
1309
					'date'       => $fact->getDate()->display(true),
1310
					'info'       => $fact->getValue(),
1311
					'name'       => $name,
1312
					'place'      => '<a href="' . $fact->getPlace()->getURL() . '">' . $fact->getPlace()->getFullName() . '</a>',
1313
					'placeid'    => $index,
1314
				];
1315
			}
1316
		}
1317
1318
		if (!empty($events)) {
1319
			$places = array_keys($unique_places);
1320
			ob_start();
1321
			// Create the normal googlemap sidebar of events and children
1322
			echo '<div class="gm-events">';
1323
			echo '<table class="wt-facts-table">';
1324
			echo '<caption class="sr-only">' . I18N::translate('Facts and events') . '</caption>';
1325
			echo '<tbody>';
1326
1327
			foreach ($events as $event) {
1328
				$index = array_search($event['placeid'], $places);
1329
				echo '<tr class="', $event['class'], '">';
1330
				echo '<th scope="row">';
1331
				echo '<a href="#" onclick="return openInfowindow(\'', $index, '\')">';
1332
				echo $event['fact_label'];
1333
				echo '</a>';
1334
				echo '</th>';
1335
				echo '<td>';
1336
				if ($event['info']) {
1337
					echo '<div><span class="field">', Html::escape($event['info']), '</span></div>';
1338
				}
1339
				if ($event['name']) {
1340
					echo '<div>', $event['name'], '</div>';
1341
				}
1342
				echo '<div>', $event['place'], '</div>';
1343
				if ($event['date']) {
1344
					echo '<div>', $event['date'], '</div>';
1345
				}
1346
				echo '</td>';
1347
				echo '</tr>';
1348
			}
1349
1350
			echo '</tbody>';
1351
			echo '</table>';
1352
			echo '</div>';
1353
			?>
1354
1355
			<script>
1356
				var map_center = new google.maps.LatLng(0, 0);
1357
				var gmarkers   = [];
1358
				var gicons     = [];
1359
				var map        = null;
1360
				var infowindow = new google.maps.InfoWindow({});
1361
1362
				gicons["red"] = {
1363
					url:    "https://maps.google.com/mapfiles/marker.png",
1364
					size:   google.maps.Size(20, 34),
1365
					origin: google.maps.Point(0, 0),
1366
					anchor: google.maps.Point(9, 34)
1367
				};
1368
1369
				function getMarkerImage(iconColor) {
1370
					if (typeof(iconColor) === 'undefined' || iconColor === null) {
1371
						iconColor = 'red';
1372
					}
1373
					if (!gicons[iconColor]) {
1374
						gicons[iconColor] = {
1375
							url:    '//maps.google.com/mapfiles/marker' + iconColor + '.png',
1376
							size:   new google.maps.Size(20, 34),
1377
							origin: new google.maps.Point(0, 0),
1378
							anchor: google.maps.Point(9, 34)
1379
						};
1380
					}
1381
					return gicons[iconColor];
1382
				}
1383
1384
				var placer   = null;
1385
1386
				// A function to create the marker and set up the event window
1387
				function createMarker(latlng, html, tooltip, marker_icon) {
1388
					// Use flag icon (if defined) instead of regular marker icon
1389
					if (marker_icon) {
1390
						var icon_image = {
1391
							url:    WT_MODULES_DIR + 'googlemap/' + marker_icon,
1392
							size:   new google.maps.Size(25, 15),
1393
							origin: new google.maps.Point(0, 0),
1394
							anchor: new google.maps.Point(12, 15)
1395
						};
1396
					} else {
1397
						var icon_image = getMarkerImage('red');
1398
					}
1399
1400
					placer = latlng;
1401
1402
					// Define the marker
1403
					var marker = new google.maps.Marker({
1404
						position: placer,
1405
						icon:     icon_image,
1406
						map:      map,
1407
						title:    tooltip,
1408
						zIndex:   Math.round(latlng.lat() * -100000) << 5
1409
					});
1410
1411
					// Store the tab and event info as marker properties
1412
					gmarkers.push(marker);
1413
1414
					// Open infowindow when marker is clicked
1415
					google.maps.event.addListener(marker, 'click', function() {
1416
						infowindow.close();
1417
						infowindow.setContent(html);
1418
						infowindow.open(map, marker);
1419
					});
1420
				}
1421
1422
				// Opens Marker infowindow when corresponding Sidebar item is clicked
1423
				function openInfowindow(i) {
1424
					infowindow.close();
1425
					google.maps.event.trigger(gmarkers[i], 'click');
1426
					return false;
1427
				}
1428
1429
				function loadMap() {
1430
					// Create the map and mapOptions
1431
					var mapOptions = {
1432
						zoom:                     7,
1433
						minZoom:                  <?= $this->getPreference('GM_MIN_ZOOM', self::GM_MIN_ZOOM_DEFAULT) ?>,
1434
						maxZoom:                  <?= $this->getPreference('GM_MAX_ZOOM', self::GM_MAX_ZOOM_DEFAULT) ?>,
1435
						center:                   map_center,
1436
						mapTypeId:                google.maps.MapTypeId.ROADMAP,
1437
						mapTypeControlOptions:    {
1438
							style: google.maps.MapTypeControlStyle.DROPDOWN_MENU  // DEFAULT, DROPDOWN_MENU, HORIZONTAL_BAR
1439
						},
1440
						navigationControl:        true,
1441
						navigationControlOptions: {
1442
							position: google.maps.ControlPosition.TOP_RIGHT,  // BOTTOM, BOTTOM_LEFT, LEFT, TOP, etc
1443
							style:    google.maps.NavigationControlStyle.SMALL  // ANDROID, DEFAULT, SMALL, ZOOM_PAN
1444
						},
1445
						scrollwheel:              true
1446
					};
1447
					map = new google.maps.Map(document.querySelector('.gm-map'), mapOptions);
1448
1449
					// Close any infowindow when map is clicked
1450
					google.maps.event.addListener(map, 'click', function() {
1451
						infowindow.close();
1452
					});
1453
1454
					// Add the markers to the map
1455
1456
					// Group the markers by location
1457
					var locations = <?= json_encode($unique_places) ?>;
1458
1459
					// Set the Marker bounds
1460
					var bounds = new google.maps.LatLngBounds();
1461
					var zoomLevel = <?= $GM_MAX_ZOOM ?>;
1462
1463
					jQuery.each(locations, function(index, location) {
1464
						var point = new google.maps.LatLng(location.lat, location.lng); // Place Latitude, Longitude
1465
						var html  =
1466
							'<div class="gm-info-window">' +
1467
							'<div class="gm-info-window-header">' + location.place + '</div>' +
1468
							'<ul class="gm-tabs">' +
1469
							'<li class="gm-tab gm-tab-active" id="gm-tab-events"><a href="#"><?= I18N::translate('Events') ?></a></li>' +
1470
							'</ul>' +
1471
							'<div class="gm-panes">' +
1472
							'<div class="gm-pane" id="gm-pane-events">' + location.events + '</div>' +
1473
							'</div>' +
1474
							'</div>';
1475
1476
						createMarker(point, html, location.tooltip, location.pl_icon);
1477
						bounds.extend(point);
1478
					}); // end loop through location markers
1479
1480
					map.setCenter(bounds.getCenter());
1481
					map.fitBounds(bounds);
1482
					google.maps.event.addListenerOnce(map, "bounds_changed", function(event) {
1483
						if (this.getZoom() > zoomLevel) {
1484
							this.setZoom(zoomLevel);
1485
						}
1486
					});
1487
				} // end loadMap()
1488
1489
			</script>
1490
			<?php
1491
			$html = ob_get_clean();
1492
		} else {
1493
			$html = '';
1494
		}
1495
1496
		return $html;
1497
	}
1498
1499
	/**
1500
	 * Get the Location ID.
1501
	 *
1502
	 * @param string $place
1503
	 *
1504
	 * @return int
1505
	 */
1506
	private function getPlaceLocationId($place) {
1507
		$par      = explode(',', $place);
1508
		$par      = array_reverse($par);
1509
		$place_id = 0;
1510
		$pl_id    = 0;
1511
		$num_par  = count($par);
1512
		for ($i = 0; $i < $num_par; $i++) {
1513
			$par[$i] = trim($par[$i]);
1514
			if (empty($par[$i])) {
1515
				$par[$i] = 'unknown';
1516
			}
1517
			$placelist = $this->createPossiblePlaceNames($par[$i], $i + 1);
1518 View Code Duplication
			foreach ($placelist as $key => $placename) {
1519
				$pl_id = (int) Database::prepare(
1520
					"SELECT pl_id FROM `##placelocation` WHERE pl_level = :level AND pl_parent_id = :parent_id AND pl_place LIKE :placename"
1521
				)->execute([
1522
					'level'     => $i,
1523
					'parent_id' => $place_id,
1524
					'placename' => $placename,
1525
				])->fetchOne();
1526
				if ($pl_id) {
1527
					break;
1528
				}
1529
			}
1530
			if (!$pl_id) {
1531
				break;
1532
			}
1533
			$place_id = $pl_id;
1534
		}
1535
1536
		return $place_id;
1537
	}
1538
1539
	/**
1540
	 * Get the place ID.
1541
	 *
1542
	 * @param string $place
1543
	 *
1544
	 * @return int
1545
	 */
1546
	private function getPlaceId($place) {
1547
		global $WT_TREE;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
1548
1549
		$par      = explode(',', $place);
1550
		$par      = array_reverse($par);
1551
		$place_id = 0;
1552
		$pl_id    = 0;
1553
		$num_par  = count($par);
1554
		for ($i = 0; $i < $num_par; $i++) {
1555
			$par[$i]   = trim($par[$i]);
1556
			$placelist = $this->createPossiblePlaceNames($par[$i], $i + 1);
1557 View Code Duplication
			foreach ($placelist as $placename) {
1558
				$pl_id = (int) Database::prepare(
1559
					"SELECT p_id FROM `##places` WHERE p_parent_id = :place_id AND p_file = :tree_id AND p_place = :placename"
1560
				)->execute([
1561
					'place_id'  => $place_id,
1562
					'tree_id'   => $WT_TREE->getTreeId(),
1563
					'placename' => $placename,
1564
				])->fetchOne();
1565
				if ($pl_id) {
1566
					break;
1567
				}
1568
			}
1569
			if (!$pl_id) {
1570
				break;
1571
			}
1572
			$place_id = $pl_id;
1573
		}
1574
1575
		return $place_id;
1576
	}
1577
1578
	/**
1579
	 * Set the place IDs.
1580
	 *
1581
	 * @param int      $level
1582
	 * @param string[] $parent
1583
	 *
1584
	 * @return int
1585
	 */
1586
	private function setPlaceIdMap($level, $parent) {
1587
		$fullplace = '';
1588
		if ($level == 0) {
1589
			return 0;
1590
		} else {
1591
			for ($i = 1; $i <= $level; $i++) {
1592
				$fullplace .= $parent[$level - $i] . ', ';
1593
			}
1594
			$fullplace = substr($fullplace, 0, -2);
1595
1596
			return $this->getPlaceId($fullplace);
1597
		}
1598
	}
1599
1600
	/**
1601
	 * Set the map level.
1602
	 *
1603
	 * @param int      $level
1604
	 * @param string[] $parent
1605
	 *
1606
	 * @return int
1607
	 */
1608
	private function setLevelMap($level, $parent) {
1609
		$fullplace = '';
1610
		if ($level == 0) {
1611
			return 0;
1612
		} else {
1613
			for ($i = 1; $i <= $level; $i++) {
1614
				if ($parent[$level - $i] != '') {
1615
					$fullplace .= $parent[$level - $i] . ', ';
1616
				} else {
1617
					$fullplace .= 'Unknown, ';
1618
				}
1619
			}
1620
			$fullplace = substr($fullplace, 0, -2);
1621
1622
			return $this->getPlaceLocationId($fullplace);
1623
		}
1624
	}
1625
1626
	/**
1627
	 * Called by placelist.php
1628
	 */
1629
	public function createMap() {
1630
		global $level, $levelm, $plzoom, $WT_TREE;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
1631
1632
		Database::updateSchema(self::SCHEMA_MIGRATION_PREFIX, self::SCHEMA_SETTING_NAME, self::SCHEMA_TARGET_VERSION);
1633
1634
		$parent = Filter::getArray('parent');
1635
		$levelm = $this->setLevelMap($level, $parent);
1636
1637
		$latlng =
1638
			Database::prepare("SELECT pl_place, pl_id, pl_lati, pl_long, pl_zoom FROM `##placelocation` WHERE pl_id=?")
1639
			->execute([$levelm])
1640
			->fetch(PDO::FETCH_ASSOC);
1641
1642
		echo '<table style="margin:auto; border-collapse: collapse;">';
1643
		echo '<tr style="vertical-align:top;"><td>';
1644
		echo '<div id="gm-hierarchy-map" class="wt-ajax-load"></div>';
1645
		echo '<script src="', $this->googleMapsScript(), '"></script>';
1646
1647
		$plzoom = $latlng['pl_zoom']; // Map zoom level
1648
1649
		if (Auth::isAdmin()) {
1650
			$adminplaces_url = 'module.php?mod=googlemap&amp;mod_action=admin_places';
1651
			if ($latlng && isset($latlng['pl_id'])) {
1652
				$adminplaces_url .= '&amp;parent=' . $latlng['pl_id'];
1653
			}
1654
			$update_places_url = 'admin_trees_places.php?ged=' . $WT_TREE->getNameHtml() . '&amp;search=' . urlencode(implode(', ', array_reverse($parent)));
1655
			echo '<div class="gm-options">';
1656
			echo '<a href="' . $adminplaces_url . '">' . I18N::translate('Geographic data') . '</a>';
1657
			echo ' | <a href="' . $update_places_url . '">' . I18N::translate('Update place names') . '</a>';
1658
			echo '</div>';
1659
		}
1660
		echo '</td>';
1661
		echo '</tr></table>';
1662
	}
1663
1664
	/**
1665
	 * Print the numbers of individuals.
1666
	 *
1667
	 * @param int      $level
1668
	 * @param string[] $parent
1669
	 */
1670
	private function printHowManyPeople($level, $parent) {
1671
		global $WT_TREE;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
1672
1673
		$stats = new Stats($WT_TREE);
1674
1675
		$place_count_indi = 0;
1676
		$place_count_fam  = 0;
1677
		if (!isset($parent[$level - 1])) {
1678
			$parent[$level - 1] = '';
1679
		}
1680
		$p_id = $this->setPlaceIdMap($level, $parent);
1681
		$indi = $stats->statsPlaces('INDI', false, $p_id);
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1682
		$fam  = $stats->statsPlaces('FAM', false, $p_id);
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1683
		foreach ($indi as $place) {
1684
			$place_count_indi = $place['tot'];
1685
		}
1686
		foreach ($fam as $place) {
1687
			$place_count_fam = $place['tot'];
1688
		}
1689
		echo '<br><br>', I18N::translate('Individuals'), ': ', $place_count_indi, ', ', I18N::translate('Families'), ': ', $place_count_fam;
1690
	}
1691
1692
	/**
1693
	 * Print the flags and markers.
1694
	 *
1695
	 * @param stdClass $place2
1696
	 * @param int      $level
1697
	 * @param string[] $parent
1698
	 * @param int      $levelm
1699
	 * @param string   $linklevels
1700
	 */
1701
	private function printGoogleMapMarkers(stdClass $place2, $level, $parent, $levelm, $linklevels) {
1702
		echo 'var icon_url = null;';
1703
		if (!$place2->pl_lati || !$place2->pl_long) {
1704
			echo 'var icon_url ="' . WT_MODULES_DIR . 'googlemap/images/marker_yellow.png";';
1705
			echo 'var point = new google.maps.LatLng(0, 0);';
1706
			echo 'var marker = createMarker(point, "<div style=\"width: 250px;\"><a href=\"?action=find', $linklevels, '&amp;parent[' . $level . ']=';
1707
1708
			if ($place2->pl_place == 'Unknown') {
1709
				echo '\"><br>';
1710
			} else {
1711
				echo addslashes($place2->pl_place), '\"><br>';
1712
			}
1713 View Code Duplication
			if ($place2->pl_icon !== null && $place2->pl_icon !== '') {
1714
				echo '<img src=\"', WT_MODULES_DIR, 'googlemap/', $place2->pl_icon, '\">&nbsp;&nbsp;';
1715
			}
1716
			if ($place2->pl_place == 'Unknown') {
1717
				echo I18N::translate('unknown');
1718
			} else {
1719
				echo addslashes($place2->pl_place);
1720
			}
1721
			echo '</a>';
1722
			$parent[$level] = $place2->pl_place;
1723
			$this->printHowManyPeople($level + 1, $parent);
1724
			echo '<br>', I18N::translate('This place has no coordinates');
1725
			if (Auth::isAdmin()) {
1726
				echo '<br><a href=\"module.php?mod=googlemap&amp;mod_action=admin_places&amp;parent=', $levelm, '&amp;display=inactive\">', I18N::translate('Geographic data'), '</a>';
1727
			}
1728
			echo '</div>", icon_url, "', str_replace(['&lrm;', '&rlm;'], [WT_UTF8_LRM, WT_UTF8_RLM], addslashes($place2->pl_place)), '");';
1729
		} else {
1730
			$lati = strtr($place2->pl_lati, ['N' => '', 'S' => '-', ',' => '.']);
1731
			$long = strtr($place2->pl_long, ['E' => '', 'W' => '-', ',' => '.']);
1732
			//delete leading zero
1733 View Code Duplication
			if ($lati >= 0) {
1734
				$lati = abs($lati);
1735
			} elseif ($lati < 0) {
1736
				$lati = '-' . abs($lati);
1737
			}
1738 View Code Duplication
			if ($long >= 0) {
1739
				$long = abs($long);
1740
			} elseif ($long < 0) {
1741
				$long = '-' . abs($long);
1742
			}
1743
1744
			if ($place2->pl_icon !== null && $place2->pl_icon !== '' && $this->getPreference('GM_PH_MARKER') === 'G_FLAG') {
1745
				echo 'icon_url = "', WT_MODULES_DIR, 'googlemap/', $place2->pl_icon, '";';
1746
			}
1747
			echo 'var point = new google.maps.LatLng(', $lati, ', ', $long, ');';
1748
			echo 'var marker = createMarker(point, "<div style=\"width: 250px;\"><a href=\"?action=find', $linklevels;
1749
			echo '&amp;parent[', $level, ']=';
1750
			if ($place2->pl_place !== 'Unknown') {
1751
				echo rawurlencode($place2->pl_place);
1752
			}
1753
			echo '\"><br>';
1754 View Code Duplication
			if ($place2->pl_icon !== null && $place2->pl_icon !== '') {
1755
				echo '<img src=\"', WT_MODULES_DIR, 'googlemap/', $place2->pl_icon, '\">&nbsp;&nbsp;';
1756
			}
1757
			if ($place2->pl_place === 'Unknown') {
1758
				echo I18N::translate('unknown');
1759
			} else {
1760
				echo Html::escape($place2->pl_place);
1761
			}
1762
			echo '</a>';
1763
			$parent[$level] = $place2->pl_place;
1764
			$this->printHowManyPeople($level + 1, $parent);
1765
			echo '</div>", icon_url, "', Html::escape($place2->pl_place), '");';
1766
		}
1767
	}
1768
1769
	/**
1770
	 * Called by placelist.php
1771
	 *
1772
	 * @param int      $numfound
1773
	 * @param int      $level
1774
	 * @param string[] $parent
1775
	 * @param string   $linklevels
1776
	 * @param string[] $place_names
1777
	 */
1778
	public function mapScripts($numfound, $level, $parent, $linklevels, $place_names) {
1779
		global $plzoom, $controller;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
1780
1781
		$controller->addInlineJavascript('
1782
			$("head").append(\'<link rel="stylesheet" type="text/css" href="' . WT_MODULES_DIR . 'googlemap/css/wt_v3_googlemap.css" />\');
1783
			var numMarkers = "' . $numfound . '";
1784
			var mapLevel   = "' . $level . '";
1785
			var placezoom  = "' . $plzoom . '";
1786
			var infowindow = new google.maps.InfoWindow({
1787
				// size: new google.maps.Size(150,50),
1788
				// maxWidth: 600
1789
			});
1790
1791
			var map_center = new google.maps.LatLng(0,0);
1792
			var map = "";
1793
			var bounds = new google.maps.LatLngBounds ();
1794
			var markers = [];
1795
			var gmarkers = [];
1796
			var i = 0;
1797
1798
			// Create the map and mapOptions
1799
			var mapOptions = {
1800
				minZoom: ' . $this->getPreference('GM_MIN_ZOOM', self::GM_MIN_ZOOM_DEFAULT) . ',
1801
				maxZoom: ' . $this->getPreference('GM_MAX_ZOOM', self::GM_MAX_ZOOM_DEFAULT) . ',
1802
				zoom: 8,
1803
				center: map_center,
1804
				mapTypeId: google.maps.MapTypeId.ROADMAP,
1805
				mapTypeControlOptions: {
1806
					style: google.maps.MapTypeControlStyle.DROPDOWN_MENU // DEFAULT, DROPDOWN_MENU, HORIZONTAL_BAR
1807
				},
1808
				navigationControl: true,
1809
				navigationControlOptions: {
1810
					position: google.maps.ControlPosition.TOP_RIGHT, // BOTTOM, BOTTOM_LEFT, LEFT, TOP, etc
1811
					style: google.maps.NavigationControlStyle.SMALL  // ANDROID, DEFAULT, SMALL, ZOOM_PAN
1812
				},
1813
				scrollwheel: true
1814
			};
1815
			map = new google.maps.Map(document.getElementById("gm-hierarchy-map"), mapOptions);
1816
1817
			// Close any infowindow when map is clicked
1818
			google.maps.event.addListener(map, "click", function() {
1819
				infowindow.close();
1820
			});
1821
1822
			// If only one marker, set zoom level to that of place in database
1823
			if (mapLevel != 0) {
1824
				var pointZoom = placezoom;
1825
			} else {
1826
				var pointZoom = 1;
1827
			}
1828
1829
			// Creates a marker whose info window displays the given name
1830
			function createMarker(point, html, icon, name) {
1831
				// Choose icon ============
1832
				if (icon && ' . $level . '<=3) {
1833
					if (icon != "' . WT_MODULES_DIR . 'googlemap/images/marker_yellow.png") {
1834
						var iconImage = {
1835
							url:    icon,
1836
							size:   new google.maps.Size(25, 15),
1837
							origin: new google.maps.Point(0,0),
1838
							anchor: new google.maps.Point(12, 15)
1839
						};
1840
					} else {
1841
						var iconImage = {
1842
							url:    icon,
1843
							size:   new google.maps.Size(20, 34),
1844
							origin: new google.maps.Point(0,0),
1845
							anchor: new google.maps.Point(9, 34)
1846
						};
1847
					}
1848
				} else {
1849
					var iconImage = {
1850
						url:    "https://maps.google.com/mapfiles/marker.png",
1851
						size:   new google.maps.Size(20, 34),
1852
						origin: new google.maps.Point(0,0),
1853
						anchor: new google.maps.Point(9, 34)
1854
					};
1855
				}
1856
				var posn = new google.maps.LatLng(0,0);
1857
				var marker = new google.maps.Marker({
1858
					position: point,
1859
					icon: iconImage,
1860
					map: map,
1861
					title: name
1862
				});
1863
				// Show this markers name in the info window when it is clicked
1864
				google.maps.event.addListener(marker, "click", function() {
1865
					infowindow.close();
1866
					infowindow.setContent(html);
1867
					infowindow.open(map, marker);
1868
				});
1869
				// === Store the tab, category and event info as marker properties ===
1870
				marker.mypoint = point;
1871
				marker.mytitle = name;
1872
				marker.myposn = posn;
1873
				gmarkers.push(marker);
1874
				bounds.extend(marker.position);
1875
1876
				// If only one marker use database place zoom level rather than fitBounds of markers
1877
				if (numMarkers > 1) {
1878
					map.fitBounds(bounds);
1879
				} else {
1880
					map.setCenter(bounds.getCenter());
1881
					map.setZoom(parseFloat(pointZoom));
1882
				}
1883
				return marker;
1884
			}
1885
		');
1886
1887
		$levelm = $this->setLevelMap($level, $parent);
1888
1889
		//create markers
1890
		ob_start();
1891
1892
		if ($numfound == 0 && $level > 0) {
1893
			// show the current place on the map
1894
1895
			$place = Database::prepare("SELECT * FROM `##placelocation` WHERE pl_id = ?")
1896
				->execute([$levelm])
1897
				->fetchOneRow();
1898
1899
			if ($place !== null) {
1900
				// re-calculate the hierarchy information required to display the current place
1901
				$thisloc = $parent;
1902
				array_pop($thisloc);
1903
				$thislevel      = $level - 1;
1904
				$thislinklevels = substr($linklevels, 0, strrpos($linklevels, '&amp;'));
1905
1906
				$this->printGoogleMapMarkers($place, $thislevel, $thisloc, $place->pl_id, $thislinklevels);
0 ignored issues
show
Bug introduced by
It seems like $place defined by \Fisharebest\Webtrees\Da...levelm))->fetchOneRow() on line 1895 can also be of type array; however, Fisharebest\Webtrees\Mod...printGoogleMapMarkers() does only seem to accept object<stdClass>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1907
			}
1908
		}
1909
1910
		// display any sub-places
1911
		$placeidlist = [];
1912
		foreach ($place_names as $placename) {
1913
			$thisloc     = $parent;
1914
			$thisloc[]   = $placename;
1915
			$this_levelm = $this->setLevelMap($level + 1, $thisloc);
1916
			if ($this_levelm) {
1917
				$placeidlist[] = $this_levelm;
1918
			}
1919
		}
1920
1921
		// flip the array (thus removing duplicates)
1922
		$placeidlist = array_flip($placeidlist);
1923
		// remove entry for parent location
1924
		unset($placeidlist[$levelm]);
1925
1926
		if (!empty($placeidlist)) {
1927
			// the keys are all we care about (this reverses the earlier array_flip, and ensures there are no "holes" in the array)
1928
			$placeidlist = array_keys($placeidlist);
1929
			// note: this implode/array_fill code generates one '?' for each entry in the $placeidlist array
1930
			$placelist =
1931
				Database::prepare(
1932
					"SELECT * FROM `##placelocation` WHERE pl_id IN (" . implode(',', array_fill(0, count($placeidlist), '?')) . ')'
1933
				)->execute($placeidlist)
1934
				->fetchAll();
1935
1936
			foreach ($placelist as $place) {
1937
				$this->printGoogleMapMarkers($place, $level, $parent, $place->pl_id, $linklevels);
0 ignored issues
show
Bug introduced by
It seems like $place defined by $place on line 1936 can also be of type array<integer,string>; however, Fisharebest\Webtrees\Mod...printGoogleMapMarkers() does only seem to accept object<stdClass>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1938
			}
1939
		}
1940
		$controller->addInlineJavascript(ob_get_clean());
1941
	}
1942
1943
	/**
1944
	 * Take a place id and find its place in the hierarchy
1945
	 * Input: place ID
1946
	 * Output: ordered array of id=>name values, starting with the Top level
1947
	 * e.g. 0=>"Top level", 16=>"England", 19=>"London", 217=>"Westminster"
1948
	 *
1949
	 * @param int $id
1950
	 *
1951
	 * @return string[]
1952
	 */
1953
	private function placeIdToHierarchy($id) {
1954
		$statement = Database::prepare("SELECT pl_parent_id, pl_place FROM `##placelocation` WHERE pl_id=?");
1955
		$arr       = [];
1956
		while ($id != 0) {
1957
			$row = $statement->execute([$id])->fetchOneRow();
1958
			$arr = [$id => $row->pl_place] + $arr;
1959
			$id  = $row->pl_parent_id;
1960
		}
1961
1962
		return $arr;
1963
	}
1964
1965
	/**
1966
	 * Get the highest index.
1967
	 *
1968
	 * @return int
1969
	 */
1970
	private function getHighestIndex() {
1971
		return (int) Database::prepare("SELECT MAX(pl_id) FROM `##placelocation`")->fetchOne();
1972
	}
1973
1974
	/**
1975
	 * Get the highest level.
1976
	 *
1977
	 * @return int
1978
	 */
1979
	private function getHighestLevel() {
1980
		return (int) Database::prepare("SELECT MAX(pl_level) FROM `##placelocation`")->fetchOne();
1981
	}
1982
1983
	/**
1984
	 * Find all of the places in the hierarchy
1985
	 *
1986
	 * NOTE: the "inactive" filter ignores the hierarchy, so that "Paris, France"
1987
	 * will match "Paris, Texas, United States".  A fully accurate match would be slow.
1988
	 *
1989
	 * @param int  $parent_id
1990
	 * @param bool $inactive
1991
	 *
1992
	 * @return array[]
1993
	 */
1994
	private function getPlaceListLocation($parent_id, $inactive = false) {
1995
		if ($inactive) {
1996
			$rows = Database::prepare(
1997
					"SELECT pl_id, pl_parent_id, pl_place, pl_lati, pl_long, pl_zoom, pl_icon" .
1998
					" FROM `##placelocation`" .
1999
					" WHERE pl_parent_id = :parent_id" .
2000
					" ORDER BY pl_place COLLATE :collation"
2001
				)->execute([
2002
					'parent_id' => $parent_id,
2003
					'collation' => I18N::collation(),
2004
				])->fetchAll();
2005
		} else {
2006
			$rows = Database::prepare(
2007
				"SELECT DISTINCT pl_id, pl_parent_id, pl_place, pl_lati, pl_long, pl_zoom, pl_icon" .
2008
				" FROM `##placelocation`" .
2009
				" JOIN `##places` ON `##placelocation`.pl_place = `##places`.p_place" .
2010
				" WHERE pl_parent_id = :parent_id" .
2011
				" ORDER BY pl_place COLLATE :collation"
2012
			)->execute([
2013
				'parent_id' => $parent_id,
2014
				'collation' => I18N::collation(),
2015
			])->fetchAll();
2016
		}
2017
2018
		$placelist = [];
2019
		foreach ($rows as $row) {
2020
			// Find/count places without co-ordinates
2021
			$children =
2022
				Database::prepare(
2023
				"SELECT SQL_CACHE COUNT(*) AS total, SUM(" .
2024
				" p1.pl_place IS NOT NULL AND IFNULL(p1.pl_lati, '') IN ('N0', '') AND IFNULL(p1.pl_long, '') IN ('E0', '') OR " .
2025
				" p2.pl_place IS NOT NULL AND IFNULL(p2.pl_lati, '') IN ('N0', '') AND IFNULL(p2.pl_long, '') IN ('E0', '') OR " .
2026
				" p3.pl_place IS NOT NULL AND IFNULL(p3.pl_lati, '') IN ('N0', '') AND IFNULL(p3.pl_long, '') IN ('E0', '') OR " .
2027
				" p4.pl_place IS NOT NULL AND IFNULL(p4.pl_lati, '') IN ('N0', '') AND IFNULL(p4.pl_long, '') IN ('E0', '') OR " .
2028
				" p5.pl_place IS NOT NULL AND IFNULL(p5.pl_lati, '') IN ('N0', '') AND IFNULL(p5.pl_long, '') IN ('E0', '') OR " .
2029
				" p6.pl_place IS NOT NULL AND IFNULL(p6.pl_lati, '') IN ('N0', '') AND IFNULL(p6.pl_long, '') IN ('E0', '') OR " .
2030
				" p7.pl_place IS NOT NULL AND IFNULL(p7.pl_lati, '') IN ('N0', '') AND IFNULL(p7.pl_long, '') IN ('E0', '') OR " .
2031
				" p8.pl_place IS NOT NULL AND IFNULL(p8.pl_lati, '') IN ('N0', '') AND IFNULL(p8.pl_long, '') IN ('E0', '') OR " .
2032
				" p9.pl_place IS NOT NULL AND IFNULL(p9.pl_lati, '') IN ('N0', '') AND IFNULL(p9.pl_long, '') IN ('E0', '')) AS missing" .
2033
				" FROM      `##placelocation` AS p1" .
2034
				" LEFT JOIN `##placelocation` AS p2 ON (p2.pl_parent_id = p1.pl_id)" .
2035
				" LEFT JOIN `##placelocation` AS p3 ON (p3.pl_parent_id = p2.pl_id)" .
2036
				" LEFT JOIN `##placelocation` AS p4 ON (p4.pl_parent_id = p3.pl_id)" .
2037
				" LEFT JOIN `##placelocation` AS p5 ON (p5.pl_parent_id = p4.pl_id)" .
2038
				" LEFT JOIN `##placelocation` AS p6 ON (p6.pl_parent_id = p5.pl_id)" .
2039
				" LEFT JOIN `##placelocation` AS p7 ON (p7.pl_parent_id = p6.pl_id)" .
2040
				" LEFT JOIN `##placelocation` AS p8 ON (p8.pl_parent_id = p7.pl_id)" .
2041
				" LEFT JOIN `##placelocation` AS p9 ON (p9.pl_parent_id = p8.pl_id)" .
2042
				" WHERE p1.pl_parent_id = :parent_id"
2043
			)
2044
			->execute([
2045
				'parent_id' => $row->pl_id,
2046
			])->fetchOneRow();
2047
2048
			$placelist[] = [
2049
				'place_id'  => (int) $row->pl_id,
2050
				'parent_id' => (int) $row->pl_parent_id,
2051
				'place'     => $row->pl_place,
2052
				'lati'      => $row->pl_lati,
2053
				'long'      => $row->pl_long,
2054
				'zoom'      => (int) $row->pl_zoom,
2055
				'icon'      => $row->pl_icon,
2056
				'is_empty'  => ($row->pl_lati === null || $row->pl_lati === 'N0') && ($row->pl_long === null || $row->pl_long === 'E0'),
2057
				'children'  => (int) $children->total,
2058
				'missing'   => (int) $children->missing,
2059
			];
2060
		}
2061
2062
		return $placelist;
2063
	}
2064
2065
	/**
2066
	 * Set the output level.
2067
	 *
2068
	 * @param int $parent_id
2069
	 */
2070
	private function outputLevel($parent_id) {
2071
		$tmp      = $this->placeIdToHierarchy($parent_id);
2072
		$maxLevel = $this->getHighestLevel();
2073
		if ($maxLevel > 8) {
2074
			$maxLevel = 8;
2075
		}
2076
		$prefix = implode(';', $tmp);
2077
		if ($prefix != '') {
2078
			$prefix .= ';';
2079
		}
2080
		$suffix = str_repeat(';', $maxLevel - count($tmp));
2081
		$level  = count($tmp);
2082
2083
		$rows = Database::prepare(
2084
			"SELECT pl_id, pl_place, pl_long, pl_lati, pl_zoom, pl_icon FROM `##placelocation` WHERE pl_parent_id=? ORDER BY pl_place"
2085
		)->execute([$parent_id])->fetchAll();
2086
2087
		foreach ($rows as $row) {
2088
			echo $level, ';', $prefix, $row->pl_place, $suffix, ';', $row->pl_long, ';', $row->pl_lati, ';', $row->pl_zoom, ';', $row->pl_icon, "\r\n";
2089
			if ($level < $maxLevel) {
2090
				$this->outputLevel($row->pl_id);
2091
			}
2092
		}
2093
	}
2094
2095
	/**
2096
	 * recursively find all of the csv files on the server
2097
	 *
2098
	 * @param string $path
2099
	 *
2100
	 * @return string[]
2101
	 */
2102
	private function findFiles($path) {
2103
		$placefiles = [];
2104
2105
		try {
2106
			$di = new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS);
2107
			$it = new \RecursiveIteratorIterator($di);
2108
2109
			foreach ($it as $file) {
2110
				if ($file->getExtension() == 'csv') {
2111
					$placefiles[] = '/' . $file->getFilename();
2112
				}
2113
			}
2114
		} catch (\Exception $ex) {
2115
		DebugBar::addThrowable($ex);
0 ignored issues
show
Documentation introduced by
$ex is of type object<Exception>, but the function expects a object<Throwable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2116
		
2117
			Log::addErrorLog(basename($ex->getFile()) . ' - line: ' . $ex->getLine() . ' - ' . $ex->getMessage());
2118
		}
2119
2120
		return $placefiles;
2121
	}
2122
2123
	/**
2124
	 * Show a form with options to upload a CSV file
2125
	 */
2126
	private function adminUploadForm() {
2127
		$parent_id = (int) Filter::get('parent_id');
2128
		$inactive  = (int) Filter::get('inactive');
2129
2130
		$controller = new PageController;
2131
		$controller
2132
			->setPageTitle(I18N::translate('Upload geographic data'))
2133
			->pageHeader();
2134
2135
		echo Bootstrap4::breadcrumbs([
2136
			route('admin-control-panel')                         => I18N::translate('Control panel'),
2137
			route('admin-modules')                               => I18N::translate('Module administration'),
2138
			$this->getConfigLink()                             => $this->getTitle(),
2139
			'module.php?mod=googlemap&mod_action=admin_places' => I18N::translate('Geographic data'),
2140
		], $controller->getPageTitle());
2141
2142
			$placefiles = $this->findFiles(WT_MODULES_DIR . 'googlemap/extra');
2143
			sort($placefiles);
2144
2145
		?>
2146
		<h1><?= $controller->getPageTitle() ?></h1>
2147
2148
		<form method="post" action="module.php?mod=googlemap&amp;mod_action=admin_upload_action" enctype="multipart/form-data">
2149
			<input type="hidden" name="parent_id" value="<?= $parent_id ?>">
2150
			<input type="hidden" name="inactive" value="<?= $inactive ?>">
2151
2152
			<!-- PLACES FILE -->
2153
			<div class="row form-group">
2154
				<label class="col-form-label col-sm-4" for="placesfile">
2155
					<?= I18N::translate('A file on your computer') ?>
2156
				</label>
2157
				<div class="col-sm-8">
2158
					<input id="placesfile" type="file" name="placesfile" class="form-control">
2159
				</div>
2160
			</div>
2161
2162
			<!-- LOCAL FILE -->
2163
			<div class="row form-group">
2164
				<label class="col-form-label col-sm-4" for="localfile">
2165
					<?= I18N::translate('A file on the server') ?>
2166
				</label>
2167
				<div class="col-sm-8">
2168
					<div class="input-group">
2169
						<span class="input-group-addon">
2170
							<?= WT_MODULES_DIR . 'googlemap/extra/' ?>
2171
						</span>
2172
						<?php
2173
						foreach ($placefiles as $p => $placefile) {
2174
							unset($placefiles[$p]);
2175
							$p = Html::escape($placefile);
2176
							if (substr($placefile, 0, 1) == '/') {
2177
								$placefiles[$p] = substr($placefile, 1);
2178
							} else {
2179
								$placefiles[$p] = $placefile;
2180
							}
2181
						}
2182
						echo Bootstrap4::select($placefiles, '', ['id' => 'localfile', ['id' => 'localfile']]);
0 ignored issues
show
Documentation introduced by
array('id' => 'localfile...y('id' => 'localfile')) is of type array<string|integer,str...{\"id\":\"string\"}>"}>, but the function expects a array<integer,string>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2183
						?>
2184
					</div>
2185
				</div>
2186
			</div>
2187
2188
			<!-- CLEAR DATABASE -->
2189
			<fieldset class="form-group">
2190
				<div class="row">
2191
					<legend class="col-form-legend col-sm-4">
2192
						<?= I18N::translate('Delete all existing geographic data before importing the file.') ?>
2193
					</legend>
2194
					<div class="col-sm-8">
2195
						<?= Bootstrap4::radioButtons('cleardatabase', [I18N::translate('no'), I18N::translate('yes')], '0', true) ?>
2196
					</div>
2197
				</div>
2198
			</fieldset>
2199
2200
			<!-- UPDATE ONLY -->
2201
			<fieldset class="form-group">
2202
2203
				<div class="row">
2204
					<legend class="col-form-legend col-sm-4">
2205
						<?= I18N::translate('Do not create new locations, just import coordinates for existing locations.') ?>
2206
					</legend>
2207
					<div class="col-sm-8">
2208
						<?= Bootstrap4::radioButtons('updateonly', [I18N::translate('no'), I18N::translate('yes')], '0', true) ?>
2209
					</div>
2210
				</div>
2211
			</fieldset>
2212
2213
			<!-- OVERWRITE DATA -->
2214
			<fieldset class="form-group">
2215
				<div class="row">
2216
					<legend class="col-form-legend col-sm-4">
2217
						<?= I18N::translate('Overwrite existing coordinates.') ?>
2218
					</legend>
2219
					<div class="col-sm-8">
2220
						<?= Bootstrap4::radioButtons('overwritedata', [I18N::translate('no'), I18N::translate('yes')], '0', true) ?>
2221
					</div>
2222
				</div>
2223
			</fieldset>
2224
2225
			<!-- SAVE BUTTON -->
2226
			<div class="row form-group">
2227
				<div class="offset-sm-4 col-sm-8">
2228
					<button type="submit" class="btn btn-primary">
2229
						<i class="fa fa-check"></i>
2230
						<?= I18N::translate('continue') ?>
2231
					</button>
2232
				</div>
2233
			</div>
2234
		</form>
2235
		<?php
2236
	}
2237
2238
	/**
2239
	 * Delete a geographic place.
2240
	 */
2241
	private function adminDeleteAction() {
2242
		$place_id  = (int) Filter::post('place_id');
2243
		$parent_id = (int) Filter::post('parent_id');
2244
		$inactive  = (int) Filter::post('inactive');
2245
2246
		try {
2247
			Database::prepare(
2248
				"DELETE FROM `##placelocation` WHERE pl_id = :place_id"
2249
			)->execute([
2250
				'place_id' => $place_id,
2251
			]);
2252
		} catch (\Exception $ex) {
2253
		DebugBar::addThrowable($ex);
0 ignored issues
show
Documentation introduced by
$ex is of type object<Exception>, but the function expects a object<Throwable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2254
		
2255
			FlashMessages::addMessage(I18N::translate('Location not removed: this location contains sub-locations'), 'danger');
2256
		}
2257
2258
		header('Location: module.php?mod=googlemap&mod_action=admin_places&parent_id=' . $parent_id . '&inactive=' . $inactive);
2259
	}
2260
2261
	/**
2262
	 * Import places from GEDCOM data.
2263
	 */
2264
	private function adminImportAction() {
2265
	}
2266
2267
	/**
2268
	 * Upload a CSV file.
2269
	 */
2270
	private function adminUploadAction() {
0 ignored issues
show
Coding Style introduced by
adminUploadAction uses the super-global variable $_FILES which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
Coding Style introduced by
adminUploadAction uses the super-global variable $_REQUEST which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
2271
		global $WT_TREE;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
2272
2273
		$country_names = [];
2274
		$stats         = new Stats($WT_TREE);
2275
		foreach ($stats->iso3166() as $key => $value) {
2276
			$country_names[$key] = I18N::translate($key);
2277
		}
2278
		if (Filter::postBool('cleardatabase')) {
2279
			Database::exec("DELETE FROM `##placelocation` WHERE 1=1");
2280
		}
2281
		if (!empty($_FILES['placesfile']['tmp_name'])) {
2282
			$lines = file($_FILES['placesfile']['tmp_name']);
2283
		} elseif (!empty($_REQUEST['localfile'])) {
2284
			$lines = file(WT_MODULES_DIR . 'googlemap/extra' . $_REQUEST['localfile']);
0 ignored issues
show
Security File Exposure introduced by
WT_MODULES_DIR . 'google... $_REQUEST['localfile'] can contain request data and is used in file inclusion context(s) leading to a potential security vulnerability.

1 path for user data to reach this point

  1. Read from $_REQUEST
    in app/Module/GoogleMapsModule.php on line 2284

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
2285
		} else {
2286
			FlashMessages::addMessage(I18N::translate('No file was received. Please try again.'), 'danger');
2287
			$lines = [''];
2288
		}
2289
		// Strip BYTE-ORDER-MARK, if present
2290
		if (!empty($lines[0]) && substr($lines[0], 0, 3) === WT_UTF8_BOM) {
2291
			$lines[0] = substr($lines[0], 3);
2292
		}
2293
		asort($lines);
2294
		$highestIndex = $this->getHighestIndex();
2295
		$placelist    = [];
2296
		$j            = 0;
2297
		$maxLevel     = 0;
2298
		foreach ($lines as $p => $placerec) {
2299
			$fieldrec = explode(';', $placerec);
2300
			if ($fieldrec[0] > $maxLevel) {
2301
				$maxLevel = $fieldrec[0];
2302
			}
2303
		}
2304
		$fields   = count($fieldrec);
0 ignored issues
show
Bug introduced by
The variable $fieldrec does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
2305
		$set_icon = true;
2306
		if (!is_dir(WT_MODULES_DIR . 'googlemap/places/flags/')) {
2307
			$set_icon = false;
2308
		}
2309
		foreach ($lines as $p => $placerec) {
2310
			$fieldrec = explode(';', $placerec);
2311
			if (is_numeric($fieldrec[0]) && $fieldrec[0] <= $maxLevel) {
2312
				$placelist[$j]          = [];
2313
				$placelist[$j]['place'] = '';
2314
				for ($ii = $fields - 4; $ii > 1; $ii--) {
2315
					if ($fieldrec[0] > $ii - 2) {
2316
						$placelist[$j]['place'] .= $fieldrec[$ii] . ',';
2317
					}
2318
				}
2319
				foreach ($country_names as $countrycode => $countryname) {
2320
					if ($countrycode == strtoupper($fieldrec[1])) {
2321
						$fieldrec[1] = $countryname;
2322
						break;
2323
					}
2324
				}
2325
				$placelist[$j]['place'] .= $fieldrec[1];
2326
				$placelist[$j]['long'] = $fieldrec[$fields - 4];
2327
				$placelist[$j]['lati'] = $fieldrec[$fields - 3];
2328
				$placelist[$j]['zoom'] = $fieldrec[$fields - 2];
2329
				if ($set_icon) {
2330
					$placelist[$j]['icon'] = trim($fieldrec[$fields - 1]);
2331
				} else {
2332
					$placelist[$j]['icon'] = '';
2333
				}
2334
				$j = $j + 1;
2335
			}
2336
		}
2337
2338
		$prevPlace     = '';
2339
		$prevLati      = '';
2340
		$prevLong      = '';
2341
		$placelistUniq = [];
2342
		$j             = 0;
2343
		foreach ($placelist as $k => $place) {
2344
			if ($place['place'] != $prevPlace) {
2345
				$placelistUniq[$j]          = [];
2346
				$placelistUniq[$j]['place'] = $place['place'];
2347
				$placelistUniq[$j]['lati']  = $place['lati'];
2348
				$placelistUniq[$j]['long']  = $place['long'];
2349
				$placelistUniq[$j]['zoom']  = $place['zoom'];
2350
				$placelistUniq[$j]['icon']  = $place['icon'];
2351
				$j                          = $j + 1;
2352
			} elseif (($place['place'] == $prevPlace) && (($place['lati'] != $prevLati) || ($place['long'] != $prevLong))) {
2353
				if (($placelistUniq[$j - 1]['lati'] == 0) || ($placelistUniq[$j - 1]['long'] == 0)) {
2354
					$placelistUniq[$j - 1]['lati'] = $place['lati'];
2355
					$placelistUniq[$j - 1]['long'] = $place['long'];
2356
					$placelistUniq[$j - 1]['zoom'] = $place['zoom'];
2357
					$placelistUniq[$j - 1]['icon'] = $place['icon'];
2358 View Code Duplication
				} elseif (($place['lati'] != '0') || ($place['long'] != '0')) {
2359
					echo 'Difference: previous value = ', $prevPlace, ', ', $prevLati, ', ', $prevLong, ' current = ', $place['place'], ', ', $place['lati'], ', ', $place['long'], '<br>';
0 ignored issues
show
Security Cross-Site Scripting introduced by
$prevPlace can contain request data and is used in output context(s) leading to a potential security vulnerability.

1 path for user data to reach this point

  1. Read from $_REQUEST, and WT_MODULES_DIR . 'googlemap/extra' . $_REQUEST['localfile'] is passed through file(), and $lines is assigned
    in app/Module/GoogleMapsModule.php on line 2284
  2. $placerec is assigned
    in app/Module/GoogleMapsModule.php on line 2309
  3. $placerec is passed through explode(), and $fieldrec is assigned
    in app/Module/GoogleMapsModule.php on line 2310
  4. $placelist is assigned
    in app/Module/GoogleMapsModule.php on line 2328
  5. $placelist is assigned
    in app/Module/GoogleMapsModule.php on line 2330
  6. $place is assigned
    in app/Module/GoogleMapsModule.php on line 2343
  7. $prevPlace is assigned
    in app/Module/GoogleMapsModule.php on line 2362

Preventing Cross-Site-Scripting Attacks

Cross-Site-Scripting allows an attacker to inject malicious code into your website - in particular Javascript code, and have that code executed with the privileges of a visiting user. This can be used to obtain data, or perform actions on behalf of that visiting user.

In order to prevent this, make sure to escape all user-provided data:

// for HTML
$sanitized = htmlentities($tainted, ENT_QUOTES);

// for URLs
$sanitized = urlencode($tainted);

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
Security Cross-Site Scripting introduced by
$prevLati can contain request data and is used in output context(s) leading to a potential security vulnerability.

1 path for user data to reach this point

  1. Read from $_REQUEST, and WT_MODULES_DIR . 'googlemap/extra' . $_REQUEST['localfile'] is passed through file(), and $lines is assigned
    in app/Module/GoogleMapsModule.php on line 2284
  2. $placerec is assigned
    in app/Module/GoogleMapsModule.php on line 2309
  3. $placerec is passed through explode(), and $fieldrec is assigned
    in app/Module/GoogleMapsModule.php on line 2310
  4. $placelist is assigned
    in app/Module/GoogleMapsModule.php on line 2328
  5. $placelist is assigned
    in app/Module/GoogleMapsModule.php on line 2330
  6. $place is assigned
    in app/Module/GoogleMapsModule.php on line 2343
  7. $prevLati is assigned
    in app/Module/GoogleMapsModule.php on line 2363

Preventing Cross-Site-Scripting Attacks

Cross-Site-Scripting allows an attacker to inject malicious code into your website - in particular Javascript code, and have that code executed with the privileges of a visiting user. This can be used to obtain data, or perform actions on behalf of that visiting user.

In order to prevent this, make sure to escape all user-provided data:

// for HTML
$sanitized = htmlentities($tainted, ENT_QUOTES);

// for URLs
$sanitized = urlencode($tainted);

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
Security Cross-Site Scripting introduced by
$prevLong can contain request data and is used in output context(s) leading to a potential security vulnerability.

1 path for user data to reach this point

  1. Read from $_REQUEST, and WT_MODULES_DIR . 'googlemap/extra' . $_REQUEST['localfile'] is passed through file(), and $lines is assigned
    in app/Module/GoogleMapsModule.php on line 2284
  2. $placerec is assigned
    in app/Module/GoogleMapsModule.php on line 2309
  3. $placerec is passed through explode(), and $fieldrec is assigned
    in app/Module/GoogleMapsModule.php on line 2310
  4. $placelist is assigned
    in app/Module/GoogleMapsModule.php on line 2328
  5. $placelist is assigned
    in app/Module/GoogleMapsModule.php on line 2330
  6. $place is assigned
    in app/Module/GoogleMapsModule.php on line 2343
  7. $prevLong is assigned
    in app/Module/GoogleMapsModule.php on line 2364

Preventing Cross-Site-Scripting Attacks

Cross-Site-Scripting allows an attacker to inject malicious code into your website - in particular Javascript code, and have that code executed with the privileges of a visiting user. This can be used to obtain data, or perform actions on behalf of that visiting user.

In order to prevent this, make sure to escape all user-provided data:

// for HTML
$sanitized = htmlentities($tainted, ENT_QUOTES);

// for URLs
$sanitized = urlencode($tainted);

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
Security Cross-Site Scripting introduced by
$place['place'] can contain request data and is used in output context(s) leading to a potential security vulnerability.

1 path for user data to reach this point

  1. Read from $_REQUEST, and WT_MODULES_DIR . 'googlemap/extra' . $_REQUEST['localfile'] is passed through file(), and $lines is assigned
    in app/Module/GoogleMapsModule.php on line 2284
  2. $placerec is assigned
    in app/Module/GoogleMapsModule.php on line 2309
  3. $placerec is passed through explode(), and $fieldrec is assigned
    in app/Module/GoogleMapsModule.php on line 2310
  4. $placelist is assigned
    in app/Module/GoogleMapsModule.php on line 2328
  5. $placelist is assigned
    in app/Module/GoogleMapsModule.php on line 2330
  6. $place is assigned
    in app/Module/GoogleMapsModule.php on line 2343

Preventing Cross-Site-Scripting Attacks

Cross-Site-Scripting allows an attacker to inject malicious code into your website - in particular Javascript code, and have that code executed with the privileges of a visiting user. This can be used to obtain data, or perform actions on behalf of that visiting user.

In order to prevent this, make sure to escape all user-provided data:

// for HTML
$sanitized = htmlentities($tainted, ENT_QUOTES);

// for URLs
$sanitized = urlencode($tainted);

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
Security Cross-Site Scripting introduced by
$place['lati'] can contain request data and is used in output context(s) leading to a potential security vulnerability.

1 path for user data to reach this point

  1. Read from $_REQUEST, and WT_MODULES_DIR . 'googlemap/extra' . $_REQUEST['localfile'] is passed through file(), and $lines is assigned
    in app/Module/GoogleMapsModule.php on line 2284
  2. $placerec is assigned
    in app/Module/GoogleMapsModule.php on line 2309
  3. $placerec is passed through explode(), and $fieldrec is assigned
    in app/Module/GoogleMapsModule.php on line 2310
  4. $placelist is assigned
    in app/Module/GoogleMapsModule.php on line 2328
  5. $placelist is assigned
    in app/Module/GoogleMapsModule.php on line 2330
  6. $place is assigned
    in app/Module/GoogleMapsModule.php on line 2343

Preventing Cross-Site-Scripting Attacks

Cross-Site-Scripting allows an attacker to inject malicious code into your website - in particular Javascript code, and have that code executed with the privileges of a visiting user. This can be used to obtain data, or perform actions on behalf of that visiting user.

In order to prevent this, make sure to escape all user-provided data:

// for HTML
$sanitized = htmlentities($tainted, ENT_QUOTES);

// for URLs
$sanitized = urlencode($tainted);

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
Security Cross-Site Scripting introduced by
$place['long'] can contain request data and is used in output context(s) leading to a potential security vulnerability.

1 path for user data to reach this point

  1. Read from $_REQUEST, and WT_MODULES_DIR . 'googlemap/extra' . $_REQUEST['localfile'] is passed through file(), and $lines is assigned
    in app/Module/GoogleMapsModule.php on line 2284
  2. $placerec is assigned
    in app/Module/GoogleMapsModule.php on line 2309
  3. $placerec is passed through explode(), and $fieldrec is assigned
    in app/Module/GoogleMapsModule.php on line 2310
  4. $placelist is assigned
    in app/Module/GoogleMapsModule.php on line 2328
  5. $placelist is assigned
    in app/Module/GoogleMapsModule.php on line 2330
  6. $place is assigned
    in app/Module/GoogleMapsModule.php on line 2343

Preventing Cross-Site-Scripting Attacks

Cross-Site-Scripting allows an attacker to inject malicious code into your website - in particular Javascript code, and have that code executed with the privileges of a visiting user. This can be used to obtain data, or perform actions on behalf of that visiting user.

In order to prevent this, make sure to escape all user-provided data:

// for HTML
$sanitized = htmlentities($tainted, ENT_QUOTES);

// for URLs
$sanitized = urlencode($tainted);

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
2360
				}
2361
			}
2362
			$prevPlace = $place['place'];
2363
			$prevLati  = $place['lati'];
2364
			$prevLong  = $place['long'];
2365
		}
2366
2367
		$default_zoom_level    = [];
2368
		$default_zoom_level[0] = 4;
2369
		$default_zoom_level[1] = 7;
2370
		$default_zoom_level[2] = 10;
2371
		$default_zoom_level[3] = 12;
2372
		foreach ($placelistUniq as $k => $place) {
2373
			$parent     = explode(',', $place['place']);
2374
			$parent     = array_reverse($parent);
2375
			$parent_id  = 0;
2376
			$num_parent = count($parent);
2377
			for ($i = 0; $i < $num_parent; $i++) {
2378
				$escparent = $parent[$i];
2379
				if ($escparent == '') {
2380
					$escparent = 'Unknown';
2381
				}
2382
				$row =
2383
					Database::prepare("SELECT pl_id, pl_long, pl_lati, pl_zoom, pl_icon FROM `##placelocation` WHERE pl_level=? AND pl_parent_id=? AND pl_place LIKE ? ORDER BY pl_place")
2384
					->execute([$i, $parent_id, $escparent])
2385
					->fetchOneRow();
2386
				if (empty($row)) {
2387
					// this name does not yet exist: create entry
2388
					if (!Filter::postBool('updateonly')) {
2389
						$highestIndex = $highestIndex + 1;
2390
						if (($i + 1) == $num_parent) {
2391
							$zoomlevel = $place['zoom'];
2392
						} elseif (isset($default_zoom_level[$i])) {
2393
							$zoomlevel = $default_zoom_level[$i];
2394
						} else {
2395
							$zoomlevel = $this->getPreference('GM_MAX_ZOOM', self::GM_MAX_ZOOM_DEFAULT);
2396
						}
2397
						if (($place['lati'] == '0') || ($place['long'] == '0') || (($i + 1) < $num_parent)) {
2398
							Database::prepare("INSERT INTO `##placelocation` (pl_id, pl_parent_id, pl_level, pl_place, pl_zoom, pl_icon) VALUES (?, ?, ?, ?, ?, ?)")
2399
								->execute([$highestIndex, $parent_id, $i, $escparent, $zoomlevel, $place['icon']]);
2400
						} else {
2401
							//delete leading zero
2402
							$pl_lati = str_replace(['N', 'S', ','], ['', '-', '.'], $place['lati']);
2403
							$pl_long = str_replace(['E', 'W', ','], ['', '-', '.'], $place['long']);
2404
							if ($pl_lati >= 0) {
2405
								$place['lati'] = 'N' . abs($pl_lati);
2406
							} elseif ($pl_lati < 0) {
2407
								$place['lati'] = 'S' . abs($pl_lati);
2408
							}
2409
							if ($pl_long >= 0) {
2410
								$place['long'] = 'E' . abs($pl_long);
2411
							} elseif ($pl_long < 0) {
2412
								$place['long'] = 'W' . abs($pl_long);
2413
							}
2414
							Database::prepare("INSERT INTO `##placelocation` (pl_id, pl_parent_id, pl_level, pl_place, pl_long, pl_lati, pl_zoom, pl_icon) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
2415
								->execute([$highestIndex, $parent_id, $i, $escparent, $place['long'], $place['lati'], $zoomlevel, $place['icon']]);
2416
						}
2417
						$parent_id = $highestIndex;
2418
					}
2419
				} else {
2420
					$parent_id = $row->pl_id;
2421
					if (Filter::postBool('overwritedata') && ($i + 1 == count($parent))) {
2422
						Database::prepare("UPDATE `##placelocation` SET pl_lati = ?, pl_long = ?, pl_zoom = ?, pl_icon = ? WHERE pl_id = ?")
2423
							->execute([$place['lati'], $place['long'], $place['zoom'], $place['icon'], $parent_id]);
2424
					} else {
2425
						// Update only if existing data is missing
2426
						if (!$row->pl_long && !$row->pl_lati) {
2427
							Database::prepare("UPDATE `##placelocation` SET pl_lati = ?, pl_long = ? WHERE pl_id = ?")
2428
								->execute([$place['lati'], $place['long'], $parent_id]);
2429
						}
2430
						if (!$row->pl_icon && $place['icon']) {
2431
							Database::prepare("UPDATE `##placelocation` SET pl_icon = ? WHERE pl_id = ?")
2432
								->execute([$place['icon'], $parent_id]);
2433
						}
2434
					}
2435
				}
2436
			}
2437
		}
2438
2439
		$parent_id = (int) Filter::post('parent_id');
2440
		$inactive  = (int) Filter::post('inactive');
2441
2442
		header('Location: module.php?mod=googlemap&mod_action=admin_places&parent_id=' . $parent_id . '&inactive=' . $inactive);
2443
	}
2444
2445
	/**
2446
	 * Export/download the place hierarchy, or a prt of it.
2447
	 */
2448
	private function adminDownload() {
2449
		$parent_id = (int) Filter::get('parent_id');
2450
		$hierarchy = $this->placeIdToHierarchy($parent_id);
2451
		$maxLevel  = min(8, $this->getHighestLevel());
2452
2453
		if (empty($hierarchy)) {
2454
			$filename = 'places.csv';
2455
		} else {
2456
			$filename = 'places-' . preg_replace('/[:;\/\\\(\)\{\}\[\] $]/', '_', implode('-', $hierarchy)) . '.csv';
2457
		}
2458
2459
		header('Content-Type: text/csv; charset=utf-8');
2460
		header('Content-Disposition: inline; filename="' . $filename . '"');
2461
2462
		echo '"', I18N::translate('Level'), '";';
2463
		echo '"', I18N::translate('Country'), '";';
2464
		if ($maxLevel > 0) {
2465
			echo '"', I18N::translate('State'), '";';
2466
		}
2467
		if ($maxLevel > 1) {
2468
			echo '"', I18N::translate('County'), '";';
2469
		}
2470
		if ($maxLevel > 2) {
2471
			echo '"', I18N::translate('City'), '";';
2472
		}
2473
		if ($maxLevel > 3) {
2474
			echo '"', I18N::translate('Place'), '";';
2475
		}
2476
		if ($maxLevel > 4) {
2477
			echo '"', I18N::translate('Place'), '";';
2478
		}
2479
		if ($maxLevel > 5) {
2480
			echo '"', I18N::translate('Place'), '";';
2481
		}
2482
		if ($maxLevel > 6) {
2483
			echo '"', I18N::translate('Place'), '";';
2484
		}
2485
		if ($maxLevel > 7) {
2486
			echo '"', I18N::translate('Place'), '";';
2487
		}
2488
		echo '"', I18N::translate('Longitude'), '";';
2489
		echo '"', I18N::translate('Latitude'), '";';
2490
		echo '"', I18N::translate('Zoom level'), '";';
2491
		echo '"', I18N::translate('Icon'), '";', WT_EOL;
2492
		$this->outputLevel($parent_id);
2493
	}
2494
2495
	/**
2496
	 * Save a new/updated geographic place.
2497
	 */
2498
	private function adminPlaceSave() {
2499
		$parent_id = (int) Filter::post('parent_id');
2500
		$place_id  = (int) Filter::post('place_id');
2501
		$inactive  = (int) Filter::post('inactive');
2502
		$level     = count($this->placeIdToHierarchy($parent_id));
2503
2504
		if ($place_id === 0) {
2505
			Database::prepare(
2506
				"INSERT INTO `##placelocation` (pl_id, pl_parent_id, pl_level, pl_place, pl_long, pl_lati, pl_zoom, pl_icon) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
2507
			)->execute([
2508
				$this->getHighestIndex() + 1,
2509
				$parent_id,
2510
				$level,
2511
				Filter::post('NEW_PLACE_NAME'),
2512
				Filter::post('LONG_CONTROL') . Filter::post('NEW_PLACE_LONG'),
2513
				Filter::post('LATI_CONTROL') . Filter::post('NEW_PLACE_LATI'),
2514
				Filter::post('NEW_ZOOM_FACTOR'),
2515
				Filter::post('icon'),
2516
			]);
2517
		} else {
2518
			Database::prepare(
2519
			"UPDATE `##placelocation` SET pl_place = ?, pl_lati = ?, pl_long = ?, pl_zoom = ?, pl_icon = ? WHERE pl_id = ?"
2520
			)->execute([
2521
				Filter::post('NEW_PLACE_NAME'),
2522
				Filter::post('LATI_CONTROL') . Filter::post('NEW_PLACE_LATI'),
2523
				Filter::post('LONG_CONTROL') . Filter::post('NEW_PLACE_LONG'),
2524
				Filter::post('NEW_ZOOM_FACTOR'),
2525
				Filter::post('icon'),
2526
				$place_id,
2527
			]);
2528
		}
2529
2530
		header('Location: module.php?mod=googlemap&mod_action=admin_places&parent_id=' . $parent_id . '&inactive=' . $inactive);
2531
	}
2532
2533
	/**
2534
	 * Create or edit a geographic place.
2535
	 */
2536
	private function adminPlaceEdit() {
2537
		$parent_id  = (int) Filter::post('parent_id', null, Filter::get('parent_id'));
2538
		$place_id   = (int) Filter::post('place_id', null, Filter::get('place_id'));
2539
		$inactive   = (int) Filter::post('inactive', null, Filter::get('inactive'));
2540
		$where_am_i = $this->placeIdToHierarchy($place_id);
2541
		$level      = count($where_am_i);
2542
2543
		$controller = new PageController;
2544
		$controller
2545
			->setPageTitle(I18N::translate('Geographic data'))
2546
			->addInlineJavascript('$("<link>", {rel: "stylesheet", type: "text/css", href: "' . WT_MODULES_DIR . 'googlemap/css/wt_v3_googlemap.css"}).appendTo("head");')
2547
			->pageHeader();
2548
2549
		// Find (or create) the record we are editing.
2550
		$record =
2551
			Database::prepare("SELECT * FROM `##placelocation` WHERE pl_id=?")
2552
			->execute([$place_id])
2553
			->fetchOneRow();
2554
2555
		$parent_record =
2556
			Database::prepare("SELECT * FROM `##placelocation` WHERE pl_id=?")
2557
			->execute([$parent_id])
2558
			->fetchOneRow();
2559
2560
		if ($parent_record === null) {
2561
			$parent_record = (object) [
2562
				'pl_id'        => 0,
2563
				'pl_parent_id' => 0,
2564
				'pl_place'     => '',
2565
				'pl_lati'      => 'N0',
2566
				'pl_long'      => 'E0',
2567
				'pl_level'     => $level - 1,
2568
				'pl_icon'      => '',
2569
				'pl_zoom'      => self::GM_MIN_ZOOM_DEFAULT,
2570
			];
2571
		}
2572
2573
		if ($record === null || $place_id === 0) {
2574
			$record = (object) [
2575
				'pl_id'        => 0,
2576
				'pl_parent_id' => $parent_id,
2577
				'pl_place'     => '',
2578
				'pl_lati'      => 'N0',
2579
				'pl_long'      => 'E0',
2580
				'pl_level'     => $level,
2581
				'pl_icon'      => '',
2582
				'pl_zoom'      => $parent_record === null ? self::GM_MIN_ZOOM_DEFAULT : $parent_record->pl_zoom,
2583
			];
2584
		}
2585
2586
		// Convert to floating point for the map.
2587
		$latitude  = (float) (str_replace(['N', 'S'], ['', '-'], $record->pl_lati));
2588
		$longitude = (float) (str_replace(['E', 'W'], ['', '-'], $record->pl_long));
2589
		if ($latitude === 0 && $longitude === 0) {
2590
			$latitude  = (float) (str_replace(['N', 'S'], ['', '-'], $record->pl_lati));
2591
			$longitude = (float) (str_replace(['E', 'W'], ['', '-'], $record->pl_long));
2592
		}
2593
2594
		$parent_url = 'module.php?mod=googlemap&mod_action=admin_places&parent_id=' . $parent_id . '&inactive=' . $inactive;
2595
2596
		$breadcrumbs = [
2597
			route('admin-control-panel') => I18N::translate('Control panel'),
2598
			route('admin-modules')       => I18N::translate('Module administration'),
2599
			$this->getConfigLink()     => $this->getTitle(),
2600
		];
2601
		$hierarchy =
2602
			[0 => I18N::translate('Geographic data')] +
2603
			$this->placeIdToHierarchy($place_id === 0 ? $parent_id : $place_id);
2604
		foreach ($hierarchy as $id => $name) {
2605
			$breadcrumbs += ['module.php?mod=googlemap&mod_action=admin_places&parent_id=' . $id . '&inactive=' . $inactive => Html::escape($name)];
2606
		}
2607
		echo Bootstrap4::breadcrumbs($breadcrumbs, $place_id === 0 ? I18N::translate('Add') : I18N::translate('Edit'));
2608
2609
		?>
2610
		<script src="<?= $this->googleMapsScript() ?>"></script>
2611
		<script>
2612
		var map;
2613
		var marker;
2614
		var zoom;
2615
		var pl_name = <?= json_encode($record->pl_place) ?>;
2616
			var latlng = new google.maps.LatLng(<?= $latitude ?>, <?= $longitude ?>);
2617
		var pl_zoom = <?= $record->pl_zoom ?>;
2618
		var polygon1;
2619
		var polygon2;
2620
		var geocoder;
2621
		var mapType;
2622
2623
		var infowindow = new google.maps.InfoWindow({});
2624
2625
		function geocodePosition(pos) {
2626
			geocoder.geocode({
2627
				latLng: pos
2628
			}, function(responses) {
2629
				if (responses && responses.length > 0) {
2630
					updateMarkerAddress(responses[0].formatted_address);
2631
				} else {
2632
					updateMarkerAddress('Cannot determine address at this location.');
2633
				}
2634
			});
2635
		}
2636
2637
		/**
2638
		 * Redraw the map, centered and zoomed on the selected point.
2639
		 *
2640
		 * @param event
2641
		 */
2642
		function updateMap(event) {
2643
			var point;
2644
			var zoom = parseInt(document.editplaces.NEW_ZOOM_FACTOR.value);
2645
			var latitude;
2646
			var longitude;
2647
2648
			if ((document.editplaces.NEW_PLACE_LATI.value === '') ||
2649
				(document.editplaces.NEW_PLACE_LONG.value === '')) {
2650
				latitude = parseFloat(document.editplaces.parent_lati.value).toFixed(5);
2651
				longitude = parseFloat(document.editplaces.parent_long.value).toFixed(5);
2652
				point = new google.maps.LatLng(latitude, longitude);
2653
			} else {
2654
				latitude = parseFloat(document.editplaces.NEW_PLACE_LATI.value).toFixed(5);
2655
				longitude = parseFloat(document.editplaces.NEW_PLACE_LONG.value).toFixed(5);
2656
				document.editplaces.NEW_PLACE_LATI.value = latitude;
2657
				document.editplaces.NEW_PLACE_LONG.value = longitude;
2658
2659
				if (event === 'flag_drag') {
2660
					if (longitude < 0.0 ) {
2661
						longitude = longitude * -1;
2662
						document.editplaces.NEW_PLACE_LONG.value = longitude;
2663
						document.editplaces.LONG_CONTROL.value = 'W';
2664
					} else {
2665
						document.editplaces.NEW_PLACE_LONG.value = longitude;
2666
						document.editplaces.LONG_CONTROL.value = 'E';
2667
					}
2668
					if (latitude < 0.0 ) {
2669
						latitude = latitude * -1;
2670
						document.editplaces.NEW_PLACE_LATI.value = latitude;
2671
						document.editplaces.LATI_CONTROL.value = 'S';
2672
					} else {
2673
						document.editplaces.NEW_PLACE_LATI.value = latitude;
2674
						document.editplaces.LATI_CONTROL.value = 'N';
2675
					}
2676
2677
					if (document.editplaces.LATI_CONTROL.value === 'S') {
2678
						latitude = latitude * -1;
2679
					}
2680
					if (document.editplaces.LONG_CONTROL.value === 'W') {
2681
						longitude = longitude * -1;
2682
					}
2683
					point = new google.maps.LatLng(latitude, longitude);
2684
				} else {
2685
					if (latitude < 0.0) {
2686
						latitude = latitude * -1;
2687
						document.editplaces.NEW_PLACE_LATI.value = latitude;
2688
					}
2689
					if (longitude < 0.0) {
2690
						longitude = longitude * -1;
2691
						document.editplaces.NEW_PLACE_LONG.value = longitude;
2692
					}
2693
					if (document.editplaces.LATI_CONTROL.value === 'S') {
2694
						latitude = latitude * -1;
2695
					}
2696
					if (document.editplaces.LONG_CONTROL.value === 'W') {
2697
						longitude = longitude * -1;
2698
					}
2699
					point = new google.maps.LatLng(latitude, longitude);
2700
				}
2701
			}
2702
2703
			map.setCenter(point);
2704
			map.setZoom(zoom);
2705
			marker.setPosition(point);
2706
		}
2707
2708
		// === Create Borders for the UK Countries =========================================================
2709
		function overlays() {
2710
			// Define place LatLng arrays
2711
2712
			<?php
2713
			$coordsAsStr = [];
2714
			switch ($record->pl_place) {
2715
				case 'England':
2716
					$coordsAsStr[] = '-4.74361,50.66750|-4.78361,50.59361|-4.91584,50.57722|-5.01750,50.54264|-5.02569,50.47271|-5.04729,50.42750|-5.15208,50.34374|-5.26805,50.27389|-5.43194,50.19326|-5.49584,50.21695|-5.54639,50.20527|-5.71000,50.12916|-5.71681,50.06083|-5.66174,50.03631|-5.58278,50.04777|-5.54166,50.07055|-5.53416,50.11569|-5.47055,50.12499|-5.33361,50.09138|-5.27666,50.05972|-5.25674,50.00514|-5.19306,49.95527|-5.16070,50.00319|-5.06555,50.03750|-5.07090,50.08166|-5.04806,50.17111|-4.95278,50.19333|-4.85750,50.23166|-4.76250,50.31138|-4.67861,50.32583|-4.54334,50.32222|-4.48278,50.32583|-4.42972,50.35139|-4.38000,50.36388|-4.16555,50.37028|-4.11139,50.33027|-4.05708,50.29791|-3.94389,50.31346|-3.87764,50.28139|-3.83653,50.22972|-3.78944,50.21222|-3.70666,50.20972|-3.65195,50.23111|-3.55139,50.43833|-3.49416,50.54639|-3.46181,50.58792|-3.41139,50.61610|-3.24416,50.67444|-3.17347,50.68833|-3.09445,50.69222|-2.97806,50.70638|-2.92750,50.73125|-2.88278,50.73111|-2.82305,50.72027|-2.77139,50.70861|-2.66195,50.67334|-2.56305,50.63222|-2.45861,50.57500|-2.44666,50.62639|-2.39097,50.64166|-2.19722,50.62611|-2.12195,50.60722|-2.05445,50.58569|-1.96437,50.59674|-1.95441,50.66536|-2.06681,50.71430|-1.93416,50.71277|-1.81639,50.72306|-1.68445,50.73888|-1.59278,50.72416|-1.33139,50.79138|-1.11695,50.80694|-1.15889,50.84083|-1.09445,50.84584|-0.92842,50.83966|-0.86584,50.79965|-0.90826,50.77396|-0.78187,50.72722|-0.74611,50.76583|-0.67528,50.78111|-0.57722,50.79527|-0.25500,50.82638|-0.19084,50.82583|-0.13805,50.81833|0.05695,50.78083|0.12334,50.75944|0.22778,50.73944|0.28695,50.76500|0.37195,50.81638|0.43084,50.83111|0.56722,50.84777|0.67889,50.87681|0.71639,50.90500|0.79334,50.93610|0.85666,50.92556|0.97125,50.98111|0.99778,51.01903|1.04555,51.04944|1.10028,51.07361|1.26250,51.10166|1.36889,51.13583|1.41111,51.20111|1.42750,51.33111|1.38556,51.38777|1.19195,51.37861|1.05278,51.36722|0.99916,51.34777|0.90806,51.34069|0.70416,51.37749|0.61972,51.38304|0.55945,51.40596|0.64236,51.44042|0.69750,51.47084|0.59195,51.48777|0.53611,51.48806|0.48916,51.48445|0.45215,51.45562|0.38894,51.44822|0.46500,51.50306|0.65195,51.53680|0.76695,51.52138|0.82084,51.53556|0.87528,51.56110|0.95250,51.60923|0.94695,51.72556|0.90257,51.73465|0.86306,51.71166|0.76140,51.69164|0.70111,51.71847|0.86211,51.77361|0.93236,51.80583|0.98278,51.82527|1.03569,51.77416|1.08834,51.77056|1.13222,51.77694|1.18139,51.78972|1.22361,51.80888|1.26611,51.83916|1.28097,51.88096|1.20834,51.95083|1.16347,52.02361|1.27750,51.98555|1.33125,51.92875|1.39028,51.96999|1.58736,52.08388|1.63000,52.19527|1.68576,52.32630|1.73028,52.41138|1.74945,52.45583|1.74590,52.62021|1.70250,52.71583|1.64528,52.77111|1.50361,52.83749|1.43222,52.87472|1.35250,52.90972|1.28222,52.92750|1.18389,52.93889|0.99472,52.95111|0.94222,52.95083|0.88472,52.96638|0.66722,52.97611|0.54778,52.96618|0.49139,52.93430|0.44431,52.86569|0.42903,52.82403|0.36334,52.78027|0.21778,52.80694|0.16125,52.86250|0.05778,52.88916|0.00211,52.87985|0.03222,52.91722|0.20389,53.02805|0.27666,53.06694|0.33916,53.09236|0.35389,53.18722|0.33958,53.23472|0.23555,53.39944|0.14347,53.47527|0.08528,53.48638|0.02694,53.50972|-0.10084,53.57306|-0.20722,53.63083|-0.26445,53.69083|-0.30166,53.71319|-0.39022,53.70794|-0.51972,53.68527|-0.71653,53.69638|-0.65445,53.72527|-0.60584,53.72972|-0.54916,53.70611|-0.42261,53.71755|-0.35728,53.73056|-0.29389,53.73666|-0.23139,53.72166|-0.10584,53.63166|-0.03472,53.62555|0.04416,53.63916|0.08916,53.62666|0.14945,53.58847|0.12639,53.64527|0.06264,53.70389|-0.12750,53.86388|-0.16916,53.91847|-0.21222,54.00833|-0.20569,54.05153|-0.16111,54.08806|-0.11694,54.13222|-0.20053,54.15171|-0.26250,54.17444|-0.39334,54.27277|-0.42166,54.33222|-0.45750,54.37694|-0.51847,54.44749|-0.56472,54.48000|-0.87584,54.57027|-1.06139,54.61722|-1.16528,54.64972|-1.30445,54.77138|-1.34556,54.87138|-1.41278,54.99944|-1.48292,55.08625|-1.51500,55.14972|-1.56584,55.28722|-1.58097,55.48361|-1.63597,55.58194|-1.69000,55.60556|-1.74695,55.62499|-1.81764,55.63306|-1.97681,55.75416|-2.02166,55.80611|-2.08361,55.78054|-2.22000,55.66499|-2.27916,55.64472|-2.27416,55.57527|-2.21528,55.50583|-2.18278,55.45985|-2.21236,55.42777|-2.46305,55.36111|-2.63055,55.25500|-2.69945,55.17722|-2.96278,55.03889|-3.01500,55.05222|-3.05103,54.97986|-3.13292,54.93139|-3.20861,54.94944|-3.28931,54.93792|-3.39166,54.87639|-3.42916,54.81555|-3.56916,54.64249|-3.61306,54.48861|-3.49305,54.40333|-3.43389,54.34806|-3.41056,54.28014|-3.38055,54.24444|-3.21472,54.09555|-3.15222,54.08194|-2.93097,54.15333|-2.81361,54.22277|-2.81750,54.14277|-2.83361,54.08500|-2.93250,53.95055|-3.05264,53.90764|-3.03708,53.74944|-2.99278,53.73277|-2.89979,53.72499|-2.97729,53.69382|-3.07306,53.59805|-3.10563,53.55993|-3.00678,53.41738|-2.95389,53.36027|-2.85736,53.32083|-2.70493,53.35062|-2.77639,53.29250|-2.89972,53.28916|-2.94250,53.31056|-3.02889,53.38191|-3.07248,53.40936|-3.16695,53.35708|-3.12611,53.32500|-3.08860,53.26001|-3.02000,53.24722|-2.95528,53.21555|-2.91069,53.17014|-2.89389,53.10416|-2.85695,53.03249|-2.77792,52.98514|-2.73109,52.96873|-2.71945,52.91902|-2.79278,52.90207|-2.85069,52.93875|-2.99389,52.95361|-3.08639,52.91611|-3.13014,52.88486|-3.13708,52.79312|-3.06806,52.77027|-3.01111,52.71166|-3.06666,52.63527|-3.11750,52.58666|-3.07089,52.55702|-3.00792,52.56902|-2.98028,52.53083|-3.02736,52.49792|-3.11916,52.49194|-3.19514,52.46722|-3.19611,52.41027|-3.02195,52.34027|-2.95486,52.33117|-2.99750,52.28139|-3.05125,52.23347|-3.07555,52.14804|-3.12222,52.11805|-3.11250,52.06945|-3.08500,52.01930|-3.04528,51.97639|-2.98889,51.92555|-2.91757,51.91569|-2.86639,51.92889|-2.77861,51.88583|-2.65944,51.81806|-2.68334,51.76957|-2.68666,51.71889|-2.66500,51.61500|-2.62916,51.64416|-2.57889,51.67777|-2.46056,51.74666|-2.40389,51.74041|-2.47166,51.72445|-2.55305,51.65722|-2.65334,51.56389|-2.77055,51.48916|-2.85278,51.44472|-2.96000,51.37499|-3.00695,51.30722|-3.01278,51.25632|-3.02834,51.20611|-3.30139,51.18111|-3.39361,51.18138|-3.43729,51.20638|-3.50722,51.22333|-3.57014,51.23027|-3.63222,51.21805|-3.70028,51.23000|-3.79250,51.23916|-3.88389,51.22416|-3.98472,51.21695|-4.11666,51.21222|-4.22805,51.18777|-4.22028,51.11054|-4.23702,51.04659|-4.30361,51.00416|-4.37639,50.99110|-4.42736,51.00958|-4.47445,51.01416|-4.52132,51.01424|-4.54334,50.92694|-4.56139,50.77625|-4.65139,50.71527|-4.74361,50.66750';
2717
					break;
2718
				case 'Scotland':
2719
					$coordsAsStr[] = '-2.02166,55.80611|-2.07972,55.86722|-2.13028,55.88583|-2.26028,55.91861|-2.37528,55.95694|-2.65722,56.05972|-2.82028,56.05694|-2.86618,56.02840|-2.89555,55.98861|-2.93500,55.96944|-3.01805,55.94944|-3.06750,55.94444|-3.25472,55.97166|-3.45472,55.99194|-3.66416,56.00652|-3.73722,56.05555|-3.57139,56.05360|-3.44111,56.01916|-3.39584,56.01083|-3.34403,56.02333|-3.13903,56.11084|-2.97611,56.19472|-2.91666,56.20499|-2.84695,56.18638|-2.78805,56.18749|-2.67937,56.21465|-2.58403,56.28264|-2.67208,56.32277|-2.76861,56.33180|-2.81528,56.37360|-2.81208,56.43958|-2.91653,56.45014|-2.99555,56.41416|-3.19042,56.35958|-3.27805,56.35750|-3.04055,56.45472|-2.95861,56.45611|-2.72084,56.48888|-2.64084,56.52250|-2.53126,56.57611|-2.48861,56.61416|-2.47805,56.71527|-2.39000,56.77166|-2.31986,56.79638|-2.21972,56.86777|-2.19708,56.94388|-2.16695,57.00055|-2.09334,57.07027|-2.05416,57.21861|-1.95889,57.33250|-1.85584,57.39889|-1.77334,57.45805|-1.78139,57.50555|-1.82195,57.57861|-1.86000,57.62138|-1.92972,57.67777|-2.02222,57.69388|-2.07555,57.69944|-2.14028,57.69056|-2.18611,57.66861|-2.39626,57.66638|-2.51000,57.67166|-2.78639,57.70222|-2.89806,57.70694|-2.96750,57.68027|-3.03847,57.66249|-3.12334,57.67166|-3.22334,57.69166|-3.28625,57.72499|-3.33972,57.72333|-3.48805,57.70945|-3.52222,57.66333|-3.59542,57.63666|-3.64063,57.63881|-3.75414,57.62504|-4.03986,57.55569|-4.19666,57.48584|-4.22889,57.51554|-4.17945,57.56249|-4.11139,57.59833|-4.08078,57.66533|-4.19139,57.67139|-4.25945,57.65527|-4.34361,57.60777|-4.41639,57.60166|-4.29666,57.67444|-4.08528,57.72611|-4.01908,57.70226|-3.96861,57.70250|-3.86556,57.76861|-3.81945,57.80458|-3.80681,57.85819|-3.85055,57.82000|-3.92639,57.80749|-4.04322,57.81438|-4.14973,57.82527|-4.29750,57.84638|-4.36250,57.89777|-4.24306,57.87028|-4.10666,57.85195|-4.01500,57.86777|-3.99166,57.90611|-3.99695,57.95056|-3.84500,58.02000|-3.56611,58.13916|-3.51319,58.16374|-3.45916,58.20305|-3.42028,58.24361|-3.33750,58.27694|-3.20555,58.30625|-3.10972,58.38166|-3.05792,58.45083|-3.02264,58.64653|-3.17639,58.64944|-3.35389,58.66055|-3.36931,58.59555|-3.57611,58.62194|-3.66028,58.61972|-3.71166,58.60374|-3.78264,58.56750|-3.84834,58.56000|-4.08056,58.55527|-4.27722,58.53361|-4.43653,58.54902|-4.50666,58.56777|-4.56055,58.57584|-4.59910,58.53027|-4.66805,58.48833|-4.76146,58.44604|-4.70195,58.50999|-4.70166,58.55861|-4.77014,58.60264|-5.00153,58.62416|-5.10945,58.50833|-5.16472,58.32527|-5.12639,58.28750|-5.07166,58.26472|-5.20361,58.25083|-5.39764,58.25055|-5.27389,58.11722|-5.31514,58.06416|-5.38416,58.08361|-5.45285,58.07416|-5.39805,58.03111|-5.26278,57.97111|-5.19334,57.95069|-5.12750,57.86944|-5.21750,57.90084|-5.33861,57.92083|-5.42876,57.90104|-5.45750,57.85889|-5.64445,57.89972|-5.62555,57.85222|-5.58153,57.81945|-5.60674,57.76618|-5.66305,57.78889|-5.71695,57.86944|-5.76695,57.86472|-5.81708,57.81944|-5.81084,57.63958|-5.69555,57.55944|-5.64361,57.55222|-5.53084,57.52833|-5.65305,57.50875|-5.75000,57.54834|-5.81569,57.57923|-5.85042,57.54972|-5.86695,57.46777|-5.81806,57.36250|-5.75111,57.34333|-5.50334,57.40111|-5.45126,57.41805|-5.49250,57.37083|-5.59884,57.33049|-5.57116,57.28411|-5.51266,57.27745|-5.40514,57.23097|-5.44972,57.22138|-5.49472,57.23888|-5.56066,57.25477|-5.64611,57.23499|-5.64751,57.16161|-5.55028,57.11639|-5.48166,57.11222|-5.40305,57.11062|-5.55945,57.09250|-5.65111,57.11611|-5.72472,57.11306|-5.77361,57.04556|-5.63139,56.98499|-5.56916,56.98972|-5.52403,56.99735|-5.57916,56.98000|-5.64611,56.97222|-5.73374,57.00909|-5.82584,57.00346|-5.91958,56.88708|-5.86528,56.87944|-5.74278,56.89374|-5.66292,56.86924|-5.73306,56.83916|-5.78584,56.83955|-5.85590,56.81430|-5.80208,56.79180|-5.84958,56.74444|-5.90500,56.75666|-5.96694,56.78027|-6.14000,56.75777|-6.19208,56.74888|-6.23452,56.71673|-6.19139,56.67972|-5.91916,56.67388|-5.82622,56.69156|-5.73945,56.71166|-5.55240,56.68886|-5.64861,56.68027|-5.69916,56.68278|-5.88261,56.65666|-5.97472,56.65138|-5.99584,56.61138|-5.93056,56.56972|-5.88416,56.55333|-5.79056,56.53805|-5.67695,56.49389|-5.56389,56.54056|-5.36334,56.66195|-5.23416,56.74333|-5.13236,56.79403|-5.31473,56.65666|-5.37405,56.55925|-5.31826,56.55633|-5.25080,56.55753|-5.37718,56.52112|-5.39866,56.47866|-5.19111,56.46194|-5.11556,56.51277|-5.07014,56.56069|-5.13555,56.48499|-5.22084,56.43583|-5.32764,56.43574|-5.42439,56.43091|-5.52611,56.37360|-5.57139,56.32833|-5.59653,56.25695|-5.57389,56.16000|-5.52000,56.16485|-5.56334,56.11333|-5.60139,56.07638|-5.64222,56.04305|-5.66039,55.98263|-5.62555,56.02055|-5.58014,56.01319|-5.63361,55.96611|-5.67697,55.88844|-5.64750,55.78139|-5.60986,55.75930|-5.66916,55.66166|-5.70166,55.58861|-5.71805,55.51500|-5.75916,55.41750|-5.79528,55.36027|-5.78166,55.29902|-5.73778,55.29222|-5.56694,55.31666|-5.51528,55.36347|-5.55520,55.41440|-5.48639,55.64306|-5.44597,55.70680|-5.38000,55.75027|-5.41889,55.90666|-5.39924,55.99972|-5.33895,56.03456|-5.30594,56.06922|-5.23889,56.11889|-5.03222,56.23250|-4.92229,56.27111|-4.97416,56.23333|-5.07222,56.18695|-5.20069,56.11861|-5.30906,56.00570|-5.34000,55.90201|-5.29250,55.84750|-5.20805,55.84444|-5.22458,55.90175|-5.17334,55.92916|-5.11000,55.90306|-5.01222,55.86694|-4.96195,55.88000|-4.89824,55.98145|-4.84623,56.08632|-4.86636,56.03178|-4.85461,55.98648|-4.77659,55.97977|-4.62723,55.94555|-4.52305,55.91861|-4.70972,55.93403|-4.75166,55.94611|-4.82406,55.94950|-4.87826,55.93653|-4.91639,55.70083|-4.87584,55.68194|-4.81361,55.64555|-4.68722,55.59750|-4.61361,55.49069|-4.63958,55.44264|-4.68250,55.43388|-4.74847,55.41055|-4.83715,55.31882|-4.84778,55.26944|-4.86542,55.22340|-4.93500,55.17860|-5.01250,55.13347|-5.05361,55.04902|-5.17834,54.98888|-5.18563,54.93622|-5.17000,54.89111|-5.11666,54.83180|-5.00500,54.76333|-4.96229,54.68125|-4.92250,54.64055|-4.85723,54.62958|-4.96076,54.79687|-4.92431,54.83708|-4.85222,54.86861|-4.80125,54.85556|-4.74055,54.82166|-4.68084,54.79972|-4.59861,54.78027|-4.55792,54.73903|-4.49639,54.69888|-4.37584,54.67666|-4.34569,54.70916|-4.35973,54.77111|-4.41111,54.82583|-4.42445,54.88152|-4.38479,54.90555|-4.35056,54.85903|-4.09555,54.76777|-3.95361,54.76749|-3.86972,54.80527|-3.81222,54.84888|-3.69250,54.88110|-3.61584,54.87527|-3.57111,54.99083|-3.44528,54.98638|-3.36056,54.97138|-3.14695,54.96500|-3.05103,54.97986|-3.01500,55.05222|-2.96278,55.03889|-2.69945,55.17722|-2.63055,55.25500|-2.46305,55.36111|-2.21236,55.42777|-2.18278,55.45985|-2.21528,55.50583|-2.27416,55.57527|-2.27916,55.64472|-2.22000,55.66499|-2.08361,55.78054|-2.02166,55.80611';
2720
					break;
2721
				case 'Ireland':
2722
					$coordsAsStr[] = '-8.17166,54.46388|-8.06555,54.37277|-7.94139,54.29944|-7.87576,54.28499|-7.86834,54.22764|-7.81805,54.19916|-7.69972,54.20250|-7.55945,54.12694|-7.31334,54.11250|-7.14584,54.22527|-7.17555,54.28916|-7.16084,54.33666|-7.05834,54.41000|-6.97445,54.40166|-6.92695,54.37916|-6.87305,54.34208|-6.85111,54.28972|-6.73473,54.18361|-6.65556,54.06527|-6.60584,54.04444|-6.44750,54.05833|-6.33889,54.11555|-6.26697,54.09983|-6.17403,54.07222|-6.10834,54.03638|-6.04389,54.03139|-5.96834,54.06389|-5.88500,54.11639|-5.87347,54.20916|-5.82500,54.23958|-5.74611,54.24806|-5.65556,54.22701|-5.60834,54.24972|-5.55916,54.29084|-5.57334,54.37704|-5.64502,54.49267|-5.70472,54.53361|-5.68055,54.57306|-5.59972,54.54194|-5.55097,54.50083|-5.54216,54.44903|-5.54643,54.40527|-5.50672,54.36444|-5.46111,54.38555|-5.43132,54.48596|-5.47945,54.53638|-5.53521,54.65090|-5.57431,54.67722|-5.62916,54.67945|-5.73674,54.67383|-5.80305,54.66138|-5.88257,54.60652|-5.92445,54.63180|-5.86681,54.68972|-5.81903,54.70972|-5.74672,54.72452|-5.68775,54.76335|-5.70931,54.83166|-5.74694,54.85361|-5.79139,54.85139|-6.03611,55.05778|-6.04250,55.10277|-6.03444,55.15458|-6.10125,55.20945|-6.14584,55.22069|-6.25500,55.21194|-6.37639,55.23916|-6.51556,55.23305|-6.61334,55.20722|-6.73028,55.18027|-6.82472,55.16806|-6.88972,55.16777|-6.96695,55.15611|-6.99416,55.11027|-7.05139,55.04680|-7.09500,55.03694|-7.25251,55.07059|-7.32639,55.04527|-7.40639,54.95333|-7.45805,54.85777|-7.55334,54.76277|-7.73916,54.71054|-7.82576,54.73416|-7.92639,54.70054|-7.85236,54.63388|-7.77750,54.62694|-7.83361,54.55389|-7.95084,54.53222|-8.04695,54.50722|-8.17166,54.46388';
2723
					break;
2724
				case 'Wales':
2725
					$coordsAsStr[] = '-3.08860,53.26001|-3.33639,53.34722|-3.38806,53.34361|-3.60986,53.27944|-3.73014,53.28944|-3.85445,53.28444|-4.01861,53.23750|-4.06639,53.22639|-4.15334,53.22556|-4.19639,53.20611|-4.33028,53.11222|-4.36097,53.02888|-4.55278,52.92889|-4.61889,52.90916|-4.72195,52.83611|-4.72778,52.78139|-4.53945,52.79306|-4.47722,52.85500|-4.41416,52.88472|-4.31292,52.90499|-4.23334,52.91499|-4.13569,52.87888|-4.13056,52.77777|-4.05334,52.71666|-4.10639,52.65084|-4.12597,52.60375|-4.08056,52.55333|-4.05972,52.48584|-4.09666,52.38583|-4.14305,52.32027|-4.19361,52.27638|-4.23166,52.24888|-4.52722,52.13083|-4.66945,52.13027|-4.73695,52.10361|-4.76778,52.06444|-4.84445,52.01388|-5.09945,51.96056|-5.23916,51.91638|-5.25889,51.87056|-5.18500,51.86958|-5.11528,51.83333|-5.10257,51.77895|-5.16111,51.76222|-5.24694,51.73027|-5.19111,51.70888|-5.00739,51.70349|-4.90875,51.71249|-4.86111,51.71334|-4.97061,51.67577|-5.02128,51.66861|-5.05139,51.62028|-5.00528,51.60638|-4.94139,51.59416|-4.89028,51.62694|-4.83569,51.64534|-4.79063,51.63340|-4.69028,51.66666|-4.64584,51.72666|-4.57445,51.73416|-4.43611,51.73722|-4.26222,51.67694|-4.19750,51.67916|-4.06614,51.66804|-4.11639,51.63416|-4.17750,51.62235|-4.25055,51.62861|-4.29208,51.60743|-4.27778,51.55666|-4.20486,51.53527|-3.94972,51.61278|-3.83792,51.61999|-3.78166,51.56750|-3.75160,51.52931|-3.67194,51.47388|-3.54250,51.39777|-3.40334,51.37972|-3.27097,51.38014|-3.16458,51.40909|-3.15166,51.45305|-3.11875,51.48750|-3.02111,51.52527|-2.95472,51.53972|-2.89278,51.53861|-2.84778,51.54500|-2.71472,51.58083|-2.66500,51.61500|-2.68666,51.71889|-2.68334,51.76957|-2.65944,51.81806|-2.77861,51.88583|-2.86639,51.92889|-2.91757,51.91569|-2.98889,51.92555|-3.04528,51.97639|-3.08500,52.01930|-3.11250,52.06945|-3.12222,52.11805|-3.07555,52.14804|-3.05125,52.23347|-2.99750,52.28139|-2.95486,52.33117|-3.02195,52.34027|-3.19611,52.41027|-3.19514,52.46722|-3.11916,52.49194|-3.02736,52.49792|-2.98028,52.53083|-3.00792,52.56902|-3.07089,52.55702|-3.11750,52.58666|-3.06666,52.63527|-3.01111,52.71166|-3.06806,52.77027|-3.13708,52.79312|-3.13014,52.88486|-3.08639,52.91611|-2.99389,52.95361|-2.85069,52.93875|-2.79278,52.90207|-2.71945,52.91902|-2.73109,52.96873|-2.77792,52.98514|-2.85695,53.03249|-2.89389,53.10416|-2.91069,53.17014|-2.95528,53.21555|-3.02000,53.24722|-3.08860,53.26001';
2726
					break;
2727
				case 'NC':
2728
					$coordsAsStr[] = '-81.65876,36.60938|-81.70390,36.55513|-81.70639,36.50804|-81.74665,36.39777|-81.90723,36.30804|-82.03195,36.12694|-82.08416,36.10146|-82.12826,36.11020|-82.21500,36.15833|-82.36375,36.11347|-82.43472,36.06013|-82.46236,36.01708|-82.56006,35.96263|-82.60042,35.99638|-82.62308,36.06121|-82.73500,36.01833|-82.84612,35.94944|-82.90451,35.88819|-82.93555,35.83846|-83.16000,35.76236|-83.24222,35.71944|-83.49222,35.57111|-83.56847,35.55861|-83.64416,35.56471|-83.73499,35.56638|-83.88222,35.51791|-83.98361,35.44944|-84.03639,35.35444|-84.04964,35.29117|-84.09042,35.25986|-84.15084,35.25388|-84.20521,35.25722|-84.29284,35.22596|-84.32471,34.98701|-83.09778,35.00027|-82.77722,35.09138|-82.59639,35.14972|-82.37999,35.21500|-82.27362,35.20583|-81.41306,35.17416|-81.05915,35.15333|-80.92666,35.10695|-80.78751,34.95610|-80.79334,34.82555|-79.66777,34.80694|-79.11555,34.34527|-78.57222,33.88166|-78.51806,33.87999|-78.43721,33.89804|-78.23735,33.91986|-78.15389,33.91471|-78.06974,33.89500|-78.02597,33.88936|-77.97611,33.94276|-77.95299,33.99243|-77.94499,34.06499|-77.92728,34.11756|-77.92250,33.99194|-77.92264,33.93715|-77.88215,34.06166|-77.86222,34.15083|-77.83501,34.19194|-77.75724,34.28527|-77.68222,34.36555|-77.63667,34.39805|-77.57363,34.43694|-77.45527,34.50403|-77.38173,34.51646|-77.37905,34.56294|-77.38572,34.61260|-77.40944,34.68916|-77.38847,34.73304|-77.33097,34.63992|-77.35024,34.60099|-77.30958,34.55972|-77.09424,34.67742|-76.75994,34.76659|-76.68325,34.79749|-76.66097,34.75781|-76.62611,34.71014|-76.50063,34.73617|-76.48138,34.77638|-76.38305,34.86423|-76.34326,34.88194|-76.27181,34.96263|-76.35125,35.02221|-76.32354,34.97429|-76.45319,34.93524|-76.43395,34.98782|-76.45356,35.06676|-76.52917,35.00444|-76.63382,34.98242|-76.69722,34.94887|-76.75306,34.90526|-76.81636,34.93944|-76.89000,34.95388|-76.93180,34.96957|-76.96501,34.99777|-77.06816,35.14978|-76.97639,35.06806|-76.86722,35.00000|-76.80531,34.98559|-76.72708,35.00152|-76.60402,35.07416|-76.56555,35.11486|-76.57305,35.16013|-76.66489,35.16694|-76.56361,35.23361|-76.48750,35.22582|-76.46889,35.27166|-76.50298,35.30791|-76.83251,35.39222|-77.02305,35.48694|-77.04958,35.52694|-76.91292,35.46166|-76.65250,35.41499|-76.61611,35.45888|-76.63195,35.52249|-76.58820,35.55104|-76.51556,35.53194|-76.56711,35.48494|-76.52251,35.40416|-76.46195,35.37221|-76.13319,35.35986|-76.04111,35.42416|-76.00223,35.46610|-75.97958,35.51666|-75.89362,35.57555|-75.83834,35.56694|-75.78944,35.57138|-75.74076,35.61846|-75.72084,35.69263|-75.72084,35.81451|-75.74917,35.87791|-75.78333,35.91972|-75.85083,35.97527|-75.94333,35.91777|-75.98944,35.88054|-75.98854,35.79110|-75.99388,35.71027|-76.02875,35.65409|-76.10320,35.66041|-76.13563,35.69239|-76.04475,35.68436|-76.04167,35.74916|-76.05305,35.79361|-76.05305,35.87375|-76.02653,35.96222|-76.07751,35.99319|-76.17472,35.99596|-76.27917,35.91915|-76.37986,35.95763|-76.42014,35.97874|-76.55375,35.93971|-76.66222,35.93305|-76.72952,35.93984|-76.73392,36.04760|-76.75384,36.09477|-76.76028,36.14513|-76.74610,36.22818|-76.70458,36.24673|-76.72764,36.16736|-76.71021,36.11752|-76.69117,36.07165|-76.65979,36.03312|-76.49527,36.00958|-76.37138,36.07694|-76.37084,36.14999|-76.21417,36.09471|-76.07591,36.17910|-76.18361,36.26915|-76.19965,36.31739|-76.13986,36.28805|-76.04274,36.21974|-76.00465,36.18110|-75.95287,36.19241|-75.97604,36.31138|-75.93895,36.28381|-75.85271,36.11069|-75.79315,36.07385|-75.79639,36.11804|-75.88333,36.29554|-75.94665,36.37194|-75.98694,36.41166|-76.03473,36.49666|-76.02899,36.55000|-78.44234,36.54986|-78.56594,36.55799|-80.27556,36.55110|-81.15361,36.56499|-81.38722,36.57695|-81.65876,36.60938';
2729
					break;
2730
				default:
2731
			}
2732
			?>
2733
			var coordStr = <?= json_encode($coordsAsStr) ?>;
2734
			jQuery.each(coordStr, function(index, value) {
2735
				var coordXY = value.split('|');
2736
				var coords  = [];
2737
				var points  = [];
2738
				jQuery.each(coordXY, function(i, v) {
2739
					coords = v.split(',');
2740
					points.push(new google.maps.LatLng(parseFloat(coords[1]), parseFloat(coords[0])));
2741
				});
2742
				// Construct the polygon
2743
				new google.maps.Polygon({
2744
					paths:         points,
2745
					strokeColor:   "#888888",
2746
					strokeOpacity: 0.8,
2747
					strokeWeight:  1,
2748
					fillColor:     "#ff0000",
2749
					fillOpacity:   0.15
2750
				}).setMap(map);
2751
			});
2752
		}
2753
2754
		function loadMap(zoom, mapType) {
2755
			var mapTyp;
2756
2757
			if (mapType) {
2758
				mapTyp = mapType;
2759
			} else {
2760
				mapTyp = google.maps.MapTypeId.ROADMAP;
2761
			}
2762
			geocoder = new google.maps.Geocoder();
2763
			if (!zoom) {
2764
				zoom = pl_zoom;
2765
			}
2766
			// Define map
2767
			var myOptions = {
2768
				zoom: zoom,
2769
				center: latlng,
2770
				mapTypeId: mapTyp,// ROADMAP, SATELLITE, HYBRID, TERRAIN
2771
				// mapTypeId: google.maps.MapTypeId.ROADMAP, // ROADMAP, SATELLITE, HYBRID, TERRAIN
2772
				mapTypeControlOptions: {
2773
					style: google.maps.MapTypeControlStyle.DROPDOWN_MENU // DEFAULT, DROPDOWN_MENU, HORIZONTAL_BAR
2774
				},
2775
				navigationControlOptions: {
2776
				position: google.maps.ControlPosition.TOP_RIGHT, // BOTTOM, BOTTOM_LEFT, LEFT, TOP, etc
2777
				style: google.maps.NavigationControlStyle.SMALL // ANDROID, DEFAULT, SMALL, ZOOM_PAN
2778
				},
2779
				scrollwheel: true
2780
			};
2781
2782
			map = new google.maps.Map(document.querySelector('.gm-map'), myOptions);
2783
2784
			overlays();
2785
2786
			// Close any infowindow when map is clicked
2787
			google.maps.event.addListener(map, 'click', function() {
2788
				infowindow.close();
2789
			});
2790
2791
			// Check for zoom changes
2792
			google.maps.event.addListener(map, 'zoom_changed', function() {
2793
				document.editplaces.NEW_ZOOM_FACTOR.value = map.zoom;
2794
			});
2795
2796
			// Create the Main Location Marker
2797
			<?php
2798
			if ($level < 3 && $record->pl_icon != '') {
2799
				echo 'var image = {
2800
						"url"    : WT_MODULES_DIR + "googlemap/" + "' . $record->pl_icon . '",
2801
						"size"   : new google.maps.Size(25, 15),
2802
						"origin" : new google.maps.Point(0, 0),
2803
						"anchor" : new google.maps.Point(12, 15)
2804
					};';
2805
				echo 'marker = new google.maps.Marker({';
2806
				echo 'icon: image,';
2807
				echo 'position: latlng,';
2808
				echo 'map: map,';
2809
				echo 'title: pl_name,';
2810
				echo 'draggable: true,';
2811
				echo 'zIndex:1';
2812
				echo '});';
2813
			} else {
2814
				echo 'marker = new google.maps.Marker({';
2815
				echo 'position: latlng,';
2816
				echo 'map: map,';
2817
				echo 'title: pl_name,';
2818
				echo 'draggable: true,';
2819
				echo 'zIndex: 1';
2820
				echo '});';
2821
			}
2822
			?>
2823
2824
			// Set marker by clicking on map ---
2825
			google.maps.event.addListener(map, 'click', function(event) {
2826
				clearMarks();
2827
				latlng = event.latLng;
2828
				marker = new google.maps.Marker({
2829
					position: latlng,
2830
					map: map,
2831
					title: pl_name,
2832
					draggable: true,
2833
					zIndex: 1
2834
				});
2835
				document.getElementById('NEW_PLACE_LATI').value = marker.getPosition().lat().toFixed(5);
2836
				document.getElementById('NEW_PLACE_LONG').value = marker.getPosition().lng().toFixed(5);
2837
				updateMap('flag_drag');
2838
				var currzoom = parseInt(document.editplaces.NEW_ZOOM_FACTOR.value);
2839
				mapType = map.getMapTypeId();
2840
				loadMap(currzoom, mapType);
2841
			});
2842
2843
			// If the marker is moved, update the location fields
2844
			google.maps.event.addListener(marker, 'drag', function() {
2845
				document.getElementById('NEW_PLACE_LATI').value = marker.getPosition().lat().toFixed(5);
2846
				document.getElementById('NEW_PLACE_LONG').value = marker.getPosition().lng().toFixed(5);
2847
			});
2848
			google.maps.event.addListener(marker, 'dragend', function() {
2849
				updateMap('flag_drag');
2850
			});
2851
		}
2852
2853
		function clearMarks() {
2854
			marker.setMap(null);
2855
		}
2856
2857
		/**
2858
		 * Called when we select one of the search results.
2859
		 *
2860
		 * @param lat
2861
		 * @param lng
2862
		 */
2863
		function setLoc(lat, lng) {
2864
			if (lat < 0.0) {
2865
				document.editplaces.NEW_PLACE_LATI.value = (lat.toFixed(5) * -1);
2866
				document.editplaces.LATI_CONTROL.value = 'S';
2867
			} else {
2868
				document.editplaces.NEW_PLACE_LATI.value = lat.toFixed(5);
2869
				document.editplaces.LATI_CONTROL.value = 'N';
2870
			}
2871
			if (lng < 0.0) {
2872
				document.editplaces.NEW_PLACE_LONG.value = (lng.toFixed(5) * -1);
2873
				document.editplaces.LONG_CONTROL.value = 'W';
2874
			} else {
2875
				document.editplaces.NEW_PLACE_LONG.value = lng.toFixed(5);
2876
				document.editplaces.LONG_CONTROL.value = 'E';
2877
			}
2878
			new google.maps.LatLng (lat.toFixed(5), lng.toFixed(5));
2879
			infowindow.close();
2880
			updateMap();
2881
		}
2882
2883
		function createMarker(i, point, name) {
2884
			 var image = {
2885
				 url:    WT_MODULES_DIR + 'googlemap/images/marker_yellow.png',
2886
				 size:   new google.maps.Size(20, 34),
2887
				 origin: new google.maps.Point(0, 0),
2888
				 anchor: new google.maps.Point(10, 34)
2889
			 };
2890
2891
			var marker = new google.maps.Marker({
2892
				icon:     image,
2893
				map:      map,
2894
				position: point,
2895
				zIndex:   0
2896
			});
2897
2898
			google.maps.event.addListener(marker, 'click', function() {
2899
				infowindow.close();
2900
				infowindow.setContent(name);
2901
				infowindow.open(map, marker);
2902
			});
2903
2904
			google.maps.event.addListener(map, 'click', function() {
2905
				infowindow.close();
2906
			});
2907
2908
			return marker;
2909
		}
2910
2911
		function change_icon() {
2912
			window.open('module.php?mod=googlemap&mod_action=admin_flags&countrySelected=', '_blank', indx_window_specs);
2913
			return false;
2914
		}
2915
2916
		function remove_icon() {
2917
			document.editplaces.icon.value = '';
2918
			document.getElementById('flagsDiv').innerHTML = '<a href="#" onclick="change_icon();return false;"><?= I18N::translate('Change flag') ?></a>';
2919
		}
2920
2921
		function addAddressToMap(response) {
2922
			var bounds = new google.maps.LatLngBounds();
2923
			if (!response) {
2924
				alert('<?= I18N::translate('No places found') ?>');
2925
			} else {
2926
				if (response.length > 0) {
2927
					for (var i=0; i<response.length; i++) {
2928
						// 5 decimal places is approx 1 metre accuracy.
2929
						var name =
2930
							'<div>' + response[i].address_components[0].short_name +
2931
							'<br><a href="#" onclick="setLoc(' + response[i].geometry.location.lat() + ', ' + response[i].geometry.location.lng() + ');">' +
2932
							'<?= I18N::translate('Use this value') ?></a>' +
2933
							'</div>';
2934
						var point = response[i].geometry.location;
2935
						var marker = createMarker(i, point, name);
2936
						bounds.extend(response[i].geometry.location);
2937
					}
2938
2939
					<?php if ($level > 0) { ?>
2940
						map.fitBounds(bounds);
2941
					<?php } ?>
2942
					var zoomlevel = map.getZoom();
2943
2944
					if (zoomlevel < <?= $this->getPreference('GM_MIN_ZOOM', self::GM_MIN_ZOOM_DEFAULT) ?>) {
2945
						zoomlevel = <?= $this->getPreference('GM_MIN_ZOOM', self::GM_MIN_ZOOM_DEFAULT) ?>;
2946
					}
2947
					if (zoomlevel > <?= $this->getPreference('GM_MAX_ZOOM', self::GM_MAX_ZOOM_DEFAULT) ?>) {
2948
						zoomlevel = <?= $this->getPreference('GM_MAX_ZOOM', self::GM_MAX_ZOOM_DEFAULT) ?>;
2949
					}
2950
					if (document.editplaces.NEW_ZOOM_FACTOR.value < zoomlevel) {
2951
						zoomlevel = document.editplaces.NEW_ZOOM_FACTOR.value;
2952
						if (zoomlevel < <?= $this->getPreference('GM_MIN_ZOOM', self::GM_MIN_ZOOM_DEFAULT) ?>) {
2953
							zoomlevel = <?= $this->getPreference('GM_MIN_ZOOM', self::GM_MIN_ZOOM_DEFAULT) ?>;
2954
						}
2955
						if (zoomlevel > <?= $this->getPreference('GM_MAX_ZOOM', self::GM_MAX_ZOOM_DEFAULT) ?>) {
2956
							zoomlevel = <?= $this->getPreference('GM_MAX_ZOOM', self::GM_MAX_ZOOM_DEFAULT) ?>;
2957
						}
2958
					}
2959
					map.setCenter(bounds.getCenter());
2960
					map.setZoom(zoomlevel);
2961
				}
2962
			}
2963
		}
2964
2965
		function showLocation_level(address) {
2966
			<?php if ($level > 0) { ?>
2967
				address += '<?= ', ' . addslashes(implode(', ', array_reverse($where_am_i, true))) ?>';
2968
			<?php } ?>
2969
				geocoder.geocode({'address': address}, addAddressToMap);
2970
		}
2971
2972
		function showLocation_all(address) {
2973
			geocoder.geocode({'address': address}, addAddressToMap);
2974
		}
2975
2976
		function paste_char(value) {
2977
			document.editplaces.NEW_PLACE_NAME.value += value;
2978
		}
2979
		window.onload = function() {
2980
			loadMap();
2981
		};
2982
	</script>
2983
2984
		<form method="post" id="editplaces" name="editplaces" action="module.php?mod=googlemap&amp;mod_action=admin_place_save">
2985
			<input type="hidden" name="place_id" value="<?= $place_id ?>">
2986
			<input type="hidden" name="level" value="<?= $level ?>">
2987
			<input type="hidden" name="parent_id" value="<?= $parent_id ?>">
2988
			<input type="hidden" name="place_long" value="<?= $longitude ?>">
2989
			<input type="hidden" name="place_lati" value="<?= $latitude ?>">
2990
2991
			<div class="form-group row">
2992
				<div class="col-sm-10 offset-sm-1">
2993
					<div class="gm-map" style="width: 100%; height: 350px;"></div>
2994
				</div>
2995
			</div>
2996
2997
			<div class="form-group row">
2998
				<label class="col-form-label col-sm-2">
2999
					<?= I18N::translate('Place') ?>
3000
				</label>
3001
				<div class="col-sm-6">
3002
					<input type="text" id="new_pl_name" name="NEW_PLACE_NAME" value="<?= Html::escape($record->pl_place) ?>" class="form-control" required>
3003
3004
					<label for="new_pl_name">
3005
						<a href="#" onclick="showLocation_all(document.getElementById('new_pl_name').value); return false">
3006
							<?= I18N::translate('Search globally') ?>
3007
						</a>
3008
					</label>
3009
					|
3010
					<label for="new_pl_name">
3011
						<a href="#" onclick="showLocation_level(document.getElementById('new_pl_name').value); return false">
3012
							<?= I18N::translate('Search locally') ?>
3013
						</a>
3014
					</label>
3015
				</div>
3016
3017
				<label class="col-form-label col-sm-2" for="NEW_ZOOM_FACTOR">
3018
					<?= I18N::translate('Zoom level') ?>
3019
				</label>
3020
				<div class="col-sm-2">
3021
					<input type="text" id="NEW_ZOOM_FACTOR" name="NEW_ZOOM_FACTOR" value="<?= $record->pl_zoom ?>" class="form-control" onchange="updateMap();" required readonly>
3022
				</div>
3023
			</div class="form-group row">
3024
3025
			<div class="form-group row">
3026
				<label class="col-form-label col-sm-2">
3027
					<?= I18N::translate('Latitude') ?>
3028
				</label>
3029
				<div class="col-sm-4">
3030
					<div class="input-group">
3031
						<input type="text" id="NEW_PLACE_LATI" name="NEW_PLACE_LATI" placeholder="<?= /* I18N: Measure of latitude/longitude */ I18N::translate('degrees') ?>" value="<?= abs($latitude) ?>" class="form-control" onchange="updateMap();" required>
3032
						<select name="LATI_CONTROL" id="LATI_CONTROL" onchange="updateMap();" class="form-control">
3033
							<option value="N"<?= $latitude >= 0 ? ' selected' : '' ?>>
3034
								<?= I18N::translate('north') ?>
3035
							</option>
3036
							<option value="S"<?= $latitude < 0 ? ' selected' : '' ?>>
3037
								<?= I18N::translate('south') ?>
3038
							</option>
3039
						</select>
3040
					</div>
3041
				</div>
3042
3043
				<label class="col-form-label col-sm-2">
3044
					<?= I18N::translate('Longitude') ?>
3045
				</label>
3046
				<div class="col-sm-4">
3047
					<div class="input-group">
3048
						<input type="text" id="NEW_PLACE_LONG" name="NEW_PLACE_LONG" placeholder="<?= I18N::translate('degrees') ?>" value="<?= abs($longitude) ?>" class="form-control" onchange="updateMap();" required>
3049
						<select name="LONG_CONTROL" id="LONG_CONTROL" onchange="updateMap();" class="form-control">
3050
							<option value="E"<?= $longitude >= 0 ? ' selected' : '' ?>>
3051
								<?= I18N::translate('east') ?>
3052
							</option>
3053
							<option value="W"<?= $longitude < 0 ? ' selected' : '' ?>>
3054
								<?= I18N::translate('west') ?>
3055
							</option>
3056
						</select>
3057
					</div>
3058
				</div>
3059
			</div>
3060
3061
			<div class="row form-group">
3062
				<label class="col-form-label col-sm-2" for="icon">
3063
					<?= I18N::translate('Flag') ?>
3064
				</label>
3065
				<div class="col-sm-10">
3066
					<div class="input-group" dir="ltr">
3067
					<span class="input-group-addon"><?= WT_MODULES_DIR ?>googlemap/places/flags/</span>
3068
						<?= FunctionsEdit::formControlFlag($record->pl_icon, ['name' => 'icon', 'id' => 'icon', 'class' => 'form-control']) ?>
3069
					</div>
3070
				</div>
3071
			</div>
3072
3073
			<div class="row form-group">
3074
				<div class="col-sm-10 offset-sm-2">
3075
					<button class="btn btn-primary" type="submit">
3076
						<?= /* I18N: A button label. */ I18N::translate('save') ?>
3077
					</button>
3078
					<a class="btn btn-secondary" href="<?= $parent_url ?>">
3079
						<?= /* I18N: A button label. */ I18N::translate('cancel') ?>
3080
					</a>
3081
				</div>
3082
			</div>
3083
		</form>
3084
		<?php
3085
	}
3086
3087
	/**
3088
	 * Places administration.
3089
	 */
3090
	private function adminPlaces() {
3091
		global $WT_TREE;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
3092
3093
		$action    = Filter::get('action');
3094
		$parent_id = (int) Filter::get('parent_id');
3095
		$inactive  = Filter::getBool('inactive');
3096
3097
		$controller = new PageController;
3098
		$controller
3099
			->setPageTitle(I18N::translate('Geographic data'))
3100
			->pageHeader();
3101
3102
		$breadcrumbs = [
3103
			route('admin-control-panel') => I18N::translate('Control panel'),
3104
			route('admin-modules')       => I18N::translate('Module administration'),
3105
			$this->getConfigLink()     => $this->getTitle(),
3106
		];
3107
		$hierarchy =
3108
			[0 => I18N::translate('Geographic data')] +
3109
			$this->placeIdToHierarchy($parent_id);
3110
		foreach (array_slice($hierarchy, 0, -1, true) as $id => $name) {
3111
			$breadcrumbs += ['module.php?mod=googlemap&mod_action=admin_places&parent_id=' . $id . '&inactive=' . $inactive => Html::escape($name)];
3112
		}
3113
		echo Bootstrap4::breadcrumbs($breadcrumbs, end($hierarchy));
0 ignored issues
show
Security Bug introduced by
It seems like end($hierarchy) targeting end() can also be of type false; however, Fisharebest\Webtrees\Bootstrap4::breadcrumbs() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
3114
3115
		if ($action == 'ImportGedcom') {
3116
			echo '<h2>' . I18N::translate('Geographic data') . '</h2>';
3117
			$placelist      = [];
3118
			$j              = 0;
3119
			$gedcom_records =
3120
				Database::prepare("SELECT i_gedcom FROM `##individuals` WHERE i_file=? UNION ALL SELECT f_gedcom FROM `##families` WHERE f_file=?")
3121
				->execute([$WT_TREE->getTreeId(), $WT_TREE->getTreeId()])
3122
				->fetchOneColumn();
3123
			foreach ($gedcom_records as $gedrec) {
3124
				$i        = 1;
3125
				$placerec = Functions::getSubRecord(2, '2 PLAC', $gedrec, $i);
3126
				while (!empty($placerec)) {
3127
					if (preg_match('/2 PLAC (.+)/', $placerec, $match)) {
3128
						$placelist[$j]          = [];
3129
						$placelist[$j]['place'] = trim($match[1]);
3130 View Code Duplication
						if (preg_match('/4 LATI (.*)/', $placerec, $match)) {
3131
							$placelist[$j]['lati'] = trim($match[1]);
3132
							if (($placelist[$j]['lati'][0] != 'N') && ($placelist[$j]['lati'][0] != 'S')) {
3133
								if ($placelist[$j]['lati'] < 0) {
3134
									$placelist[$j]['lati'][0] = 'S';
3135
								} else {
3136
									$placelist[$j]['lati'] = 'N' . $placelist[$j]['lati'];
3137
								}
3138
							}
3139
						} else {
3140
							$placelist[$j]['lati'] = null;
3141
						}
3142 View Code Duplication
						if (preg_match('/4 LONG (.*)/', $placerec, $match)) {
3143
							$placelist[$j]['long'] = trim($match[1]);
3144
							if (($placelist[$j]['long'][0] != 'E') && ($placelist[$j]['long'][0] != 'W')) {
3145
								if ($placelist[$j]['long'] < 0) {
3146
									$placelist[$j]['long'][0] = 'W';
3147
								} else {
3148
									$placelist[$j]['long'] = 'E' . $placelist[$j]['long'];
3149
								}
3150
							}
3151
						} else {
3152
							$placelist[$j]['long'] = null;
3153
						}
3154
						$j = $j + 1;
3155
					}
3156
					$i        = $i + 1;
3157
					$placerec = Functions::getSubRecord(2, '2 PLAC', $gedrec, $i);
3158
				}
3159
			}
3160
			asort($placelist);
3161
3162
			$prevPlace     = '';
3163
			$prevLati      = '';
3164
			$prevLong      = '';
3165
			$placelistUniq = [];
3166
			$j             = 0;
3167
			foreach ($placelist as $k => $place) {
3168
				if ($place['place'] != $prevPlace) {
3169
					$placelistUniq[$j]          = [];
3170
					$placelistUniq[$j]['place'] = $place['place'];
3171
					$placelistUniq[$j]['lati']  = $place['lati'];
3172
					$placelistUniq[$j]['long']  = $place['long'];
3173
					$j                          = $j + 1;
3174
				} elseif (($place['place'] == $prevPlace) && (($place['lati'] != $prevLati) || ($place['long'] != $prevLong))) {
3175
					if (($placelistUniq[$j - 1]['lati'] == 0) || ($placelistUniq[$j - 1]['long'] == 0)) {
3176
						$placelistUniq[$j - 1]['lati'] = $place['lati'];
3177
						$placelistUniq[$j - 1]['long'] = $place['long'];
3178 View Code Duplication
					} elseif (($place['lati'] != '0') || ($place['long'] != '0')) {
3179
						echo 'Difference: previous value = ', $prevPlace, ', ', $prevLati, ', ', $prevLong, ' current = ', $place['place'], ', ', $place['lati'], ', ', $place['long'], '<br>';
3180
					}
3181
				}
3182
				$prevPlace = $place['place'];
3183
				$prevLati  = $place['lati'];
3184
				$prevLong  = $place['long'];
3185
			}
3186
3187
			$highestIndex = $this->getHighestIndex();
3188
3189
			$default_zoom_level = [4, 7, 10, 12];
3190
			foreach ($placelistUniq as $k => $place) {
3191
				$parent     = preg_split('/ *, */', $place['place']);
3192
				$parent     = array_reverse($parent);
3193
				$parent_id  = 0;
3194
				$num_parent = count($parent);
3195
				for ($i = 0; $i < $num_parent; $i++) {
3196
					if (!isset($default_zoom_level[$i])) {
3197
						$default_zoom_level[$i] = $default_zoom_level[$i - 1];
3198
					}
3199
					$escparent = $parent[$i];
3200
					if ($escparent == '') {
3201
						$escparent = 'Unknown';
3202
					}
3203
					$row =
3204
						Database::prepare("SELECT pl_id, pl_long, pl_lati, pl_zoom FROM `##placelocation` WHERE pl_level=? AND pl_parent_id=? AND pl_place LIKE ?")
3205
						->execute([$i, $parent_id, $escparent])
3206
						->fetchOneRow();
3207
					if ($i < $num_parent - 1) {
3208
						// Create higher-level places, if necessary
3209
						if (empty($row)) {
3210
							$highestIndex++;
3211
							Database::prepare("INSERT INTO `##placelocation` (pl_id, pl_parent_id, pl_level, pl_place, pl_zoom) VALUES (?, ?, ?, ?, ?)")
3212
								->execute([$highestIndex, $parent_id, $i, $escparent, $default_zoom_level[$i]]);
3213
							echo Html::escape($escparent), '<br>';
3214
							$parent_id = $highestIndex;
3215
						} else {
3216
							$parent_id = $row->pl_id;
3217
						}
3218
					} else {
3219
						// Create lowest-level place, if necessary
3220
						if (empty($row->pl_id)) {
3221
							$highestIndex++;
3222
							Database::prepare("INSERT INTO `##placelocation` (pl_id, pl_parent_id, pl_level, pl_place, pl_long, pl_lati, pl_zoom) VALUES (?, ?, ?, ?, ?, ?, ?)")
3223
								->execute([$highestIndex, $parent_id, $i, $escparent, $place['long'], $place['lati'], $default_zoom_level[$i]]);
3224
							echo Html::escape($escparent), '<br>';
3225
						} else {
3226
							if (empty($row->pl_long) && empty($row->pl_lati) && $place['lati'] != '0' && $place['long'] != '0') {
3227
								Database::prepare("UPDATE `##placelocation` SET pl_lati=?, pl_long=? WHERE pl_id=?")
3228
									->execute([$place['lati'], $place['long'], $row->pl_id]);
3229
								echo Html::escape($escparent), '<br>';
3230
							}
3231
						}
3232
					}
3233
				}
3234
			}
3235
		}
3236
3237
		$placelist = $this->getPlaceListLocation($parent_id, $inactive);
3238
		?>
3239
		<form class="form-inline">
3240
		<input type="hidden" name="mod" value="googlemap">
3241
		<input type="hidden" name="mod_action" value="admin_places">
3242
		<input type="hidden" name="parent_id" value="<?= $parent_id ?>">
3243
			<?= Bootstrap4::checkbox(I18N::translate('Show inactive places'), false, ['name' => 'inactive', 'checked' => $inactive, 'onclick' => 'this.form.submit()']) ?>
0 ignored issues
show
Documentation introduced by
array('name' => 'inactiv...> 'this.form.submit()') is of type array<string,string|bool...n","onclick":"string"}>, but the function expects a array<integer,string>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
3244
			<p class="small text-muted">
3245
				<?= I18N::translate('By default, the list shows only those places which can be found in your family trees. You may have details for other places, such as those imported in bulk from an external file. Selecting this option will show all places, including ones that are not currently used.') ?>
3246
				<?= I18N::translate('If you have a large number of inactive places, it can be slow to generate the list.') ?>
3247
			</p>
3248
		</form>
3249
3250
		<div class="gm_plac_edit">
3251
			<table class="table table-bordered table-sm table-hover">
3252
				<thead>
3253
					<tr>
3254
						<th><?= I18N::translate('Place') ?></th>
3255
						<th><?= I18N::translate('Latitude') ?></th>
3256
						<th><?= I18N::translate('Longitude') ?></th>
3257
						<th><?= I18N::translate('Zoom level') ?></th>
3258
						<th><?= I18N::translate('Icon') ?> / <?= I18N::translate('Flag') ?></th>
3259
						<th><?= I18N::translate('Edit') ?></th>
3260
						<th><?= I18N::translate('Delete') ?></th>
3261
					</tr>
3262
				</thead>
3263
				<tbody>
3264
3265
				<?php foreach ($placelist as $place): ?>
3266
					<?php $noRows = Database::prepare("SELECT COUNT(pl_id) FROM `##placelocation` WHERE pl_parent_id=?")->execute([$place['place_id']])->fetchOne(); ?>
3267
					<tr>
3268
						<td>
3269
							<a href="module.php?mod=googlemap&mod_action=admin_places&amp;parent_id=<?= $place['place_id'] ?>&inactive=<?= $inactive ?>">
3270
								<?php if ($place['place'] === 'Unknown'): ?>
3271
									<?= I18N::translate('unknown') ?>
3272
								<?php else: ?>
3273
									<?= Html::escape($place['place']) ?>
3274
								<?php endif ?>
3275
								<?php if ($place['missing'] > 0): ?>
3276
								<span class="badge badge-pill badge-warning">
3277
									<?= I18N::number($place['children']) ?>
3278
								</span>
3279
								<?php elseif ($place['children'] > 0): ?>
3280
								<span class="badge badge-pill badge-default">
3281
									<?= I18N::number($place['children']) ?>
3282
								</span>
3283
								<?php endif ?>
3284
							</a>
3285
						</td>
3286
						<td>
3287
							<?= $place['is_empty'] ? FontAwesome::decorativeIcon('warning') : $place['lati'] ?>
3288
						</td>
3289
						<td>
3290
							<?= $place['is_empty'] ? FontAwesome::decorativeIcon('warning') : $place['long'] ?>
3291
						</td>
3292
						<td>
3293
							<?= $place['zoom'] ?>
3294
						</td>
3295
						<td>
3296
								<?php if ($place['icon']): ?>
3297
									<img src="<?= WT_MODULES_DIR ?>googlemap/places/flags/<?= Html::escape($place['icon']) ?>" width="25" height="15" title="<?= Html::escape($place['icon']) ?>" alt="<?= I18N::translate('Flag') ?>">
3298
								<?php else: ?>
3299
									<img src="<?= WT_MODULES_DIR ?>googlemap/images/mm_20_red.png">
3300
								<?php endif ?>
3301
						</td>
3302
						<td>
3303
							<?= FontAwesome::linkIcon('edit', I18N::translate('Edit'), ['href' => 'module.php?mod=googlemap&mod_action=admin_place_edit&action=update&place_id=' . $place['place_id'] . '&parent_id=' . $place['parent_id'], 'class' => 'btn btn-primary']) ?>
3304
						</td>
3305
						<td>
3306
							<?php if ($noRows == 0): ?>
3307
								<form method="POST" action="module.php?mod=googlemap&amp;mod_action=admin_delete_action" onsubmit="return confirm('<?= I18N::translate('Remove this location?') ?>')">
3308
									<input type="hidden" name="parent_id" value="<?= $parent_id ?>">
3309
									<input type="hidden" name="place_id" value="<?= $place['place_id'] ?>">
3310
									<input type="hidden" name="inactive" value="<?= $inactive ?>">
3311
									<button type="submit" class="btn btn-danger">
3312
										<?= FontAwesome::semanticIcon('delete', I18N::translate('Delete')) ?>
3313
									</button>
3314
								</form>
3315
							<?php else: ?>
3316
								<button type="button" class="btn btn-danger" disabled>
3317
									<?= FontAwesome::decorativeIcon('delete') ?>
3318
								</button>
3319
							<?php endif ?>
3320
						</td>
3321
					</tr>
3322
					<?php endforeach ?>
3323
				</tbody>
3324
				<tfoot>
3325
					<tr>
3326
						<td colspan="7">
3327
							<a href="module.php?mod=googlemap&mod_action=admin_place_edit&parent_id=<?= $parent_id ?>" class="btn btn-primary">
3328
								<?= FontAwesome::decorativeIcon('add') ?>
3329
								<?= /* I18N: A button label. */ I18N::translate('add') ?>
3330
							</a>
3331
3332
							<a href="module.php?mod=googlemap&mod_action=admin_download&parent_id=<?= $parent_id ?>" class="btn btn-primary">
3333
								<?= FontAwesome::decorativeIcon('download') ?>
3334
								<?= /* I18N: A button label. */ I18N::translate('download') ?>
3335
							</a>
3336
3337
							<a href="module.php?mod=googlemap&amp;mod_action=admin_upload&amp;parent_id=<?= $parent_id ?>&amp;inactive=<?= $inactive ?>" class="btn btn-primary">
3338
								<?= FontAwesome::decorativeIcon('upload') ?>
3339
								<?= /* I18N: A button label. */ I18N::translate('upload') ?>
3340
							</a>
3341
						</td>
3342
					</tr>
3343
				</tfoot>
3344
			</table>
3345
		</div>
3346
3347
		<hr>
3348
3349
		<form class="form-horizontal" action="module.php">
3350
			<input type="hidden" name="mod" value="googlemap">
3351
			<input type="hidden" name="mod_action" value="admin_places">
3352
			<input type="hidden" name="action" value="ImportGedcom">
3353
			<div class="row form-group">
3354
				<label class="form-control-static col-sm-4" for="ged">
3355
					<?= I18N::translate('Import all places from a family tree') ?>
3356
				</label>
3357
				<div class="col-sm-6">
3358
					<?= Bootstrap4::select(Tree::getNameList(), $WT_TREE->getName(), ['id' => 'ged', 'name' => 'ged']) ?>
3359
				</div>
3360
				<div class="col-sm-2">
3361
					<button type="submit" class="btn btn-primary">
3362
						<?= FontAwesome::decorativeIcon('add') ?>
3363
						<?= /* I18N: A button label. */ I18N::translate('import') ?>
3364
					</button>
3365
				</div>
3366
			</div>
3367
		</form>
3368
		<?php
3369
	}
3370
}
3371