]> dev.renevier.net Git - syp.git/blob - js/admin.js
259c914ae8c446165e8a5c613463687845c5612c
[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: "media/marker-normal.png",
60         MARKER_SELECT_ICON: "media/marker-selected.png",
61         MARKER_TEMPORARY_ICON: "media/marker-temp.png",
62         MARKER_HEIGHT: 25
63     },
64
65     map: null,
66     baseLayer: null,
67     dataLayer: null,
68     selFeatureControl: null,
69     moveFeatureControl: null,
70     addFeatureControl: null,
71
72     currentFeature: null,
73     currentFeatureLocation: null,
74
75     init: function () {
76         this.map = new OpenLayers.Map ("map", {
77                 controls:[
78                     new OpenLayers.Control.Navigation (),
79                     new OpenLayers.Control.PanZoom ()
80                 ],
81                 projection: new OpenLayers.Projection("EPSG:900913"),
82                 displayProjection: new OpenLayers.Projection("EPSG:4326")
83          });
84
85          this.baseLayer = this.createBaseLayer ();
86          this.dataLayer = this.createDataLayer ();
87          this.map.addLayers([this.baseLayer, this.dataLayer]);
88
89          // controls
90          this.selFeatureControl = this.createSelectFeatureControl();
91          this.map.addControl(this.selFeatureControl);
92          this.moveFeatureControl = this.createMoveFeatureControl();
93          this.map.addControl(this.moveFeatureControl);
94          this.addFeatureControl = this.createNewfeatureControl();
95          this.map.addControl(this.addFeatureControl);
96
97          // position
98          var centerBounds = new OpenLayers.Bounds();
99
100          var mapProj = this.map.getProjectionObject();
101          var sypOrigProj = new OpenLayers.Projection("EPSG:4326");
102
103          var bottomLeft = new OpenLayers.LonLat(sypOrig[0],sypOrig[1]);
104          bottomLeft = bottomLeft.transform(sypOrigProj, mapProj);
105          var topRight = new OpenLayers.LonLat(sypOrig[2],sypOrig[3])
106          topRight = topRight.transform(sypOrigProj, mapProj);
107
108          centerBounds.extend(bottomLeft);
109          centerBounds.extend(topRight);
110
111          // at that moment, ie does not know size of the map, we need to update
112          // manually
113          this.map.updateSize();
114          this.map.zoomToExtent(centerBounds);
115
116          this.reset();
117     },
118
119     reset: function() {
120         this.addFeatureControl.deactivate();
121         this.moveFeatureControl.deactivate();
122         this.selFeatureControl.activate();
123         this.checkForFeatures();
124         $("#newfeature_button").show().val(SypStrings.AddItem);
125         $("#newfeature_button").unbind("click").click(function () {
126             Admin.addNewFeature();
127         });
128     },
129
130     createBaseLayer: function () {
131         return new OpenLayers.Layer.OSM("OSM");
132     },
133
134     createDataLayer: function () {
135         var styleMap = new OpenLayers.StyleMap (
136                         {"default": {
137                              externalGraphic: this.Settings.MARKER_ICON,
138                              graphicHeight: this.Settings.MARKER_HEIGHT || 32 
139                                 },
140                          "temporary": { 
141                              externalGraphic: this.Settings.MARKER_TEMPORARY_ICON,
142                              graphicHeight: this.Settings.MARKER_HEIGHT || 32 
143                          },
144                          "select": { 
145                              externalGraphic: this.Settings.MARKER_SELECT_ICON,
146                              graphicHeight: this.Settings.MARKER_HEIGHT || 32 
147                     }});
148
149         var layer = new OpenLayers.Layer.GML("KML", "items.php", 
150            {
151             styleMap: styleMap,
152             format: OpenLayers.Format.KML, 
153             projection: this.map.displayProjection,
154             eventListeners: { scope: this,
155                 loadend: this.checkForFeatures
156             }
157        });
158
159         return layer;
160     },
161
162     createMoveFeatureControl: function () {
163         var control = new OpenLayers.Control.SypDragFeature(
164                 this.dataLayer, {
165                          });
166         return control;
167     },
168
169     createSelectFeatureControl: function () {
170         var control = new OpenLayers.Control.SelectFeature(
171                 this.dataLayer, {
172                         onSelect: OpenLayers.Function.bind(this.onFeatureSelect, this)
173                          });
174         return control;
175     },
176
177     createNewfeatureControl: function () {
178         var control = new OpenLayers.Control ();
179         var handler = new OpenLayers.Handler.Click(control, {
180                 'click': OpenLayers.Function.bind(FeatureMgr.add, FeatureMgr)
181             });
182         control.handler = handler;
183         return control;
184     },
185
186     onFeatureSelect: function (feature) {
187         this.showEditor(feature);
188         FeatureMgr.reset();
189         this.selFeatureControl.deactivate();
190         this.moveFeatureControl.activate();
191     },
192
193     closeEditor: function() {
194         if (this.currentFeature && this.currentFeature.layer) {
195             this.selFeatureControl.unselect(this.currentFeature);
196         }
197         this.currentFeature = null;
198         this.currentFeatureLocation = null;
199         $("#img").removeAttr('src');
200         $("#img").parent().html($("#img").parent().html());
201         $("#img").parent().show();
202         $("#title, #description").val("");
203         $("#editor").hide();
204         // do it once before hidding and once after hidding to work in all cases
205         $("#title, #description").val(""); 
206         $("#image_file").parent().html($("#image_file").parent().html());
207         $(document).unbind("keydown");
208         this.checkForFeatures();
209         this.reset();
210     },
211
212     showEditor: function (feature) {
213         $("#newfeature_button").hide();
214         if (feature.fid) {
215             $("#delete").show();
216         } else {
217             $("#delete").hide();
218         }
219         $(document).unbind("keydown").keydown(function(e) { 
220             if (e.keyCode == 27) {
221                 Admin.cancelCurrentFeature()
222                 e.preventDefault();
223             }
224         });
225         this.currentFeature = feature;
226         this.currentFeatureLocation = new OpenLayers.Pixel(feature.geometry.x, feature.geometry.y);
227         $("#editor").show();
228         $("#instructions").text(SypStrings.DragDropHowto);
229         $("#title").val(feature.attributes.name);
230         var fullDesc = $(feature.attributes.description).parent();
231         $("#description").val(fullDesc.find('p').text());
232         var src = fullDesc.find('img').attr('src');
233         if (src) {
234             $("#img").parent().show();
235             $("#img").attr('src', src);
236             $("#image_file").parent().hide();
237             $("#image_delete").show();
238         } else {
239             $("#img").parent().hide();
240             $("#image_file").parent().show();
241             $("#image_delete").hide();
242         }
243         $("#title").select().focus(); 
244     },
245
246     checkForFeatures: function () {
247         if (this.dataLayer.features.length != 0) {
248             $("#instructions").text(SypStrings.SelectHowto);
249         }
250     },
251
252     addNewFeature: function () {
253         function cancel() {
254             $(document).unbind("keydown");
255             Admin.reset()
256         }
257         $(document).unbind("keydown").keydown(function(e) { 
258             if (e.keyCode == 27) {
259                 e.preventDefault();
260                 cancel();
261             }
262         });
263
264         $("#newfeature_button").val("annuler");
265         $("#newfeature_button").unbind("click").click(cancel);
266
267         $("#instructions").text(SypStrings.AddHowto);
268         this.selFeatureControl.deactivate();
269         this.addFeatureControl.activate();
270         FeatureMgr.reset();
271     },
272
273     cancelCurrentFeature: function() {
274         if (AjaxMgr.running) {
275             return;
276         }
277         var feature = this.currentFeature;
278         if (feature.fid) {
279             FeatureMgr.move (feature, this.currentFeatureLocation);
280         } else {
281             this.dataLayer.removeFeatures([feature]);
282         }
283         this.closeEditor();
284     },
285
286     reloadLayer: function (layer) {
287         layer.destroyFeatures();
288         layer.loaded = false;
289         layer.loadGML();
290     },
291
292     Utils: {
293         escapeHTML: function (str) {
294             if (!str) {
295                 return "";
296             }
297             return str.
298              replace(/&/gm, '&').
299              replace(/'/gm, ''').
300              replace(/"/gm, '"').
301              replace(/>/gm, '>').
302              replace(/</gm, '&lt;');
303         },
304
305         startsWith: function (str, prefix) {
306             return (str.slice(0, prefix.length) == prefix);
307         },
308
309         indexOf: function (array, item) {
310             if (array.indexOf !== undefined) {
311                 return array.indexOf(item);
312             } else {
313                 return OpenLayers.Util.indexOf(array, item);
314             }
315         }
316     }
317 }
318
319 var FeatureMgr = {
320     reset: function() {
321         this.commError("");
322     },
323
324     add: function(evt) {
325         var map = Admin.map;
326         var pos = map.getLonLatFromViewPortPx(evt.xy);
327         feature = this.update (null, pos, "", "", "");
328         Admin.addFeatureControl.deactivate();
329         Admin.selFeatureControl.select(feature);
330     },
331
332     move: function (feature, aLocation) {
333         if (!feature || !aLocation) {
334             return;
335         }
336         var curLoc = feature.geometry;
337         feature.geometry.move(aLocation.x - curLoc.x, aLocation.y - curLoc.y);
338         feature.layer.drawFeature(feature); 
339     },
340
341     update: function(feature, lonlat, imgurl, title, description) {
342         var point = new OpenLayers.Geometry.Point(lonlat.lon, lonlat.lat);
343         if (!feature) {
344             feature = new OpenLayers.Feature.Vector(point);
345             Admin.dataLayer.addFeatures([feature]);
346         } else {
347             this.move (feature, point);
348         }
349         feature.attributes.name = title;
350         feature.attributes.description = "<p>" + Admin.Utils.escapeHTML(description) + "</p>"
351                                 + "<img src=\"" + imgurl + "\">"
352         return feature;
353     },
354
355     del: function (feature) {
356         var form = $("#feature_delete");
357         form.find('input[name="fid"]').val(feature.fid);
358         AjaxMgr.add({
359             form: form,
360             oncomplete: OpenLayers.Function.bind(this.ajaxReply, this),
361             onsend: function() { $("#editor_throbber").css("visibility", "visible"); }
362         });
363     },
364
365     save: function (feature) {
366         var x = feature.geometry.x;
367         var y = feature.geometry.y;
368
369         var mapProj = feature.layer.map.getProjectionObject();
370         var lonlat = new OpenLayers.LonLat(x, y).
371                                     transform(mapProj,
372                                               new OpenLayers.Projection("EPSG:4326"));
373         var form = $("#feature_update");
374         form.find('input[name="lon"]').val(lonlat.lon);
375         form.find('input[name="lat"]').val(lonlat.lat);
376         form.find('input[name="fid"]').val(feature.fid);
377         form.find('input[name="keep_img"]').val(
378             $("#img").attr("src") ? "yes": "no"
379         );
380
381         if (feature.fid) {
382             form.find('input[name="request"]').val("update");
383         } else {
384             form.find('input[name="request"]').val("add");
385         }
386         AjaxMgr.add({
387             form: form,
388             oncomplete: OpenLayers.Function.bind(this.ajaxReply, this),
389             onsend: function() { $("#editor_throbber").css("visibility", "visible"); }
390         });
391     },
392
393     ajaxReply: function (data) {
394         $("#editor_throbber").css("visibility", "hidden");
395         if (!data) {
396             this.commError(SypStrings.ServerError);
397             return;
398         }
399
400         var xml = new OpenLayers.Format.XML().read(data);
401
402         switch (xml.documentElement.nodeName.toLowerCase()) {
403             case "error":
404                 switch (xml.documentElement.getAttribute("reason")) {
405                     case "unauthorized":
406                         $("#login_area").show();
407                         $("#cookie_warning").show();
408                         this.reset();
409                         Admin.cancelCurrentFeature();
410                         Admin.reset();
411                     break;
412                     case "server":
413                         this.commError(SypStrings.ServerError);
414                         $("title").focus();
415                     break;
416                     case "unreferenced":
417                         this.commError(SypStrings.UnreferencedError);
418                         Admin.reloadLayer(Admin.dataLayer);
419                         Admin.closeEditor();
420                     break;
421                     case "nochange":
422                         this.commError(SypStrings.NochangeError);
423                         Admin.closeEditor();
424                     break;
425                     case "request":
426                         this.commError(SypStrings.RequestError);
427                         $("title").focus();
428                     break;
429                     case "toobig":
430                         this.commError(SypStrings.ToobigError);
431                         $("#image_file").parent().html($("#image_file").parent().html());
432                         $("#image_file").focus();
433                     break;
434                     case "notimage":
435                         this.commError(SypStrings.NotimageError);
436                         $("#image_file").parent().html($("#image_file").parent().html());
437                         $("#image_file").focus();
438                     break;
439                     default:
440                         this.commError(SypStrings.UnknownError);
441                         $("title").focus();
442                     break;
443                 }
444             break;
445             case "success":
446                 switch (xml.documentElement.getAttribute("request")) {
447                     case "del":
448                         this.commSuccess(SypStrings.DelSucces);
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(SypStrings.UnconsistentError);
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(SypStrings.UnconsistentError);
507                         } else {
508                             this.commSuccess(SypStrings.UpdateSucces);
509                             Admin.closeEditor();
510                         }
511
512                     break;
513                     default:
514                         this.commError(SypStrings.UnconsistentError);
515                    break;
516                 }
517             break;
518             default:
519                 this.commError(SypStrings.UnconsistentError);
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     }
533 }
534
535 /* maintains a queue of ajax queries, so I'm sure they all execute in the same
536  * order they were defined */
537 var AjaxMgr = {
538     _queue: [],
539
540     running: false,
541
542     add: function(query) {
543         this._queue.push(query);
544         if (this._queue.length > 1) {
545             return;
546         } else {
547             this._runQuery(query);
548         }
549     },
550
551     _runQuery: function(query) {
552         var self = this;
553         $('#api_frame').one("load", function() {
554             self.running = false;
555             self._reqEnd();
556             if (typeof (query.oncomplete) == "function") {
557                 var body = null;
558                 try {
559                     if (this.contentDocument) {
560                         body = this.contentDocument.body;
561                     } else if (this.contentWindow) {
562                         body = this.contentWindow.document.body;
563                     } else {
564                         body = document.frames[this.id].document.body;
565                     }
566                 } catch (e) {}
567                     if (body) {
568                         query.oncomplete(body.innerHTML);
569                     } else {
570                         query.oncomplete(null);
571                     }
572             }
573         });
574         query.form.attr("action", "api.php");
575         query.form.attr("target", "api_frame");
576         query.form.attr("method", "post");
577         this.running = true;
578         query.form.get(0).submit();
579         if (typeof (query.onsend) == "function") {
580             query.onsend();
581         }
582     },
583
584     _reqEnd: function() {
585         this._queue.shift();
586         if (this._queue.length > 0) {
587             this._reqEnd(this._queue[0]);
588         }
589     }
590 }
591
592 var pwdMgr = {
593
594     init: function () {
595         $("#login_form").submit(this.submit);
596         $("#password").focus().select();
597     },
598
599     reset: function() {
600         this.commError ("");
601     },
602
603     submit: function () {
604         try {
605             pwdMgr.commError("");
606             var req = {
607                 form:  $("#login_form"),
608                 onsend: function() {
609                     $("#pwd_throbber").css("visibility", "visible");
610                     $("#login_error").hide();
611
612                     // we need a timeout; otherwise those fields will not be submitted
613                     window.setTimeout(function() { 
614                             // removes focus from #password before disabling it. Otherwise, opera
615                             // prevents re-focusing it after re-enabling it.
616                             $("#password").blur(); 
617                             $("#login_submit, #password").attr("disabled", "disabled");
618                     }, 0)
619                 },
620                 oncomplete: OpenLayers.Function.bind(pwdMgr.ajaxReply, pwdMgr)
621             };
622             AjaxMgr.add(req);
623         } catch(e) {}
624         return false;
625     },
626
627     ajaxReply: function (data) {
628
629         $("#pwd_throbber").css("visibility", "hidden");
630         // here, we need a timeout because onsend timeout sometimes has not been triggered yet
631         window.setTimeout(function() {
632             $("#login_submit, #password").removeAttr("disabled");
633         }, 0);
634
635         if (!data) {
636             this.commError(SypStrings.ServerError);
637             $("#login_error").show();
638             window.setTimeout(function() {
639                     $("#password").focus().select();
640             }, 0);
641             return;
642         }
643         var xml = new OpenLayers.Format.XML().read(data);
644
645         switch (xml.documentElement.nodeName.toLowerCase()) {
646             case "error":
647                 switch (xml.documentElement.getAttribute("reason")) {
648                     case "server":
649                         this.commError(SypStrings.ServerError);
650                     break;
651                     case "unauthorized":
652                         this.commError(SypStrings.UnauthorizedError);
653                     break;
654                     case "request":
655                         this.commError(SypStrings.RequestError);
656                     break;
657                     default:
658                         this.commError(SypStrings.UnknownError);
659                     break;
660                 }
661                 $("#login_error").show();
662                 window.setTimeout(function() {
663                         $("#password").focus().select();
664                 }, 0);
665             break;
666             case "success":
667                 this.reset();
668                 $("#login_area").hide();
669             break;
670             default:
671                 this.commError(SypStrings.UnconsistentError);
672             break;
673         }
674     },
675
676     commError: function (message) {
677         $("#login_error").text(message);
678         if (message) {
679             $("#login_error").show();
680         } else {
681             $("#login_error").hide();
682         }
683     }
684 }
685
686 $(window).load(function () {
687     // if using .ready, ie triggers an error when trying to access
688     // document.namespaces
689     pwdMgr.init();
690     $("#newfeature_button").click(function () {
691         Admin.addNewFeature();
692     });
693     $("#editor_close").click(function () {
694         Admin.cancelCurrentFeature()
695     });
696     $("#feature_update").submit(function() {
697         try {
698             FeatureMgr.save(Admin.currentFeature);
699         } catch(e) {}
700         return false;
701     });
702     $("#feature_delete").submit(function() {
703         try {
704             FeatureMgr.del(Admin.currentFeature);
705         } catch(e) {}
706         return false;
707     });
708     $("#image_delete").click(function() {
709             $("#img").removeAttr('src');
710             // needs to rebuild element otherwise some browsers still
711             // display image.
712             $("#img").parent().html($("#img").parent().html());
713             $("#img").parent().hide();
714             $("#image_delete").hide();
715             $("#image_file").parent().show();
716     });
717
718     Admin.init();
719 });