]> dev.renevier.net Git - syp.git/blob - js/admin.js
02c88e3cb5d0d557815b4f9c49bff6b1701d84e8
[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(SypStrings.AddItem);
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(SypStrings.DragDropHowto);
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(SypStrings.SelectHowto);
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(SypStrings.AddHowto);
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(SypStrings.ServerError);
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                         $("#cookie_warning").show();
410                         this.reset();
411                         Admin.cancelCurrentFeature();
412                         Admin.reset();
413                     break;
414                     case "server":
415                         this.commError(SypStrings.ServerError);
416                         $("title").focus();
417                     break;
418                     case "unreferenced":
419                         this.commError(SypStrings.UnreferencedError);
420                         Admin.reloadLayer(Admin.dataLayer);
421                         Admin.closeEditor();
422                     break;
423                     case "nochange":
424                         this.commError(SypStrings.NochangeError);
425                         Admin.closeEditor();
426                     break;
427                     case "request":
428                         this.commError(SypStrings.RequestError);
429                         $("title").focus();
430                     break;
431                     case "toobig":
432                         this.commError(SypStrings.ToobigError);
433                         $("#image_file").parent().html($("#image_file").parent().html());
434                         $("#image_file").focus();
435                     break;
436                     case "notimage":
437                         this.commError(SypStrings.NotimageError);
438                         $("#image_file").parent().html($("#image_file").parent().html());
439                         $("#image_file").focus();
440                     break;
441                     default:
442                         this.commError(SypStrings.UnknownError);
443                         $("title").focus();
444                     break;
445                 }
446             break;
447             case "success":
448                 switch (xml.documentElement.getAttribute("request")) {
449                     case "del":
450                         this.commSuccess(SypStrings.DelSucces);
451                         var someFeature = false;
452                         var self = this;
453                         $.each($(xml).find("FEATURE,feature"), function () {
454                              someFeature = true;
455                              var id = parseFloat($(this).find("ID:first,id:first").text());
456                              if ((id === null) || isNaN (id)) {
457                                 return;;
458                              }
459                              var features = Admin.dataLayer.features;
460                              for (var idx = 0; idx < features.length; idx++) {
461                                  if (features[idx].fid == id) {
462                                      Admin.dataLayer.removeFeatures([features[idx]]);
463                                  }
464                              }
465                         });
466                         if (someFeature == false) {
467                             this.commError(SypStrings.UnconsistentError);
468                         } else {
469                             Admin.closeEditor();
470                         }
471                     break;
472                     case "update":
473                     case "add":
474                         var someFeature = false;
475                         var self = this;
476                         $.each($(xml).find("FEATURE,feature"), function () {
477                                 someFeature = true;
478                                 var id = parseFloat($(this).find("ID:first,id:first").text());
479                                 if ((id === null) || isNaN (id)) {
480                                     return;;
481                                 }
482
483                                 var lon = parseFloat($(this).find("LON:first,lon:first").text());
484                                 if ((typeof (lon) != "number") || isNaN (lon) ||
485                                         (lon < -180) || (lon > 180)) {
486                                     return;;
487                                 }
488
489                                 var lat = parseFloat($(this).find("LAT:first,lat:first").text());
490                                 if ((typeof (lat) != "number") || isNaN (lat) ||
491                                         (lat < -90) || (lat > 90)) {
492                                     return;;
493                                 }
494
495                                 var mapProj = Admin.map.getProjectionObject();
496                                 var lonlat = new OpenLayers.LonLat (lon, lat).
497                                                 transform( new OpenLayers.Projection("EPSG:4326"), mapProj);
498
499                                 var imgurl = $(this).find("IMGURL:first,imgurl:first").text();
500                                 var title = $(this).find("HEADING:first,heading:first").text();
501                                 var description = $(this).find("DESCRIPTION:first,description:first").text();
502
503                                 feature = self.update (Admin.currentFeature, lonlat, imgurl, title, description); 
504                                 feature.fid = id;
505                         });
506
507                         if (someFeature == false) {
508                             this.commError(SypStrings.UnconsistentError);
509                         } else {
510                             this.commSuccess(SypStrings.UpdateSucces);
511                             Admin.closeEditor();
512                         }
513
514                     break;
515                     default:
516                         this.commError(SypStrings.UnconsistentError);
517                    break;
518                 }
519             break;
520             default:
521                 this.commError(SypStrings.UnconsistentError);
522             break;
523         }
524     },
525
526     commSuccess: function (message) {
527         $("#server_comm").text(message);
528         $("#server_comm").removeClass().addClass("success");
529     },
530
531     commError: function (message) {
532         $("#server_comm").text(message);
533         $("#server_comm").removeClass().addClass("error");
534     }
535 }
536
537 /* maintains a queue of ajax queries, so I'm sure they all execute in the same
538  * order they were defined */
539 var AjaxMgr = {
540     _queue: [],
541
542     running: false,
543
544     add: function(query) {
545         this._queue.push(query);
546         if (this._queue.length > 1) {
547             return;
548         } else {
549             this._runQuery(query);
550         }
551     },
552
553     _runQuery: function(query) {
554         var self = this;
555         $('#api_frame').one("load", function() {
556             self.running = false;
557             self._reqEnd();
558             if (typeof (query.oncomplete) == "function") {
559                 var body = null;
560                 try {
561                     if (this.contentDocument) {
562                         body = this.contentDocument.body;
563                     } else if (this.contentWindow) {
564                         body = this.contentWindow.document.body;
565                     } else {
566                         body = document.frames[this.id].document.body;
567                     }
568                 } catch (e) {}
569                     if (body) {
570                         query.oncomplete(body.innerHTML);
571                     } else {
572                         query.oncomplete(null);
573                     }
574             }
575         });
576         query.form.attr("action", "api.php");
577         query.form.attr("target", "api_frame");
578         query.form.attr("method", "post");
579         this.running = true;
580         query.form.get(0).submit();
581         if (typeof (query.onsend) == "function") {
582             query.onsend();
583         }
584     },
585
586     _reqEnd: function() {
587         this._queue.shift();
588         if (this._queue.length > 0) {
589             this._reqEnd(this._queue[0]);
590         }
591     }
592 }
593
594 var pwdMgr = {
595
596     init: function () {
597         $("#login_form").submit(this.submit);
598         $("#password").focus().select();
599     },
600
601     reset: function() {
602         this.commError ("");
603     },
604
605     submit: function () {
606         try {
607             pwdMgr.commError("");
608             var req = {
609                 form:  $("#login_form"),
610                 onsend: function() {
611                     $("#pwd_throbber").css("visibility", "visible");
612                     $("#login_error").hide();
613
614                     // we need a timeout; otherwise those fields will not be submitted
615                     window.setTimeout(function() { 
616                             // removes focus from #password before disabling it. Otherwise, opera
617                             // prevents re-focusing it after re-enabling it.
618                             $("#password").blur(); 
619                             $("#login_submit, #password").attr("disabled", "disabled");
620                     }, 0)
621                 },
622                 oncomplete: OpenLayers.Function.bind(pwdMgr.ajaxReply, pwdMgr)
623             };
624             AjaxMgr.add(req);
625         } catch(e) {}
626         return false;
627     },
628
629     ajaxReply: function (data) {
630
631         $("#pwd_throbber").css("visibility", "hidden");
632         // here, we need a timeout because onsend timeout sometimes has not been triggered yet
633         window.setTimeout(function() {
634             $("#login_submit, #password").removeAttr("disabled");
635         }, 0);
636
637         if (!data) {
638             this.commError(SypStrings.ServerError);
639             $("#login_error").show();
640             window.setTimeout(function() {
641                     $("#password").focus().select();
642             }, 0);
643             return;
644         }
645         var xml = new OpenLayers.Format.XML().read(data);
646
647         switch (xml.documentElement.nodeName.toLowerCase()) {
648             case "error":
649                 switch (xml.documentElement.getAttribute("reason")) {
650                     case "server":
651                         this.commError(SypStrings.ServerError);
652                     break;
653                     case "unauthorized":
654                         this.commError(SypStrings.UnauthorizedError);
655                     break;
656                     case "request":
657                         this.commError(SypStrings.RequestError);
658                     break;
659                     default:
660                         this.commError(SypStrings.UnknownError);
661                     break;
662                 }
663                 $("#login_error").show();
664                 window.setTimeout(function() {
665                         $("#password").focus().select();
666                 }, 0);
667             break;
668             case "success":
669                 this.reset();
670                 $("#login_area").hide();
671             break;
672             default:
673                 this.commError(SypStrings.UnconsistentError);
674             break;
675         }
676     },
677
678     commError: function (message) {
679         $("#login_error").text(message);
680         if (message) {
681             $("#login_error").show();
682         } else {
683             $("#login_error").hide();
684         }
685     }
686 }
687
688 $(window).load(function () {
689     // if using .ready, ie triggers an error when trying to access
690     // document.namespaces
691     pwdMgr.init();
692     $("#newfeature_button").click(function () {
693         Admin.addNewFeature();
694     });
695     $("#editor_close").click(function () {
696         Admin.cancelCurrentFeature()
697     });
698     $("#feature_update").submit(function() {
699         try {
700             FeatureMgr.save(Admin.currentFeature);
701         } catch(e) {}
702         return false;
703     });
704     $("#feature_delete").submit(function() {
705         try {
706             FeatureMgr.del(Admin.currentFeature);
707         } catch(e) {}
708         return false;
709     });
710     $("#image_delete").click(function() {
711             $("#img").removeAttr('src');
712             // needs to rebuild element otherwise some browsers still
713             // display image.
714             $("#img").parent().html($("#img").parent().html());
715             $("#img").parent().hide();
716             $("#image_delete").hide();
717             $("#image_file").parent().show();
718     });
719
720     Admin.init();
721 });