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