1/*
2  All scheduleMap behaviors managed by schedule plugin
3
4  scheduleMapList: Map of
5    mapId => scheduleMap
6
7  scheduleMap
8    mapId:        div map ID
9    map:          map
10    poi:          Point Of Interest
11    poiSize:      # of number
12    clusterLayer: clusterLayer
13*/
14var scheduleMapList = {};
15
16var scheduleUseMap          = JSINFO['schedule']['useMap'];
17var scheduleZoom            = JSINFO['schedule']['defaultZoom'];
18var scheduleCenter          = JSINFO['schedule']['defaultCenter'];
19var schedulePoiUri          = DOKU_BASE+'lib/plugins/schedule/images/poi.png';
20var scheduleSelectedPoiUri  = DOKU_BASE+'lib/plugins/schedule/images/poiRed.png';
21var scheduleEmptyUri        = DOKU_BASE+'lib/plugins/schedule/images/empty.png';
22var scheduleIconCalendarUri = DOKU_BASE+'lib/plugins/schedule/images/calendar.png';
23var scheduleAjaxPoiUri      = DOKU_BASE+"lib/plugins/schedule/ajaxPOI.php";
24var schedulePrecision       = 5;
25
26var locationFormatRE       = new RegExp ("^\\s*\\(\\s*([\\-0-9.]+)\\s*\\|\\s*([\\-0-9.]+)\\s*\\)\\s*(.*)$");
27
28var iconStyle;
29var iconSelectedStyle;
30
31/* selected LI on city UL */
32var scheduleSelectedCity = null;
33
34// ========================================
35/* format location */
36function scheduleGetLocation (lat, lon) {
37    return "("+
38	parseFloat (parseFloat (lat).toFixed (schedulePrecision))+"|"+
39	parseFloat (parseFloat (lon).toFixed (schedulePrecision))+")";
40}
41
42// ========================================
43/* convert and reverse location */
44function scheduleGetCoordFromLocation (latLonArray) {
45    return ol.proj.transform ([latLonArray [1], latLonArray [0]], 'EPSG:4326', 'EPSG:3857');
46}
47
48// ========================================
49/* convert and reverse location */
50function scheduleGetLocationFromCoord (coord) {
51    var lonLat = ol.proj.transform (coord, 'EPSG:3857', 'EPSG:4326');
52    return [parseFloat (lonLat[1]).toFixed (schedulePrecision), parseFloat (lonLat [0]).toFixed (schedulePrecision)];
53}
54
55// ========================================
56/* center scheduleMap according POI markers */
57function scheduleCenterMap (scheduleMap) {
58    if (!scheduleUseMap)
59    	return;
60    if (scheduleMap.poiSize) {
61	scheduleMap.map.getView ().fit (scheduleMap.poi.getExtent (), scheduleMap.map.getSize ());
62	scheduleMap.map.getView ().setZoom (Math.min (scheduleMap.map.getView ().getZoom ()*.9,
63						      scheduleZoom));
64	return;
65    }
66    scheduleMap.map.getView ().setCenter (scheduleGetCoordFromLocation (scheduleCenter));
67    scheduleMap.map.getView ().setZoom (6); //scheduleZoom);
68}
69
70// ========================================
71/* remove all POI markers */
72function scheduleClearMarkers (scheduleMap) {
73    if (!scheduleUseMap)
74    	return;
75    scheduleMap.poiSize = 0;
76    if (scheduleMap.poi !== undefined)
77	scheduleMap.poi.clear ();
78}
79
80// ========================================
81/* add a wellknown city marker */
82function scheduleAddCityMarker (scheduleMap, cityCode) {
83    if (! cityCode)
84	return;
85    var inseeLocation = inseeCityNameLatLon[cityCode];
86    if (!style) {
87	// XXX not found
88	return;
89    }
90    scheduleAddLatLonCityMarker (scheduleMap, cityCode,
91				 inseeCityNameLatLon[cityCode][1],
92				 inseeCityNameLatLon[cityCode][2]);
93}
94
95// ========================================
96/* add a positioned city marker */
97function scheduleAddLatLonCityMarker (scheduleMap, cityCode, lat, lon) {
98    if (!scheduleUseMap)
99    	return;
100    scheduleMap.poiSize++;
101    if (scheduleMap.poi === undefined)
102	return;
103    scheduleMap.poi.addFeature (new ol.Feature({
104	geometry: new ol.geom.Point (scheduleGetCoordFromLocation ([lat, lon])),
105	cityCode: cityCode,
106	location: scheduleGetLocation (lat, lon),
107	lat: lat,
108	lon: lon,
109	selected: false
110    }));
111}
112
113// ========================================
114/* highlight all cities on the list of adress */
115function scheduleHighlightAddress (locations) {
116    if (!locations) {
117	jQuery ('div.scheduleAddresse').removeClass("poiAdrLight");
118	return;
119    }
120    jQuery ('div.scheduleAddresse').each (function () {
121	var adrBlock = jQuery (this);
122	var location = adrBlock.attr ('location');
123	if (location && (','+locations).indexOf (location) >= 0)
124	    adrBlock.addClass("poiAdrLight");
125	else
126	    adrBlock.removeClass("poiAdrLight");
127    });
128}
129
130function scheduleHighlightLocation (location) {
131    scheduleHighlightAddress (location);
132    scheduleHighlightPOI (location);
133}
134
135// ========================================
136/* display cities from a UL */
137function scheduleAddAllCityFromUl (scheduleMap, ul) {
138    scheduleClearMarkers (scheduleMap);
139    ul.find ('li').each (function (index) {
140	var li = jQuery (this);
141	scheduleAddLatLonCityMarker (scheduleMap, li.attr ('insee'),
142				     li.attr ('lat'), li.attr ('lon'));
143    });
144    if (!scheduleUseMap)
145    	return;
146    if (scheduleMap.poi !== undefined){
147	scheduleMap.poi.changed ();
148	scheduleMap.map.render ();
149    }
150}
151
152// ========================================
153function scheduleAddCityToUl (ul, cityName) {
154    var cityCode = /[0-9AB]{5,}/.exec (cityName);
155    var lat = "";
156    var lon = "";
157    if (cityCode != null)
158	cityCode = cityCode[0];
159    if (!cityCode)
160	cityCode = inseeCityNameInsee [cityName.toLowerCase ()];
161    if (!cityCode)
162	cityName = cityCode = cityName.replace(/[^A-ZÀÂÆÇÉÈÊËÎÏÑÔŒÙÛÜÿ a-zàâæçéèêëîïñôœùûüÿ'\-]/gi,'');
163    else {
164	cityName = inseeCityNameLatLon[cityCode][0];
165	lat = inseeCityNameLatLon[cityCode][1];
166	lon = inseeCityNameLatLon[cityCode][2];
167    }
168    var recorded = false;
169    ul.find ('li[insee="'+cityCode+'"][lat="'+lat+'"][lon="'+lon+'"]').each (function (index) {
170	recorded = true;
171	scheduleSetSelectCity (jQuery (this));
172    });
173    if (recorded)
174	return;
175    var lonLat =
176	(lat && lon) ?
177	' lat="'+lat+'" lon="'+lon+'"' :
178	' class="unknown"';
179    ul.append ('<li insee="'+cityCode+'"'+lonLat+' onclick="scheduleSelectCity (this)">'+
180	       '<img class="checked" src="'+scheduleEmptyUri+'" width="10" height="16" onclick="scheduleRemoveCity (this)"/>'+
181	       ' <span class="addresse"></span> '+
182	       '<span class="city">'+cityName+'</span></li>');
183    scheduleSetSelectCity (ul.find ("li").last ());
184}
185
186// ========================================
187/* Sort UL by city name (XXX what about addresse ?) */
188function scheduleSortUlCities (ul) {
189    ul.children ('li').sort (function (a, b) {
190	var upa = jQuery (a).find ('span.city').text ().toUpperCase ();
191	var upb = jQuery (b).find ('span.city').text ().toUpperCase ();
192	return (upa < upb) ? -1 : (upa > upb) ? 1 : 0;
193    }).appendTo (ul[0]);
194}
195
196function scheduleAjaxPOILine (action) {
197    if (!scheduleSelectedCity)
198	// XXX dire quelque chose
199	return;
200    var insee = scheduleSelectedCity.attr ('insee');
201    var lat = scheduleSelectedCity.attr ('lat');
202    var lon = scheduleSelectedCity.attr ('lon');
203    var addr = scheduleSelectedCity.find ('span.addresse').html ().replace (/<br>/gi, '~br~');
204    line = new Array ();
205    line.push (insee);
206    line.push (lat);
207    line.push (lon);
208    line.push (addr);
209    jQuery.ajax ({
210	type: "POST",
211	url: scheduleAjaxPoiUri,
212	cache: true,
213	async: true,
214        data: { action: action, line: line.join ('|') },
215	success: function (response) { alert (response); }
216    });
217}
218
219function scheduleAddInsee () {
220    scheduleAjaxPOILine ("add");
221}
222function scheduleRemoveInsee () {
223    scheduleAjaxPOILine ("remove");
224}
225function scheduleTestInsee () {
226}
227
228function scheduleSetSelectCity (li) {
229    scheduleSelectedCity = li;
230    // change autocomplete
231    var cityCode = scheduleSelectedCity.attr ('insee');
232    var form = scheduleSelectedCity.closest ('.scheduleCitiesForm');
233    var input = form.find ('input[name="addr"]');
234    input. val (scheduleSelectedCity.find ('span.addresse').html ().replace (/<br>/gi, '~br~'));
235    jQuery.ajax ({
236	type: "POST",
237	url: scheduleAjaxPoiUri,
238	cache: true,
239	async: true,
240        data: { action: "list", insee: cityCode },
241	success: function (response) {
242	    var db = jQuery.parseJSON (response);
243	    source = new Array ();
244	    for (var i = 0; i < db.length; i++) {
245		var vals = db[i].split ("|");
246		source.push ("("+vals[0]+"|"+vals[1]+") "+vals[2]);
247	    }
248	    input.autocomplete ({ source: source });
249	}
250    });
251}
252
253function scheduleFocusCity () {
254    if (!scheduleUseMap)
255	return;
256    var form = scheduleSelectedCity.closest ('.scheduleCitiesForm');
257    // focus on map
258    form.find ('.scheduleMap').each (function () {
259	var scheduleMap = scheduleMapList [jQuery (this).attr ('id')];
260	scheduleMap.map.getView ().setCenter (scheduleGetCoordFromLocation ([scheduleSelectedCity.attr ('lat'), scheduleSelectedCity.attr ('lon')]));
261	scheduleMap.map.getView ().setZoom (Math.min (scheduleMap.map.getView ().getZoom (), scheduleZoom));
262    });
263}
264
265// ========================================
266/* User LI selection and focus on map */
267function scheduleSelectCity (item) {
268    scheduleSetSelectCity (jQuery (item).closest ('li'));
269    scheduleFocusCity ();
270}
271
272// ========================================
273/* User remove LI */
274function scheduleRemoveCity (item) {
275    var li = jQuery (item).closest ('li');
276    var ul = li.closest ('ul');
277    li.remove ();
278    scheduleUpdateUlCity (ul);
279}
280
281function scheduleUpdateUlCity (ul) {
282    ul.closest ('.scheduleCitiesForm'). find ('.scheduleMap').each (function () {
283	var scheduleMap = scheduleMapList [jQuery (this).attr ('id')];
284	scheduleAddAllCityFromUl (scheduleMap, ul);
285	scheduleCenterMap (scheduleMap);
286    });
287    scheduleCheckInputs ();
288}
289
290// ========================================
291/* Find all initial values */
292function scheduleInitPOI () {
293    jQuery ('.schedulePOI').each (function () {
294	var poiDiv = jQuery (this);
295	var scheduleMap = scheduleMapList [poiDiv.find ('.scheduleMap').attr ('id')];
296	scheduleClearMarkers (scheduleMap);
297	poiDiv.find ('span.poiLocations').each (function () {
298	    var poiLocations = jQuery (this).text ().split (',');
299	    for (var i = 0; i < poiLocations.length; i++) {
300		var latLon = poiLocations[i];
301		var match = latLon.match (locationFormatRE);
302		if (match) {
303		    var lat = parseFloat (match [1]).toFixed (schedulePrecision);
304		    var lon = parseFloat (match [2]).toFixed (schedulePrecision);
305		    scheduleAddLatLonCityMarker (scheduleMap, "", lat, lon);
306		} else
307		    // cityCode = latLon
308		    scheduleAddCityMarker (scheduleMap, latLon);
309	    }
310	    scheduleCenterMap (scheduleMap);
311	});
312	poiDiv.find ('ul.poiLatLon').each (function () {
313	    jQuery (this).find ('li').each (function (index) {
314		var li = jQuery (this);
315		scheduleAddLatLonCityMarker (scheduleMap, li.text (),
316					     li.attr ('lat'), li.attr ('lon'));
317	    });
318	    scheduleCenterMap (scheduleMap);
319	});
320    });
321    jQuery ('.scheduleCitiesForm').each (function () {
322	var scheduleCitiesForm = jQuery (this);
323	var scheduleMapDiv = scheduleCitiesForm.find ('.scheduleMap');
324	if (scheduleMapDiv[0] === undefined)
325	    return;
326	var scheduleMap = scheduleMapList [scheduleMapDiv.attr ('id')];
327	scheduleAddAllCityFromUl (scheduleMap, jQuery (this).find ('.cities'));
328	scheduleCenterMap (scheduleMap);
329    });
330}
331
332// ========================================
333/* Find all maps */
334function scheduleInitMaps () {
335    if (!scheduleUseMap)
336    	return;
337
338    jQuery ('.scheduleMap').each (function () {
339	var mapDiv = jQuery (this).find ('div');
340	if (mapDiv !== undefined && mapDiv && mapDiv.length)
341	    return;
342	var mapId = jQuery (this).attr ('id');
343	var osm = new ol.layer.Tile ({
344            source: new ol.source.OSM ()
345	});
346	var poi = new ol.source.Vector ({
347	    features: []
348	});
349	iconStyle = new ol.style.Style ({
350	    image: new ol.style.Icon (/** @type {olx.style.IconOptions} */ ({
351		anchor: [.5, 1],
352		anchorXUnits: 'fraction',
353		anchorYUnits: 'fraction',
354		opacity: 0.75,
355		src: schedulePoiUri
356	    }))
357	});
358	iconSelectedStyle = new ol.style.Style ({
359	    image: new ol.style.Icon (/** @type {olx.style.IconOptions} */ ({
360		anchor: [.5, 1],
361		anchorXUnits: 'fraction',
362		anchorYUnits: 'fraction',
363		opacity: 0.75,
364		src: scheduleSelectedPoiUri
365	    }))
366	});
367	var clusters = new ol.source.Cluster({
368	    distance: 10,
369	    source: poi,
370	});
371	var styleCache = {false: {}, true: {}};
372	var clusterLayer = new ol.layer.Vector({
373	    source: clusters,
374	    style: function (feature) {
375		var features = feature.get ('features');
376	     	var size = features.length;
377		if (size < 2)
378		    return features [0].get ('selected') ? iconSelectedStyle : iconStyle;
379		var selected = false;
380		features.forEach (function (item) {
381		    if (item.get ('selected'))
382			selected = true;
383		});
384
385		var style = styleCache[selected][size];
386		if (!style) {
387		    style = [new ol.style.Style({
388			image: new ol.style.Icon (/** @type {olx.style.IconOptions} */ ({
389			    anchor: [.5, 1],
390			    anchorXUnits: 'fraction',
391			    anchorYUnits: 'fraction',
392			    opacity: 0.75,
393			    src: selected ? scheduleSelectedPoiUri : schedulePoiUri
394			})),
395			text: new ol.style.Text({
396                            text: size.toString (),
397                            offsetY: -24 // XXX textpos conf ?
398			})
399		    })];
400		    styleCache[selected][size] = style;
401		}
402		return style;
403	    }
404	});
405	var map = new ol.Map ({
406            target: mapId,
407            layers: [osm, clusterLayer],
408            //logo: false,
409            controls: ol.control.defaults ({
410		zoom: true,
411		attribution: false,
412		rotate: false
413            }),
414	    view: new ol.View ({
415		center: ol.proj.fromLonLat (scheduleCenter),
416		zoom: scheduleZoom
417	    })
418	});
419	if (jQuery (this).hasClass ("scheduleMapDisplay"))
420	    map.set ("mapType", "display");
421	if (jQuery (this).hasClass ("scheduleMapForm"))
422	    map.set ("mapType", "form");
423	if (jQuery (this).hasClass ("scheduleMapCalendar"))
424	    map.set ("mapType", "calendar");
425
426	map.on ("singleclick", function (evt) {
427	    var f;
428	    map.forEachFeatureAtPixel (evt.pixel, function (feature) {
429		f = feature;
430		return true;
431	    });
432	    switch (map.get ("mapType")) {
433	    case "display":
434		if (f) {
435		    if (f.get ('features').length)
436			f = f.get ('features')[0];
437		    window.open ("https://www.openstreetmap.org/?mlat="+f.get ('lat')+"&mlon="+f.get ('lon')+"&zoom="+scheduleZoom, "_self");
438		}
439		break;
440	    case "form":
441		var location = scheduleGetLocationFromCoord (evt.coordinate);
442		scheduleSelectedCity.attr ('lat', location[0]);
443		scheduleSelectedCity.attr ('lon', location[1]);
444		scheduleAddAllCityFromUl  (scheduleMap, scheduleSelectedCity.closest ('ul'));
445		break;
446	    }
447	});
448	map.on ("pointermove", function (evt) {
449	    var locations = new Set ();
450	    map.forEachFeatureAtPixel (evt.pixel, function (feature) {
451		feature.get ('features').forEach (function (item) {
452		    locations.add (item.get ('location'));
453		});
454	    });
455	    locations = Array.from (locations);
456	    scheduleHighlightDays (locations);
457	    scheduleHighlightLocation (locations.join (','));
458	});
459	var scheduleMap = {
460	    mapId: mapId,
461	    map: map,
462	    poiSize: 0,
463	    poi: poi,
464	    clusterLayer: clusterLayer
465	};
466
467	scheduleMapList[mapId] = scheduleMap;
468	scheduleCenterMap (scheduleMap);
469    });
470}
471
472// ========================================
473/* Initialisation and attach function */
474jQuery (function () {
475    jQuery (function () {
476	// create tabs before OpenLayers stuff (important !)
477	jQuery (".scheduleTabForm").tabs ({
478	    active: 0
479	});
480	jQuery ("#scheduleHelp").accordion ({
481	    collapsible: true,
482	    animated: false,
483	    active: false
484	});
485	jQuery.datepicker.setDefaults ({
486	    showOn: "both",
487	    buttonImageOnly: true,
488	    buttonImage: scheduleIconCalendarUri,
489	    buttonText: "",
490	    firstDay: 0
491	});
492	jQuery.datepicker.formatDate ("yy-mm-dd");
493	jQuery (".scheduleTabForm .date").datepicker ();
494
495	divError = jQuery ("div.schedule").prev ("div.error");
496	if (divError !== undefined && divError && divError.length)
497	    // XXX a tester (avant divError.size ())
498	    scheduleForceCheckInputs ();
499    });
500
501    // check and format form request
502    jQuery ('.scheduleFinalForm').submit (function () {
503	var scheduleFinalForm = jQuery (this);
504	if (!scheduleForceCheckInputs ())
505	    return false;
506	var scheduleForm = scheduleFinalForm.closest ('.scheduleTabForm');
507	var scheduleCitiesForm = scheduleForm.find ('.scheduleCitiesForm');
508	var cities = new Array ();
509	var lats = new Array ();
510	var lons = new Array ();
511	var addrs = new Array ();
512	scheduleCitiesForm.find ('li').each (function (index) {
513	    var li = jQuery (this);
514	    cities.push (li.attr ('insee'));
515	    lats.push (li.attr ('lat'));
516	    lons.push (li.attr ('lon'));
517	    addrs.push (li.find ('span.addresse').html ());
518	});
519	scheduleFinalForm.append ('<input type="hidden" name="schd[where]" value="'+cities.join (",")+'"/>');
520	scheduleFinalForm.append ('<input type="hidden" name="schd[lat]" value="'+lats.join (",")+'"/>');
521	scheduleFinalForm.append ('<input type="hidden" name="schd[lon]" value="'+lons.join (",")+'"/>');
522	scheduleFinalForm.append ('<input type="hidden" name="schd[addr]" value="'+addrs.join ("|").replace (/<br>/gi, '~br~')+'"/>');
523
524	var scheduleMiscForm = scheduleForm.find ('.scheduleMiscForm');
525	scheduleMiscForm.find ('input[type="text"]').each (function (index) {
526	    if (this.value)
527		scheduleFinalForm.append ('<input type="hidden" name="'+this.name+'" value="'+this.value.replace(/"/gi, "''")+'"/>');
528	});
529	scheduleMiscForm.find ('select:not([class="members"])').each (function (index) {
530	    scheduleFinalForm.append ('<input type="hidden" name="'+this.name+'" value="'+this.value+'"/>');
531	});
532	var members = new Array ();
533	scheduleMiscForm.find ('select[class="members"]').each (function (index) {
534	    jQuery (this).find ('option:selected').each (function (index) {
535		members.push (jQuery (this).val ());
536	    });
537	});
538	if (members.length > 0)
539	    scheduleFinalForm.append ('<input type="hidden" name="schd[member]" value="'+members.join (",")+'"/>');
540	scheduleMiscForm.find ('textarea').each (function (index) {
541	    var val = jQuery (this).val ().replace(/"/gi, "''");
542	    var name = jQuery (this).attr ('name');
543	    scheduleFinalForm.append ('<input type="hidden" name="'+name+'" value="'+val+'"/>');
544	});
545	return true;
546    });
547
548    // city validation
549    jQuery ('.scheduleCitiesForm input[name="city"]').keypress (function (e) {
550	if (e.which != 13)
551	    return;
552	var input = jQuery (this);
553	var form = input.closest ('.scheduleCitiesForm');
554	var cityName = input.val ();
555	if (!cityName)
556	    return false;
557	input.val ("");
558	form.find ('input[name="addr"]').val ("");
559	var ul = form.find ('ul');
560	scheduleAddCityToUl (ul, cityName);
561	scheduleSortUlCities (ul);
562	scheduleUpdateUlCity (ul);
563	return false;
564    });
565
566    // full adress validation
567    jQuery ('.scheduleCitiesForm input[name="addr"]').keypress (function (e) {
568	if (e.which != 13)
569	    return;
570	if (!scheduleSelectedCity)
571	    return;
572	var input = jQuery (this);
573	var form = input.closest ('.scheduleCitiesForm');
574	var addr = input.val ();
575	var match = addr.match (locationFormatRE);
576	if (match) {
577	    addr = match [3];
578	    scheduleSelectedCity.attr ('lat', parseFloat (match [1]).toFixed (schedulePrecision));
579	    scheduleSelectedCity.attr ('lon', parseFloat (match [2]).toFixed (schedulePrecision));
580	    var scheduleMap = scheduleMapList [form.find ('.scheduleMap').attr ('id')];
581	    scheduleAddAllCityFromUl (scheduleMap, scheduleSelectedCity.closest ('ul'));
582	    scheduleFocusCity ();
583	}
584	input.val (addr);
585	scheduleSelectedCity.find ('span.addresse').html (addr.replace (/~br~/gi, '<br/>'));
586    });
587
588    // default form validation for each text field
589    jQuery ('.scheduleMiscForm input').keypress (function (e) {
590	if (e.which != 13)
591	    return;
592	jQuery (this).closest ('.scheduleTabForm').find ('input[type="submit"]').first ().trigger ('click');
593    });
594
595    // init maps
596    scheduleInitMaps ();
597    scheduleInitPOI ();
598});
599
600// ========================================
601