1<?php
2// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
3//
4// All Rights Reserved. See copyright.txt for details and a complete list of authors.
5// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
6// $Id$
7
8function wikiplugin_map_info()
9{
10	return [
11		'name' => tra('Map'),
12		'format' => 'html',
13		'documentation' => 'PluginMap',
14		'description' => tra('Display a map'),
15		'prefs' => [ 'wikiplugin_map', 'feature_search' ],
16		'iconname' => 'map',
17		'introduced' => 1,
18		'tags' => [ 'basic' ],
19		'filter' => 'wikicontent',
20		'body' => tr('Instructions to load content'),
21		'params' => [
22			'scope' => [
23				'required' => false,
24				'name' => tra('Scope'),
25				'description' => tr('Display the geolocated items represented in the page (%0all%1, %0center%1, or
26					%0custom%1 as a CSS selector). Default: %0center%1', '<code>', '</code>'),
27				'since' => '8.0',
28				'filter' => 'text',
29				'default' => 'center',
30			],
31			'controls' => [
32				'required' => false,
33				'name' => tra('Controls'),
34				'description' => tr('Comma-separated list of map controls will be displayed on the map and around it'),
35				'since' => '9.0',
36				'filter' => 'word',
37				'accepted' => 'controls, layers, search_location, levels, current_location, scale, streetview,
38					navigation, coordinates, overview',
39				'separator' => ',',
40				'default' => wp_map_default_controls(),
41			],
42			'width' => [
43				'required' => false,
44				'name' => tra('Width'),
45				'description' => tra('Width of the map in pixels'),
46				'since' => '1',
47				'filter' => 'digits',
48			],
49			'height' => [
50				'required' => false,
51				'name' => tra('Height'),
52				'description' => tra('Height of the map in pixels'),
53				'since' => '1',
54				'filter' => 'digits',
55			],
56			'center' => [
57				'requied' => false,
58				'name' => tra('Center'),
59				'description' => tr('Format: %0x,y,zoom%1 where %0x%1 is the longitude, and %0y%1 is the latitude.
60					%0zoom%1 is between %00%1 (view Earth) and %019%1.', '<code>', '</code>'),
61				'since' => '9.0',
62				'filter' => 'text',
63			],
64			'popupstyle' => [
65				'required' => false,
66				'name' => tr('Popup Style'),
67				'description' => tr('Alter the way the information is displayed when objects are loaded on the map.'),
68				'since' => '10.0',
69				'filter' => 'word',
70				'default' => 'bubble',
71				'options' => [
72					['text' => '', 'value' => ''],
73					['text' => tr('Bubble'), 'value' => 'bubble'],
74					['text' => tr('Dialog'), 'value' => 'dialog'],
75				],
76			],
77			'mapfile' => [
78				'required' => false,
79				'name' => tra('MapServer File'),
80				'description' => tra('MapServer file identifier. Only fill this in if you are using MapServer.'),
81				'since' => '1',
82				'filter' => 'url',
83				'advanced' => true,
84			],
85			'extents' => [
86				'required' => false,
87				'name' => tra('Extents'),
88				'description' => tra('Extents'),
89				'since' => '1',
90				'filter' => 'text',
91				'advanced' => true,
92			],
93			'size' => [
94				'required' => false,
95				'name' => tra('Size'),
96				'description' => tra('Size of the map'),
97				'since' => '1',
98				'filter' => 'digits',
99				'advanced' => true,
100			],
101			'tooltips' => [
102				'required' => false,
103				'name' => tra('Tooltips'),
104				'description' => tra('Show item name in a tooltip on hover'),
105				'since' => '12.1',
106				'default' => 'n',
107				'filter' => 'alpha',
108				'options' => [
109					['text' => '', 'value' => ''],
110					['text' => tra('Yes'), 'value' => 'y'],
111					['text' => tra('No'), 'value' => 'n']
112				],
113				'advanced' => true,
114			],
115			'library' => [
116				'required' => false,
117				'name' => tra('Open Layers Version'),
118				'description' => tra('OL2 or OL3+ so far (default ol2)'),
119				'since' => '20.1',
120				'default' => 'ol2',
121				'filter' => 'text',
122				'options' => [
123					['text' => '', 'value' => ''],
124					['text' => tra('OpenLayers 2.x'), 'value' => 'ol2'],
125					['text' => tra('OpenLayers 3+ (experimental)'), 'value' => 'ol3']
126				],
127				'advanced' => true,
128			],
129			'tilesets' => [
130				'required' => false,
131				'name' => tra('Tileset layers'),
132				'description' => tra('Tilesets to use for background layers, comma separated. Tileset groups can be added separated by a tilde character (requires Open Layers v3+, default is the geo_tilesets preference)'),
133				'since' => '20.1',
134				'default' => "86, 134, 200",
135				'filter' => 'text',
136				'advanced' => true,
137			],
138			'cluster' => [
139				'required' => false,
140				'name' => tra('Cluster Distance'),
141				'description' => tra('Distance between features before they are "clustered", 0 (off) to 100. (requires Open Layers v3+, default is 0)'),
142				'since' => '20.0',
143				'default' => 0,
144				'filter' => 'digits',
145				'advanced' => true,
146			],
147			'clusterHover' => [
148				'required' => false,
149				'name' => tra('Cluster Hover Behavior'),
150				'description' => tra('Appearance of clusters on mouse over. (requires Open Layers v3+, default is features)'),
151				'since' => '20.1',
152				'default' => 'features',
153				'filter' => 'text',
154				'options' => [
155					['text' => '', 'value' => ''],
156					['text' => tra('Show Features'), 'value' => 'features'],
157					['text' => tra('None'), 'value' => 'none'],
158				],
159				'advanced' => true,
160			],
161			'clusterFillColor' => [
162				'required' => false,
163				'name' => tra('Cluster Fill Color'),
164				'description' => tra('Cluster fill color in RGB. (requires Open Layers v3+, default is 86, 134, 200)'),
165				'since' => '20.1',
166				'default' => "86, 134, 200",
167				'filter' => 'text',
168				'advanced' => true,
169			],
170			'clusterTextColor' => [
171				'required' => false,
172				'name' => tra('Cluster Text Color'),
173				'description' => tra('Cluster text and outline color in RGB. (requires Open Layers v3+, default is 255, 255, 255)'),
174				'since' => '20.1',
175				'default' => "255, 255, 255",
176				'filter' => 'text',
177				'advanced' => true,
178			],
179		],
180	];
181}
182
183function wikiplugin_map($data, $params)
184{
185	$smarty = TikiLib::lib('smarty');
186	$smarty->loadPlugin('smarty_modifier_escape');
187
188	$width = '100%';
189	if (isset($params['width'])) {
190		$width = (int)$params['width'] . 'px';
191	}
192
193	$height = '100%';
194	if (isset($params['height'])) {
195		$height = (int)$params['height'] . 'px';
196	}
197
198	if (! isset($params['controls'])) {
199		$params['controls'] = wp_map_default_controls();
200	}
201
202	if (! is_array($params['controls'])) {
203		$params['controls'] = explode(',', $params['controls']);
204	}
205
206	if (! isset($params['popupstyle'])) {
207		$params['popupstyle'] = 'bubble';
208	}
209
210	$popupStyle = smarty_modifier_escape($params['popupstyle']);
211
212	if (! empty($params['tooltips']) && $params['tooltips'] === 'y') {
213		$tooltips = ' data-tooltips="1"';
214	} else {
215		$tooltips = '';
216	}
217
218	if (isset($params['cluster'])) {
219		$cluster = (int) $params['cluster'];
220	} else {
221		$cluster = 0;
222	}
223	if (isset($params['clusterHover'])) {
224		$clusterHover = ' data-clusterhover="' . $params['clusterHover'] . '"';
225	} else {
226		$clusterHover = ' data-clusterhover="features"';
227	}
228	if (isset($params['clusterFillColor'])) {
229		$clusterFillColor = ' data-clusterfillcolor="' . $params['clusterFillColor'] . '"';
230	} else {
231		$clusterFillColor = '';
232	}
233	if (isset($params['clusterTextColor'])) {
234		$clusterTextColor = ' data-clustertextcolor="' . $params['clusterTextColor'] . '"';
235	} else {
236		$clusterTextColor = '';
237	}
238	if (isset($params['tilesets'])) {
239		$tilesets = ' data-tilesets="' . $params['tilesets'] . '"';
240	} else {
241		$tilesets = '';
242	}
243
244	$controls = array_intersect($params['controls'], wp_map_available_controls());
245	$controls = implode(',', $controls);
246
247	$center = null;
248	$geolib = TikiLib::lib('geo');
249	if (isset($params['center'])) {
250		if ($coords = $geolib->parse_coordinates($params['center'])) {
251			$center = ' data-geo-center="' . smarty_modifier_escape($geolib->build_location_string($coords)) . '" ';
252		}
253	} else {
254		$center = $geolib->get_default_center();
255	}
256
257	TikiLib::lib('header')->add_map();
258
259	global $prefs;
260
261	if (! isset($params['library'])) {
262		$params['library'] = $prefs['geo_openlayers_version'];
263	}
264
265	if ($params['library'] === 'ol3' && $prefs['geo_openlayers_version'] === 'ol2') {
266		TikiLib::lib('header')
267			->drop_cssfile('lib/openlayers/theme/default/style.css')
268			->drop_jsfile('lib/openlayers/OpenLayers.js')
269			->drop_jsfile('lib/jquery_tiki/tiki-maps.js')
270			->add_cssfile('vendor_bundled/vendor/openlayers/openlayers/ol.css')
271			->add_jsfile('lib/jquery_tiki/tiki-maps-ol3.js')
272			->add_jsfile('vendor_bundled/vendor/openlayers/openlayers/ol.js')
273			->add_cssfile('vendor_bundled/vendor/walkermatt/ol-layerswitcher/src/ol-layerswitcher.css')
274			->add_jsfile('vendor_bundled/vendor/walkermatt/ol-layerswitcher/dist/ol-layerswitcher.js')
275		;
276	} else if ($params['library'] === 'ol2' && $prefs['geo_openlayers_version'] === 'ol3') {
277		TikiLib::lib('header')
278			->drop_cssfile('vendor_bundled/vendor/openlayers/openlayers/ol.css')
279			->drop_jsfile('lib/jquery_tiki/tiki-maps-ol3.js')
280			->drop_jsfile('vendor_bundled/vendor/openlayers/openlayers/ol.js')
281			->drop_cssfile('vendor_bundled/vendor/walkermatt/ol-layerswitcher/src/ol-layerswitcher.css')
282			->drop_jsfile('vendor_bundled/vendor/walkermatt/ol-layerswitcher/dist/ol-layerswitcher.js')
283			->add_cssfile('lib/openlayers/theme/default/style.css')
284			->add_jsfile('lib/openlayers/OpenLayers.js')
285			->add_jsfile('lib/jquery_tiki/tiki-maps.js')
286		;
287	}
288
289	$scope = smarty_modifier_escape(wp_map_getscope($params));
290
291	$output = "<div class=\"map-container\" data-marker-filter=\"$scope\" data-map-controls=\"$controls\" data-popup-style=\"$popupStyle\"" .
292		" data-cluster=\"$cluster\" style=\"width: $width; height: $height;\" $center $tooltips $clusterFillColor $clusterTextColor $tilesets $clusterHover>";
293
294	$argumentParser = new WikiParser_PluginArgumentParser;
295	$matches = WikiParser_PluginMatcher::match($data);
296	foreach ($matches as $match) {
297		$name = $match->getName();
298		$arguments = $argumentParser->parse($match->getArguments());
299
300		$function = 'wp_map_plugin_' . $name;
301		if (function_exists($function)) {
302			$output .= $function($match->getBody(), new JitFilter($arguments));
303		}
304	}
305
306	$output .= "</div>";
307
308	return $output;
309}
310
311function wp_map_getscope($params)
312{
313	$scope = 'center';
314	if (isset($params['scope'])) {
315		$scope = $params['scope'];
316	}
317
318	switch ($scope) {
319		case 'center':
320			return '#col1 .geolocated';
321		case 'all':
322			return '.geolocated';
323		default:
324			return $scope;
325	}
326}
327
328function wp_map_default_controls()
329{
330	return 'controls,layers,search_location';
331}
332
333function wp_map_available_controls()
334{
335	return [
336		'controls',
337		'layers',
338		'levels',
339		'search_location',
340		'current_location',
341		'scale',
342		'streetview',
343		'navigation',
344		'coordinates',
345		'overview',
346	];
347}
348
349function wp_map_plugin_searchlayer($body, $args)
350{
351	$layer = $args->layer->text();
352	$refresh = $args->refresh->int();
353	$suffix = $args->suffix->word();
354	$maxRecords = $args->maxRecords->digits();
355	$sort_mode = $args->sort_mode->word();
356	$load_delay = $args->load_delay->int();
357	$popup_width = $args->popup_width->text();	// plain numeric xx for pixels or xx% for percentage (only on dialog popups)
358	$popup_height = $args->popup_height->text();
359
360	$args->replaceFilter('fields', 'word');
361	$fields = $args->asArray('fields', ',');
362
363	unset($args['layer']);
364	unset($args['refresh']);
365	unset($args['suffix']);
366	unset($args['maxRecords']);
367	unset($args['fields']);
368	unset($args['sort_mode']);
369	unset($args['load_delay']);
370	unset($args['popup_width'], $args['popup_height']);
371
372	$args->setDefaultFilter('text');
373
374	TikiLib::lib('smarty')->loadPlugin('smarty_modifier_escape');
375
376	$filters = '';
377	foreach ($args as $key => $arg) {
378		$filters .= '<input type="hidden" name="filter~' . $key . '" value="' . smarty_modifier_escape($arg) . '"/>';
379	}
380
381	if ($maxRecords) {
382		$maxRecords = '<input type="hidden" name="maxRecords" value="' . (int)$maxRecords . '"/>';
383	}
384
385	if ($sort_mode) {
386		$sort_mode = '<input type="hidden" name="sort_mode" value="' . $sort_mode . '"/>';
387	}
388
389	$fieldList = '';
390	if (! empty($fields)) {
391		$fieldList = '<input type="hidden" name="fields" value="' . smarty_modifier_escape(implode(',', $fields)) . '"/>';
392	}
393
394	$popup_config = [];
395	if ($popup_width && preg_match('/\d+[%]?/', $popup_width)) {
396		$popup_config['width'] = $popup_width;
397	}
398	if ($popup_height && preg_match('/\d+[%]?/', $popup_height)) {
399		$popup_config['height'] = $popup_height;
400	}
401	if ($popup_config) {
402		$popup_config = 'data-popup-config=\'' . json_encode($popup_config) . '\'';
403	} else {
404		$popup_config = '';
405	}
406
407	$escapedLayer = smarty_modifier_escape($layer);
408	$escapedSuffix = smarty_modifier_escape($suffix);
409	return <<<OUT
410<form method="post" action="tiki-searchindex.php" class="search-box onload" style="display: none" data-result-refresh="$refresh" data-result-layer="$escapedLayer" data-result-suffix="$escapedSuffix" data-load-delay="$load_delay"{$popup_config}>
411	<p>$maxRecords$sort_mode$fieldList$filters<input type="submit" class="btn btn-primary btn-sm" /></p>
412
413</form>
414OUT;
415}
416
417function wp_map_plugin_colorpicker($body, $args)
418{
419	$headerlib = TikiLib::lib('header');
420	static $counter = 0;
421
422	$args->replaceFilter('colors', 'word');
423	$colors = array_map('wp_map_color_filter', $args->asArray('colors', ','));
424
425	if (count($colors)) {
426		$size = '25px';
427		$json = json_encode($colors);
428		$methods = <<<METHOD
429function setColor(color) {
430	$(dialog).find('.current')
431		.css('background', color);
432	feature.attributes.color = color;
433}
434function init() {
435	$(dialog)
436		.dialog({
437			autoOpen: false,
438			width: 200,
439			title: $(dialog).data('title'),
440			close: function (e) {
441				$.each(container.map.getControlsByClass('OpenLayers.Control.ModifyFeature'), function (k, control) {
442					if (feature && control) {
443						control.unselectFeature(feature);
444					}
445				});
446				$.each(container.map.getControlsByClass('OpenLayers.Control.SelectFeature'), function (k, control) {
447					if (feature && control) {
448						control.unselect(feature);
449					}
450				});
451			}
452		})
453		.append($('<div class="current" style="height: $size;"/>'));
454
455	$.each($json, function (k, color) {
456		$(dialog).append(
457			$('<div style="float: left; width: $size; height: $size;"/>')
458				.css('background', color)
459				.click(function () {
460					setColor(color);
461					vlayer.redraw();
462					if (feature.executor) {
463						feature.executor();
464					}
465				})
466		);
467	});
468}
469METHOD;
470	} else {
471		$headerlib->add_jsfile('vendor_bundled/vendor/jquery-plugins/colorpicker/js/colorpicker.js');
472		$headerlib->add_cssfile('vendor_bundled/vendor/jquery-plugins/colorpicker/css/colorpicker.css');
473		$methods = <<<METHOD
474function setColor(color) {
475	$(dialog).ColorPickerSetColor(color);
476}
477function init() {
478	$(dialog)
479		.dialog({
480			autoOpen: false,
481			width: 400,
482			title: $(dialog).data('title'),
483			close: function (e) {
484				$.each(container.map.getControlsByClass('OpenLayers.Control.ModifyFeature'), function (k, control) {
485					if (feature && control) {
486						control.unselectFeature(feature);
487					}
488				});
489				$.each(container.map.getControlsByClass('OpenLayers.Control.SelectFeature'), function (k, control) {
490					if (feature && control) {
491						control.unselect(feature);
492					}
493				});
494			}
495		})
496		.ColorPicker({
497			flat: true,
498			onChange: function (hsb, hex) {
499				feature.attributes.color = '#' + hex;
500				vlayer.redraw();
501				if (feature.executor) {
502					feature.executor();
503				}
504			}
505		});
506}
507METHOD;
508	}
509
510	$target = 'map-colorpicker-' . ++$counter;
511
512	$full = <<<FULL
513$("#$target").closest('.map-container').bind('initialized', function () {
514	var container = this
515		, vlayer
516		, feature
517		, dialog = '#$target'
518		, defaultRules
519		;
520
521	$methods
522
523	vlayer = container.vectors;
524
525	vlayer.events.on({
526		featureselected: function (ev) {
527			var active = false;
528
529			feature = ev.feature;
530
531			$.each(container.map.getControlsByClass('OpenLayers.Control.ModifyFeature'), function (k, control) {
532				active = active || control.active;
533				if (active) {
534					control.selectFeature(feature);
535				}
536			});
537
538			if (active && feature.attributes.intent !== 'marker') {
539				setColor(feature.attributes.color);
540				vlayer.redraw();
541				$(dialog).dialog('open');
542			}
543		},
544		featureunselected: function (ev) {
545			feature = null;
546			$(dialog).dialog('close');
547
548			vlayer.styleMap = container.defaultStyleMap;
549			$.each(container.map.getControlsByClass('OpenLayers.Control.ModifyFeature'), function (k, control) {
550				if (ev.feature && control.active) {
551					control.unselectFeature(ev.feature);
552				}
553			});
554		},
555		beforefeaturemodified: function (ev) {
556			defaultRules = this.styleMap.styles["default"].rules;
557			this.styleMap.styles["default"].rules = [];
558		},
559		afterfeaturemodified: function (ev) {
560			this.styleMap.styles["default"].rules = defaultRules;
561			this.redraw();
562		}
563	});
564
565	init();
566});
567FULL;
568
569	$headerlib->add_js($full);
570
571	$title = tr('Color Picker');
572	return "<div id=\"$target\" data-title=\"$title\"></div>";
573}
574
575function wp_map_color_filter($color)
576{
577	$color = strtolower($color);
578	if (preg_match('/^[0-9a-f]{3}([0-9a-f]{3})?$/', $color)) {
579		return "#$color";
580	} else {
581		return $color;
582	}
583}
584