1<?php
2/**
3 * DokuWiki Plugin Googlemaps3
4 *
5 * @license		GPL 3 (https://www.gnu.org/licenses/gpl-3.0.html)
6 * @author		Bernard Condrau <bernard@condrau.com>
7 * @version		2021-05-12, for Google Maps v3 API and DokuWiki Hogfather
8 * @see			https://www.dokuwiki.org/plugin:googlemaps3
9 * @see			https://www.dokuwiki.org/plugin:googlemaps
10 *
11 * Complete rewrite of Christopher Smith's Google Maps Plugin from 2008 with additional functionality
12 * syntax.php	plugin syntax definition
13 */
14// Must be run within Dokuwiki
15if(!defined('DOKU_INC')) die();
16
17/**
18 * Syntax for Google Maps v3
19 */
20class syntax_plugin_googlemaps3 extends DokuWiki_Syntax_Plugin {
21
22	private $mapID = 0;
23	private $markerID = 0;
24
25	private $defaultMapOptions = array(
26		'mapID' => 0,						// used to allow css override
27		'type' => 'roadmap',				// roadmap, hybrid, satellite, terrain
28		'width' => '',						// default style in css file
29		'height' => '',						// default style in css file
30		'lat'  => 12.57076,					// lat+lng are mandatory
31		'lng' => 99.96260,					// lat+lng are mandatory
32		'address' => '',
33		'zoom' => 0,						// zoom is mandatory
34		'language' => '',					// google maps defaults to language set in browser
35		'region' => '',						// google maps defaults region bias to US
36		'disableDefaultUI' => 0,			// google maps UI defaults
37		'zoomControl' => 1,
38		'mapTypeControl' => 1,
39		'scaleControl' => 1,
40		'streetViewControl' => 1,
41		'rotateControl' => 1,
42		'fullscreenControl' => 1,
43		'kml' => 'off',
44    );
45	private $defaultMarkerOptions = array(
46		'markerID' => 0,					// used to allow css override
47		'lat'  => 0,
48		'lng' => 0,
49		'title' => '',
50		'icon' => '',
51		'info' => '',
52		'dir' => '',
53		'img' => '',
54		'width' => '',
55	);
56	/**
57     * Syntax Type
58     *
59     * Needs to return one of the mode types defined in $PARSER_MODES in parser.php
60     *
61     * @return string
62     */
63    public function getType() {
64        return 'substition';
65    }
66
67    /**
68     * Paragraph Handling
69     *
70     * @return string
71     */
72    public function getPType() {
73        return 'block';
74    }
75
76    /**
77     * Sort for applying this mode
78     *
79     * @return int
80     */
81    public function getSort() {
82        return 900;
83    }
84
85    /**
86     * @param string $mode
87     */
88    function connectTo($mode) {
89	   $this->Lexer->addSpecialPattern('<googlemaps3 ?[^>\n]*>.*?</googlemaps3>',$mode,'plugin_googlemaps3');
90    }
91
92    /**
93     * Handler to prepare matched data for the rendering process
94     *
95     * @param   string       $match   The text matched by the patterns
96     * @param   int          $state   The lexer state for the match
97     * @param   int          $pos     The character position of the matched text
98     * @param   Doku_Handler $handler The Doku_Handler object
99     * @return  bool|array Return an array with all data you want to use in render, false don't add an instruction
100     */
101	function handle($match, $state, $pos, Doku_Handler $handler){
102
103		static $initialised = false;
104
105		list($mapOptions, $markerOptions) = explode('>',substr($match,12,-14),2);
106
107		$map = $this->getMapOptions($mapOptions);
108		$markers = $this->getMarkers($markerOptions);
109
110		// determine width and height (inline styles) for the map image
111		if ($map['width'] || $map['height']) {
112			$style = $map['width'] ? 'width: '.(is_numeric($map['width']) ? $map['width'].'px' : $map['width']).";" : "";
113			$style .= $map['height'] ? 'height: '.(is_numeric($map['height']) ? $map['height'].'px' : $map['height']).";" : "";
114//			$style = $map['width'] ? 'width: '.$map['width'].";" : "";
115//			$style .= $map['height'] ? 'height: '.$map['height'].";" : "";
116			$style = "style='$style'";
117		} else {
118			$style = '';
119		}
120		unset($map['width'],$map['height']);
121
122		// determine region and language
123		$lang  = ($map['region'] ? '&region='.$map['region'] : ($this->getConf('region') ? '&region='.$this->getConf('region') : ''));
124		$lang .= ($map['language'] ? '&language='.$map['language'] : ($this->getConf('language') ? '&language='.$this->getConf('language') : ''));
125		unset($map['region'],$map['language']);
126
127		// create a javascript parameter string for the map
128		$jsOptions = '';
129		foreach ($map as $key => $val) {
130			$jsOptions .= is_numeric($val) ? "$key : $val," : (is_bool($val) ? "$key : ".(int)$val."," : "$key : '".hsc($val)."',");
131		}
132
133		// create a javascript serialisation of the markers data
134		$jsMarker = '';
135		if (!empty($markers)) {
136			foreach ($markers as $marker) {
137				$jsMarker .=	"{markerID:".$marker['markerID'].", lat:".$marker['lat'].", lng:".$marker['lng'].
138									($marker['title'] ? ", title:'".$marker['title']."'" : "").
139									($marker['icon'] ? ", icon:'".$marker['icon']."'" : "").
140									($marker['info'] ? ", info:'".$marker['info']."'" : "").
141									($marker['dir'] ? ", dir:'".$marker['dir']."'" : "").
142									($marker['img'] ? ", img:'".$marker['img']."'" : "").
143									($marker['width'] ? ", width:'".$marker['width']."'" : "").
144          						"},";
145				}
146				$jsMarker = "marker : [ ".$jsMarker." ]";
147		}
148
149		if ($initialised) {
150			$jsData = '';
151		} else {
152			$initialised = true;
153			$jsData = 'var googlemaps3 = new Array();';
154		}
155		$jsData .= "googlemaps3[googlemaps3.length] = {".$jsOptions.$jsMarker." };";
156		return array($map['mapID'], $style, $lang, $jsData);
157	}
158
159    /**
160     * Renders the map in the wiki page
161     *
162     * @param string        $mode     output format being rendered
163     * @param Doku_Renderer $renderer the current renderer object
164     * @param array         $data     data created by handler()
165     * @return  boolean                 rendered correctly? (however, returned value is not used at the moment)
166     */
167	function render($mode, Doku_Renderer $renderer, $data) {
168
169		static $initialised = false;
170
171		if ($mode == 'xhtml') {
172			list($mapID, $style, $lang, $jsData) = $data;
173
174			// include script only once
175			if (!$initialised) {
176				$initialised = true;
177				$renderer->doc .= "<script src='https://maps.googleapis.com/maps/api/js?key=".$this->getConf('key').$lang."&callback=initMap' defer></script>";
178			}
179			$renderer->doc .= "<script>$jsData</script>";
180			$renderer->doc .= "<div id='googlemaps3map".$mapID."' class='googlemaps3'".($style ? ' '.$style : '')."></div>";
181			return true;
182		}
183		return false;
184	}
185
186    /**
187     * extract map options
188     *
189     * @param	string	$pattern	string of map options
190     * @return	array				associative array of map options
191     */
192	private function getMapOptions($pattern) {
193
194		$options = array();
195		preg_match_all('/(\w*)="(.*?)"/us', $pattern, $options, PREG_SET_ORDER);
196
197		// parse match for instructions
198		$map = $this->defaultMapOptions;
199		$map['mapID'] = ++$this->mapID;
200		foreach($options as $option) {
201			list($match, $key, $val) = $option;
202			if (isset($map[$key])) if ($key=='kml') $map[$key] = $val; else $map[$key] = strtolower($val);
203			if (isset($map[$key])) {
204				if ($val=='true') $val = 1; elseif ($val=='false') $val = 0;
205				$map[$key] = $val;
206			}
207		}
208		return $map;
209	}
210
211    /**
212     * extract markers information
213     *
214     * @param	string	$pattern	multi-line string of markers
215     * @return	array				multi-dimensional associative array of markers
216     */
217	private function getMarkers($pattern) {
218
219		$dlm = $this->getConf('delim');
220		$markers = array();
221		preg_match_all('/.+/', $pattern, $lines, PREG_PATTERN_ORDER); // get all markers
222		foreach ($lines[0] AS $line) {
223			preg_match_all('/(?<=\\'.$dlm.'|^|\n)(.*?)(?=\\'.$dlm.'|$|\n)/u', $line, $matches,PREG_PATTERN_ORDER); // get marker options
224			$markers[] = array_combine(array_keys($this->defaultMarkerOptions), array_merge(array(0), $matches[0], array_fill(0, count($this->defaultMarkerOptions)-count($matches[0])-1,'')));
225		}
226
227		// trim leading "delimiter" if any and get default if option empty
228		foreach ($markers as $mark => $marker) {
229			foreach ($marker as $option => $value) {
230				if ($value) $markers[$mark][$option] = ltrim($markers[$mark][$option], $dlm);
231				if (!$value) $markers[$mark][$option] = $this->defaultMarkerOptions[$option];
232			}
233		}
234		foreach ($markers as $mark => $marker) {
235			$markers[$mark]['markerID'] = ++$this->markerID;
236			if ($markers[$mark]['lat'] == 'address') {
237				$markers[$mark]['lat'] = "'".$markers[$mark]['lat']."'";
238				$markers[$mark]['lng'] = "'".$markers[$mark]['lng']."'";
239			} else {
240				$markers[$mark]['lat'] = is_numeric($marker['lat']) ? floatval($marker['lat']) : 0;
241				$markers[$mark]['lng'] = is_numeric($marker['lng']) ? floatval($marker['lng']) : 0;
242			}
243			$markers[$mark]['icon'] = ($marker['icon'] && strpos($marker['icon'], '.') ? $this->getConf('path').$marker['icon'] : $marker['icon']);
244			$markers[$mark]['info'] = str_replace("\n","", p_render("xhtml", p_get_instructions($markers[$mark]['info']), $info));
245		}
246		return $markers;
247    }
248}
249