c7be3dbb00abedaf53790952a4459e23bee1ea39
[syp.git] / js / admin.js
1 /* Copyright (c) 2009 Arnaud Renevier, Inc, published under the modified BSD
2  * license. */
3
4 // drag feature with tolerance
5 OpenLayers.Control.SypDragFeature = OpenLayers.Class (OpenLayers.Control.DragFeature, {
6     startPixel: null,
7     dragStart: null,
8     pixelTolerance : 0,
9     timeTolerance: 300,
10
11     downFeature: function(pixel) {
12         OpenLayers.Control.DragFeature.prototype.downFeature.apply(this, arguments);
13         this.dragStart = (new Date()).getTime(); 
14         this.startPixel = pixel; 
15     },
16
17     doneDragging: function(pixel) {
18         OpenLayers.Control.DragFeature.prototype.doneDragging.apply(this, arguments);
19         // Check tolerance. 
20         var passesTimeTolerance =  
21                     (new Date()).getTime() > this.dragStart + this.timeTolerance; 
22
23         var xDiff = this.startPixel.x - pixel.x; 
24         var yDiff = this.startPixel.y - pixel.y; 
25
26         var passesPixelTolerance =  
27         Math.sqrt(Math.pow(xDiff,2) + Math.pow(yDiff,2)) > this.pixelTolerance; 
28
29         if(passesTimeTolerance && passesPixelTolerance){ 
30             this.onComplete(this.feature, pixel);    
31         } else { 
32             var feature = this.feature; 
33             var res = this.map.getResolution(); 
34             this.feature.geometry.move(res * (this.startPixel.x - this.lastPixel.x), 
35                     res * (this.lastPixel.y - this.startPixel.y)); 
36             this.layer.drawFeature(this.feature); 
37         }
38         this.layer.drawFeature(this.feature, "select");
39     },
40
41     moveFeature: function(pixel) {
42         OpenLayers.Control.DragFeature.prototype.moveFeature.apply(this, arguments);
43         this.layer.drawFeature(this.feature, "temporary");
44     },
45
46     overFeature: function (feature) {
47         // can only drag and drop currently selected feature
48         if (feature != Admin.currentFeature) {
49             return;
50         }
51         OpenLayers.Control.DragFeature.prototype.overFeature.apply(this, arguments);
52     },
53
54     CLASS_NAME: "OpenLayers.Control.SypDragFeature"
55 });
56
57 var Admin = {
58     Settings: {
59         MARKER_ICON: "openlayers/img/marker-blue.png",
60         MARKER_ICON_HEIGHT: 25,
61         MARKER_SELECT_ICON: "openlayers/img/marker-green.png",
62         MARKER_SELECT_ICON_HEIGHT: 25,
63         MARKER_TEMPORARY_ICON: "openlayers/img/marker-gold.png",
64         MARKER_TEMPORARY_ICON_HEIGHT: 25
65     },
66
67     map: null,
68     baseLayer: null,
69     dataLayer: null,
70     selFeatureControl: null,
71     moveFeatureControl: null,
72     addFeatureControl: null,
73
74     currentFeature: null,
75     currentFeatureLocation: null,
76
77     init: function () {
78         this.map = new OpenLayers.Map ("map", {
79                 controls:[
80                     new OpenLayers.Control.Navigation (),
81                     new OpenLayers.Control.PanZoom ()
82                 ],
83                 projection: new OpenLayers.Projection("EPSG:900913"),
84                 displayProjection: new OpenLayers.Projection("EPSG:4326")
85          });
86
87          this.baseLayer = this.createBaseLayer ();
88          this.dataLayer = this.createDataLayer ();
89          this.map.addLayers([this.baseLayer, this.dataLayer]);
90
91          // controls
92          this.selFeatureControl = this.createSelectFeatureControl();
93          this.map.addControl(this.selFeatureControl);
94          this.moveFeatureControl = this.createMoveFeatureControl();
95          this.map.addControl(this.moveFeatureControl);
96          this.addFeatureControl = this.createNewfeatureControl();
97          this.map.addControl(this.addFeatureControl);
98
99          // position
100          var centerBounds = new OpenLayers.Bounds();
101
102          var mapProj = this.map.getProjectionObject();
103          var sypOrigProj = new OpenLayers.Projection("EPSG:4326");
104
105          var bottomLeft = new OpenLayers.LonLat(sypOrig[0],sypOrig[1]);
106          bottomLeft = bottomLeft.transform(sypOrigProj, mapProj);
107          var topRight = new OpenLayers.LonLat(sypOrig[2],sypOrig[3])
108          topRight = topRight.transform(sypOrigProj, mapProj);
109
110          centerBounds.extend(bottomLeft);
111          centerBounds.extend(topRight);
112
113          // at that moment, ie does not know size of the map, we need to update
114          // manually
115          this.map.updateSize();
116          this.map.zoomToExtent(centerBounds);
117
118          this.reset();
119     },
120
121     reset: function() {
122         this.addFeatureControl.deactivate();
123         this.moveFeatureControl.deactivate();
124         this.selFeatureControl.activate();
125         this.checkForFeatures();
126         $("#newfeature_button").show().val("ajouter un emplacement");
127         $("#newfeature_button").unbind("click").click(function () {
128             Admin.addNewFeature();
129         });
130     },
131
132     createBaseLayer: function () {
133         return new OpenLayers.Layer.OSM("OSM");
134     },
135
136     createDataLayer: function () {
137         var styleMap = new OpenLayers.StyleMap (
138                         {"default": {
139                              externalGraphic: this.Settings.MARKER_ICON,
140                              graphicHeight: this.Settings.MARKER_ICON_HEIGHT
141                                                   || 32 
142                                 },
143                          "temporary": { 
144                              externalGraphic: this.Settings.MARKER_TEMPORARY_ICON,
145                              graphicHeight: this.Settings.MARKER_TEMPORARY_ICON_HEIGHT
146                                                   || 32 
147                          },
148                          "select": { 
149                              externalGraphic: this.Settings.MARKER_SELECT_ICON,
150                              graphicHeight: this.Settings.MARKER_SELECT_ICON_HEIGHT
151                                                   || 32 
152                     }});
153
154         var layer = new OpenLayers.Layer.GML("KML", "items.php", 
155            {
156             styleMap: styleMap,
157             format: OpenLayers.Format.KML, 
158             projection: this.map.displayProjection,
159             eventListeners: { scope: this,
160                 loadend: this.checkForFeatures
161             }
162        });
163
164         return layer;
165     },
166
167     createMoveFeatureControl: function () {
168         var control = new OpenLayers.Control.SypDragFeature(
169                 this.dataLayer, {
170                          });
171         return control;
172     },
173
174     createSelectFeatureControl: function () {
175         var control = new OpenLayers.Control.SelectFeature(
176                 this.dataLayer, {
177                         onSelect: OpenLayers.Function.bind(this.onFeatureSelect, this)
178                          });
179         return control;
180     },
181
182     createNewfeatureControl: function () {
183         var control = new OpenLayers.Control ();
184         var handler = new OpenLayers.Handler.Click(control, {
185                 'click': OpenLayers.Function.bind(FeatureMgr.add, FeatureMgr)
186             });
187         control.handler = handler;
188         return control;
189     },
190
191     onFeatureSelect: function (feature) {
192         this.showEditor(feature);
193         FeatureMgr.reset();
194         this.selFeatureControl.deactivate();
195         this.moveFeatureControl.activate();
196     },
197
198     closeEditor: function() {
199         if (this.currentFeature && this.currentFeature.layer) {
200             this.selFeatureControl.unselect(this.currentFeature);
201         }
202         this.currentFeature = null;
203         this.currentFeatureLocation = null;
204         $("#img").removeAttr('src');
205         $("#img").parent().html($("#img").parent().html());
206         $("#img").parent().show();
207         $("#title, #description").val("");
208         $("#editor").hide();
209         // do it once before hidding and once after hidding to work in all cases
210         $("#title, #description").val(""); 
211         $("#image_file").parent().html($("#image_file").parent().html());
212         $(document).unbind("keydown");
213         this.checkForFeatures();
214         this.reset();
215     },
216
217     showEditor: function (feature) {
218         $("#newfeature_button").hide();
219         if (feature.fid) {
220             $("#delete").show();
221         } else {
222             $("#delete").hide();
223         }
224         $(document).unbind("keydown").keydown(function(e) { 
225             if (e.keyCode == 27) {
226                 Admin.cancelCurrentFeature()
227                 e.preventDefault();
228             }
229         });
230         this.currentFeature = feature;
231         this.currentFeatureLocation = new OpenLayers.Pixel(feature.geometry.x, feature.geometry.y);
232         $("#editor").show();
233         $("#instructions").text("Vous pouvez déplacer le marqueur en effectuant un glisser-déposer.");
234         $("#title").val(feature.attributes.name);
235         var fullDesc = $(feature.attributes.description).parent();
236         $("#description").val(fullDesc.find('p').text());
237         var src = fullDesc.find('img').attr('src');
238         if (src) {
239             $("#img").parent().show();
240             $("#img").attr('src', src);
241             $("#image_file").parent().hide();
242             $("#image_delete").show();
243         } else {
244             $("#img").parent().hide();
245             $("#image_file").parent().show();
246             $("#image_delete").hide();
247         }
248         $("#title").select().focus(); 
249     },
250
251     checkForFeatures: function () {
252         if (this.dataLayer.features.length != 0) {
253             $("#instructions").text("Pour modifier les données d'une image, sélectionnez le marqueur correspondant.");
254         }
255     },
256
257     addNewFeature: function () {
258         function cancel() {
259             $(document).unbind("keydown");
260             Admin.reset()
261         }
262         $(document).unbind("keydown").keydown(function(e) { 
263             if (e.keyCode == 27) {
264                 e.preventDefault();
265                 cancel();
266             }
267         });
268
269         $("#newfeature_button").val("annuler");
270         $("#newfeature_button").unbind("click").click(cancel);
271
272         $("#instructions").text("Cliquez sur la carte pour ajouter un marqueur.");
273         this.selFeatureControl.deactivate();
274         this.addFeatureControl.activate();
275         FeatureMgr.reset();
276     },
277
278     cancelCurrentFeature: function() {
279         if (AjaxMgr.running) {
280             return;
281         }
282         var feature = this.currentFeature;
283         if (feature.fid) {
284             FeatureMgr.move (feature, this.currentFeatureLocation);
285         } else {
286             this.dataLayer.removeFeatures([feature]);
287         }
288         this.closeEditor();
289     },
290
291     reloadLayer: function (layer) {
292         layer.destroyFeatures();
293         layer.loaded = false;
294         layer.loadGML();
295     },
296
297     Utils: {
298         escapeHTML: function (str) {
299             if (!str) {
300                 return "";
301             }
302             return str.
303              replace(/&/gm, '&').
304              replace(/'/gm, ''').
305              replace(/"/gm, '"').
306              replace(/>/gm, '>').
307              replace(/</gm, '&lt;');
308         },
309
310         startsWith: function (str, prefix) {
311             return (str.slice(0, prefix.length) == prefix);
312         },
313
314         indexOf: function (array, item) {
315             if (array.indexOf !== undefined) {
316                 return array.indexOf(item);
317             } else {
318                 return OpenLayers.Util.indexOf(array, item);
319             }
320         }
321     }
322 }
323
324 var FeatureMgr = {
325     reset: function() {
326         this.commError("");
327     },
328
329     add: function(evt) {
330         var map = Admin.map;
331         var pos = map.getLonLatFromViewPortPx(evt.xy);
332         feature = this.update (null, pos, "", "", "");
333         Admin.addFeatureControl.deactivate();
334         Admin.selFeatureControl.select(feature);
335     },
336
337     move: function (feature, aLocation) {
338         if (!feature || !aLocation) {
339             return;
340         }
341         var curLoc = feature.geometry;
342         feature.geometry.move(aLocation.x - curLoc.x, aLocation.y - curLoc.y);
343         feature.layer.drawFeature(feature); 
344     },
345
346     update: function(feature, lonlat, imgurl, title, description) {
347         var point = new OpenLayers.Geometry.Point(lonlat.lon, lonlat.lat);
348         if (!feature) {
349             feature = new OpenLayers.Feature.Vector(point);
350             Admin.dataLayer.addFeatures([feature]);
351         } else {
352             this.move (feature, point);
353         }
354         feature.attributes.name = title;
355         feature.attributes.description = "<p>" + Admin.Utils.escapeHTML(description) + "</p>"
356                                 + "<img src=\"" + imgurl + "\">"
357         return feature;
358     },
359
360     del: function (feature) {
361         var form = $("#feature_delete");
362         form.find('input[name="fid"]').val(feature.fid);
363         AjaxMgr.add({
364             form: form,
365             oncomplete: OpenLayers.Function.bind(this.ajaxReply, this)
366         });
367     },
368
369     save: function (feature) {
370         var x = feature.geometry.x;
371         var y = feature.geometry.y;
372
373         var mapProj = feature.layer.map.getProjectionObject();
374         var lonlat = new OpenLayers.LonLat(x, y).
375                                     transform(mapProj,
376                                               new OpenLayers.Projection("EPSG:4326"));
377         var form = $("#feature_update");
378         form.find('input[name="lon"]').val(lonlat.lon);
379         form.find('input[name="lat"]').val(lonlat.lat);
380         form.find('input[name="fid"]').val(feature.fid);
381         form.find('input[name="keep_img"]').val(
382             $("#img").attr("src") ? "yes": "no"
383         );
384
385         if (feature.fid) {
386             form.find('input[name="request"]').val("update");
387         } else {
388             form.find('input[name="request"]').val("add");
389         }
390         AjaxMgr.add({
391             form: form,
392             oncomplete: OpenLayers.Function.bind(this.ajaxReply, this)
393         });
394     },
395
396     ajaxReply: function (data) {
397         if (!data) {
398             this.commError("Il s'est produit une erreur serveur.");
399             return;
400         }
401
402         var xml = new OpenLayers.Format.XML().read(data);
403
404         switch (xml.documentElement.nodeName.toLowerCase()) {
405             case "error":
406                 switch (xml.documentElement.getAttribute("reason")) {
407                     case "unauthorized":
408                         $("#login_area").show();
409                         this.reset();
410                         Admin.reset();
411                     break;
412                     case "server":
413                         this.commError("Il s'est produit une erreur serveur.");
414                         $("title").focus();
415                     break;
416                     case "unreferenced":
417                         this.commError("La fiche n'était pas référencée sur le serveur.");
418                         Admin.reloadLayer(Admin.dataLayer);
419                         Admin.closeEditor();
420                     break;
421                     case "nochange":
422                         this.commError("Aucun changement n'a été effectué.");
423                         Admin.closeEditor();
424                     break;
425                     case "request":
426                         this.commError("Le serveur n'a pas compris la requête. Il s'agit probablement d'un bug dans SYP.");
427                         $("title").focus();
428                     break;
429                     case "toobig":
430                         this.commError("L'image est trop grande et n'a pas été acceptée par le serveur.");
431                         $("#image_file").parent().html($("#image_file").parent().html());
432                         $("#image_file").focus();
433                     break;
434                     case "notimage":
435                         this.commError("Le fichier ne semble pas être une image.");
436                         $("#image_file").parent().html($("#image_file").parent().html());
437                         $("#image_file").focus();
438                     break;
439                     default:
440                         this.commError("Il s'est produit une erreur inconnue.");
441                         $("title").focus();
442                     break;
443                 }
444             break;
445             case "success":
446                 switch (xml.documentElement.getAttribute("request")) {
447                     case "del":
448                         this.commSuccess("La suppression s'est déroulée correctement.");
449                         var someFeature = false;
450                         var self = this;
451                         $.each($(xml).find("FEATURE,feature"), function () {
452                              someFeature = true;
453                              var id = parseFloat($(this).find("ID:first,id:first").text());
454                              if ((id === null) || isNaN (id)) {
455                                 return;;
456                              }
457                              var features = Admin.dataLayer.features;
458                              for (var idx = 0; idx < features.length; idx++) {
459                                  if (features[idx].fid == id) {
460                                      Admin.dataLayer.removeFeatures([features[idx]]);
461                                  }
462                              }
463                         });
464                         if (someFeature == false) {
465                             this.commError("Le serveur a fait une réponse incohérente.");
466                         } else {
467                             Admin.closeEditor();
468                         }
469                     break;
470                     case "update":
471                     case "add":
472                         var someFeature = false;
473                         var self = this;
474                         $.each($(xml).find("FEATURE,feature"), function () {
475                                 someFeature = true;
476                                 var id = parseFloat($(this).find("ID:first,id:first").text());
477                                 if ((id === null) || isNaN (id)) {
478                                     return;;
479                                 }
480
481                                 var lon = parseFloat($(this).find("LON:first,lon:first").text());
482                                 if ((typeof (lon) != "number") || isNaN (lon) ||
483                                         (lon < -180) || (lon > 180)) {
484                                     return;;
485                                 }
486
487                                 var lat = parseFloat($(this).find("LAT:first,lat:first").text());
488                                 if ((typeof (lat) != "number") || isNaN (lat) ||
489                                         (lat < -90) || (lat > 90)) {
490                                     return;;
491                                 }
492
493                                 var mapProj = Admin.map.getProjectionObject();
494                                 var lonlat = new OpenLayers.LonLat (lon, lat).
495                                                 transform( new OpenLayers.Projection("EPSG:4326"), mapProj);
496
497                                 var imgurl = $(this).find("IMGURL:first,imgurl:first").text();
498                                 var title = $(this).find("HEADING:first,heading:first").text();
499                                 var description = $(this).find("DESCRIPTION:first,description:first").text();
500
501                                 feature = self.update (Admin.currentFeature, lonlat, imgurl, title, description); 
502                                 feature.fid = id;
503                         });
504
505                         if (someFeature == false) {
506                             this.commError("Le serveur a fait une réponse incohérente.");
507                         } else {
508                             this.commSuccess("La sauvegarde s'est déroulée correctement.");
509                             Admin.closeEditor();
510                         }
511
512                     break;
513                     default:
514                         this.commError("Le serveur a fait une réponse incohérente.");
515                    break;
516                 }
517             break;
518             default:
519                 this.commError("Le serveur a fait une réponse incohérente.");
520             break;
521         }
522     },
523
524     commSuccess: function (message) {
525         $("#server_comm").text(message);
526         $("#server_comm").removeClass().addClass("success");
527     },
528
529     commError: function (message) {
530         $("#server_comm").text(message);
531         $("#server_comm").removeClass().addClass("error");
532         if (message.length) {
533       //      this.move(Admin.currentFeature, Admin.currentFeatureLocation);
534     //        Admin.reset();
535         }
536     }
537 }
538
539 /* maintains a queue of ajax queries, so I'm sure they all execute in the same
540  * order they were defined */
541 var AjaxMgr = {
542     _queue: [],
543
544     running: false,
545
546     add: function(query) {
547         this._queue.push(query);
548         if (this._queue.length > 1) {
549             return;
550         } else {
551             this._runQuery(query);
552         }
553     },
554
555     _runQuery: function(query) {
556         var self = this;
557         $('#api_frame').one("load", function() {
558             self.running = false;
559             self._reqEnd();
560             if (typeof (query.oncomplete) == "function") {
561                 var body = null;
562                 try {
563                     if (this.contentDocument) {
564                         body = this.contentDocument.body;
565                     } else if (this.contentWindow) {
566                         body = this.contentWindow.document.body;
567                     } else {
568                         body = document.frames[this.id].document.body;
569                     }
570                 } catch (e) {}
571                     if (body) {
572                         query.oncomplete(body.innerHTML);
573                     } else {
574                         query.oncomplete(null);
575                     }
576             }
577         });
578         query.form.attr("action", "api.php");
579         query.form.attr("target", "api_frame");
580         query.form.attr("method", "post");
581         this.running = true;
582         query.form.get(0).submit();
583         if (typeof (query.onsend) == "function") {
584             query.onsend();
585         }
586     },
587
588     _reqEnd: function() {
589         this._queue.shift();
590         if (this._queue.length > 0) {
591             this._reqEnd(this._queue[0]);
592         }
593     }
594 }
595
596 var pwdMgr = {
597
598     init: function () {
599         $("#login_form").submit(this.submit);
600         $("#password").focus().select();
601     },
602
603     reset: function() {
604         this.commError ("");
605     },
606
607     submit: function () {
608         try {
609             pwdMgr.commError("");
610             var req = {
611                 form:  $("#login_form"),
612                 onsend: function() {
613                     $("#pwd_throbber").css("visibility", "visible");
614                     $("#login_error").hide();
615
616                     // we need a timeout; otherwise those fields will not be submitted
617                     window.setTimeout(function() { 
618                             // removes focus from #password before disabling it. Otherwise, opera
619                             // prevents re-focusing it after re-enabling it.
620                             $("#password").blur(); 
621                             $("#login_submit, #password").attr("disabled", "disabled");
622                     }, 0)
623                 },
624                 oncomplete: OpenLayers.Function.bind(pwdMgr.ajaxReply, pwdMgr)
625             };
626             AjaxMgr.add(req);
627         } catch(e) {}
628         return false;
629     },
630
631     ajaxReply: function (data) {
632
633         $("#pwd_throbber").css("visibility", "hidden");
634         // here, we need a timeout because onsend timeout sometimes has not been triggered yet
635         window.setTimeout(function() {
636             $("#login_submit, #password").removeAttr("disabled");
637         }, 0);
638
639         if (!data) {
640             this.commError("Il s'est produit une erreur serveur.");
641             $("#login_error").show();
642             window.setTimeout(function() {
643                     $("#password").focus().select();
644             }, 0);
645             return;
646         }
647         var xml = new OpenLayers.Format.XML().read(data);
648
649         switch (xml.documentElement.nodeName.toLowerCase()) {
650             case "error":
651                 switch (xml.documentElement.getAttribute("reason")) {
652                     case "server":
653                         this.commError("Il s'est produit une erreur serveur.");
654                     break;
655                     case "unauthorized":
656                         this.commError("Le mot de passe n'est pas correct.");
657                     break;
658                     case "request":
659                         this.commError("Le serveur n'a pas compris la requête. Il s'agit probablement d'un bug dans SYP.");
660                     break;
661                     default:
662                         this.commError("Il s'est produit une erreur inconnue.");
663                     break;
664                 }
665                 $("#login_error").show();
666                 window.setTimeout(function() {
667                         $("#password").focus().select();
668                 }, 0);
669             break;
670             case "success":
671                 this.reset();
672                 $("#login_area").hide();
673             break;
674             default:
675                 this.commError("Le serveur a fait une réponse incohérente.");
676             break;
677         }
678     },
679
680     commError: function (message) {
681         $("#login_error").text(message);
682         if (message) {
683             $("#login_error").show();
684         } else {
685             $("#login_error").hide();
686         }
687     }
688 }
689
690 $(window).load(function () {
691     // if using .ready, ie triggers an error when trying to access
692     // document.namespaces
693     pwdMgr.init();
694     $("#newfeature_button").click(function () {
695         Admin.addNewFeature();
696     });
697     $("#editor_close").click(function () {
698         Admin.cancelCurrentFeature()
699     });
700     $("#feature_update").submit(function() {
701         try {
702             FeatureMgr.save(Admin.currentFeature);
703         } catch(e) {}
704         return false;
705     });
706     $("#feature_delete").submit(function() {
707         try {
708             FeatureMgr.del(Admin.currentFeature);
709         } catch(e) {}
710         return false;
711     });
712     $("#image_delete").click(function() {
713             $("#img").removeAttr('src');
714             // needs to rebuild element otherwise some browsers still
715             // display image.
716             $("#img").parent().html($("#img").parent().html());
717             $("#img").parent().hide();
718             $("#image_delete").hide();
719             $("#image_file").parent().show();
720     });
721
722     Admin.init();
723 });