Completed
Push — develop ( 9087a8...c9b4ef )
by Greg
16:31 queued 05:44
created

GoogleMapsModule::adminPlaceEdit()   F

Complexity

Conditions 23
Paths 1152

Size

Total Lines 554
Code Lines 166

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 23
eloc 166
nc 1152
nop 0
dl 0
loc 554
rs 2
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
/**
3
 * webtrees: online genealogy
4
 * Copyright (C) 2018 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();
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 canLoadAjax() {
161
		return true;
162
	}
163
164
	/** {@inheritdoc} */
165
	public function getTabContent(Individual $individual) {
166
		Database::updateSchema(self::SCHEMA_MIGRATION_PREFIX, self::SCHEMA_SETTING_NAME, self::SCHEMA_TARGET_VERSION);
167
168
		if ($this->checkMapData($individual)) {
169
			// This call can return an empty string if no facts with map co-ordinates exist
170
			$map_data = $this->buildIndividualMap($individual);
171
		} else {
172
			$map_data = '';
173
		}
174
175
		return view('tabs/map', [
176
			'google_map_css'       => WT_MODULES_DIR . 'googlemap/css/wt_v3_googlemap.css',
177
			'google_map_js'        => $this->googleMapsScript(),
178
			'individual'           => $individual,
179
			'is_admin'             => Auth::isAdmin(),
180
			'map_data'             => $map_data,
181
		]);
182
	}
183
184
	/** {@inheritdoc} */
185
	public function hasTabContent(Individual $individual) {
186
		return Module::getModuleByName('googlemap') || Auth::isAdmin();
187
	}
188
189
	/** {@inheritdoc} */
190
	public function isGrayedOut(Individual $individual) {
191
		return false;
192
	}
193
194
	/**
195
	 * Return a menu item for this chart.
196
	 *
197
	 * @param Individual $individual
198
	 *
199
	 * @return Menu
200
	 */
201
	public function getChartMenu(Individual $individual) {
202
		return new Menu(
203
			I18N::translate('Pedigree map'),
204
			'module.php?mod=googlemap&amp;mod_action=pedigree_map&amp;rootid=' . $individual->getXref() . '&amp;ged=' . $individual->getTree()->getNameUrl(),
205
			'menu-chart-pedigreemap',
206
			['rel' => 'nofollow']
207
		);
208
	}
209
210
	/**
211
	 * Return a menu item for this chart - for use in individual boxes.
212
	 *
213
	 * @param Individual $individual
214
	 *
215
	 * @return Menu
216
	 */
217
	public function getBoxChartMenu(Individual $individual) {
218
		return $this->getChartMenu($individual);
219
	}
220
221
	/**
222
	 * A form to edit the module configuration.
223
	 */
224
	private function config() {
225
		$controller = new PageController;
226
		$controller->setPageTitle(I18N::translate('Google Maps™'));
227
228
		if (Filter::post('action') === 'update') {
229
			$this->setPreference('GM_API_KEY', Filter::post('GM_API_KEY'));
230
			$this->setPreference('GM_MIN_ZOOM', Filter::post('GM_MIN_ZOOM'));
231
			$this->setPreference('GM_MAX_ZOOM', Filter::post('GM_MAX_ZOOM'));
232
			$this->setPreference('GM_PLACE_HIERARCHY', Filter::post('GM_PLACE_HIERARCHY'));
233
			$this->setPreference('GM_PH_MARKER', Filter::post('GM_PH_MARKER'));
234
			$this->setPreference('GM_PREFIX_1', Filter::post('GM_PREFIX_1'));
235
			$this->setPreference('GM_PREFIX_2', Filter::post('GM_PREFIX_2'));
236
			$this->setPreference('GM_PREFIX_3', Filter::post('GM_PREFIX_3'));
237
			$this->setPreference('GM_PREFIX_4', Filter::post('GM_PREFIX_4'));
238
			$this->setPreference('GM_PREFIX_5', Filter::post('GM_PREFIX_5'));
239
			$this->setPreference('GM_PREFIX_6', Filter::post('GM_PREFIX_6'));
240
			$this->setPreference('GM_PREFIX_7', Filter::post('GM_PREFIX_7'));
241
			$this->setPreference('GM_PREFIX_8', Filter::post('GM_PREFIX_8'));
242
			$this->setPreference('GM_PREFIX_9', Filter::post('GM_PREFIX_9'));
243
			$this->setPreference('GM_POSTFIX_1', Filter::post('GM_POSTFIX_1'));
244
			$this->setPreference('GM_POSTFIX_2', Filter::post('GM_POSTFIX_2'));
245
			$this->setPreference('GM_POSTFIX_3', Filter::post('GM_POSTFIX_3'));
246
			$this->setPreference('GM_POSTFIX_4', Filter::post('GM_POSTFIX_4'));
247
			$this->setPreference('GM_POSTFIX_5', Filter::post('GM_POSTFIX_5'));
248
			$this->setPreference('GM_POSTFIX_6', Filter::post('GM_POSTFIX_6'));
249
			$this->setPreference('GM_POSTFIX_7', Filter::post('GM_POSTFIX_7'));
250
			$this->setPreference('GM_POSTFIX_8', Filter::post('GM_POSTFIX_8'));
251
			$this->setPreference('GM_POSTFIX_9', Filter::post('GM_POSTFIX_9'));
252
253
			FlashMessages::addMessage(I18N::translate('The preferences for the module “%s” have been updated.', $this->getTitle()), 'success');
254
			header('Location: module.php?mod=googlemap&mod_action=admin_config');
255
256
			return;
257
		}
258
259
		$controller->pageHeader();
260
261
		echo Bootstrap4::breadcrumbs([
262
			route('admin-control-panel') => I18N::translate('Control panel'),
263
			route('admin-modules')       => I18N::translate('Module administration'),
264
		], $controller->getPageTitle());
265
		?>
266
267
		<h2><?= I18N::translate('Google Maps™ preferences') ?></h2>
268
269
		<form class="form-horizontal" method="post" name="configform" action="module.php?mod=googlemap&mod_action=admin_config">
270
			<input type="hidden" name="action" value="update">
271
272
			<div class="row form-group">
273
				<div class="col-sm-3 col-form-label">
274
					<?= I18N::translate('Geographic data') ?>
275
				</div>
276
				<div class="col-sm-9">
277
					<a class="btn btn-primary" href="module.php?mod=googlemap&amp;mod_action=admin_places">
278
						<?= FontAwesome::decorativeIcon('edit') ?>
279
						<?= I18N::translate('edit') ?>
280
					</a>
281
				</div>
282
			</div>
283
284
			<!-- GM_API_KEY -->
285
			<div class="row form-group">
286
				<label class="col-sm-3 col-form-label" for="GM_API_KEY">
287
					<?= /* I18N: https://en.wikipedia.org/wiki/API_key */ I18N::translate('API key') ?>
288
				</label>
289
				<div class="col-sm-9">
290
					<input id="GM_API_KEY" class="form-control" type="text" name="GM_API_KEY" value="<?= $this->getPreference('GM_API_KEY') ?>">
291
					<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.') ?>
292
						<a href="https://developers.google.com/maps/documentation/javascript/get-api-key">
293
							<?= /* I18N: https://en.wikipedia.org/wiki/API_key */ I18N::translate('Get an API key from Google.') ?>
294
						</a>
295
					</p>
296
				</div>
297
			</div>
298
299
			<!-- GM_MIN_ZOOM / GM_MAX_ZOOM -->
300
			<fieldset class="form-group">
301
				<div class="row">
302
					<legend class="col-form-label col-sm-3">
303
						<?= I18N::translate('Zoom level of map') ?>
304
					</legend>
305
					<div class="col-sm-9">
306
						<div class="row">
307
							<div class="col-sm-6">
308
								<div class="input-group">
309
									<div class="input-group-prepend">
310
									<label class="input-group-text" for="GM_MIN_ZOOM">
311
									<?= I18N::translate('minimum') ?>
312
									</label>
313
									</div>
314
									<?= 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']) ?>
315
								</div>
316
							</div>
317
							<div class="col-sm-6">
318
								<div class="input-group">
319
									<div class="input-group-prepend">
320
									<label class="input-group-text" for="GM_MAX_ZOOM">
321
									<?= I18N::translate('maximum') ?>
322
									</label>
323
									</div>
324
									<?= 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']) ?>
325
								</div>
326
							</div>
327
						</div>
328
						<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>
329
					</div>
330
				</div>
331
			</fieldset>
332
333
			<!-- GM_PREFIX / GM_POSTFIX -->
334
			<fieldset class="form-group">
335
				<div class="row">
336
					<legend class="col-form-label col-sm-3">
337
						<?= I18N::translate('Optional prefixes and suffixes') ?>
338
					</legend>
339
					<div class="col-sm-9">
340
						<div class="row">
341
							<div class ="col-sm-6">
342
								<p class="form-control-static"><strong><?= I18N::translate('Prefixes') ?></strong></p>
343
								<?php for ($level = 1; $level < 10; $level++): ?>
344
								<?php
345
								if ($level == 1) {
346
									$label = I18N::translate('Country');
347
								} else {
348
									$label = I18N::translate('Level') . ' ' . $level;
349
								}
350
								?>
351
								<div class="input-group">
352
									<div class="input-group-prepend">
353
									<label class="input-group-text" for="GM_PREFIX_<?= $level ?>">
354
									<?= $label ?>
355
									</label>
356
									</div>
357
									<input class="form-control" type="text" name="GM_PREFIX_<?= $level ?>" value="<?= $this->getPreference('GM_PREFIX_' . $level) ?>">
358
								</div>
359
								<?php endfor ?>
360
							</div>
361
							<div class="col-sm-6">
362
								<p class="form-control-static"><strong><?= I18N::translate('Suffixes') ?></strong></p>
363
								<?php for ($level = 1; $level < 10; $level++): ?>
364
								<?php
365
								if ($level == 1) {
366
									$label = I18N::translate('Country');
367
								} else {
368
									$label = I18N::translate('Level') . ' ' . $level;
369
								}
370
								?>
371
								<div class="input-group">
372
									<div class="input-group-prepend">
373
									<label class="input-group-addon" for="GM_POSTFIX_<?= $level ?>">
374
									<?= $label ?>
375
									</label>
376
									</div>
377
									<input class="form-control" type="text" name="GM_POSTFIX_<?= $level ?>" value="<?= $this->getPreference('GM_POSTFIX_' . $level) ?>">
378
								</div>
379
								<?php endfor ?>
380
							</div>
381
						</div>
382
						<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>
383
					</div>
384
				</div>
385
			</fieldset>
386
387
			<h3><?= I18N::translate('Place hierarchy') ?></h3>
388
389
			<!-- GM_PLACE_HIERARCHY -->
390
			<fieldset class="form-group">
391
				<div class="row">
392
					<legend class="col-form-label col-sm-3">
393
						<?= I18N::translate('Use Google Maps™ for the place hierarchy') ?>
394
					</legend>
395
					<div class="col-sm-9">
396
						<?= Bootstrap4::radioButtons('GM_PLACE_HIERARCHY', [I18N::translate('no'), I18N::translate('yes')], $this->getPreference('GM_PLACE_HIERARCHY', '0'), true) ?>
397
					</div>
398
				</div>
399
			</fieldset>
400
401
			<!-- GM_PH_MARKER -->
402
			<div class="row form-group">
403
				<label class="col-sm-3 col-form-label" for="GM_PH_MARKER">
404
					<?= I18N::translate('Type of place markers in the place hierarchy') ?>
405
				</label>
406
				<div class="col-sm-9">
407
					<?php
408
					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']);
409
					?>
410
				</div>
411
			</div>
412
413
			<!-- SAVE BUTTON -->
414
			<div class="row form-group">
415
				<div class="offset-sm-3 col-sm-9">
416
					<button type="submit" class="btn btn-primary">
417
						<i class="fas fa-check"></i>
418
						<?= I18N::translate('save') ?>
419
					</button>
420
				</div>
421
			</div>
422
		</form>
423
		<?php
424
	}
425
426
	/**
427
	 * Google Maps API script
428
	 *
429
	 * @return string
430
	 */
431
	private function googleMapsScript() {
432
		$key = $this->getPreference('GM_API_KEY');
433
434
		return 'https://maps.googleapis.com/maps/api/js?v=3&amp;key=' . $key . '&amp;language=' . WT_LOCALE;
435
	}
436
437
	/**
438
	 * Display a map showing the origins of ones ancestors.
439
	 */
440
	private function pedigreeMap() {
441
		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...
442
443
		$controller = new ChartController();
444
		$controller->restrictAccess(Module::isActiveChart($WT_TREE, 'googlemap'));
445
446
		// Limit this to match available number of icons.
447
		// 8 generations equals 255 individuals
448
		$MAX_PEDIGREE_GENERATIONS = $WT_TREE->getPreference('MAX_PEDIGREE_GENERATIONS');
449
		$MAX_PEDIGREE_GENERATIONS = min($MAX_PEDIGREE_GENERATIONS, 8);
450
		$generations              = Filter::getInteger('PEDIGREE_GENERATIONS', 2, $MAX_PEDIGREE_GENERATIONS, $WT_TREE->getPreference('DEFAULT_PEDIGREE_GENERATIONS'));
451
		$this->treesize           = pow(2, $generations) - 1;
452
		$this->ancestors          = array_values($controller->sosaAncestors($generations));
453
454
		// Only generate the content for interactive users (not search robots).
455
		if (Filter::getBool('ajax') && Session::has('initiated')) {
456
			// count records by type
457
			$curgen   = 1;
458
			$priv     = 0;
459
			$count    = 0;
460
			$miscount = 0;
461
			$missing  = [];
462
463
			$latlongval = [];
464
			$lat        = [];
465
			$lon        = [];
466
			for ($i = 0; $i < ($this->treesize); $i++) {
467
				// -- check to see if we have moved to the next generation
468
				if ($i + 1 >= pow(2, $curgen)) {
469
					$curgen++;
470
				}
471
				$person = $this->ancestors[$i];
472
				if (!empty($person)) {
473
					$name = $person->getFullName();
474
					if ($name == I18N::translate('Private')) {
475
						$priv++;
476
					}
477
					$place = $person->getBirthPlace()->getGedcomName();
478
					if (empty($place)) {
479
						$latlongval[$i] = null;
480
					} else {
481
						$latlongval[$i] = $this->getLatitudeAndLongitudeFromPlaceLocation($person->getBirthPlace()->getGedcomName());
482
					}
483
					if ($latlongval[$i]) {
484
						$lat[$i] = strtr($latlongval[$i]->pl_lati, ['N' => '', 'S' => '-', ',' => '.']);
485
						$lon[$i] = strtr($latlongval[$i]->pl_long, ['N' => '', 'S' => '-', ',' => '.']);
486
						if ($lat[$i] && $lon[$i]) {
487
							$count++;
488
						} else {
489
							// The place is in the table but has empty values
490
							if ($name) {
491
								$missing[] = '<a href="' . e($person->url()) . '">' . $name . '</a>';
492
								$miscount++;
493
							}
494
						}
495
					} else {
496
						// There was no place, or not listed in the map table
497
						if ($name) {
498
							$missing[] = '<a href="' . e($person->url()) . '">' . $name . '</a>';
499
							$miscount++;
500
						}
501
					}
502
				}
503
			}
504
505
			//<!-- end of count records by type -->
506
			//<!-- start of map display -->
507
			echo '<div class="gm-pedigree-map">';
508
			echo '<div class="gm-wrapper">';
509
			echo '<div class="gm-map wt-ajax-load"></div>';
510
			echo '<div class="gm-ancestors"></div>';
511
			echo '</div>';
512
513
			if (Auth::isAdmin()) {
514
				echo '<div class="gm-options">';
515
				echo '<a href="module.php?mod=' . $this->getName() . '&amp;mod_action=admin_config">' . I18N::translate('Google Maps™ preferences') . '</a>';
516
				echo ' | <a href="module.php?mod=' . $this->getName() . '&amp;mod_action=admin_places">' . I18N::translate('Geographic data') . '</a>';
517
				echo '</div>';
518
			}
519
			// display info under map
520
			echo '<hr>';
521
522
			// print summary statistics
523
			if (isset($curgen)) {
524
				$total = pow(2, $curgen) - 1;
525
				echo '<div>';
526
				echo I18N::plural(
527
					'%1$s individual displayed, out of the normal total of %2$s, from %3$s generations.',
528
					'%1$s individuals displayed, out of the normal total of %2$s, from %3$s generations.',
529
					$count,
530
					I18N::number($count), I18N::number($total), I18N::number($curgen)
531
				);
532
				echo '</div>';
533
				if ($priv) {
534
					echo '<div>' . I18N::plural('%s individual is private.', '%s individuals are private.', $priv, $priv), '</div>';
535
				}
536
				if ($count + $priv != $total) {
537
					if ($miscount == 0) {
538
						echo '<div>' . I18N::translate('No ancestors in the database.'), '</div>';
539
					} else {
540
						echo '<div>' . /* I18N: %1$s is a count of individuals, %2$s is a list of their names */ I18N::plural(
541
								'%1$s individual is missing birthplace map coordinates: %2$s.',
542
								'%1$s individuals are missing birthplace map coordinates: %2$s.',
543
								$miscount, I18N::number($miscount), implode(I18N::$list_separator, $missing)),
544
						'</div>';
545
					}
546
				}
547
			}
548
549
			echo '</div>';
550
			echo '</div>';
551
			?>
552
			<script>
553
				function initialiZePedigreeMap() {
554
					// this variable will collect the html which will eventually be placed in the side bar
555
					var gm_ancestors_html = "";
556
					// arrays to hold copies of the markers and html used by the side bar
557
					// because the function closure trick doesnt work there
558
					var gmarkers = [];
559
					var index = 0;
560
					var lastlinkid;
561
					var infowindow = new google.maps.InfoWindow({});
562
					// === Create an associative array of GIcons()
563
					var gicons = [];
564
					gicons["1"]  = {
565
						url: WT_MODULES_DIR + "googlemap/images/icon1.png"
566
					};
567
					gicons["2"]  = {
568
						url: WT_MODULES_DIR + "googlemap/images/icon2.png"
569
					};
570
					gicons["2L"] = {
571
						url: WT_MODULES_DIR + "googlemap/images/icon2L.png",
572
						size: new google.maps.Size(32, 32),
573
						origin: new google.maps.Point(0, 0),
574
						anchor: new google.maps.Point(28, 28)
575
					};
576
					gicons["2R"] = {
577
						url: WT_MODULES_DIR + "googlemap/images/icon2R.png",
578
						size:  new google.maps.Size(32, 32),
579
						origin: new google.maps.Point(0, 0),
580
						anchor: new google.maps.Point(4, 28)
581
					};
582
					gicons["2Ls"] = {
583
						url: WT_MODULES_DIR+"googlemap/images/icon2Ls.png",
584
						size:  new google.maps.Size(24, 24),
585
						origin: new google.maps.Point(0, 0),
586
						anchor: new google.maps.Point(22, 22)
587
					};
588
					gicons["2Rs"] = {
589
						url: WT_MODULES_DIR + "googlemap/images/icon2Rs.png",
590
						size: new google.maps.Size(24, 24),
591
						origin: new google.maps.Point(0, 0),
592
						anchor: new google.maps.Point(2, 22)
593
					};
594
					gicons["3"]   = {
595
						url: WT_MODULES_DIR + "googlemap/images/icon3.png"
596
					};
597
					gicons["3L"] = {
598
						url: WT_MODULES_DIR + "googlemap/images/icon3L.png",
599
						size: new google.maps.Size(32, 32),
600
						origin: new google.maps.Point(0, 0),
601
						anchor: new google.maps.Point(28, 28)
602
					};
603
					gicons["3R"]  = {
604
						url: WT_MODULES_DIR + "googlemap/images/icon3R.png",
605
						size: new google.maps.Size(32, 32),
606
						origin: new google.maps.Point(0, 0),
607
						anchor: new google.maps.Point(4, 28)
608
					};
609
					gicons["3Ls"] = {
610
						url: WT_MODULES_DIR + "googlemap/images/icon3Ls.png",
611
						size: new google.maps.Size(24, 24),
612
						origin: new google.maps.Point(0, 0),
613
						anchor: new google.maps.Point(22, 22)
614
					};
615
					gicons["3Rs"] = {
616
						url: WT_MODULES_DIR + "googlemap/images/icon3Rs.png",
617
						size: new google.maps.Size(24, 24),
618
						origin: new google.maps.Point(0, 0),
619
						anchor: new google.maps.Point(2, 22)
620
					};
621
					gicons["4"]   = {
622
						url: WT_MODULES_DIR + "googlemap/images/icon4.png"
623
					};
624
					gicons["4L"]  = {
625
						url: WT_MODULES_DIR + "googlemap/images/icon4L.png",
626
						size: new google.maps.Size(32, 32),
627
						origin: new google.maps.Point(0, 0),
628
						anchor: new google.maps.Point(28, 28)
629
					};
630
					gicons["4R"] = {
631
						url: WT_MODULES_DIR + "googlemap/images/icon4R.png",
632
						size: new google.maps.Size(32, 32),
633
						origin: new google.maps.Point(0, 0),
634
						anchor: new google.maps.Point(4, 28)
635
					};
636
					gicons["4Ls"] = {
637
						url: WT_MODULES_DIR + "googlemap/images/icon4Ls.png",
638
						size: new google.maps.Size(24, 24),
639
						origin: new google.maps.Point(0, 0),
640
						anchor: new google.maps.Point(22, 22)
641
					};
642
					gicons["4Rs"] = {
643
						url: WT_MODULES_DIR + "googlemap/images/icon4Rs.png",
644
						size: new google.maps.Size(24, 24),
645
						origin: new google.maps.Point(0, 0),
646
						anchor: new google.maps.Point(2, 22)
647
					};
648
					gicons["5"] = {
649
						url: WT_MODULES_DIR + "googlemap/images/icon5.png"
650
					};
651
					gicons["5L"] = {
652
						url: WT_MODULES_DIR + "googlemap/images/icon5L.png",
653
						size: new google.maps.Size(32, 32),
654
						origin: new google.maps.Point(0, 0),
655
						anchor: new google.maps.Point(28, 28)
656
					};
657
					gicons["5R"] = {
658
						url: WT_MODULES_DIR + "googlemap/images/icon5R.png",
659
						size: new google.maps.Size(32, 32),
660
						origin: new google.maps.Point(0, 0),
661
						anchor: new google.maps.Point(4, 28)
662
					};
663
					gicons["5Ls"] = {
664
						url: WT_MODULES_DIR + "googlemap/images/icon5Ls.png",
665
						size: new google.maps.Size(24, 24),
666
						origin: new google.maps.Point(0, 0),
667
						anchor: new google.maps.Point(22, 22)
668
					};
669
					gicons["5Rs"] = {
670
						url: WT_MODULES_DIR + "googlemap/images/icon5Rs.png",
671
						size: new google.maps.Size(24, 24),
672
						origin: new google.maps.Point(0, 0),
673
						anchor: new google.maps.Point(2, 22)
674
					};
675
					gicons["6"] = {
676
						url: WT_MODULES_DIR + "googlemap/images/icon6.png"
677
					};
678
					gicons["6L"] = {
679
						url: WT_MODULES_DIR + "googlemap/images/icon6L.png",
680
						size: new google.maps.Size(32, 32),
681
						origin: new google.maps.Point(0, 0),
682
						anchor: new google.maps.Point(28, 28)
683
					};
684
					gicons["6R"] = {
685
						url: WT_MODULES_DIR + "googlemap/images/icon6R.png",
686
						size: new google.maps.Size(32, 32),
687
						origin: new google.maps.Point(0, 0),
688
						anchor: new google.maps.Point(4, 28)
689
					};
690
					gicons["6Ls"] = {
691
						url: WT_MODULES_DIR + "googlemap/images/icon6Ls.png",
692
						size: new google.maps.Size(24, 24),
693
						origin: new google.maps.Point(0, 0),
694
						anchor: new google.maps.Point(22, 22)
695
					};
696
					gicons["6Rs"] = {
697
						url: WT_MODULES_DIR + "googlemap/images/icon6Rs.png",
698
						size: new google.maps.Size(24, 24),
699
						origin: new google.maps.Point(0, 0),
700
						anchor: new google.maps.Point(2, 22)
701
					};
702
					gicons["7"]   = {
703
						url: WT_MODULES_DIR + "googlemap/images/icon7.png"
704
					};
705
					gicons["7L"]  = {
706
						url: WT_MODULES_DIR + "googlemap/images/icon7L.png",
707
						size: new google.maps.Size(32, 32),
708
						origin: new google.maps.Point(0, 0),
709
						anchor: new google.maps.Point(28, 28)
710
					};
711
					gicons["7R"]  = {
712
						url: WT_MODULES_DIR + "googlemap/images/icon7R.png",
713
						size: new google.maps.Size(32, 32),
714
						origin: new google.maps.Point(0, 0),
715
						anchor: new google.maps.Point(4, 28)
716
					};
717
					gicons["7Ls"] = {
718
						url: WT_MODULES_DIR + "googlemap/images/icon7Ls.png",
719
						size: new google.maps.Size(24, 24),
720
						origin: new google.maps.Point(0, 0),
721
						anchor: new google.maps.Point(22, 22)
722
					};
723
					gicons["7Rs"] = {
724
						url: WT_MODULES_DIR + "googlemap/images/icon7Rs.png",
725
						size: new google.maps.Size(24, 24),
726
						origin: new google.maps.Point(0, 0),
727
						anchor: new google.maps.Point(2, 22)
728
					};
729
					gicons["8"]   = {
730
						url: WT_MODULES_DIR + "googlemap/images/icon8.png"
731
					};
732
					gicons["8L"]  = {
733
						url: WT_MODULES_DIR + "googlemap/images/icon8L.png",
734
						size: new google.maps.Size(32, 32),
735
						origin: new google.maps.Point(0, 0),
736
						anchor: new google.maps.Point(28, 28)
737
					};
738
					gicons["8R"]  = {
739
						url: WT_MODULES_DIR + "googlemap/images/icon8R.png",
740
						size: new google.maps.Size(32, 32),
741
						origin: new google.maps.Point(0, 0),
742
						anchor: new google.maps.Point(4, 28)
743
					};
744
					gicons["8Ls"] = {
745
						url: WT_MODULES_DIR + "googlemap/images/icon8Ls.png",
746
						size: new google.maps.Size(24, 24),
747
						origin: new google.maps.Point(0, 0),
748
						anchor: new google.maps.Point(22, 22)
749
					};
750
					gicons["8Rs"] = {
751
						url: WT_MODULES_DIR + "googlemap/images/icon8Rs.png",
752
						size: new google.maps.Size(24, 24),
753
						origin: new google.maps.Point(0, 0),
754
						anchor: new google.maps.Point(2, 22)
755
					};
756
					// / A function to create the marker and set up the event window
757
					function createMarker(point, name, html, mhtml, icontype) {
758
						// Create a marker with the requested icon
759
						var marker = new google.maps.Marker({
760
							icon:     gicons[icontype],
761
							map:      pm_map,
762
							position: point,
763
							id:       index,
764
							zIndex:   0
765
						});
766
						google.maps.event.addListener(marker, "click", function() {
767
							infowindow.close();
768
							infowindow.setContent(mhtml);
769
							infowindow.open(pm_map, marker);
770
							var el = $(".gm-ancestor[data-marker=" + marker.id + "]");
771
							if(el.hasClass("person_box")) {
772
								el
773
									.removeClass("person_box")
774
									.addClass("gm-ancestor-visited");
775
								infowindow.close();
776
							} else {
777
								el
778
									.addClass("person_box")
779
									.removeClass("gm-ancestor-visited");
780
							}
781
							var anchor = infowindow.getAnchor();
782
							lastlinkid = anchor ? anchor.id : null;
783
						});
784
						// save the info we need to use later for the side bar
785
						gmarkers[index] = marker;
786
						gm_ancestors_html += "<div data-marker =" + index++ + " class=\"gm-ancestor\">" + html +"</div>";
787
788
						return marker;
789
					}
790
					// create the map
791
					var myOptions = {
792
					  gestureHandling:          'cooperative',
793
						zoom:                     6,
794
						minZoom:                  <?= $this->getPreference('GM_MIN_ZOOM', self::GM_MIN_ZOOM_DEFAULT) ?>,
795
						maxZoom:                  <?= $this->getPreference('GM_MAX_ZOOM', self::GM_MAX_ZOOM_DEFAULT) ?>,
796
						center:                   new google.maps.LatLng(0, 0),
797
						mapTypeId:                google.maps.MapTypeId.TERRAIN,  // ROADMAP, SATELLITE, HYBRID, TERRAIN
798
						mapTypeControlOptions:    {
799
							style: google.maps.MapTypeControlStyle.DROPDOWN_MENU  // DEFAULT, DROPDOWN_MENU, HORIZONTAL_BAR
800
						},
801
						navigationControlOptions: {
802
							position: google.maps.ControlPosition.TOP_RIGHT,  // BOTTOM, BOTTOM_LEFT, LEFT, TOP, etc
803
							style:    google.maps.NavigationControlStyle.SMALL   // ANDROID, DEFAULT, SMALL, ZOOM_PAN
804
						},
805
						scrollwheel:              true
806
					};
807
					var pm_map = new google.maps.Map(document.querySelector(".gm-map"), myOptions);
808
					google.maps.event.addListener(pm_map, "click", function() {
809
						$(".gm-ancestor.person_box")
810
							.removeClass("person_box")
811
							.addClass("gm-ancestor-visited");
812
						infowindow.close();
813
						lastlinkid = null;
814
					});
815
					// create the map bounds
816
					var bounds = new google.maps.LatLngBounds();
817
					<?php
818
					// add the points
819
					$curgen       = 1;
820
					$count        = 0;
821
					$colored_line = [
822
						'1' => '#FF0000',
823
						'2' => '#0000FF',
824
						'3' => '#00FF00',
825
						'4' => '#FFFF00',
826
						'5' => '#00FFFF',
827
						'6' => '#FF00FF',
828
						'7' => '#C0C0FF',
829
						'8' => '#808000',
830
					];
831
					$lat        = [];
832
					$lon        = [];
833
					$latlongval = [];
834
					for ($i = 0; $i < $this->treesize; $i++) {
835
						// moved up to grab the sex of the individuals
836
						$person = $this->ancestors[$i];
837
						if ($person) {
838
							$name = $person->getFullName();
839
840
							// -- check to see if we have moved to the next generation
841
							if ($i + 1 >= pow(2, $curgen)) {
842
								$curgen++;
843
							}
844
845
							$relationship = FunctionsCharts::getSosaName($i + 1);
846
847
							// get thumbnail image
848
							if ($person->getTree()->getPreference('SHOW_HIGHLIGHT_IMAGES')) {
849
								$image = $person->displayImage(40, 50, 'crop', []);
850
							} else {
851
								$image = '';
852
							}
853
854
							$event = '<img src="' . WT_MODULES_DIR . 'googlemap/images/sq' . $curgen . '.png" width="10" height="10"> ';
855
							$event .= '<strong>' . $relationship . '</strong>';
856
857
							$birth = $person->getFirstFact('BIRT');
858
							$data  = addslashes($image . '<div class="gm-ancestor-link">' . $event . ' <span><a href="' . e($person->url()) . '">' . $name . '</a></span>');
859
							$data .= $birth ? addslashes($birth->summary()) : '';
860
							$data .= '</div>';
861
862
							$latlongval[$i] = $this->getLatitudeAndLongitudeFromPlaceLocation($person->getBirthPlace()->getGedcomName());
863
							if ($latlongval[$i]) {
864
								$lat[$i] = (float) strtr($latlongval[$i]->pl_lati, ['N' => '', 'S' => '-', ',' => '.']);
865
								$lon[$i] = (float) strtr($latlongval[$i]->pl_long, ['E' => '', 'W' => '-', ',' => '.']);
866
								if ($lat[$i] || $lon[$i]) {
867
									$marker_number = $curgen;
868
									$dups          = 0;
869
									for ($k = 0; $k < $i; $k++) {
870
										if ($latlongval[$i] == $latlongval[$k]) {
871
											$dups++;
872
											switch ($dups) {
873
												case 1:
874
													$marker_number = $curgen . 'L';
875
													break;
876
												case 2:
877
													$marker_number = $curgen . 'R';
878
													break;
879
												case 3:
880
													$marker_number = $curgen . 'Ls';
881
													break;
882
												case 4:
883
													$marker_number = $curgen . 'Rs';
884
													break;
885
												case 5: //adjust position where markers have same coodinates
886
												default:
887
													$marker_number = $curgen;
888
													$lon[$i] += 0.0025;
889
													$lat[$i] += 0.0025;
890
													break;
891
											}
892
										}
893
									}
894
895
									?>
896
									var point = new google.maps.LatLng(<?= $lat[$i] ?>, <?= $lon[$i] ?>);
897
									var marker = createMarker(point, "<?= addslashes($name) ?>", "<?= $data ?>", "<div class=\"gm-info-window\"><?= $data ?></div>", "<?= $marker_number ?>");
898
									<?php
899
									// Construct the polygon lines
900
									$to_child = (intval(($i - 1) / 2)); // Draw a line from parent to child
901
									if (array_key_exists($to_child, $lat) && $lat[$to_child] != 0 && $lon[$to_child] != 0) {
902
										?>
903
										var linecolor;
904
										var plines;
905
										var lines = [
906
											new google.maps.LatLng(<?= $lat[$i] ?>, <?= $lon[$i] ?>),
907
											new google.maps.LatLng(<?= $lat[$to_child] ?>, <?= $lon[$to_child] ?>)
908
										];
909
										linecolor = "<?= $colored_line[$curgen] ?>";
910
										plines = new google.maps.Polygon({
911
											paths: lines,
912
											strokeColor: linecolor,
913
											strokeOpacity: 0.8,
914
											strokeWeight: 3,
915
											fillColor: "#FF0000",
916
											fillOpacity: 0.1
917
										});
918
										plines.setMap(pm_map);
919
										<?php
920
									}
921
									// Extend and fit marker bounds
922
923
									?>
924
									bounds.extend(point);
925
									<?php
926
									$count++;
927
								}
928
							}
929
						} else {
930
							$latlongval[$i] = null;
931
						}
932
					}
933
					?>
934
					pm_map.setCenter(bounds.getCenter());
935
					pm_map.fitBounds(bounds);
936
					google.maps.event.addListenerOnce(pm_map, "bounds_changed", function(event) {
937
						var maxZoom = <?= $this->getPreference('GM_MAX_ZOOM', self::GM_MAX_ZOOM_DEFAULT) ?>;
938
						if (this.getZoom() > maxZoom) {
939
							this.setZoom(maxZoom);
940
						}
941
					});
942
943
					// Close the sidebar highlight when the infowindow is closed
944
					google.maps.event.addListener(infowindow, "closeclick", function() {
945
						$(".gm-ancestor[data-marker=" + lastlinkid + "]").toggleClass("gm-ancestor-visited person_box");
946
						lastlinkid = null;
947
					});
948
					// put the assembled gm_ancestors_html contents into the gm-ancestors div
949
					document.querySelector(".gm-ancestors").innerHTML = gm_ancestors_html;
950
951
					$(".gm-ancestor-link")
952
						.on("click", "a", function(e) {
953
							e.stopPropagation();
954
						})
955
						.on("click", function(e) {
956
							if (lastlinkid !== null) {
957
								$(".gm-ancestor[data-marker=" + lastlinkid + "]").toggleClass("person_box gm-ancestor-visited");
958
							}
959
							var target = $(this).closest(".gm-ancestor").data("marker");
960
							google.maps.event.trigger(gmarkers[target], "click");
961
						});
962
				}
963
			</script>
964
			<script src="<?= $this->googleMapsScript() ?>&amp;callback=initialiZePedigreeMap"></script>
965
			<?php
966
967
			return;
968
		}
969
970
		$controller
971
			->setPageTitle(/* I18N: %s is an individual’s name */ I18N::translate('Pedigree map of %s', $controller->root->getFullName()))
972
			/* prepending the module css in the page head allows the theme to over-ride it*/
973
			->addInlineJavascript('$("head").prepend(\'<link type="text/css" href ="' . WT_MODULES_DIR . 'googlemap/css/wt_v3_googlemap.css" rel="stylesheet">\');')
974
			->pageHeader();
975
?>
976
977
	<div id="pedigreemap-page">
978
		<h2><?= $controller->getPageTitle() ?></h2>
979
980
		<form class="wt-page-options wt-page-options-pedigree-map d-print-none">
981
			<input type="hidden" name="ged" value="<?= $WT_TREE->getNameHtml() ?>">
982
			<input type="hidden" name="mod" value="googlemap">
983
			<input type="hidden" name="mod_action" value="pedigree_map">
984
985
			<div class="row form-group">
986
				<label class="col-sm-3 col-form-label wt-page-options-label" for="rootid">
987
					<?= I18N::translate('Individual') ?>
988
				</label>
989
				<div class="col-sm-9 wt-page-options-value">
990
					<?= FunctionsEdit::formControlIndividual($controller->root, ['id' => 'rootid', 'name' => 'rootid']) ?>
991
				</div>
992
			</div>
993
994
			<div class="row form-group">
995
				<label class="col-sm-3 col-form-label wt-page-options-label" for="PEDIGREE_GENERATIONS">
996
					<?= I18N::translate('Generations') ?>
997
				</label>
998
				<div class="col-sm-9 wt-page-options-value">
999
					<?= Bootstrap4::select(FunctionsEdit::numericOptions(range(2, $WT_TREE->getPreference('MAX_PEDIGREE_GENERATIONS'))), $generations, ['id' => 'PEDIGREE_GENERATIONS', 'name' => 'PEDIGREE_GENERATIONS']) ?>
1000
				</div>
1001
			</div>
1002
1003
			<div class="row form-group">
1004
				<div class="col-sm-3 wt-page-options-label"></div>
1005
				<div class="col-sm-9 wt-page-options-value">
1006
					<input class="btn btn-primary" type="submit" value="<?= /* I18N: A button label. */ I18N::translate('view') ?>">
1007
				</div>
1008
			</div>
1009
		</form>
1010
1011
		<div class="wt-ajax-load wt-page-content"></div>
1012
		<script>
1013
			document.addEventListener("DOMContentLoaded", function () {
1014
		    $(".wt-page-content").load(location.search + "&ajax=1");
1015
			});
1016
		</script>
1017
		<?php
1018
	}
1019
1020
	/**
1021
	 * Does an individual (or their spouse-families) have any facts with places?
1022
	 *
1023
	 * @param Individual $individual
1024
	 *
1025
	 * @return bool
1026
	 */
1027
	private function checkMapData(Individual $individual) {
1028
		$statement = Database::prepare(
1029
			"SELECT COUNT(*) FROM `##placelinks` WHERE pl_gid = :xref AND pl_file = :tree_id"
1030
		);
1031
		$args = [
1032
			'xref'    => $individual->getXref(),
1033
			'tree_id' => $individual->getTree()->getTreeId(),
1034
		];
1035
1036
		if ($statement->execute($args)->fetchOne()) {
1037
			return true;
1038
		}
1039
1040
		foreach ($individual->getSpouseFamilies() as $family) {
1041
			$args['xref'] = $family->getXref();
1042
			if ($statement->execute($args)->fetchOne()) {
1043
				return true;
1044
			}
1045
		}
1046
1047
		return false;
1048
	}
1049
1050
	/**
1051
	 * Remove prefixes from a place name to allow it to be matched.
1052
	 *
1053
	 * @param string   $prefix_list
1054
	 * @param string   $place
1055
	 * @param string[] $placelist
1056
	 *
1057
	 * @return string[]
1058
	 */
1059
	private function removePrefixFromPlaceName($prefix_list, $place, $placelist) {
1060
		if ($prefix_list) {
1061
			foreach (explode(';', $prefix_list) as $prefix) {
1062
				if ($prefix && substr($place, 0, strlen($prefix) + 1) == $prefix . ' ') {
1063
					$placelist[] = substr($place, strlen($prefix) + 1);
1064
				}
1065
			}
1066
		}
1067
1068
		return $placelist;
1069
	}
1070
1071
	/**
1072
	 * Remove suffixes from a place name to allow it to be matched.
1073
	 *
1074
	 * @param string   $suffix_list
1075
	 * @param string   $place
1076
	 * @param string[] $placelist
1077
	 *
1078
	 * @return string[]
1079
	 */
1080
	private function removeSuffixFromPlaceName($suffix_list, $place, $placelist) {
1081
		if ($suffix_list) {
1082
			foreach (explode(';', $suffix_list) as $postfix) {
1083
				if ($postfix && substr($place, -strlen($postfix) - 1) == ' ' . $postfix) {
1084
					$placelist[] = substr($place, 0, strlen($place) - strlen($postfix) - 1);
1085
				}
1086
			}
1087
		}
1088
1089
		return $placelist;
1090
	}
1091
1092
	/**
1093
	 * Remove prefixes and sufixes to allow place names to be matched.
1094
	 *
1095
	 * @param string   $prefix_list
1096
	 * @param string   $suffix_list
1097
	 * @param string   $place
1098
	 * @param string[] $placelist
1099
	 *
1100
	 * @return string[]
1101
	 */
1102
	private function removePrefixAndSuffixFromPlaceName($prefix_list, $suffix_list, $place, $placelist) {
1103
		if ($prefix_list && $suffix_list) {
1104
			foreach (explode(';', $prefix_list) as $prefix) {
1105
				foreach (explode(';', $suffix_list) as $postfix) {
1106
					if ($prefix && $postfix && substr($place, 0, strlen($prefix) + 1) == $prefix . ' ' && substr($place, -strlen($postfix) - 1) == ' ' . $postfix) {
1107
						$placelist[] = substr($place, strlen($prefix) + 1, strlen($place) - strlen($prefix) - strlen($postfix) - 2);
1108
					}
1109
				}
1110
			}
1111
		}
1112
1113
		return $placelist;
1114
	}
1115
1116
	/**
1117
	 * Match placenames with different prefixes and suffixes.
1118
	 *
1119
	 * @param string $placename
1120
	 * @param int    $level
1121
	 *
1122
	 * @return string[]
1123
	 */
1124
	private function createPossiblePlaceNames($placename, $level) {
1125
		$retlist = [];
1126
		if ($level <= 9) {
1127
			$retlist = $this->removePrefixAndSuffixFromPlaceName($this->getPreference('GM_PREFIX_' . $level), $this->getPreference('GM_POSTFIX_' . $level), $placename, $retlist); // Remove both
1128
			$retlist = $this->removePrefixFromPlaceName($this->getPreference('GM_PREFIX_' . $level), $placename, $retlist); // Remove prefix
1129
			$retlist = $this->removeSuffixFromPlaceName($this->getPreference('GM_POSTFIX_' . $level), $placename, $retlist); // Remove suffix
1130
		}
1131
		$retlist[] = $placename; // Exact
1132
1133
		return $retlist;
1134
	}
1135
1136
	/**
1137
	 * Get the map co-ordinates of a place.
1138
	 *
1139
	 * @param string $place
1140
	 *
1141
	 * @return null|\stdClass
1142
	 */
1143
	private function getLatitudeAndLongitudeFromPlaceLocation($place) {
1144
		$parent     = explode(',', $place);
1145
		$parent     = array_reverse($parent);
1146
		$place_id   = 0;
1147
		$num_parent = count($parent);
1148
		for ($i = 0; $i < $num_parent; $i++) {
1149
			$parent[$i] = trim($parent[$i]);
1150
			if (empty($parent[$i])) {
1151
				$parent[$i] = 'unknown'; // GoogleMap module uses "unknown" while GEDCOM uses , ,
1152
			}
1153
			$placelist = $this->createPossiblePlaceNames($parent[$i], $i + 1);
1154
			foreach ($placelist as $placename) {
1155
				$pl_id = Database::prepare(
1156
					"SELECT pl_id FROM `##placelocation` WHERE pl_level=? AND pl_parent_id=? AND pl_place LIKE ? ORDER BY pl_place"
1157
				)->execute([$i, $place_id, $placename])->fetchOne();
1158
				if (!empty($pl_id)) {
1159
					break;
1160
				}
1161
			}
1162
			if (empty($pl_id)) {
1163
				break;
1164
			}
1165
			$place_id = $pl_id;
1166
		}
1167
1168
		return Database::prepare(
1169
			"SELECT pl_lati, pl_long, pl_zoom, pl_icon, pl_level" .
1170
			" FROM `##placelocation`" .
1171
			" WHERE pl_id = ?" .
1172
			" ORDER BY pl_place"
1173
		)->execute([$place_id])->fetchOneRow();
1174
	}
1175
1176
	/**
1177
	 * @param Fact $fact
1178
	 *
1179
	 * @return array
1180
	 */
1181
	private function getPlaceData(Fact $fact) {
1182
		$result = [];
1183
1184
		$has_latitude  = preg_match('/\n4 LATI (.+)/', $fact->getGedcom(), $match1);
1185
		$has_longitude = preg_match('/\n4 LONG (.+)/', $fact->getGedcom(), $match2);
1186
1187
		// If co-ordinates are stored in the GEDCOM then use them
1188
		if ($has_latitude && $has_longitude) {
1189
			$result = [
1190
				'index'   => 'ID' . $match1[1] . $match2[1],
1191
				'mapdata' => [
1192
					'class'   => 'optionbox',
1193
					'place'   => $fact->getPlace()->getFullName(),
1194
					'tooltip' => $fact->getPlace()->getGedcomName(),
1195
					'lat'     => strtr($match1[1], ['N' => '', 'S' => '-', ',' => '.']),
1196
					'lng'     => strtr($match2[1], ['E' => '', 'W' => '-', ',' => '.']),
1197
					'pl_icon' => '',
1198
					'pl_zoom' => '0',
1199
					'events'  => '',
1200
				],
1201
			];
1202
		} else {
1203
			$place_location = $this->getLatitudeAndLongitudeFromPlaceLocation($fact->getPlace()->getGedcomName());
1204
			if ($place_location && $place_location->pl_lati && $place_location->pl_long) {
1205
				$result = [
1206
					'index'   => 'ID' . $place_location->pl_lati . $place_location->pl_long,
1207
					'mapdata' => [
1208
						'class'   => 'optionbox',
1209
						'place'   => $fact->getPlace()->getFullName(),
1210
						'tooltip' => $fact->getPlace()->getGedcomName(),
1211
						'lat'     => strtr($place_location->pl_lati, ['N' => '', 'S' => '-', ',' => '.']),
1212
						'lng'     => strtr($place_location->pl_long, ['E' => '', 'W' => '-', ',' => '.']),
1213
						'pl_icon' => $place_location->pl_icon,
1214
						'pl_zoom' => $place_location->pl_zoom,
1215
						'events'  => '',
1216
					],
1217
				];
1218
			}
1219
		}
1220
1221
		return $result;
1222
	}
1223
1224
	/**
1225
	 * Build a map for an individual.
1226
	 *
1227
	 * @param Individual $indi
1228
	 *
1229
	 * @return string
1230
	 */
1231
	private function buildIndividualMap(Individual $indi) {
1232
		$GM_MAX_ZOOM = $this->getPreference('GM_MAX_ZOOM', self::GM_MAX_ZOOM_DEFAULT);
1233
		$facts       = $indi->getFacts();
1234
		foreach ($indi->getSpouseFamilies() as $family) {
1235
			$facts = array_merge($facts, $family->getFacts());
1236
			// Add birth of children from this family to the facts array
1237
			foreach ($family->getChildren() as $child) {
1238
				foreach ($child->getFacts(WT_EVENTS_BIRT, true) as $fact) {
1239
					if ($fact->getPlace() !== null) {
1240
						$facts[] = $fact;
1241
						break;
1242
					}
1243
				}
1244
			}
1245
		}
1246
1247
		Functions::sortFacts($facts);
1248
1249
		// At this point we have an array of valid sorted facts
1250
		// so now build the data structures needed for the map display
1251
		$events        = [];
1252
		$unique_places = [];
1253
1254
		foreach ($facts as $fact) {
1255
			$place_data = $this->getPlaceData($fact);
1256
1257
			if (!empty($place_data)) {
1258
				$index = $place_data['index'];
1259
1260
				if ($place_data['mapdata']['pl_zoom']) {
1261
					$GM_MAX_ZOOM = min($GM_MAX_ZOOM, $place_data['mapdata']['pl_zoom']);
1262
				}
1263
				// Produce the html for the sidebar
1264
				$parent = $fact->getParent();
1265
				if ($parent instanceof Individual && $parent->getXref() !== $indi->getXref()) {
1266
					// Childs birth
1267
					$name   = '<a href="' . e($parent->url()) . '">' . $parent->getFullName() . '</a>';
1268
					$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')]);
1269
					$class  = 'wt-gender-' . $parent->getSex();
1270
					$evtStr = '<div class="gm-event">' . $label . '<div><strong>' . $name . '</strong></div>' . $fact->getDate()->display(true) . '</div>';
1271
				} else {
1272
					$spouse = $parent instanceof Family ? $parent->getSpouse($indi) : null;
1273
					$name   = $spouse ? '<a href="' . e($spouse->url()) . '">' . $spouse->getFullName() . '</a>' : '';
1274
					$label  = $fact->getLabel();
1275
					$class  = '';
1276
					if ($fact->getValue() && $spouse) {
1277
						$evtStr = '<div class="gm-event">' . $label . '<div>' . $fact->getValue() . '</div><strong>' . $name . '</strong>' . $fact->getDate()->display(true) . '</div>';
1278
					} elseif ($spouse) {
1279
						$evtStr = '<div class="gm-event">' . $label . '<div><strong>' . $name . '</strong></div>' . $fact->getDate()->display(true) . '</div>';
1280
					} elseif ($fact->getValue()) {
1281
						$evtStr = '<div class="gm-event">' . $label . '<div> ' . $fact->getValue() . '</div>' . $fact->getDate()->display(true) . '</div>';
1282
					} else {
1283
						$evtStr = '<div class="gm-event">' . $label . '<div>' . $fact->getDate()->display(true) . '</div></div>';
1284
					}
1285
				}
1286
1287
				if (empty($unique_places[$index])) {
1288
					$unique_places[$index] = $place_data['mapdata'];
1289
				}
1290
				$unique_places[$index]['events'] .= $evtStr;
1291
				$events[] = [
1292
					'class'      => $class,
1293
					'fact_label' => $label,
1294
					'date'       => $fact->getDate()->display(true),
1295
					'info'       => $fact->getValue(),
1296
					'name'       => $name,
1297
					'place'      => '<a href="' . $fact->getPlace()->getURL() . '">' . $fact->getPlace()->getFullName() . '</a>',
1298
					'placeid'    => $index,
1299
				];
1300
			}
1301
		}
1302
1303
		if (!empty($events)) {
1304
			$places = array_keys($unique_places);
1305
			ob_start();
1306
			// Create the normal googlemap sidebar of events and children
1307
			echo '<div class="gm-events">';
1308
			echo '<table class="wt-facts-table">';
1309
			echo '<caption class="sr-only">' . I18N::translate('Facts and events') . '</caption>';
1310
			echo '<tbody>';
1311
1312
			foreach ($events as $event) {
1313
				$index = array_search($event['placeid'], $places);
1314
				echo '<tr class="', $event['class'], '">';
1315
				echo '<th scope="row">';
1316
				echo '<a href="#" onclick="return openInfowindow(\'', $index, '\')">';
1317
				echo $event['fact_label'];
1318
				echo '</a>';
1319
				echo '</th>';
1320
				echo '<td>';
1321
				if ($event['info']) {
1322
					echo '<div><span class="field">', e($event['info']), '</span></div>';
1323
				}
1324
				if ($event['name']) {
1325
					echo '<div>', $event['name'], '</div>';
1326
				}
1327
				echo '<div>', $event['place'], '</div>';
1328
				if ($event['date']) {
1329
					echo '<div>', $event['date'], '</div>';
1330
				}
1331
				echo '</td>';
1332
				echo '</tr>';
1333
			}
1334
1335
			echo '</tbody>';
1336
			echo '</table>';
1337
			echo '</div>';
1338
			?>
1339
1340
			<script>
1341
				var gmarkers   = [];
1342
				var infowindow;
1343
1344
				// Opens Marker infowindow when corresponding Sidebar item is clicked
1345
				function openInfowindow(i) {
1346
					infowindow.close();
1347
					google.maps.event.trigger(gmarkers[i], 'click');
1348
					return false;
1349
				}
1350
1351
				function loadMap() {
1352
				var map_center = new google.maps.LatLng(0, 0);
1353
				var gicons     = [];
1354
				var map        = null;
1355
1356
				infowindow = new google.maps.InfoWindow({});
1357
1358
				gicons["red"] = {
1359
					url:    "https://maps.google.com/mapfiles/marker.png",
1360
					size:   google.maps.Size(20, 34),
1361
					origin: google.maps.Point(0, 0),
1362
					anchor: google.maps.Point(9, 34)
1363
				};
1364
1365
				function getMarkerImage(iconColor) {
1366
					if (typeof(iconColor) === 'undefined' || iconColor === null) {
1367
						iconColor = 'red';
1368
					}
1369
					if (!gicons[iconColor]) {
1370
						gicons[iconColor] = {
1371
							url:    '//maps.google.com/mapfiles/marker' + iconColor + '.png',
1372
							size:   new google.maps.Size(20, 34),
1373
							origin: new google.maps.Point(0, 0),
1374
							anchor: google.maps.Point(9, 34)
1375
						};
1376
					}
1377
					return gicons[iconColor];
1378
				}
1379
1380
				var placer   = null;
1381
1382
				// A function to create the marker and set up the event window
1383
				function createMarker(latlng, html, tooltip, marker_icon) {
1384
					// Use flag icon (if defined) instead of regular marker icon
1385
					if (marker_icon) {
1386
						var icon_image = {
1387
							url:    WT_MODULES_DIR + 'googlemap/' + marker_icon,
1388
							size:   new google.maps.Size(25, 15),
1389
							origin: new google.maps.Point(0, 0),
1390
							anchor: new google.maps.Point(12, 15)
1391
						};
1392
					} else {
1393
						var icon_image = getMarkerImage('red');
1394
					}
1395
1396
					placer = latlng;
1397
1398
					// Define the marker
1399
					var marker = new google.maps.Marker({
1400
						position: placer,
1401
						icon:     icon_image,
1402
						map:      map,
1403
						title:    tooltip,
1404
						zIndex:   Math.round(latlng.lat() * -100000) << 5
1405
					});
1406
1407
					// Store the tab and event info as marker properties
1408
					gmarkers.push(marker);
1409
1410
					// Open infowindow when marker is clicked
1411
					google.maps.event.addListener(marker, 'click', function() {
1412
						infowindow.close();
1413
						infowindow.setContent(html);
1414
						infowindow.open(map, marker);
1415
					});
1416
				}
1417
1418
					// Create the map and mapOptions
1419
					var mapOptions = {
1420
						zoom:                     7,
1421
						minZoom:                  <?= $this->getPreference('GM_MIN_ZOOM', self::GM_MIN_ZOOM_DEFAULT) ?>,
1422
						maxZoom:                  <?= $this->getPreference('GM_MAX_ZOOM', self::GM_MAX_ZOOM_DEFAULT) ?>,
1423
						center:                   map_center,
1424
						mapTypeId:                google.maps.MapTypeId.ROADMAP,
1425
						mapTypeControlOptions:    {
1426
							style: google.maps.MapTypeControlStyle.DROPDOWN_MENU  // DEFAULT, DROPDOWN_MENU, HORIZONTAL_BAR
1427
						},
1428
						navigationControl:        true,
1429
						navigationControlOptions: {
1430
							position: google.maps.ControlPosition.TOP_RIGHT,  // BOTTOM, BOTTOM_LEFT, LEFT, TOP, etc
1431
							style:    google.maps.NavigationControlStyle.SMALL  // ANDROID, DEFAULT, SMALL, ZOOM_PAN
1432
						},
1433
						scrollwheel:              true
1434
					};
1435
					map = new google.maps.Map(document.querySelector('.gm-map'), mapOptions);
1436
1437
					// Close any infowindow when map is clicked
1438
					google.maps.event.addListener(map, 'click', function() {
1439
						infowindow.close();
1440
					});
1441
1442
					// Add the markers to the map
1443
1444
					// Group the markers by location
1445
					var locations = <?= json_encode($unique_places) ?>;
1446
1447
					// Set the Marker bounds
1448
					var bounds = new google.maps.LatLngBounds();
1449
					var zoomLevel = <?= $GM_MAX_ZOOM ?>;
1450
1451
					jQuery.each(locations, function(index, location) {
1452
						var point = new google.maps.LatLng(location.lat, location.lng); // Place Latitude, Longitude
1453
						var html  =
1454
							'<div class="gm-info-window">' +
1455
							'<div class="gm-info-window-header">' + location.place + '</div>' +
1456
							'<ul class="gm-tabs">' +
1457
							'<li class="gm-tab gm-tab-active" id="gm-tab-events"><a href="#"><?= I18N::translate('Events') ?></a></li>' +
1458
							'</ul>' +
1459
							'<div class="gm-panes">' +
1460
							'<div class="gm-pane" id="gm-pane-events">' + location.events + '</div>' +
1461
							'</div>' +
1462
							'</div>';
1463
1464
						createMarker(point, html, location.tooltip, location.pl_icon);
1465
						bounds.extend(point);
1466
					}); // end loop through location markers
1467
1468
					map.setCenter(bounds.getCenter());
1469
					map.fitBounds(bounds);
1470
					google.maps.event.addListenerOnce(map, "bounds_changed", function(event) {
1471
						if (this.getZoom() > zoomLevel) {
1472
							this.setZoom(zoomLevel);
1473
						}
1474
					});
1475
				} // end loadMap()
1476
1477
			</script>
1478
			<?php
1479
			$html = ob_get_clean();
1480
		} else {
1481
			$html = '';
1482
		}
1483
1484
		return $html;
1485
	}
1486
1487
	/**
1488
	 * Get the Location ID.
1489
	 *
1490
	 * @param string $place
1491
	 *
1492
	 * @return int
1493
	 */
1494
	private function getPlaceLocationId($place) {
1495
		$par      = explode(',', $place);
1496
		$par      = array_reverse($par);
1497
		$place_id = 0;
1498
		$pl_id    = 0;
1499
		$num_par  = count($par);
1500
		for ($i = 0; $i < $num_par; $i++) {
1501
			$par[$i] = trim($par[$i]);
1502
			if (empty($par[$i])) {
1503
				$par[$i] = 'unknown';
1504
			}
1505
			$placelist = $this->createPossiblePlaceNames($par[$i], $i + 1);
1506
			foreach ($placelist as $key => $placename) {
1507
				$pl_id = (int) Database::prepare(
1508
					"SELECT pl_id FROM `##placelocation` WHERE pl_level = :level AND pl_parent_id = :parent_id AND pl_place LIKE :placename"
1509
				)->execute([
1510
					'level'     => $i,
1511
					'parent_id' => $place_id,
1512
					'placename' => $placename,
1513
				])->fetchOne();
1514
				if ($pl_id) {
1515
					break;
1516
				}
1517
			}
1518
			if (!$pl_id) {
1519
				break;
1520
			}
1521
			$place_id = $pl_id;
1522
		}
1523
1524
		return $place_id;
1525
	}
1526
1527
	/**
1528
	 * Get the place ID.
1529
	 *
1530
	 * @param string $place
1531
	 *
1532
	 * @return int
1533
	 */
1534
	private function getPlaceId($place) {
1535
		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...
1536
1537
		$par      = explode(',', $place);
1538
		$par      = array_reverse($par);
1539
		$place_id = 0;
1540
		$pl_id    = 0;
1541
		$num_par  = count($par);
1542
		for ($i = 0; $i < $num_par; $i++) {
1543
			$par[$i]   = trim($par[$i]);
1544
			$placelist = $this->createPossiblePlaceNames($par[$i], $i + 1);
1545
			foreach ($placelist as $placename) {
1546
				$pl_id = (int) Database::prepare(
1547
					"SELECT p_id FROM `##places` WHERE p_parent_id = :place_id AND p_file = :tree_id AND p_place = :placename"
1548
				)->execute([
1549
					'place_id'  => $place_id,
1550
					'tree_id'   => $WT_TREE->getTreeId(),
1551
					'placename' => $placename,
1552
				])->fetchOne();
1553
				if ($pl_id) {
1554
					break;
1555
				}
1556
			}
1557
			if (!$pl_id) {
1558
				break;
1559
			}
1560
			$place_id = $pl_id;
1561
		}
1562
1563
		return $place_id;
1564
	}
1565
1566
	/**
1567
	 * Set the place IDs.
1568
	 *
1569
	 * @param int      $level
1570
	 * @param string[] $parent
1571
	 *
1572
	 * @return int
1573
	 */
1574
	private function setPlaceIdMap($level, $parent) {
1575
		$fullplace = '';
1576
		if ($level == 0) {
1577
			return 0;
1578
		} else {
1579
			for ($i = 1; $i <= $level; $i++) {
1580
				$fullplace .= $parent[$level - $i] . ', ';
1581
			}
1582
			$fullplace = substr($fullplace, 0, -2);
1583
1584
			return $this->getPlaceId($fullplace);
1585
		}
1586
	}
1587
1588
	/**
1589
	 * Set the map level.
1590
	 *
1591
	 * @param int      $level
1592
	 * @param string[] $parent
1593
	 *
1594
	 * @return int
1595
	 */
1596
	private function setLevelMap($level, $parent) {
1597
		$fullplace = '';
1598
		if ($level == 0) {
1599
			return 0;
1600
		} else {
1601
			for ($i = 1; $i <= $level; $i++) {
1602
				if ($parent[$level - $i] != '') {
1603
					$fullplace .= $parent[$level - $i] . ', ';
1604
				} else {
1605
					$fullplace .= 'Unknown, ';
1606
				}
1607
			}
1608
			$fullplace = substr($fullplace, 0, -2);
1609
1610
			return $this->getPlaceLocationId($fullplace);
1611
		}
1612
	}
1613
1614
	/**
1615
	 * Called by placelist.php
1616
	 */
1617
	public function createMap() {
1618
		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...
1619
1620
		Database::updateSchema(self::SCHEMA_MIGRATION_PREFIX, self::SCHEMA_SETTING_NAME, self::SCHEMA_TARGET_VERSION);
1621
1622
		$parent = Filter::getArray('parent');
1623
		$levelm = $this->setLevelMap($level, $parent);
1624
1625
		$latlng =
1626
			Database::prepare("SELECT pl_place, pl_id, pl_lati, pl_long, pl_zoom FROM `##placelocation` WHERE pl_id=?")
1627
			->execute([$levelm])
1628
			->fetch(PDO::FETCH_ASSOC);
1629
1630
		echo '<table style="margin:auto; border-collapse: collapse;">';
1631
		echo '<tr style="vertical-align:top;"><td>';
1632
		echo '<div id="gm-hierarchy-map" class="wt-ajax-load"></div>';
1633
		echo '<script src="', $this->googleMapsScript(), '"></script>';
1634
1635
		$plzoom = $latlng['pl_zoom']; // Map zoom level
1636
1637
		if (Auth::isAdmin()) {
1638
			$adminplaces_url = 'module.php?mod=googlemap&amp;mod_action=admin_places';
1639
			if ($latlng && isset($latlng['pl_id'])) {
1640
				$adminplaces_url .= '&amp;parent=' . $latlng['pl_id'];
1641
			}
1642
			$update_places_url = 'admin_trees_places.php?ged=' . $WT_TREE->getNameHtml() . '&amp;search=' . urlencode(implode(', ', array_reverse($parent)));
1643
			echo '<div class="gm-options">';
1644
			echo '<a href="' . $adminplaces_url . '">' . I18N::translate('Geographic data') . '</a>';
1645
			echo ' | <a href="' . $update_places_url . '">' . I18N::translate('Update place names') . '</a>';
1646
			echo '</div>';
1647
		}
1648
		echo '</td>';
1649
		echo '</tr></table>';
1650
	}
1651
1652
	/**
1653
	 * Print the numbers of individuals.
1654
	 *
1655
	 * @param int      $level
1656
	 * @param string[] $parent
1657
	 */
1658
	private function printHowManyPeople($level, $parent) {
1659
		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...
1660
1661
		$stats = new Stats($WT_TREE);
1662
1663
		$place_count_indi = 0;
1664
		$place_count_fam  = 0;
1665
		if (!isset($parent[$level - 1])) {
1666
			$parent[$level - 1] = '';
1667
		}
1668
		$p_id = $this->setPlaceIdMap($level, $parent);
1669
		$indi = $stats->statsPlaces('INDI', false, $p_id);
1670
		$fam  = $stats->statsPlaces('FAM', false, $p_id);
1671
		foreach ($indi as $place) {
1672
			$place_count_indi = $place['tot'];
1673
		}
1674
		foreach ($fam as $place) {
1675
			$place_count_fam = $place['tot'];
1676
		}
1677
		echo '<br><br>', I18N::translate('Individuals'), ': ', $place_count_indi, ', ', I18N::translate('Families'), ': ', $place_count_fam;
1678
	}
1679
1680
	/**
1681
	 * Print the flags and markers.
1682
	 *
1683
	 * @param stdClass $place2
1684
	 * @param int      $level
1685
	 * @param string[] $parent
1686
	 * @param int      $levelm
1687
	 * @param string   $linklevels
1688
	 */
1689
	private function printGoogleMapMarkers(stdClass $place2, $level, $parent, $levelm, $linklevels) {
1690
		echo 'var icon_url = null;';
1691
		if (!$place2->pl_lati || !$place2->pl_long) {
1692
			echo 'var icon_url ="' . WT_MODULES_DIR . 'googlemap/images/marker_yellow.png";';
1693
			echo 'var point = new google.maps.LatLng(0, 0);';
1694
			echo 'var marker = createMarker(point, "<div style=\"width: 250px;\"><a href=\"?action=find', $linklevels, '&amp;parent[' . $level . ']=';
1695
1696
			if ($place2->pl_place == 'Unknown') {
1697
				echo '\"><br>';
1698
			} else {
1699
				echo addslashes($place2->pl_place), '\"><br>';
1700
			}
1701
			if ($place2->pl_icon !== null && $place2->pl_icon !== '') {
1702
				echo '<img src=\"', WT_MODULES_DIR, 'googlemap/', $place2->pl_icon, '\">&nbsp;&nbsp;';
1703
			}
1704
			if ($place2->pl_place == 'Unknown') {
1705
				echo I18N::translate('unknown');
1706
			} else {
1707
				echo addslashes($place2->pl_place);
1708
			}
1709
			echo '</a>';
1710
			$parent[$level] = $place2->pl_place;
1711
			$this->printHowManyPeople($level + 1, $parent);
1712
			echo '<br>', I18N::translate('This place has no coordinates');
1713
			if (Auth::isAdmin()) {
1714
				echo '<br><a href=\"module.php?mod=googlemap&amp;mod_action=admin_places&amp;parent=', $levelm, '&amp;display=inactive\">', I18N::translate('Geographic data'), '</a>';
1715
			}
1716
			echo '</div>", icon_url, "', str_replace(['&lrm;', '&rlm;'], [WT_UTF8_LRM, WT_UTF8_RLM], addslashes($place2->pl_place)), '");';
1717
		} else {
1718
			$lati = strtr($place2->pl_lati, ['N' => '', 'S' => '-', ',' => '.']);
1719
			$long = strtr($place2->pl_long, ['E' => '', 'W' => '-', ',' => '.']);
1720
			//delete leading zero
1721
			if ($lati >= 0) {
1722
				$lati = abs($lati);
1723
			} elseif ($lati < 0) {
1724
				$lati = '-' . abs($lati);
1725
			}
1726
			if ($long >= 0) {
1727
				$long = abs($long);
1728
			} elseif ($long < 0) {
1729
				$long = '-' . abs($long);
1730
			}
1731
1732
			if ($place2->pl_icon !== null && $place2->pl_icon !== '' && $this->getPreference('GM_PH_MARKER') === 'G_FLAG') {
1733
				echo 'icon_url = "', WT_MODULES_DIR, 'googlemap/', $place2->pl_icon, '";';
1734
			}
1735
			echo 'var point = new google.maps.LatLng(', $lati, ', ', $long, ');';
1736
			echo 'var marker = createMarker(point, "<div style=\"width: 250px;\"><a href=\"?action=find', $linklevels;
1737
			echo '&amp;parent[', $level, ']=';
1738
			if ($place2->pl_place !== 'Unknown') {
1739
				echo rawurlencode($place2->pl_place);
1740
			}
1741
			echo '\"><br>';
1742
			if ($place2->pl_icon !== null && $place2->pl_icon !== '') {
1743
				echo '<img src=\"', WT_MODULES_DIR, 'googlemap/', $place2->pl_icon, '\">&nbsp;&nbsp;';
1744
			}
1745
			if ($place2->pl_place === 'Unknown') {
1746
				echo I18N::translate('unknown');
1747
			} else {
1748
				echo e($place2->pl_place);
1749
			}
1750
			echo '</a>';
1751
			$parent[$level] = $place2->pl_place;
1752
			$this->printHowManyPeople($level + 1, $parent);
1753
			echo '</div>", icon_url, ', json_encode($place2->pl_place), ');';
1754
		}
1755
	}
1756
1757
	/**
1758
	 * Called by placelist.php
1759
	 *
1760
	 * @param int      $numfound
1761
	 * @param int      $level
1762
	 * @param string[] $parent
1763
	 * @param string   $linklevels
1764
	 * @param string[] $place_names
1765
	 */
1766
	public function mapScripts($numfound, $level, $parent, $linklevels, $place_names) {
1767
		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...
1768
1769
		$controller->addInlineJavascript('
1770
			$("head").append(\'<link rel="stylesheet" type="text/css" href="' . WT_MODULES_DIR . 'googlemap/css/wt_v3_googlemap.css" />\');
1771
			var numMarkers = "' . $numfound . '";
1772
			var mapLevel   = "' . $level . '";
1773
			var placezoom  = "' . $plzoom . '";
1774
			var infowindow = new google.maps.InfoWindow({
1775
				// size: new google.maps.Size(150,50),
1776
				// maxWidth: 600
1777
			});
1778
1779
			var map_center = new google.maps.LatLng(0,0);
1780
			var map = "";
1781
			var bounds = new google.maps.LatLngBounds ();
1782
			var markers = [];
1783
			var gmarkers = [];
1784
			var i = 0;
1785
1786
			// Create the map and mapOptions
1787
			var mapOptions = {
1788
				minZoom: ' . $this->getPreference('GM_MIN_ZOOM', self::GM_MIN_ZOOM_DEFAULT) . ',
1789
				maxZoom: ' . $this->getPreference('GM_MAX_ZOOM', self::GM_MAX_ZOOM_DEFAULT) . ',
1790
				zoom: 8,
1791
				center: map_center,
1792
				mapTypeId: google.maps.MapTypeId.ROADMAP,
1793
				mapTypeControlOptions: {
1794
					style: google.maps.MapTypeControlStyle.DROPDOWN_MENU // DEFAULT, DROPDOWN_MENU, HORIZONTAL_BAR
1795
				},
1796
				navigationControl: true,
1797
				navigationControlOptions: {
1798
					position: google.maps.ControlPosition.TOP_RIGHT, // BOTTOM, BOTTOM_LEFT, LEFT, TOP, etc
1799
					style: google.maps.NavigationControlStyle.SMALL  // ANDROID, DEFAULT, SMALL, ZOOM_PAN
1800
				},
1801
				scrollwheel: true
1802
			};
1803
			map = new google.maps.Map(document.getElementById("gm-hierarchy-map"), mapOptions);
1804
1805
			// Close any infowindow when map is clicked
1806
			google.maps.event.addListener(map, "click", function() {
1807
				infowindow.close();
1808
			});
1809
1810
			// If only one marker, set zoom level to that of place in database
1811
			if (mapLevel != 0) {
1812
				var pointZoom = placezoom;
1813
			} else {
1814
				var pointZoom = 1;
1815
			}
1816
1817
			// Creates a marker whose info window displays the given name
1818
			function createMarker(point, html, icon, name) {
1819
				// Choose icon ============
1820
				if (icon && ' . $level . '<=3) {
1821
					if (icon != "' . WT_MODULES_DIR . 'googlemap/images/marker_yellow.png") {
1822
						var iconImage = {
1823
							url:    icon,
1824
							size:   new google.maps.Size(25, 15),
1825
							origin: new google.maps.Point(0,0),
1826
							anchor: new google.maps.Point(12, 15)
1827
						};
1828
					} else {
1829
						var iconImage = {
1830
							url:    icon,
1831
							size:   new google.maps.Size(20, 34),
1832
							origin: new google.maps.Point(0,0),
1833
							anchor: new google.maps.Point(9, 34)
1834
						};
1835
					}
1836
				} else {
1837
					var iconImage = {
1838
						url:    "https://maps.google.com/mapfiles/marker.png",
1839
						size:   new google.maps.Size(20, 34),
1840
						origin: new google.maps.Point(0,0),
1841
						anchor: new google.maps.Point(9, 34)
1842
					};
1843
				}
1844
				var posn = new google.maps.LatLng(0,0);
1845
				var marker = new google.maps.Marker({
1846
					position: point,
1847
					icon: iconImage,
1848
					map: map,
1849
					title: name
1850
				});
1851
				// Show this markers name in the info window when it is clicked
1852
				google.maps.event.addListener(marker, "click", function() {
1853
					infowindow.close();
1854
					infowindow.setContent(html);
1855
					infowindow.open(map, marker);
1856
				});
1857
				// === Store the tab, category and event info as marker properties ===
1858
				marker.mypoint = point;
1859
				marker.mytitle = name;
1860
				marker.myposn = posn;
1861
				gmarkers.push(marker);
1862
				bounds.extend(marker.position);
1863
1864
				// If only one marker use database place zoom level rather than fitBounds of markers
1865
				if (numMarkers > 1) {
1866
					map.fitBounds(bounds);
1867
				} else {
1868
					map.setCenter(bounds.getCenter());
1869
					map.setZoom(parseFloat(pointZoom));
1870
				}
1871
				return marker;
1872
			}
1873
		');
1874
1875
		$levelm = $this->setLevelMap($level, $parent);
1876
1877
		//create markers
1878
		ob_start();
1879
1880
		if ($numfound == 0 && $level > 0) {
1881
			// show the current place on the map
1882
1883
			$place = Database::prepare("SELECT * FROM `##placelocation` WHERE pl_id = ?")
1884
				->execute([$levelm])
1885
				->fetchOneRow();
1886
1887
			if ($place !== null) {
1888
				// re-calculate the hierarchy information required to display the current place
1889
				$thisloc = $parent;
1890
				array_pop($thisloc);
1891
				$thislevel      = $level - 1;
1892
				$thislinklevels = substr($linklevels, 0, strrpos($linklevels, '&amp;'));
1893
1894
				$this->printGoogleMapMarkers($place, $thislevel, $thisloc, $place->pl_id, $thislinklevels);
1895
			}
1896
		}
1897
1898
		// display any sub-places
1899
		$placeidlist = [];
1900
		foreach ($place_names as $placename) {
1901
			$thisloc     = $parent;
1902
			$thisloc[]   = $placename;
1903
			$this_levelm = $this->setLevelMap($level + 1, $thisloc);
1904
			if ($this_levelm) {
1905
				$placeidlist[] = $this_levelm;
1906
			}
1907
		}
1908
1909
		// flip the array (thus removing duplicates)
1910
		$placeidlist = array_flip($placeidlist);
1911
		// remove entry for parent location
1912
		unset($placeidlist[$levelm]);
1913
1914
		if (!empty($placeidlist)) {
1915
			// the keys are all we care about (this reverses the earlier array_flip, and ensures there are no "holes" in the array)
1916
			$placeidlist = array_keys($placeidlist);
1917
			// note: this implode/array_fill code generates one '?' for each entry in the $placeidlist array
1918
			$placelist =
1919
				Database::prepare(
1920
					"SELECT * FROM `##placelocation` WHERE pl_id IN (" . implode(',', array_fill(0, count($placeidlist), '?')) . ')'
1921
				)->execute($placeidlist)
1922
				->fetchAll();
1923
1924
			foreach ($placelist as $place) {
1925
				$this->printGoogleMapMarkers($place, $level, $parent, $place->pl_id, $linklevels);
1926
			}
1927
		}
1928
		$controller->addInlineJavascript(ob_get_clean());
1929
	}
1930
1931
	/**
1932
	 * Take a place id and find its place in the hierarchy
1933
	 * Input: place ID
1934
	 * Output: ordered array of id=>name values, starting with the Top level
1935
	 * e.g. 0=>"Top level", 16=>"England", 19=>"London", 217=>"Westminster"
1936
	 *
1937
	 * @param int $id
1938
	 *
1939
	 * @return string[]
1940
	 */
1941
	private function placeIdToHierarchy($id) {
1942
		$statement = Database::prepare("SELECT pl_parent_id, pl_place FROM `##placelocation` WHERE pl_id=?");
1943
		$arr       = [];
1944
		while ($id != 0) {
1945
			$row = $statement->execute([$id])->fetchOneRow();
1946
			$arr = [$id => $row->pl_place] + $arr;
1947
			$id  = $row->pl_parent_id;
1948
		}
1949
1950
		return $arr;
1951
	}
1952
1953
	/**
1954
	 * Get the highest index.
1955
	 *
1956
	 * @return int
1957
	 */
1958
	private function getHighestIndex() {
1959
		return (int) Database::prepare("SELECT MAX(pl_id) FROM `##placelocation`")->fetchOne();
1960
	}
1961
1962
	/**
1963
	 * Get the highest level.
1964
	 *
1965
	 * @return int
1966
	 */
1967
	private function getHighestLevel() {
1968
		return (int) Database::prepare("SELECT MAX(pl_level) FROM `##placelocation`")->fetchOne();
1969
	}
1970
1971
	/**
1972
	 * Find all of the places in the hierarchy
1973
	 *
1974
	 * NOTE: the "inactive" filter ignores the hierarchy, so that "Paris, France"
1975
	 * will match "Paris, Texas, United States".  A fully accurate match would be slow.
1976
	 *
1977
	 * @param int  $parent_id
1978
	 * @param bool $inactive
1979
	 *
1980
	 * @return array[]
1981
	 */
1982
	private function getPlaceListLocation($parent_id, $inactive = false) {
1983
		if ($inactive) {
1984
			$rows = Database::prepare(
1985
					"SELECT pl_id, pl_parent_id, pl_place, pl_lati, pl_long, pl_zoom, pl_icon" .
1986
					" FROM `##placelocation`" .
1987
					" WHERE pl_parent_id = :parent_id" .
1988
					" ORDER BY pl_place COLLATE :collation"
1989
				)->execute([
1990
					'parent_id' => $parent_id,
1991
					'collation' => I18N::collation(),
1992
				])->fetchAll();
1993
		} else {
1994
			$rows = Database::prepare(
1995
				"SELECT DISTINCT pl_id, pl_parent_id, pl_place, pl_lati, pl_long, pl_zoom, pl_icon" .
1996
				" FROM `##placelocation`" .
1997
				" JOIN `##places` ON `##placelocation`.pl_place = `##places`.p_place" .
1998
				" WHERE pl_parent_id = :parent_id" .
1999
				" ORDER BY pl_place COLLATE :collation"
2000
			)->execute([
2001
				'parent_id' => $parent_id,
2002
				'collation' => I18N::collation(),
2003
			])->fetchAll();
2004
		}
2005
2006
		$placelist = [];
2007
		foreach ($rows as $row) {
2008
			// Find/count places without co-ordinates
2009
			$children =
2010
				Database::prepare(
2011
				"SELECT SQL_CACHE COUNT(*) AS total, SUM(" .
2012
				" p1.pl_place IS NOT NULL AND IFNULL(p1.pl_lati, '') IN ('N0', '') AND IFNULL(p1.pl_long, '') IN ('E0', '') OR " .
2013
				" p2.pl_place IS NOT NULL AND IFNULL(p2.pl_lati, '') IN ('N0', '') AND IFNULL(p2.pl_long, '') IN ('E0', '') OR " .
2014
				" p3.pl_place IS NOT NULL AND IFNULL(p3.pl_lati, '') IN ('N0', '') AND IFNULL(p3.pl_long, '') IN ('E0', '') OR " .
2015
				" p4.pl_place IS NOT NULL AND IFNULL(p4.pl_lati, '') IN ('N0', '') AND IFNULL(p4.pl_long, '') IN ('E0', '') OR " .
2016
				" p5.pl_place IS NOT NULL AND IFNULL(p5.pl_lati, '') IN ('N0', '') AND IFNULL(p5.pl_long, '') IN ('E0', '') OR " .
2017
				" p6.pl_place IS NOT NULL AND IFNULL(p6.pl_lati, '') IN ('N0', '') AND IFNULL(p6.pl_long, '') IN ('E0', '') OR " .
2018
				" p7.pl_place IS NOT NULL AND IFNULL(p7.pl_lati, '') IN ('N0', '') AND IFNULL(p7.pl_long, '') IN ('E0', '') OR " .
2019
				" p8.pl_place IS NOT NULL AND IFNULL(p8.pl_lati, '') IN ('N0', '') AND IFNULL(p8.pl_long, '') IN ('E0', '') OR " .
2020
				" p9.pl_place IS NOT NULL AND IFNULL(p9.pl_lati, '') IN ('N0', '') AND IFNULL(p9.pl_long, '') IN ('E0', '')) AS missing" .
2021
				" FROM      `##placelocation` AS p1" .
2022
				" LEFT JOIN `##placelocation` AS p2 ON (p2.pl_parent_id = p1.pl_id)" .
2023
				" LEFT JOIN `##placelocation` AS p3 ON (p3.pl_parent_id = p2.pl_id)" .
2024
				" LEFT JOIN `##placelocation` AS p4 ON (p4.pl_parent_id = p3.pl_id)" .
2025
				" LEFT JOIN `##placelocation` AS p5 ON (p5.pl_parent_id = p4.pl_id)" .
2026
				" LEFT JOIN `##placelocation` AS p6 ON (p6.pl_parent_id = p5.pl_id)" .
2027
				" LEFT JOIN `##placelocation` AS p7 ON (p7.pl_parent_id = p6.pl_id)" .
2028
				" LEFT JOIN `##placelocation` AS p8 ON (p8.pl_parent_id = p7.pl_id)" .
2029
				" LEFT JOIN `##placelocation` AS p9 ON (p9.pl_parent_id = p8.pl_id)" .
2030
				" WHERE p1.pl_parent_id = :parent_id"
2031
			)
2032
			->execute([
2033
				'parent_id' => $row->pl_id,
2034
			])->fetchOneRow();
2035
2036
			$placelist[] = [
2037
				'place_id'  => (int) $row->pl_id,
2038
				'parent_id' => (int) $row->pl_parent_id,
2039
				'place'     => $row->pl_place,
2040
				'lati'      => $row->pl_lati,
2041
				'long'      => $row->pl_long,
2042
				'zoom'      => (int) $row->pl_zoom,
2043
				'icon'      => $row->pl_icon,
2044
				'is_empty'  => ($row->pl_lati === null || $row->pl_lati === 'N0') && ($row->pl_long === null || $row->pl_long === 'E0'),
2045
				'children'  => (int) $children->total,
2046
				'missing'   => (int) $children->missing,
2047
			];
2048
		}
2049
2050
		return $placelist;
2051
	}
2052
2053
	/**
2054
	 * Set the output level.
2055
	 *
2056
	 * @param int $parent_id
2057
	 */
2058
	private function outputLevel($parent_id) {
2059
		$tmp      = $this->placeIdToHierarchy($parent_id);
2060
		$maxLevel = $this->getHighestLevel();
2061
		if ($maxLevel > 8) {
2062
			$maxLevel = 8;
2063
		}
2064
		$prefix = implode(';', $tmp);
2065
		if ($prefix != '') {
2066
			$prefix .= ';';
2067
		}
2068
		$suffix = str_repeat(';', $maxLevel - count($tmp));
2069
		$level  = count($tmp);
2070
2071
		$rows = Database::prepare(
2072
			"SELECT pl_id, pl_place, pl_long, pl_lati, pl_zoom, pl_icon FROM `##placelocation` WHERE pl_parent_id=? ORDER BY pl_place"
2073
		)->execute([$parent_id])->fetchAll();
2074
2075
		foreach ($rows as $row) {
2076
			echo $level, ';', $prefix, $row->pl_place, $suffix, ';', $row->pl_long, ';', $row->pl_lati, ';', $row->pl_zoom, ';', $row->pl_icon, "\r\n";
2077
			if ($level < $maxLevel) {
2078
				$this->outputLevel($row->pl_id);
2079
			}
2080
		}
2081
	}
2082
2083
	/**
2084
	 * recursively find all of the csv files on the server
2085
	 *
2086
	 * @param string $path
2087
	 *
2088
	 * @return string[]
2089
	 */
2090
	private function findFiles($path) {
2091
		$placefiles = [];
2092
2093
		try {
2094
			$di = new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS);
2095
			$it = new \RecursiveIteratorIterator($di);
2096
2097
			foreach ($it as $file) {
2098
				if ($file->getExtension() == 'csv') {
2099
					$placefiles[] = '/' . $file->getFilename();
2100
				}
2101
			}
2102
		} catch (\Exception $ex) {
2103
		DebugBar::addThrowable($ex);
2104
2105
			Log::addErrorLog(basename($ex->getFile()) . ' - line: ' . $ex->getLine() . ' - ' . $ex->getMessage());
2106
		}
2107
2108
		return $placefiles;
2109
	}
2110
2111
	/**
2112
	 * Show a form with options to upload a CSV file
2113
	 */
2114
	private function adminUploadForm() {
2115
		$parent_id = (int) Filter::get('parent_id');
2116
		$inactive  = (int) Filter::get('inactive');
2117
2118
		$controller = new PageController;
2119
		$controller
2120
			->setPageTitle(I18N::translate('Upload geographic data'))
2121
			->pageHeader();
2122
2123
		echo Bootstrap4::breadcrumbs([
2124
			route('admin-control-panel')                         => I18N::translate('Control panel'),
2125
			route('admin-modules')                               => I18N::translate('Module administration'),
2126
			$this->getConfigLink()                             => $this->getTitle(),
2127
			'module.php?mod=googlemap&mod_action=admin_places' => I18N::translate('Geographic data'),
2128
		], $controller->getPageTitle());
2129
2130
			$placefiles = $this->findFiles(WT_MODULES_DIR . 'googlemap/extra');
2131
			sort($placefiles);
2132
2133
		?>
2134
		<h1><?= $controller->getPageTitle() ?></h1>
2135
2136
		<form method="post" action="module.php?mod=googlemap&amp;mod_action=admin_upload_action" enctype="multipart/form-data">
2137
			<input type="hidden" name="parent_id" value="<?= $parent_id ?>">
2138
			<input type="hidden" name="inactive" value="<?= $inactive ?>">
2139
2140
			<!-- PLACES FILE -->
2141
			<div class="row form-group">
2142
				<label class="col-form-label col-sm-4" for="placesfile">
2143
					<?= I18N::translate('A file on your computer') ?>
2144
				</label>
2145
				<div class="col-sm-8">
2146
					<input id="placesfile" type="file" name="placesfile" class="form-control">
2147
				</div>
2148
			</div>
2149
2150
			<!-- LOCAL FILE -->
2151
			<div class="row form-group">
2152
				<label class="col-form-label col-sm-4" for="localfile">
2153
					<?= I18N::translate('A file on the server') ?>
2154
				</label>
2155
				<div class="col-sm-8">
2156
					<div class="input-group" dir="ltr">
2157
						<div class="input-group-prepend">
2158
							<span class="input-group-text">
2159
								<?= WT_MODULES_DIR . 'googlemap/extra/' ?>
2160
							</span>
2161
						</div>
2162
						<?php
2163
						foreach ($placefiles as $p => $placefile) {
2164
							unset($placefiles[$p]);
2165
							$p = e($placefile);
2166
							if (substr($placefile, 0, 1) == '/') {
2167
								$placefiles[$p] = substr($placefile, 1);
2168
							} else {
2169
								$placefiles[$p] = $placefile;
2170
							}
2171
						}
2172
						echo Bootstrap4::select($placefiles, '', ['id' => 'localfile', ['id' => 'localfile']]);
2173
						?>
2174
					</div>
2175
				</div>
2176
			</div>
2177
2178
			<!-- CLEAR DATABASE -->
2179
			<fieldset class="form-group">
2180
				<div class="row">
2181
					<legend class="col-form-label col-sm-4">
2182
						<?= I18N::translate('Delete all existing geographic data before importing the file.') ?>
2183
					</legend>
2184
					<div class="col-sm-8">
2185
						<?= Bootstrap4::radioButtons('cleardatabase', [I18N::translate('no'), I18N::translate('yes')], '0', true) ?>
2186
					</div>
2187
				</div>
2188
			</fieldset>
2189
2190
			<!-- UPDATE ONLY -->
2191
			<fieldset class="form-group">
2192
2193
				<div class="row">
2194
					<legend class="col-form-label col-sm-4">
2195
						<?= I18N::translate('Do not create new locations, just import coordinates for existing locations.') ?>
2196
					</legend>
2197
					<div class="col-sm-8">
2198
						<?= Bootstrap4::radioButtons('updateonly', [I18N::translate('no'), I18N::translate('yes')], '0', true) ?>
2199
					</div>
2200
				</div>
2201
			</fieldset>
2202
2203
			<!-- OVERWRITE DATA -->
2204
			<fieldset class="form-group">
2205
				<div class="row">
2206
					<legend class="col-form-label col-sm-4">
2207
						<?= I18N::translate('Overwrite existing coordinates.') ?>
2208
					</legend>
2209
					<div class="col-sm-8">
2210
						<?= Bootstrap4::radioButtons('overwritedata', [I18N::translate('no'), I18N::translate('yes')], '0', true) ?>
2211
					</div>
2212
				</div>
2213
			</fieldset>
2214
2215
			<!-- SAVE BUTTON -->
2216
			<div class="row form-group">
2217
				<div class="offset-sm-4 col-sm-8">
2218
					<button type="submit" class="btn btn-primary">
2219
						<i class="fas fa-check"></i>
2220
						<?= I18N::translate('continue') ?>
2221
					</button>
2222
				</div>
2223
			</div>
2224
		</form>
2225
		<?php
2226
	}
2227
2228
	/**
2229
	 * Delete a geographic place.
2230
	 */
2231
	private function adminDeleteAction() {
2232
		$place_id  = (int) Filter::post('place_id');
2233
		$parent_id = (int) Filter::post('parent_id');
2234
		$inactive  = (int) Filter::post('inactive');
2235
2236
		try {
2237
			Database::prepare(
2238
				"DELETE FROM `##placelocation` WHERE pl_id = :place_id"
2239
			)->execute([
2240
				'place_id' => $place_id,
2241
			]);
2242
		} catch (\Exception $ex) {
2243
		DebugBar::addThrowable($ex);
2244
2245
			FlashMessages::addMessage(I18N::translate('Location not removed: this location contains sub-locations'), 'danger');
2246
		}
2247
2248
		header('Location: module.php?mod=googlemap&mod_action=admin_places&parent_id=' . $parent_id . '&inactive=' . $inactive);
2249
	}
2250
2251
	/**
2252
	 * Import places from GEDCOM data.
2253
	 */
2254
	private function adminImportAction() {
2255
	}
2256
2257
	/**
2258
	 * Upload a CSV file.
2259
	 */
2260
	private function adminUploadAction() {
2261
		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...
2262
2263
		$country_names = [];
2264
		$stats         = new Stats($WT_TREE);
2265
		foreach ($stats->iso3166() as $key => $value) {
2266
			$country_names[$key] = I18N::translate($key);
2267
		}
2268
		if (Filter::postBool('cleardatabase')) {
2269
			Database::exec("DELETE FROM `##placelocation` WHERE 1=1");
2270
		}
2271
		if (!empty($_FILES['placesfile']['tmp_name'])) {
2272
			$lines = file($_FILES['placesfile']['tmp_name']);
2273
		} elseif (!empty($_REQUEST['localfile'])) {
2274
			$lines = file(WT_MODULES_DIR . 'googlemap/extra' . $_REQUEST['localfile']);
2275
		} else {
2276
			FlashMessages::addMessage(I18N::translate('No file was received. Please try again.'), 'danger');
2277
			$lines = [''];
2278
		}
2279
		// Strip BYTE-ORDER-MARK, if present
2280
		if (!empty($lines[0]) && substr($lines[0], 0, 3) === WT_UTF8_BOM) {
2281
			$lines[0] = substr($lines[0], 3);
2282
		}
2283
		asort($lines);
2284
		$highestIndex = $this->getHighestIndex();
2285
		$placelist    = [];
2286
		$j            = 0;
2287
		$maxLevel     = 0;
2288
		foreach ($lines as $p => $placerec) {
2289
			$fieldrec = explode(';', $placerec);
2290
			if ($fieldrec[0] > $maxLevel) {
2291
				$maxLevel = $fieldrec[0];
2292
			}
2293
		}
2294
		$fields   = count($fieldrec);
2295
		$set_icon = true;
2296
		if (!is_dir(WT_MODULES_DIR . 'googlemap/places/flags/')) {
2297
			$set_icon = false;
2298
		}
2299
		foreach ($lines as $p => $placerec) {
2300
			$fieldrec = explode(';', $placerec);
2301
			if (is_numeric($fieldrec[0]) && $fieldrec[0] <= $maxLevel) {
2302
				$placelist[$j]          = [];
2303
				$placelist[$j]['place'] = '';
2304
				for ($ii = $fields - 4; $ii > 1; $ii--) {
2305
					if ($fieldrec[0] > $ii - 2) {
2306
						$placelist[$j]['place'] .= $fieldrec[$ii] . ',';
2307
					}
2308
				}
2309
				foreach ($country_names as $countrycode => $countryname) {
2310
					if ($countrycode == strtoupper($fieldrec[1])) {
2311
						$fieldrec[1] = $countryname;
2312
						break;
2313
					}
2314
				}
2315
				$placelist[$j]['place'] .= $fieldrec[1];
2316
				$placelist[$j]['long'] = $fieldrec[$fields - 4];
2317
				$placelist[$j]['lati'] = $fieldrec[$fields - 3];
2318
				$placelist[$j]['zoom'] = $fieldrec[$fields - 2];
2319
				if ($set_icon) {
2320
					$placelist[$j]['icon'] = trim($fieldrec[$fields - 1]);
2321
				} else {
2322
					$placelist[$j]['icon'] = '';
2323
				}
2324
				$j = $j + 1;
2325
			}
2326
		}
2327
2328
		$prevPlace     = '';
2329
		$prevLati      = '';
2330
		$prevLong      = '';
2331
		$placelistUniq = [];
2332
		$j             = 0;
2333
		foreach ($placelist as $k => $place) {
2334
			if ($place['place'] != $prevPlace) {
2335
				$placelistUniq[$j]          = [];
2336
				$placelistUniq[$j]['place'] = $place['place'];
2337
				$placelistUniq[$j]['lati']  = $place['lati'];
2338
				$placelistUniq[$j]['long']  = $place['long'];
2339
				$placelistUniq[$j]['zoom']  = $place['zoom'];
2340
				$placelistUniq[$j]['icon']  = $place['icon'];
2341
				$j                          = $j + 1;
2342
			} elseif (($place['place'] == $prevPlace) && (($place['lati'] != $prevLati) || ($place['long'] != $prevLong))) {
2343
				if (($placelistUniq[$j - 1]['lati'] == 0) || ($placelistUniq[$j - 1]['long'] == 0)) {
2344
					$placelistUniq[$j - 1]['lati'] = $place['lati'];
2345
					$placelistUniq[$j - 1]['long'] = $place['long'];
2346
					$placelistUniq[$j - 1]['zoom'] = $place['zoom'];
2347
					$placelistUniq[$j - 1]['icon'] = $place['icon'];
2348
				} elseif (($place['lati'] != '0') || ($place['long'] != '0')) {
2349
					echo 'Difference: previous value = ', $prevPlace, ', ', $prevLati, ', ', $prevLong, ' current = ', $place['place'], ', ', $place['lati'], ', ', $place['long'], '<br>';
2350
				}
2351
			}
2352
			$prevPlace = $place['place'];
2353
			$prevLati  = $place['lati'];
2354
			$prevLong  = $place['long'];
2355
		}
2356
2357
		$default_zoom_level    = [];
2358
		$default_zoom_level[0] = 4;
2359
		$default_zoom_level[1] = 7;
2360
		$default_zoom_level[2] = 10;
2361
		$default_zoom_level[3] = 12;
2362
		foreach ($placelistUniq as $k => $place) {
2363
			$parent     = explode(',', $place['place']);
2364
			$parent     = array_reverse($parent);
2365
			$parent_id  = 0;
2366
			$num_parent = count($parent);
2367
			for ($i = 0; $i < $num_parent; $i++) {
2368
				$escparent = $parent[$i];
2369
				if ($escparent == '') {
2370
					$escparent = 'Unknown';
2371
				}
2372
				$row =
2373
					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")
2374
					->execute([$i, $parent_id, $escparent])
2375
					->fetchOneRow();
2376
				if (empty($row)) {
2377
					// this name does not yet exist: create entry
2378
					if (!Filter::postBool('updateonly')) {
2379
						$highestIndex = $highestIndex + 1;
2380
						if (($i + 1) == $num_parent) {
2381
							$zoomlevel = $place['zoom'];
2382
						} elseif (isset($default_zoom_level[$i])) {
2383
							$zoomlevel = $default_zoom_level[$i];
2384
						} else {
2385
							$zoomlevel = $this->getPreference('GM_MAX_ZOOM', self::GM_MAX_ZOOM_DEFAULT);
2386
						}
2387
						if (($place['lati'] == '0') || ($place['long'] == '0') || (($i + 1) < $num_parent)) {
2388
							Database::prepare("INSERT INTO `##placelocation` (pl_id, pl_parent_id, pl_level, pl_place, pl_zoom, pl_icon) VALUES (?, ?, ?, ?, ?, ?)")
2389
								->execute([$highestIndex, $parent_id, $i, $escparent, $zoomlevel, $place['icon']]);
2390
						} else {
2391
							//delete leading zero
2392
							$pl_lati = str_replace(['N', 'S', ','], ['', '-', '.'], $place['lati']);
2393
							$pl_long = str_replace(['E', 'W', ','], ['', '-', '.'], $place['long']);
2394
							if ($pl_lati >= 0) {
2395
								$place['lati'] = 'N' . abs($pl_lati);
2396
							} elseif ($pl_lati < 0) {
2397
								$place['lati'] = 'S' . abs($pl_lati);
2398
							}
2399
							if ($pl_long >= 0) {
2400
								$place['long'] = 'E' . abs($pl_long);
2401
							} elseif ($pl_long < 0) {
2402
								$place['long'] = 'W' . abs($pl_long);
2403
							}
2404
							Database::prepare("INSERT INTO `##placelocation` (pl_id, pl_parent_id, pl_level, pl_place, pl_long, pl_lati, pl_zoom, pl_icon) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
2405
								->execute([$highestIndex, $parent_id, $i, $escparent, $place['long'], $place['lati'], $zoomlevel, $place['icon']]);
2406
						}
2407
						$parent_id = $highestIndex;
2408
					}
2409
				} else {
2410
					$parent_id = $row->pl_id;
2411
					if (Filter::postBool('overwritedata') && ($i + 1 == count($parent))) {
2412
						Database::prepare("UPDATE `##placelocation` SET pl_lati = ?, pl_long = ?, pl_zoom = ?, pl_icon = ? WHERE pl_id = ?")
2413
							->execute([$place['lati'], $place['long'], $place['zoom'], $place['icon'], $parent_id]);
2414
					} else {
2415
						// Update only if existing data is missing
2416
						if (!$row->pl_long && !$row->pl_lati) {
2417
							Database::prepare("UPDATE `##placelocation` SET pl_lati = ?, pl_long = ? WHERE pl_id = ?")
2418
								->execute([$place['lati'], $place['long'], $parent_id]);
2419
						}
2420
						if (!$row->pl_icon && $place['icon']) {
2421
							Database::prepare("UPDATE `##placelocation` SET pl_icon = ? WHERE pl_id = ?")
2422
								->execute([$place['icon'], $parent_id]);
2423
						}
2424
					}
2425
				}
2426
			}
2427
		}
2428
2429
		$parent_id = (int) Filter::post('parent_id');
2430
		$inactive  = (int) Filter::post('inactive');
2431
2432
		header('Location: module.php?mod=googlemap&mod_action=admin_places&parent_id=' . $parent_id . '&inactive=' . $inactive);
2433
	}
2434
2435
	/**
2436
	 * Export/download the place hierarchy, or a prt of it.
2437
	 */
2438
	private function adminDownload() {
2439
		$parent_id = (int) Filter::get('parent_id');
2440
		$hierarchy = $this->placeIdToHierarchy($parent_id);
2441
		$maxLevel  = min(8, $this->getHighestLevel());
2442
2443
		if (empty($hierarchy)) {
2444
			$filename = 'places.csv';
2445
		} else {
2446
			$filename = 'places-' . preg_replace('/[:;\/\\\(\)\{\}\[\] $]/', '_', implode('-', $hierarchy)) . '.csv';
2447
		}
2448
2449
		header('Content-Type: text/csv; charset=utf-8');
2450
		header('Content-Disposition: inline; filename="' . $filename . '"');
2451
2452
		echo '"', I18N::translate('Level'), '";';
2453
		echo '"', I18N::translate('Country'), '";';
2454
		if ($maxLevel > 0) {
2455
			echo '"', I18N::translate('State'), '";';
2456
		}
2457
		if ($maxLevel > 1) {
2458
			echo '"', I18N::translate('County'), '";';
2459
		}
2460
		if ($maxLevel > 2) {
2461
			echo '"', I18N::translate('City'), '";';
2462
		}
2463
		if ($maxLevel > 3) {
2464
			echo '"', I18N::translate('Place'), '";';
2465
		}
2466
		if ($maxLevel > 4) {
2467
			echo '"', I18N::translate('Place'), '";';
2468
		}
2469
		if ($maxLevel > 5) {
2470
			echo '"', I18N::translate('Place'), '";';
2471
		}
2472
		if ($maxLevel > 6) {
2473
			echo '"', I18N::translate('Place'), '";';
2474
		}
2475
		if ($maxLevel > 7) {
2476
			echo '"', I18N::translate('Place'), '";';
2477
		}
2478
		echo '"', I18N::translate('Longitude'), '";';
2479
		echo '"', I18N::translate('Latitude'), '";';
2480
		echo '"', I18N::translate('Zoom level'), '";';
2481
		echo '"', I18N::translate('Icon'), '";', WT_EOL;
2482
		$this->outputLevel($parent_id);
2483
	}
2484
2485
	/**
2486
	 * Save a new/updated geographic place.
2487
	 */
2488
	private function adminPlaceSave() {
2489
		$parent_id = (int) Filter::post('parent_id');
2490
		$place_id  = (int) Filter::post('place_id');
2491
		$inactive  = (int) Filter::post('inactive');
2492
		$level     = count($this->placeIdToHierarchy($parent_id));
2493
2494
		if ($place_id === 0) {
2495
			Database::prepare(
2496
				"INSERT INTO `##placelocation` (pl_id, pl_parent_id, pl_level, pl_place, pl_long, pl_lati, pl_zoom, pl_icon) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
2497
			)->execute([
2498
				$this->getHighestIndex() + 1,
2499
				$parent_id,
2500
				$level,
2501
				Filter::post('NEW_PLACE_NAME'),
2502
				Filter::post('LONG_CONTROL') . Filter::post('NEW_PLACE_LONG'),
2503
				Filter::post('LATI_CONTROL') . Filter::post('NEW_PLACE_LATI'),
2504
				Filter::post('NEW_ZOOM_FACTOR'),
2505
				Filter::post('icon'),
2506
			]);
2507
		} else {
2508
			Database::prepare(
2509
			"UPDATE `##placelocation` SET pl_place = ?, pl_lati = ?, pl_long = ?, pl_zoom = ?, pl_icon = ? WHERE pl_id = ?"
2510
			)->execute([
2511
				Filter::post('NEW_PLACE_NAME'),
2512
				Filter::post('LATI_CONTROL') . Filter::post('NEW_PLACE_LATI'),
2513
				Filter::post('LONG_CONTROL') . Filter::post('NEW_PLACE_LONG'),
2514
				Filter::post('NEW_ZOOM_FACTOR'),
2515
				Filter::post('icon'),
2516
				$place_id,
2517
			]);
2518
		}
2519
2520
		header('Location: module.php?mod=googlemap&mod_action=admin_places&parent_id=' . $parent_id . '&inactive=' . $inactive);
2521
	}
2522
2523
	/**
2524
	 * Create or edit a geographic place.
2525
	 */
2526
	private function adminPlaceEdit() {
2527
		$parent_id  = (int) Filter::post('parent_id', null, Filter::get('parent_id'));
2528
		$place_id   = (int) Filter::post('place_id', null, Filter::get('place_id'));
2529
		$inactive   = (int) Filter::post('inactive', null, Filter::get('inactive'));
2530
		$where_am_i = $this->placeIdToHierarchy($place_id);
2531
		$level      = count($where_am_i);
2532
2533
		$controller = new PageController;
2534
		$controller
2535
			->setPageTitle(I18N::translate('Geographic data'))
2536
			->addInlineJavascript('$("<link>", {rel: "stylesheet", type: "text/css", href: "' . WT_MODULES_DIR . 'googlemap/css/wt_v3_googlemap.css"}).appendTo("head");')
2537
			->pageHeader();
2538
2539
		// Find (or create) the record we are editing.
2540
		$record =
2541
			Database::prepare("SELECT * FROM `##placelocation` WHERE pl_id=?")
2542
			->execute([$place_id])
2543
			->fetchOneRow();
2544
2545
		$parent_record =
2546
			Database::prepare("SELECT * FROM `##placelocation` WHERE pl_id=?")
2547
			->execute([$parent_id])
2548
			->fetchOneRow();
2549
2550
		if ($parent_record === null) {
2551
			$parent_record = (object) [
2552
				'pl_id'        => 0,
2553
				'pl_parent_id' => 0,
2554
				'pl_place'     => '',
2555
				'pl_lati'      => 'N0',
2556
				'pl_long'      => 'E0',
2557
				'pl_level'     => $level - 1,
2558
				'pl_icon'      => '',
2559
				'pl_zoom'      => self::GM_MIN_ZOOM_DEFAULT,
2560
			];
2561
		}
2562
2563
		if ($record === null || $place_id === 0) {
2564
			$record = (object) [
2565
				'pl_id'        => 0,
2566
				'pl_parent_id' => $parent_id,
2567
				'pl_place'     => '',
2568
				'pl_lati'      => 'N0',
2569
				'pl_long'      => 'E0',
2570
				'pl_level'     => $level,
2571
				'pl_icon'      => '',
2572
				'pl_zoom'      => $parent_record === null ? self::GM_MIN_ZOOM_DEFAULT : $parent_record->pl_zoom,
2573
			];
2574
		}
2575
2576
		// Convert to floating point for the map.
2577
		$latitude  = (float) (str_replace(['N', 'S'], ['', '-'], $record->pl_lati));
2578
		$longitude = (float) (str_replace(['E', 'W'], ['', '-'], $record->pl_long));
2579
		if ($latitude === 0 && $longitude === 0) {
2580
			$latitude  = (float) (str_replace(['N', 'S'], ['', '-'], $record->pl_lati));
2581
			$longitude = (float) (str_replace(['E', 'W'], ['', '-'], $record->pl_long));
2582
		}
2583
2584
		$parent_url = 'module.php?mod=googlemap&mod_action=admin_places&parent_id=' . $parent_id . '&inactive=' . $inactive;
2585
2586
		$breadcrumbs = [
2587
			route('admin-control-panel') => I18N::translate('Control panel'),
2588
			route('admin-modules')       => I18N::translate('Module administration'),
2589
			$this->getConfigLink()     => $this->getTitle(),
2590
		];
2591
		$hierarchy =
2592
			[0 => I18N::translate('Geographic data')] +
2593
			$this->placeIdToHierarchy($place_id === 0 ? $parent_id : $place_id);
2594
		foreach ($hierarchy as $id => $name) {
2595
			$breadcrumbs += ['module.php?mod=googlemap&mod_action=admin_places&parent_id=' . $id . '&inactive=' . $inactive => e($name)];
2596
		}
2597
		echo Bootstrap4::breadcrumbs($breadcrumbs, $place_id === 0 ? I18N::translate('Add') : I18N::translate('Edit'));
2598
2599
		?>
2600
		<script src="<?= $this->googleMapsScript() ?>"></script>
2601
		<script>
2602
		var map;
2603
		var marker;
2604
		var zoom;
2605
		var pl_name = <?= json_encode($record->pl_place) ?>;
2606
			var latlng = new google.maps.LatLng(<?= $latitude ?>, <?= $longitude ?>);
2607
		var pl_zoom = <?= $record->pl_zoom ?>;
2608
		var polygon1;
2609
		var polygon2;
2610
		var geocoder;
2611
		var mapType;
2612
2613
		var infowindow = new google.maps.InfoWindow({});
2614
2615
		function geocodePosition(pos) {
2616
			geocoder.geocode({
2617
				latLng: pos
2618
			}, function(responses) {
2619
				if (responses && responses.length > 0) {
2620
					updateMarkerAddress(responses[0].formatted_address);
2621
				} else {
2622
					updateMarkerAddress('Cannot determine address at this location.');
2623
				}
2624
			});
2625
		}
2626
2627
		/**
2628
		 * Redraw the map, centered and zoomed on the selected point.
2629
		 *
2630
		 * @param event
2631
		 */
2632
		function updateMap(event) {
2633
			var point;
2634
			var zoom = parseInt(document.editplaces.NEW_ZOOM_FACTOR.value);
2635
			var latitude;
2636
			var longitude;
2637
2638
			if ((document.editplaces.NEW_PLACE_LATI.value === '') ||
2639
				(document.editplaces.NEW_PLACE_LONG.value === '')) {
2640
				latitude = parseFloat(document.editplaces.parent_lati.value).toFixed(5);
2641
				longitude = parseFloat(document.editplaces.parent_long.value).toFixed(5);
2642
				point = new google.maps.LatLng(latitude, longitude);
2643
			} else {
2644
				latitude = parseFloat(document.editplaces.NEW_PLACE_LATI.value).toFixed(5);
2645
				longitude = parseFloat(document.editplaces.NEW_PLACE_LONG.value).toFixed(5);
2646
				document.editplaces.NEW_PLACE_LATI.value = latitude;
2647
				document.editplaces.NEW_PLACE_LONG.value = longitude;
2648
2649
				if (event === 'flag_drag') {
2650
					if (longitude < 0.0 ) {
2651
						longitude = longitude * -1;
2652
						document.editplaces.NEW_PLACE_LONG.value = longitude;
2653
						document.editplaces.LONG_CONTROL.value = 'W';
2654
					} else {
2655
						document.editplaces.NEW_PLACE_LONG.value = longitude;
2656
						document.editplaces.LONG_CONTROL.value = 'E';
2657
					}
2658
					if (latitude < 0.0 ) {
2659
						latitude = latitude * -1;
2660
						document.editplaces.NEW_PLACE_LATI.value = latitude;
2661
						document.editplaces.LATI_CONTROL.value = 'S';
2662
					} else {
2663
						document.editplaces.NEW_PLACE_LATI.value = latitude;
2664
						document.editplaces.LATI_CONTROL.value = 'N';
2665
					}
2666
2667
					if (document.editplaces.LATI_CONTROL.value === 'S') {
2668
						latitude = latitude * -1;
2669
					}
2670
					if (document.editplaces.LONG_CONTROL.value === 'W') {
2671
						longitude = longitude * -1;
2672
					}
2673
					point = new google.maps.LatLng(latitude, longitude);
2674
				} else {
2675
					if (latitude < 0.0) {
2676
						latitude = latitude * -1;
2677
						document.editplaces.NEW_PLACE_LATI.value = latitude;
2678
					}
2679
					if (longitude < 0.0) {
2680
						longitude = longitude * -1;
2681
						document.editplaces.NEW_PLACE_LONG.value = longitude;
2682
					}
2683
					if (document.editplaces.LATI_CONTROL.value === 'S') {
2684
						latitude = latitude * -1;
2685
					}
2686
					if (document.editplaces.LONG_CONTROL.value === 'W') {
2687
						longitude = longitude * -1;
2688
					}
2689
					point = new google.maps.LatLng(latitude, longitude);
2690
				}
2691
			}
2692
2693
			map.setCenter(point);
2694
			map.setZoom(zoom);
2695
			marker.setPosition(point);
2696
		}
2697
2698
		// === Create Borders for the UK Countries =========================================================
2699
		function overlays() {
2700
			// Define place LatLng arrays
2701
2702
			<?php
2703
			$coordsAsStr = [];
2704
			switch ($record->pl_place) {
2705
				case 'England':
2706
					$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';
2707
					break;
2708
				case 'Scotland':
2709
					$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';
2710
					break;
2711
				case 'Ireland':
2712
					$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';
2713
					break;
2714
				case 'Wales':
2715
					$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';
2716
					break;
2717
				case 'NC':
2718
					$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';
2719
					break;
2720
				default:
2721
			}
2722
			?>
2723
			var coordStr = <?= json_encode($coordsAsStr) ?>;
2724
			jQuery.each(coordStr, function(index, value) {
2725
				var coordXY = value.split('|');
2726
				var coords  = [];
2727
				var points  = [];
2728
				jQuery.each(coordXY, function(i, v) {
2729
					coords = v.split(',');
2730
					points.push(new google.maps.LatLng(parseFloat(coords[1]), parseFloat(coords[0])));
2731
				});
2732
				// Construct the polygon
2733
				new google.maps.Polygon({
2734
					paths:         points,
2735
					strokeColor:   "#888888",
2736
					strokeOpacity: 0.8,
2737
					strokeWeight:  1,
2738
					fillColor:     "#ff0000",
2739
					fillOpacity:   0.15
2740
				}).setMap(map);
2741
			});
2742
		}
2743
2744
		function loadMap(zoom, mapType) {
2745
			var mapTyp;
2746
2747
			if (mapType) {
2748
				mapTyp = mapType;
2749
			} else {
2750
				mapTyp = google.maps.MapTypeId.ROADMAP;
2751
			}
2752
			geocoder = new google.maps.Geocoder();
2753
			if (!zoom) {
2754
				zoom = pl_zoom;
2755
			}
2756
			// Define map
2757
			var myOptions = {
2758
				zoom: zoom,
2759
				center: latlng,
2760
				mapTypeId: mapTyp,// ROADMAP, SATELLITE, HYBRID, TERRAIN
2761
				// mapTypeId: google.maps.MapTypeId.ROADMAP, // ROADMAP, SATELLITE, HYBRID, TERRAIN
2762
				mapTypeControlOptions: {
2763
					style: google.maps.MapTypeControlStyle.DROPDOWN_MENU // DEFAULT, DROPDOWN_MENU, HORIZONTAL_BAR
2764
				},
2765
				navigationControlOptions: {
2766
				position: google.maps.ControlPosition.TOP_RIGHT, // BOTTOM, BOTTOM_LEFT, LEFT, TOP, etc
2767
				style: google.maps.NavigationControlStyle.SMALL // ANDROID, DEFAULT, SMALL, ZOOM_PAN
2768
				},
2769
				scrollwheel: true
2770
			};
2771
2772
			map = new google.maps.Map(document.querySelector('.gm-map'), myOptions);
2773
2774
			overlays();
2775
2776
			// Close any infowindow when map is clicked
2777
			google.maps.event.addListener(map, 'click', function() {
2778
				infowindow.close();
2779
			});
2780
2781
			// Check for zoom changes
2782
			google.maps.event.addListener(map, 'zoom_changed', function() {
2783
				document.editplaces.NEW_ZOOM_FACTOR.value = map.zoom;
2784
			});
2785
2786
			// Create the Main Location Marker
2787
			<?php
2788
			if ($level < 3 && $record->pl_icon != '') {
2789
				echo 'var image = {
2790
						"url"    : WT_MODULES_DIR + "googlemap/" + "' . $record->pl_icon . '",
2791
						"size"   : new google.maps.Size(25, 15),
2792
						"origin" : new google.maps.Point(0, 0),
2793
						"anchor" : new google.maps.Point(12, 15)
2794
					};';
2795
				echo 'marker = new google.maps.Marker({';
2796
				echo 'icon: image,';
2797
				echo 'position: latlng,';
2798
				echo 'map: map,';
2799
				echo 'title: pl_name,';
2800
				echo 'draggable: true,';
2801
				echo 'zIndex:1';
2802
				echo '});';
2803
			} else {
2804
				echo 'marker = new google.maps.Marker({';
2805
				echo 'position: latlng,';
2806
				echo 'map: map,';
2807
				echo 'title: pl_name,';
2808
				echo 'draggable: true,';
2809
				echo 'zIndex: 1';
2810
				echo '});';
2811
			}
2812
			?>
2813
2814
			// Set marker by clicking on map ---
2815
			google.maps.event.addListener(map, 'click', function(event) {
2816
				clearMarks();
2817
				latlng = event.latLng;
2818
				marker = new google.maps.Marker({
2819
					position: latlng,
2820
					map: map,
2821
					title: pl_name,
2822
					draggable: true,
2823
					zIndex: 1
2824
				});
2825
				document.getElementById('NEW_PLACE_LATI').value = marker.getPosition().lat().toFixed(5);
2826
				document.getElementById('NEW_PLACE_LONG').value = marker.getPosition().lng().toFixed(5);
2827
				updateMap('flag_drag');
2828
				var currzoom = parseInt(document.editplaces.NEW_ZOOM_FACTOR.value);
2829
				mapType = map.getMapTypeId();
2830
				loadMap(currzoom, mapType);
2831
			});
2832
2833
			// If the marker is moved, update the location fields
2834
			google.maps.event.addListener(marker, 'drag', function() {
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
			});
2838
			google.maps.event.addListener(marker, 'dragend', function() {
2839
				updateMap('flag_drag');
2840
			});
2841
		}
2842
2843
		function clearMarks() {
2844
			marker.setMap(null);
2845
		}
2846
2847
		/**
2848
		 * Called when we select one of the search results.
2849
		 *
2850
		 * @param lat
2851
		 * @param lng
2852
		 */
2853
		function setLoc(lat, lng) {
2854
			if (lat < 0.0) {
2855
				document.editplaces.NEW_PLACE_LATI.value = (lat.toFixed(5) * -1);
2856
				document.editplaces.LATI_CONTROL.value = 'S';
2857
			} else {
2858
				document.editplaces.NEW_PLACE_LATI.value = lat.toFixed(5);
2859
				document.editplaces.LATI_CONTROL.value = 'N';
2860
			}
2861
			if (lng < 0.0) {
2862
				document.editplaces.NEW_PLACE_LONG.value = (lng.toFixed(5) * -1);
2863
				document.editplaces.LONG_CONTROL.value = 'W';
2864
			} else {
2865
				document.editplaces.NEW_PLACE_LONG.value = lng.toFixed(5);
2866
				document.editplaces.LONG_CONTROL.value = 'E';
2867
			}
2868
			new google.maps.LatLng (lat.toFixed(5), lng.toFixed(5));
2869
			infowindow.close();
2870
			updateMap();
2871
		}
2872
2873
		function createMarker(i, point, name) {
2874
			 var image = {
2875
				 url:    WT_MODULES_DIR + 'googlemap/images/marker_yellow.png',
2876
				 size:   new google.maps.Size(20, 34),
2877
				 origin: new google.maps.Point(0, 0),
2878
				 anchor: new google.maps.Point(10, 34)
2879
			 };
2880
2881
			var marker = new google.maps.Marker({
2882
				icon:     image,
2883
				map:      map,
2884
				position: point,
2885
				zIndex:   0
2886
			});
2887
2888
			google.maps.event.addListener(marker, 'click', function() {
2889
				infowindow.close();
2890
				infowindow.setContent(name);
2891
				infowindow.open(map, marker);
2892
			});
2893
2894
			google.maps.event.addListener(map, 'click', function() {
2895
				infowindow.close();
2896
			});
2897
2898
			return marker;
2899
		}
2900
2901
		function change_icon() {
2902
			window.open('module.php?mod=googlemap&mod_action=admin_flags&countrySelected=', '_blank', indx_window_specs);
2903
			return false;
2904
		}
2905
2906
		function remove_icon() {
2907
			document.editplaces.icon.value = '';
2908
			document.getElementById('flagsDiv').innerHTML = '<a href="#" onclick="change_icon();return false;"><?= I18N::translate('Change flag') ?></a>';
2909
		}
2910
2911
		function addAddressToMap(response) {
2912
			var bounds = new google.maps.LatLngBounds();
2913
			if (!response) {
2914
				alert('<?= I18N::translate('No places found') ?>');
2915
			} else {
2916
				if (response.length > 0) {
2917
					for (var i=0; i<response.length; i++) {
2918
						// 5 decimal places is approx 1 metre accuracy.
2919
						var name =
2920
							'<div>' + response[i].address_components[0].short_name +
2921
							'<br><a href="#" onclick="setLoc(' + response[i].geometry.location.lat() + ', ' + response[i].geometry.location.lng() + ');">' +
2922
							'<?= I18N::translate('Use this value') ?></a>' +
2923
							'</div>';
2924
						var point = response[i].geometry.location;
2925
						var marker = createMarker(i, point, name);
2926
						bounds.extend(response[i].geometry.location);
2927
					}
2928
2929
					<?php if ($level > 0) { ?>
2930
						map.fitBounds(bounds);
2931
					<?php } ?>
2932
					var zoomlevel = map.getZoom();
2933
2934
					if (zoomlevel < <?= $this->getPreference('GM_MIN_ZOOM', self::GM_MIN_ZOOM_DEFAULT) ?>) {
2935
						zoomlevel = <?= $this->getPreference('GM_MIN_ZOOM', self::GM_MIN_ZOOM_DEFAULT) ?>;
2936
					}
2937
					if (zoomlevel > <?= $this->getPreference('GM_MAX_ZOOM', self::GM_MAX_ZOOM_DEFAULT) ?>) {
2938
						zoomlevel = <?= $this->getPreference('GM_MAX_ZOOM', self::GM_MAX_ZOOM_DEFAULT) ?>;
2939
					}
2940
					if (document.editplaces.NEW_ZOOM_FACTOR.value < zoomlevel) {
2941
						zoomlevel = document.editplaces.NEW_ZOOM_FACTOR.value;
2942
						if (zoomlevel < <?= $this->getPreference('GM_MIN_ZOOM', self::GM_MIN_ZOOM_DEFAULT) ?>) {
2943
							zoomlevel = <?= $this->getPreference('GM_MIN_ZOOM', self::GM_MIN_ZOOM_DEFAULT) ?>;
2944
						}
2945
						if (zoomlevel > <?= $this->getPreference('GM_MAX_ZOOM', self::GM_MAX_ZOOM_DEFAULT) ?>) {
2946
							zoomlevel = <?= $this->getPreference('GM_MAX_ZOOM', self::GM_MAX_ZOOM_DEFAULT) ?>;
2947
						}
2948
					}
2949
					map.setCenter(bounds.getCenter());
2950
					map.setZoom(zoomlevel);
2951
				}
2952
			}
2953
		}
2954
2955
		function showLocation_level(address) {
2956
			<?php if ($level > 0) { ?>
2957
				address += '<?= ', ' . addslashes(implode(', ', array_reverse($where_am_i, true))) ?>';
2958
			<?php } ?>
2959
				geocoder.geocode({'address': address}, addAddressToMap);
2960
		}
2961
2962
		function showLocation_all(address) {
2963
			geocoder.geocode({'address': address}, addAddressToMap);
2964
		}
2965
2966
		function paste_char(value) {
2967
			document.editplaces.NEW_PLACE_NAME.value += value;
2968
		}
2969
		window.onload = function() {
2970
			loadMap();
2971
		};
2972
	</script>
2973
2974
		<form method="post" id="editplaces" name="editplaces" action="module.php?mod=googlemap&amp;mod_action=admin_place_save">
2975
			<input type="hidden" name="place_id" value="<?= $place_id ?>">
2976
			<input type="hidden" name="level" value="<?= $level ?>">
2977
			<input type="hidden" name="parent_id" value="<?= $parent_id ?>">
2978
			<input type="hidden" name="place_long" value="<?= $longitude ?>">
2979
			<input type="hidden" name="place_lati" value="<?= $latitude ?>">
2980
2981
			<div class="form-group row">
2982
				<div class="col-sm-10 offset-sm-1">
2983
					<div class="gm-map" style="width: 100%; height: 350px;"></div>
2984
				</div>
2985
			</div>
2986
2987
			<div class="form-group row">
2988
				<label class="col-form-label col-sm-2">
2989
					<?= I18N::translate('Place') ?>
2990
				</label>
2991
				<div class="col-sm-6">
2992
					<input type="text" id="new_pl_name" name="NEW_PLACE_NAME" value="<?= e($record->pl_place) ?>" class="form-control" required>
2993
2994
					<label for="new_pl_name">
2995
						<a href="#" onclick="showLocation_all(document.getElementById('new_pl_name').value); return false">
2996
							<?= I18N::translate('Search globally') ?>
2997
						</a>
2998
					</label>
2999
					|
3000
					<label for="new_pl_name">
3001
						<a href="#" onclick="showLocation_level(document.getElementById('new_pl_name').value); return false">
3002
							<?= I18N::translate('Search locally') ?>
3003
						</a>
3004
					</label>
3005
				</div>
3006
3007
				<label class="col-form-label col-sm-2" for="NEW_ZOOM_FACTOR">
3008
					<?= I18N::translate('Zoom level') ?>
3009
				</label>
3010
				<div class="col-sm-2">
3011
					<input type="text" id="NEW_ZOOM_FACTOR" name="NEW_ZOOM_FACTOR" value="<?= $record->pl_zoom ?>" class="form-control" onchange="updateMap();" required readonly>
3012
				</div>
3013
			</div class="form-group row">
3014
3015
			<div class="form-group row">
3016
				<label class="col-form-label col-sm-2">
3017
					<?= I18N::translate('Latitude') ?>
3018
				</label>
3019
				<div class="col-sm-4">
3020
					<div class="input-group">
3021
						<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>
3022
						<select name="LATI_CONTROL" id="LATI_CONTROL" onchange="updateMap();" class="form-control">
3023
							<option value="N"<?= $latitude >= 0 ? ' selected' : '' ?>>
3024
								<?= I18N::translate('north') ?>
3025
							</option>
3026
							<option value="S"<?= $latitude < 0 ? ' selected' : '' ?>>
3027
								<?= I18N::translate('south') ?>
3028
							</option>
3029
						</select>
3030
					</div>
3031
				</div>
3032
3033
				<label class="col-form-label col-sm-2">
3034
					<?= I18N::translate('Longitude') ?>
3035
				</label>
3036
				<div class="col-sm-4">
3037
					<div class="input-group">
3038
						<input type="text" id="NEW_PLACE_LONG" name="NEW_PLACE_LONG" placeholder="<?= I18N::translate('degrees') ?>" value="<?= abs($longitude) ?>" class="form-control" onchange="updateMap();" required>
3039
						<select name="LONG_CONTROL" id="LONG_CONTROL" onchange="updateMap();" class="form-control">
3040
							<option value="E"<?= $longitude >= 0 ? ' selected' : '' ?>>
3041
								<?= I18N::translate('east') ?>
3042
							</option>
3043
							<option value="W"<?= $longitude < 0 ? ' selected' : '' ?>>
3044
								<?= I18N::translate('west') ?>
3045
							</option>
3046
						</select>
3047
					</div>
3048
				</div>
3049
			</div>
3050
3051
			<div class="row form-group">
3052
				<label class="col-form-label col-sm-2" for="icon">
3053
					<?= I18N::translate('Flag') ?>
3054
				</label>
3055
				<div class="col-sm-10">
3056
					<div class="input-group" dir="ltr">
3057
						<div class="input-group-prepend">
3058
						<span class="input-group-text">
3059
						<?= WT_MODULES_DIR ?>googlemap/places/flags/
3060
						</span>
3061
						</div>
3062
						<?= FunctionsEdit::formControlFlag($record->pl_icon, ['name' => 'icon', 'id' => 'icon', 'class' => 'form-control']) ?>
3063
					</div>
3064
				</div>
3065
			</div>
3066
3067
			<div class="row form-group">
3068
				<div class="col-sm-10 offset-sm-2">
3069
					<button class="btn btn-primary" type="submit">
3070
						<?= /* I18N: A button label. */ I18N::translate('save') ?>
3071
					</button>
3072
					<a class="btn btn-secondary" href="<?= $parent_url ?>">
3073
						<?= /* I18N: A button label. */ I18N::translate('cancel') ?>
3074
					</a>
3075
				</div>
3076
			</div>
3077
		</form>
3078
		<?php
3079
	}
3080
3081
	/**
3082
	 * Places administration.
3083
	 */
3084
	private function adminPlaces() {
3085
		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...
3086
3087
		$action    = Filter::get('action');
3088
		$parent_id = (int) Filter::get('parent_id');
3089
		$inactive  = Filter::getBool('inactive');
3090
3091
		$controller = new PageController;
3092
		$controller
3093
			->setPageTitle(I18N::translate('Geographic data'))
3094
			->pageHeader();
3095
3096
		$breadcrumbs = [
3097
			route('admin-control-panel') => I18N::translate('Control panel'),
3098
			route('admin-modules')       => I18N::translate('Module administration'),
3099
			$this->getConfigLink()     => $this->getTitle(),
3100
		];
3101
		$hierarchy =
3102
			[0 => I18N::translate('Geographic data')] +
3103
			$this->placeIdToHierarchy($parent_id);
3104
		foreach (array_slice($hierarchy, 0, -1, true) as $id => $name) {
3105
			$breadcrumbs += ['module.php?mod=googlemap&mod_action=admin_places&parent_id=' . $id . '&inactive=' . $inactive => e($name)];
3106
		}
3107
		echo Bootstrap4::breadcrumbs($breadcrumbs, end($hierarchy));
3108
3109
		if ($action == 'ImportGedcom') {
3110
			echo '<h2>' . I18N::translate('Geographic data') . '</h2>';
3111
			$placelist      = [];
3112
			$j              = 0;
3113
			$gedcom_records =
3114
				Database::prepare("SELECT i_gedcom FROM `##individuals` WHERE i_file=? UNION ALL SELECT f_gedcom FROM `##families` WHERE f_file=?")
3115
				->execute([$WT_TREE->getTreeId(), $WT_TREE->getTreeId()])
3116
				->fetchOneColumn();
3117
			foreach ($gedcom_records as $gedrec) {
3118
				$i        = 1;
3119
				$placerec = Functions::getSubRecord(2, '2 PLAC', $gedrec, $i);
3120
				while (!empty($placerec)) {
3121
					if (preg_match('/2 PLAC (.+)/', $placerec, $match)) {
3122
						$placelist[$j]          = [];
3123
						$placelist[$j]['place'] = trim($match[1]);
3124
						if (preg_match('/4 LATI (.*)/', $placerec, $match)) {
3125
							$placelist[$j]['lati'] = trim($match[1]);
3126
							if (($placelist[$j]['lati'][0] != 'N') && ($placelist[$j]['lati'][0] != 'S')) {
3127
								if ($placelist[$j]['lati'] < 0) {
3128
									$placelist[$j]['lati'][0] = 'S';
3129
								} else {
3130
									$placelist[$j]['lati'] = 'N' . $placelist[$j]['lati'];
3131
								}
3132
							}
3133
						} else {
3134
							$placelist[$j]['lati'] = null;
3135
						}
3136
						if (preg_match('/4 LONG (.*)/', $placerec, $match)) {
3137
							$placelist[$j]['long'] = trim($match[1]);
3138
							if (($placelist[$j]['long'][0] != 'E') && ($placelist[$j]['long'][0] != 'W')) {
3139
								if ($placelist[$j]['long'] < 0) {
3140
									$placelist[$j]['long'][0] = 'W';
3141
								} else {
3142
									$placelist[$j]['long'] = 'E' . $placelist[$j]['long'];
3143
								}
3144
							}
3145
						} else {
3146
							$placelist[$j]['long'] = null;
3147
						}
3148
						$j = $j + 1;
3149
					}
3150
					$i        = $i + 1;
3151
					$placerec = Functions::getSubRecord(2, '2 PLAC', $gedrec, $i);
3152
				}
3153
			}
3154
			asort($placelist);
3155
3156
			$prevPlace     = '';
3157
			$prevLati      = '';
3158
			$prevLong      = '';
3159
			$placelistUniq = [];
3160
			$j             = 0;
3161
			foreach ($placelist as $k => $place) {
3162
				if ($place['place'] != $prevPlace) {
3163
					$placelistUniq[$j]          = [];
3164
					$placelistUniq[$j]['place'] = $place['place'];
3165
					$placelistUniq[$j]['lati']  = $place['lati'];
3166
					$placelistUniq[$j]['long']  = $place['long'];
3167
					$j                          = $j + 1;
3168
				} elseif (($place['place'] == $prevPlace) && (($place['lati'] != $prevLati) || ($place['long'] != $prevLong))) {
3169
					if (($placelistUniq[$j - 1]['lati'] == 0) || ($placelistUniq[$j - 1]['long'] == 0)) {
3170
						$placelistUniq[$j - 1]['lati'] = $place['lati'];
3171
						$placelistUniq[$j - 1]['long'] = $place['long'];
3172
					} elseif (($place['lati'] != '0') || ($place['long'] != '0')) {
3173
						echo 'Difference: previous value = ', $prevPlace, ', ', $prevLati, ', ', $prevLong, ' current = ', $place['place'], ', ', $place['lati'], ', ', $place['long'], '<br>';
3174
					}
3175
				}
3176
				$prevPlace = $place['place'];
3177
				$prevLati  = $place['lati'];
3178
				$prevLong  = $place['long'];
3179
			}
3180
3181
			$highestIndex = $this->getHighestIndex();
3182
3183
			$default_zoom_level = [4, 7, 10, 12];
3184
			foreach ($placelistUniq as $k => $place) {
3185
				$parent     = preg_split('/ *, */', $place['place']);
3186
				$parent     = array_reverse($parent);
3187
				$parent_id  = 0;
3188
				$num_parent = count($parent);
3189
				for ($i = 0; $i < $num_parent; $i++) {
3190
					if (!isset($default_zoom_level[$i])) {
3191
						$default_zoom_level[$i] = $default_zoom_level[$i - 1];
3192
					}
3193
					$escparent = $parent[$i];
3194
					if ($escparent == '') {
3195
						$escparent = 'Unknown';
3196
					}
3197
					$row =
3198
						Database::prepare("SELECT pl_id, pl_long, pl_lati, pl_zoom FROM `##placelocation` WHERE pl_level=? AND pl_parent_id=? AND pl_place LIKE ?")
3199
						->execute([$i, $parent_id, $escparent])
3200
						->fetchOneRow();
3201
					if ($i < $num_parent - 1) {
3202
						// Create higher-level places, if necessary
3203
						if (empty($row)) {
3204
							$highestIndex++;
3205
							Database::prepare("INSERT INTO `##placelocation` (pl_id, pl_parent_id, pl_level, pl_place, pl_zoom) VALUES (?, ?, ?, ?, ?)")
3206
								->execute([$highestIndex, $parent_id, $i, $escparent, $default_zoom_level[$i]]);
3207
							echo e($escparent), '<br>';
3208
							$parent_id = $highestIndex;
3209
						} else {
3210
							$parent_id = $row->pl_id;
3211
						}
3212
					} else {
3213
						// Create lowest-level place, if necessary
3214
						if (empty($row->pl_id)) {
3215
							$highestIndex++;
3216
							Database::prepare("INSERT INTO `##placelocation` (pl_id, pl_parent_id, pl_level, pl_place, pl_long, pl_lati, pl_zoom) VALUES (?, ?, ?, ?, ?, ?, ?)")
3217
								->execute([$highestIndex, $parent_id, $i, $escparent, $place['long'], $place['lati'], $default_zoom_level[$i]]);
3218
							echo e($escparent), '<br>';
3219
						} else {
3220
							if (empty($row->pl_long) && empty($row->pl_lati) && $place['lati'] != '0' && $place['long'] != '0') {
3221
								Database::prepare("UPDATE `##placelocation` SET pl_lati=?, pl_long=? WHERE pl_id=?")
3222
									->execute([$place['lati'], $place['long'], $row->pl_id]);
3223
								echo e($escparent), '<br>';
3224
							}
3225
						}
3226
					}
3227
				}
3228
			}
3229
		}
3230
3231
		$placelist = $this->getPlaceListLocation($parent_id, $inactive);
3232
		?>
3233
		<form class="form-inline">
3234
		<input type="hidden" name="mod" value="googlemap">
3235
		<input type="hidden" name="mod_action" value="admin_places">
3236
		<input type="hidden" name="parent_id" value="<?= $parent_id ?>">
3237
			<?= Bootstrap4::checkbox(I18N::translate('Show inactive places'), false, ['name' => 'inactive', 'checked' => $inactive, 'onclick' => 'this.form.submit()']) ?>
3238
			<p class="small text-muted">
3239
				<?= 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.') ?>
3240
				<?= I18N::translate('If you have a large number of inactive places, it can be slow to generate the list.') ?>
3241
			</p>
3242
		</form>
3243
3244
		<div class="gm_plac_edit">
3245
			<table class="table table-bordered table-sm table-hover">
3246
				<thead>
3247
					<tr>
3248
						<th><?= I18N::translate('Place') ?></th>
3249
						<th><?= I18N::translate('Latitude') ?></th>
3250
						<th><?= I18N::translate('Longitude') ?></th>
3251
						<th><?= I18N::translate('Zoom level') ?></th>
3252
						<th><?= I18N::translate('Icon') ?> / <?= I18N::translate('Flag') ?></th>
3253
						<th><?= I18N::translate('Edit') ?></th>
3254
						<th><?= I18N::translate('Delete') ?></th>
3255
					</tr>
3256
				</thead>
3257
				<tbody>
3258
3259
				<?php foreach ($placelist as $place): ?>
3260
					<?php $noRows = Database::prepare("SELECT COUNT(pl_id) FROM `##placelocation` WHERE pl_parent_id=?")->execute([$place['place_id']])->fetchOne(); ?>
3261
					<tr>
3262
						<td>
3263
							<a href="module.php?mod=googlemap&mod_action=admin_places&amp;parent_id=<?= $place['place_id'] ?>&inactive=<?= $inactive ?>">
3264
								<?php if ($place['place'] === 'Unknown'): ?>
3265
									<?= I18N::translate('unknown') ?>
3266
								<?php else: ?>
3267
									<?= e($place['place']) ?>
3268
								<?php endif ?>
3269
								<?php if ($place['missing'] > 0): ?>
3270
								<span class="badge badge-pill badge-warning">
3271
									<?= I18N::number($place['children']) ?>
3272
								</span>
3273
								<?php elseif ($place['children'] > 0): ?>
3274
								<span class="badge badge-pill badge-default">
3275
									<?= I18N::number($place['children']) ?>
3276
								</span>
3277
								<?php endif ?>
3278
							</a>
3279
						</td>
3280
						<td>
3281
							<?= $place['is_empty'] ? FontAwesome::decorativeIcon('warning') : $place['lati'] ?>
3282
						</td>
3283
						<td>
3284
							<?= $place['is_empty'] ? FontAwesome::decorativeIcon('warning') : $place['long'] ?>
3285
						</td>
3286
						<td>
3287
							<?= $place['zoom'] ?>
3288
						</td>
3289
						<td>
3290
								<?php if ($place['icon']): ?>
3291
									<img src="<?= WT_MODULES_DIR ?>googlemap/places/flags/<?= e($place['icon']) ?>" width="25" height="15" title="<?= e($place['icon']) ?>" alt="<?= I18N::translate('Flag') ?>">
3292
								<?php else: ?>
3293
									<img src="<?= WT_MODULES_DIR ?>googlemap/images/mm_20_red.png">
3294
								<?php endif ?>
3295
						</td>
3296
						<td>
3297
							<?= 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']) ?>
3298
						</td>
3299
						<td>
3300
							<?php if ($noRows == 0): ?>
3301
								<form method="POST" action="module.php?mod=googlemap&amp;mod_action=admin_delete_action" onsubmit="return confirm('<?= I18N::translate('Remove this location?') ?>')">
3302
									<input type="hidden" name="parent_id" value="<?= $parent_id ?>">
3303
									<input type="hidden" name="place_id" value="<?= $place['place_id'] ?>">
3304
									<input type="hidden" name="inactive" value="<?= $inactive ?>">
3305
									<button type="submit" class="btn btn-danger">
3306
										<?= FontAwesome::semanticIcon('delete', I18N::translate('Delete')) ?>
3307
									</button>
3308
								</form>
3309
							<?php else: ?>
3310
								<button type="button" class="btn btn-danger" disabled>
3311
									<?= FontAwesome::decorativeIcon('delete') ?>
3312
								</button>
3313
							<?php endif ?>
3314
						</td>
3315
					</tr>
3316
					<?php endforeach ?>
3317
				</tbody>
3318
				<tfoot>
3319
					<tr>
3320
						<td colspan="7">
3321
							<a href="module.php?mod=googlemap&mod_action=admin_place_edit&parent_id=<?= $parent_id ?>" class="btn btn-primary">
3322
								<?= FontAwesome::decorativeIcon('add') ?>
3323
								<?= /* I18N: A button label. */ I18N::translate('add') ?>
3324
							</a>
3325
3326
							<a href="module.php?mod=googlemap&mod_action=admin_download&parent_id=<?= $parent_id ?>" class="btn btn-primary">
3327
								<?= FontAwesome::decorativeIcon('download') ?>
3328
								<?= /* I18N: A button label. */ I18N::translate('download') ?>
3329
							</a>
3330
3331
							<a href="module.php?mod=googlemap&amp;mod_action=admin_upload&amp;parent_id=<?= $parent_id ?>&amp;inactive=<?= $inactive ?>" class="btn btn-primary">
3332
								<?= FontAwesome::decorativeIcon('upload') ?>
3333
								<?= /* I18N: A button label. */ I18N::translate('upload') ?>
3334
							</a>
3335
						</td>
3336
					</tr>
3337
				</tfoot>
3338
			</table>
3339
		</div>
3340
3341
		<hr>
3342
3343
		<form class="form-horizontal" action="module.php">
3344
			<input type="hidden" name="mod" value="googlemap">
3345
			<input type="hidden" name="mod_action" value="admin_places">
3346
			<input type="hidden" name="action" value="ImportGedcom">
3347
			<div class="row form-group">
3348
				<label class="form-control-static col-sm-4" for="ged">
3349
					<?= I18N::translate('Import all places from a family tree') ?>
3350
				</label>
3351
				<div class="col-sm-6">
3352
					<?= Bootstrap4::select(Tree::getNameList(), $WT_TREE->getName(), ['id' => 'ged', 'name' => 'ged']) ?>
3353
				</div>
3354
				<div class="col-sm-2">
3355
					<button type="submit" class="btn btn-primary">
3356
						<?= FontAwesome::decorativeIcon('add') ?>
3357
						<?= /* I18N: A button label. */ I18N::translate('import') ?>
3358
					</button>
3359
				</div>
3360
			</div>
3361
		</form>
3362
		<?php
3363
	}
3364
}
3365